bare metalでのgdb入門
第二回のARMのアセンブリを学習する時には、gdbを使うと理解の助けになります。
ですが、既存のgdbの解説はC言語やデバッグシンボルの存在を前提にした物が主流です。 我々は今回バイナリ自身を見ていくのが中心なので、 少し標準的な使い方と違う所があり、ググって出てくる入門の内容はあまり適切ではありません。
gdbでのリモートデバッグを普段から使っている人ならbare metal関連でググって出てくる知識を足すだけで一通り用が足せると思いますが、gdbをあまり使った事が無い人にとってはどの知識とどの知識を組み合わせたらいいのか、なかなか分かりにくいと思います。
そこで、ここにbare metalでのバイナリを中心に、gdbを使った事が無い人を対象に簡単に使い方を解説するページを作る事にしました。
内容的には比較的独立しているので、ページを分けておきます。
入門動画
概要的な動画を最初に作ってみました。
この動画を見なくてもここの解説は理解出来ると思いますが、概要をまず知りたい、という人は良かったら見てみてください。
hello_arm.binをgdbでデバッグ実行する
gdbを使ってQEMU内のプロセスをデバッグ実行する場合は、リモートデバッグという形になります。 QEMUの中でデバッグモニタと呼ばれるプログラムを動かして、 それとQEMUの外のホスト(つまりQEMUを実行しているWindowsマシンとかMacマシンの事)のgdbプロセスとTCP/IPで通信しながらデバッグする事になります。
デバッグモニタとはなんぞや、とか説明してもいいのですが、知らなくても構わないので説明しません。 興味がある人はリモートデバッグなどでググってみてください。
QEMU側のコマンド
普段実行しているQEMUコマンドに-sと-Sのオプションを付けます。例えば以下のようになります。
qemu-system-arm -M versatilepb -m 128M -nographic -kernel hello_arm.bin -serial mon:stdio -s -S
最後に-sと-Sがついていますね。 こうするとhello_arm.binをロードしてデバッグモニタ付きでデバッグプロセスが立ち上がります。
プロセスが立ち上がった状態でデバッガが来るのを待ちます。
-sと-Sの意味について知りたい方は以下のサイトなどが参考になりますが、ゆとりは気にせずコピペで行きましょう。 これらのオプションでポート1234が使われます。(気にしなくてもいいです)
https://elinux.org/Virtual_Development_Board
ホスト側のコマンド
さて、QEMUを動かしているのとは別のターミナルでgdbを実行します。 arm用のgdbという事で、arm-none-eabi-gdbというコマンドを実行します。
arm-none-eabi-gdb
(gdb)
以後はgdb内でのコマンドとなります。終わるのはquitです。
まず、以下のコマンドでデバッグモニタとつなげます。
(gdb) target remote :1234
これでQEMU内のプロセスとつながります。
以後はgdbのコマンドを実行していく事でデバッグを行います。 なお、この時点ではpcは0を指しています。 実行すると0x10000まではnopなので何もせずに0x10000まで行くっぽい?(あまり分かってません。嘘かも)
良く使うgdbのコマンド
通常gdbはC言語などの言語で書かれたプログラムをデバッグするのに使うので、入門などではC言語用のコマンドをいろいろ説明しています。
ですが我らとしてはアセンブリやバイナリの解析を先に行うので、これらの入門で出てくるコマンドとは違ったコマンドを主に使っていきます。 そこでここでは、バイナリやアセンブリを解析するのに良く使うものだけを簡単に説明します。
レジスタの中身の表示
以下のようにすると、全レジスタの現在の値が表示されます。
(gdb) info all-registers
ただちょっと多すぎるので目的のレジスタを探すのが大変です。
レジスタを一つ表示するにはprintコマンドを使います。
(gdb) print $r0
16進表記にするには/xをつけます。
(gdb) print /x $r0
なおgdbは、他のコマンドとぶつからない限りコマンド名は途中まででも実行出来ます。 例えば、printはprinでもpriでもpでも表示されます。
(gdb) p /x $r0
空白無しでも動きます。
(gdb) p/x $r0
読みにくいですが、あとに残すコードでは無くて操作する為のコマンド体系ですからね。
一命令ずつ実行
siで一つの命令が実行出来ます。 ですが、先ほど書いたように最初はpcが0にあります。
versatilepbのOS無しでは、プログラムは0x10000にロードされるので、 実行する前に現在の場所を0x10000に変更する必要があります。 (前半64k個の命令は無害なので、6万4000回くらいsiすればいいのですが、大変ですしね)
レジスタはsetというコマンドで変更出来ます。
(gdb) p/x $pc
(gdb) set $pc=0x10000
(gdb) p/x $pc
これで0x10000から実行されるようになりました。
この後はsiコマンドで一命令ずつ実行してけます。
(gdb) si
今どこを実行しているか、などは、pcを表示すれば分かりますが、次のdisaasembleでどこが実行されているかが表示されるので、これと組み合わせて使います。
disassemble
メモリの特定の領域内のバイナリを逆アセンブルする、というコマンドがあります。 以下のようにやるとロードされたプログラムの逆アセンブル結果が見れます。
(gdb) disassemble 0x10000, 0x10020
この範囲に現在pcがあれば、どこにあるかも表示してくれているはずです。 なお、printと同様にこれも適当に途中までで実行できます。 どこまで縮められるかは知らないですが、自分は普段disaくらいで実行している気がする。
基本的な作業としては、disaしながらsiしていって、たまにpやsetする感じです。
hello_arm.binを実行していって、レジスタに値がロードされたりUARTに書かれたらQEMU側のコンソールにちゃんとその文字が出ているのを確認したりしてみてください。
メモリの内容の表示
xというコマンドでメモリの内容が表示できる。 このコマンドは良く使うのだが、hello_arm.binはメモリ使わないから例が書きにくい…
例えばロードされたプログラムを表示してみます。 すると以下みたいなコマンドで表示できる。
(gdb) x /5xw 0x10000
5xwは
- 読む単位はw (4バイト)
- 表記はx (16進数)
- 以上の前提で5単位読む(つまり4バイトの数字を5つ読む)
という意味になります。 この時点ではdisaしてればいいじゃん、としか感じられないと思うので、スタックのあたりまで行ったら試してみてください。