mcommit's message

ソフトウェア開発の仕事をしているsimotinといいます。記事の内容でご質問やご意見がありましたらお気軽にコメントしてください\^o^/

Raspberry Pi 用にGDBをクロスコンパイルしてリモートデバッグしてみた

デバッグに便利なGDBですが、Raspberry Pi用にクロスコンパイルしてリモートデバッグを試してみました。

GNU関係のソフトは、必ずしもバージョンが新しければよいというわけではない(むしろ新しいバージョンだと思いもよらないバグがあったりする)のですが、今回はなんとなく新しめということで8.0で試してみました。
※よく見ると gdb-8.0.1.tar.gz というバージョンが出ているのでそっちの方がいいかも。

構成と手順

ホスト環境 : Linux Mint

linuxmint.com

ターゲット環境: RaspberryPi (RASPBIAN STRETCH WITH DESKTOP 2017-09-07)

www.raspberrypi.org

の環境で試しました。

大まかな作業の流れですが、

  1. (ホスト環境で)ターゲット(RaspberryPi)上で動かすGDBをクロスコンパイルする
  2. ホスト環境上で動作するGDBクライアントをビルドする
  3. ロスコンパイルしたモジュールをRaspberryPiにコピーする
  4. デバッグ用サンプルプログラムをビルドし、gdbserverを起動する
  5. ホスト環境上のGDBクライアントから(RaspberryPi上)のGdbserverに接続する

のような流れになります。結構やることが多いですね。

wgetからのクロスコンパイルまで

ターゲット用GDBのクロスコンパイル

私の手元の環境はUbuntu系のLinux Mintですが、arm用のクロスコンパイラ(arm-linux-gnueabihf)はapt-getでインストールできます。

$ sudo apt-get install gcc-arm-linux-gnueabihf

ロスコンパイラの用意ができれば、まずはターゲット上で動くGDBをクロスコンパイルします。

$ wget http://ftp.gnu.org/gnu/gdb/gdb-8.0.tar.gz
$ tar xzfv gdb-8.0.tar.gz
$ cd gdb-8.0
$ ./configure --target=arm-linux-gnueabihf --host=arm-linux-gnueabihf
$ make

ビルドが終わると gdb フォルダ以下にビルド結果が出来上がっています。実行モジュールとしては、

  • gdb
  • gdbserver/gdbserver
  • gdbserver/gdbreplay

が出来上がっています。

$file gdb/gdb
gdb/gdb: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=01ea845e822ebef8d1db986a768ab9725828b02b, not stripped

問題なくクロスコンパイルできてそうです。ラズパイ側へコピーして動かしてみます。
gdbreplay は何に使うのか分かりませんが、いつか何かをreplayする時に役に立ちそうなので一緒にコピーしておきます。

ホスト環境用GDBのビルド

ホスト環境にプリインストールされているGDBはホストOSのアーキテクチャ向けのGDBなので、これをそのままターゲット(RaspberryPi)のリモートデバッグには使えません
GDBのリモートデバッグをする場合、ターゲットのアーキテクチャ用のGDBクライアントが必要になります。

といっても話は簡単で、先ほどクロスコンパイルの手順にあったconfigure時に、

$ ./configure --target=arm-linux-gnueabihf
$ make

とすればホストマシン上で動作する指定したtarget(今回はarm)用のgdbクライアントをビルドできます。
コンパイルしてできるモジュールは gdb というファイルになります。

これでGDBのビルドに関する作業は完了です。

RaspberryPi上でGDBを動かす

ここからはRspberryPi上での作業になります。コピーしておいたgdbをRaspberryPi上で動かしてみます。

と、その前にGDBは高機能なので、依存関係とかが気になるところです。
ラズパイ上で念のためlddしてみました。

$ldd gdbserver                                                                               
        linux-vdso.so.1 (0x7ef96000)
        /usr/lib/arm-linux-gnueabihf/libarmmem.so (0x76ef8000)
        libdl.so.2 => /lib/arm-linux-gnueabihf/libdl.so.2 (0x76ecc000)
        libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0x76d8b000)
        /lib/ld-linux-armhf.so.3 (0x54b65000)

うむ。ライブラリ全部解決できてる。素晴らしい!
※注意点として、raspbian の場合、普通にgdb を実行すると標準でインストールされているされているgdbが動きます。

$ gdb -v
GNU gdb (Raspbian 7.7.1+dfsg-5) 7.7.1

手元のラズパイ環境では、7.7.1 が入っていました。


コピーしてきたGDBの動作を確認します。
以下、クロスコンパイルしてコピーしたgdb,gdbserver,gdbreplayが直下にあるものとします。

$ ./gdb -v
GNU gdb (GDB) 8.0

gdbを起動して show configuration とたたくとビルド時の configuration を確認できるようです。

(gdb) show configuration                                                                                       
This GDB was configured as follows:
   configure --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf
             --with-auto-load-dir=$debugdir:$datadir/auto-load
             --with-auto-load-safe-path=$debugdir:$datadir/auto-load
             --with-expat
             --with-gdb-datadir=/usr/share/gdb (relocatable)
             --with-jit-reader-dir=/usr/lib/gdb (relocatable)
             --without-libunwind-ia64
             --with-lzma
             --with-python=/usr (relocatable)
             --with-separate-debug-dir=/usr/lib/debug (relocatable)
             --with-system-gdbinit=/etc/gdb/gdbinit
             --with-zlib
             --without-babeltrace

とりあえず何かデバッグしてみる。

ロスコンパイルしたGDB動いてそうということで、何かデバッグしてみます。

デバッグするプログラム(main.c)

#include <stdio.h>

int main(int argc, char **argv)
{
    int i = 0;
    int total = 0;
    for (i = 0; i < 10; i++)
    {
        total += i;
    }
    printf("%d\n", total);
    return 0;
}


例によって0から10の合計を求める通称ずいぶんとダサいコードで動作を確かめてみます。

$ gcc main.c -g
$ ./gdb a.out
(gdb)b main
(gdb)r
Starting program: /home/pi/a.out 

Breakpoint 1, main (argc=1, argv=0x7efff744) at main.c:5
5               int i = 0;
(gdb) info registers 
r0             0x1      1
r1             0x7efff744       2130704196
r2             0x7efff74c       2130704204
r3             0x1042c  66604
r4             0x0      0
r5             0x0      0
r6             0x102f8  66296
r7             0x0      0
r8             0x0      0
r9             0x0      0
r10            0x76fff000       1996484608
r11            0x7efff5ec       2130703852
r12            0x76f9d000       1996083200
sp             0x7efff5d8       0x7efff5d8
lr             0x76e78294       1994883732
pc             0x10440  0x10440 <main+20>
cpsr           0x60000010       1610612752
(gdb) 

汎用レジスタr0~r12まである。リンクレジスタもある。

間違いない。こいつはARMだ。

ということでGDBは問題なく動いてそうです。

いよいよリモートデバッグに挑戦!

リモートデバッグの手順は大きく、

  1. ターゲット上でGDBサーバを起動
  2. ホスト環境からGDBクライアントで接続してデバッグ

の2フェーズになります。

ターゲット上でGDBサーバを起動

gdbserverをコピーし動かします。デバッグするプログラムは先ほどのサンプルプログラムです。

$./gdbserver localhost:10000 a.out

これで、ポート10000でGDBクライアントからの接続を待機している状態になります。

ホスト環境からの接続してデバッグ

さて、ターゲット側での準備が整いました。
後はホスト環境のGDBクライアントから接続をするのですが、

GDBクライアントを使ってリモートデバッグを行う場合、

の2つが必要になります。

今回の例の場合、

デバッグしたいモジュール → a.out
デバッグしたいモジュールのソースコード → main.c(ずいぶんとダサいコード)

になります。これら2つはターゲット環境からホスト環境にコピーします。

コピーが終わったら、ホスト環境上でGDBクライアントを起動しリモート接続します。

リモート接続は、

$gdb デバッグするプログラム名

gdbを起動した後、

target remote ターゲットのIPアドレス(ホスト名):ポート番号

を実行するとリモートのGDBサーバーに接続します。
私の手元の環境では、RaspberryPiのIPアドレスは192.168.11.17 になっています。gdbserver側のポート番号は10000を指定して起動しました。

$ ./gdb a.out
(gdb) target remote 192.168.11.17:10000                                                                        
Remote debugging using 192.168.11.17:10000
Reading /lib/ld-linux-armhf.so.3 from remote target...
warning: File transfers from remote targets can be slow. Use "set sysroot" to access files locally instead.
Reading /lib/ld-linux-armhf.so.3 from remote target...
Reading symbols from target:/lib/ld-linux-armhf.so.3...Reading /lib/f72fb00897d4f06093d6f0451c9ca7d1f6e14c.debug from remote target...
Reading /lib/.debug/f72fb00897d4f06093d6f0451c9ca7d1f6e14c.debug from remote target...
(no debugging symbols found)...done.
Python Exception <type 'exceptions.NameError'> Installation error: gdb.execute_unwinders function is missing: 
0x76fce9e0 in ?? () from target:/lib/ld-linux-armhf.so.3
(gdb) 

何やらメッセージがパラパラとでましたが、なんとなくつながってそうですね。

ちなみにこの時点でターゲット側のターミナルには、

Remote debugging from host 192.168.11.16
Remote side has terminated connection.  GDBserver will reopen the connection.
Listening on port 10000
Remote debugging from host 192.168.11.16

というログが出力されていました。

デバッグ操作

無事リモート接続できたら後の操作は普通のGDBデバッグ時と同じ感覚で操作できるようです。
ただし、 run コマンドは実行できなくてブレークポイントを張って continue するところからデバッグが始められるようです。

私の環境では、

Python Exception <type 'exceptions.NameError'> Installation error: gdb.execute_unwinders function is missing:

というエラーメッセージが出ていましたがとりあえずステップ実行(next)や変数の値確認(print)などは問題なくできていました。

感想

こうして書いてみると長くなりましたが、記事が長くなったように実際の手順としても、リモートデバッグは準備するのが少し面倒だなと感じました。
小さなモジュールであればprintfデバッグの方が圧倒的に早そうですが、規模の大きなプログラムであればリモートデバッグの環境を整えてGDBデバッグすることのメリットが活かせるように思います。

今回はターミナル上でのリモートデバッグを試してみましたが、Eclipse CDTからもリモートデバッグができるようなので試してみたと思います。

ちなみに、GDBに関する書籍は何冊か出ているのですが、printfデバッグ派なのであまり読んだことがありません。
組み込み開発(というよりは組み込みLinuxかも)でGDBを使う場合にはやはり「GDBを使った実践的デバッグ手法」が参考になります。

実践 デバッグ技法 ―GDB、DDD、Eclipseによるデバッギング

実践 デバッグ技法 ―GDB、DDD、Eclipseによるデバッギング

GDBを使った実践的デバッグ手法―Emacs,Eclipse,Cygwin,Insi (Interface増刊)

GDBを使った実践的デバッグ手法―Emacs,Eclipse,Cygwin,Insi (Interface増刊)

GDBプロトコルとかDWARFとか興味はあるので、勉強したいなぁとかよく思います。
そういえば、以前DWARFのドキュメントを読みかけたことがあるのですが、結構奥が深くて、挫折したままになっているのでまた気合をいれて勉強できればと思います。