gdbとは
実行可能ファイル(バイナリ)を動的に(実際に動かして)解析するツールです。
実行可能ファイルの中身は機械語の羅列になっており、それらを一命令ずつステップ実行していくことができます。
環境
Ubuntu 16.04 LTS
とりあえずインストール
$ sudo apt update -y
$ sudo apt install gdb
対象バイナリを用意。
$ vim test.c
#include <stdio.h>
int main(void) {
int hoge = 10;
char fuga[] = "Hello, gdb!";
printf("%s\n", fuga);
}
$ make test
$ ./test
Hello, gdb!
gdbを使ってみる
$ gdb test
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from test...(no debugging symbols found)...done.
(gdb)
gdbプロンプトに入りました。以降はgdbプロンプトでのコマンドになります。
まずは、main関数の中身を見てみましょう。
(gdb) disass main
Dump of assembler code for function main:
0x0000000000400596 <+0>: push %rbp
0x0000000000400597 <+1>: mov %rsp,%rbp
0x000000000040059a <+4>: sub $0x30,%rsp
0x000000000040059e <+8>: mov %fs:0x28,%rax
0x00000000004005a7 <+17>: mov %rax,-0x8(%rbp)
0x00000000004005ab <+21>: xor %eax,%eax
0x00000000004005ad <+23>: movl $0xa,-0x24(%rbp)
0x00000000004005b4 <+30>: movabs $0x67202c6f6c6c6548,%rax
0x00000000004005be <+40>: mov %rax,-0x20(%rbp)
0x00000000004005c2 <+44>: movl $0x216264,-0x18(%rbp)
0x00000000004005c9 <+51>: lea -0x20(%rbp),%rax
0x00000000004005cd <+55>: mov %rax,%rdi
0x00000000004005d0 <+58>: callq 0x400460 <puts@plt>
0x00000000004005d5 <+63>: mov $0x0,%eax
0x00000000004005da <+68>: mov -0x8(%rbp),%rdx
0x00000000004005de <+72>: xor %fs:0x28,%rdx
0x00000000004005e7 <+81>: je 0x4005ee <main+88>
0x00000000004005e9 <+83>: callq 0x400470 <__stack_chk_fail@plt>
0x00000000004005ee <+88>: leaveq
0x00000000004005ef <+89>: retq
End of assembler dump.
startコマンドでプログラムを開始します。
(gdb) start
Temporary breakpoint 1 at 0x40059a
Starting program: /home/miyagaw61/tmp/test
Temporary breakpoint 1, 0x000000000040059a in main ()
main関数内のどこかのbreakponitで止まりました(詳細は省きますが、ここではエントリーポイントと呼ばれている部分で止まっています)。
breakpointというのはプログラムの実行を止めるアドレスのことです。
次にCPUが実行するであろう命令を見てみましょう。
(gdb) x/5i $rip
=> 0x40059a <main+4>: sub $0x30,%rsp
0x40059e <main+8>: mov %fs:0x28,%rax
0x4005a7 <main+17>: mov %rax,-0x8(%rbp)
0x4005ab <main+21>: xor %eax,%eax
0x4005ad <main+23>: movl $0xa,-0x24(%rbp)
xというのが、メモリの中を表示するコマンドで、その後スラッシュで区切り、「表示する個数」「表示するフォーマット」を指定します。そして、引数に開始アドレスを指定します。
ここでは、RIPレジスタという、プログラムカウンタの役割をしているレジスタに格納されているアドレスを開始アドレスとして、i(instructionの略で、命令を表すフォーマット指定子)フォーマットで5つ分表示します。
ここで表示されているのが、アセンブリ言語です。機械語と一対一になっています。
アセンブリには表記形式がいくつあり、宗教上の理由で私はintel表記しか読めないため、intel表記に変更します。
(gdb) set disassembly-flavor intel
(gdb) x/5i $rip
=> 0x40059a <main+4>: sub rsp,0x30
0x40059e <main+8>: mov rax,QWORD PTR fs:0x28
0x4005a7 <main+17>: mov QWORD PTR [rbp-0x8],rax
0x4005ab <main+21>: xor eax,eax
0x4005ad <main+23>: mov DWORD PTR [rbp-0x24],0xa
sub rsp,0x30
であれば, rspレジスタを0x30だけ減算します。
アセンブリ言語は一つ一つの命令は非常に簡単です。
わからない命令があれば、このサイトで調べてみましょう。
mov命令の意味が知りたければ「x86 mov」とかでggるとすぐにこのサイトが出てくるはずです。
一つ命令を進めてみましょう。
(gdb) nexti
0x000000000040059e in main ()
(gdb) x/5i $rip
=> 0x40059e <main+8>: mov rax,QWORD PTR fs:0x28
0x4005a7 <main+17>: mov QWORD PTR [rbp-0x8],rax
0x4005ab <main+21>: xor eax,eax
0x4005ad <main+23>: mov DWORD PTR [rbp-0x24],0xa
0x4005b4 <main+30>: movabs rax,0x67202c6f6c6c6548
一つ進んだことがわかると思います。
20個の命令を表示してみます。
(gdb) x/20i $rip
=> 0x40059e <main+8>: mov rax,QWORD PTR fs:0x28
0x4005a7 <main+17>: mov QWORD PTR [rbp-0x8],rax
0x4005ab <main+21>: xor eax,eax
0x4005ad <main+23>: mov DWORD PTR [rbp-0x24],0xa
0x4005b4 <main+30>: movabs rax,0x67202c6f6c6c6548
0x4005be <main+40>: mov QWORD PTR [rbp-0x20],rax
0x4005c2 <main+44>: mov DWORD PTR [rbp-0x18],0x216264
0x4005c9 <main+51>: lea rax,[rbp-0x20]
0x4005cd <main+55>: mov rdi,rax
0x4005d0 <main+58>: call 0x400460 <puts@plt>
0x4005d5 <main+63>: mov eax,0x0
0x4005da <main+68>: mov rdx,QWORD PTR [rbp-0x8]
0x4005de <main+72>: xor rdx,QWORD PTR fs:0x28
0x4005e7 <main+81>: je 0x4005ee <main+88>
0x4005e9 <main+83>: call 0x400470 <__stack_chk_fail@plt>
0x4005ee <main+88>: leave
0x4005ef <main+89>: ret
0x4005f0 <__libc_csu_init>: push r15
0x4005f2 <__libc_csu_init+2>: push r14
0x4005f4 <__libc_csu_init+4>: mov r15d,edi
puts関数が見えると思います(0x4005d0 <main+58>: call 0x400460 <puts@plt>
)。
puts関数が呼ばれるのは0x4005d0らしいので、そこにbreakpointを仕掛けます。
(gdb) break *0x4005d0
Breakpoint 2 at 0x4005d0
そこまで一気に進みます。
(gdb) continue
Continuing.
Breakpoint 2, 0x00000000004005d0 in main ()
実際にputs関数を呼ぶところで処理が止まったことを確認しましょう。
(gdb) x/5i $rip
=> 0x4005d0 <main+58>: call 0x400460 <puts@plt>
0x4005d5 <main+63>: mov eax,0x0
0x4005da <main+68>: mov rdx,QWORD PTR [rbp-0x8]
0x4005de <main+72>: xor rdx,QWORD PTR fs:0x28
0x4005e7 <main+81>: je 0x4005ee <main+88>
RBPレジスタに格納されているアドレスよりも8*4バイト上位のアドレスにあるデータを見てみましょう。
(gdb) x/s $rbp-8*4
0x7fffffffdfa0: "Hello, gdb!"
この値が引数として使われます。
一つ命令を進めてみましょう。
(gdb) nexti
Hello, gdb!
0x00000000004005d5 in main ()
引数の値が出力されたことが分かると思います。
breakpointを仕掛けずに最後まで走らせてみます。
(gdb) continue
Continuing.
[Inferior 1 (process 26356) exited normally]
終了しました。
breakpointの一覧を見てみます。
(gdb) info break
Num Type Disp Enb Address What
2 breakpoint keep y 0x00000000004005d0 <main+58>
breakpoint already hit 1 time
breakpointを削除します。
(gdb) delete 2
(gdb) info break
No breakpoints or watchpoints.
最初から再開してみます。
(gdb) run
Starting program: /home/miyagaw61/tmp/test
Hello, gdb!
[Inferior 1 (process 12759) exited normally]
一度も止まらずに最初から最後まで走りました。
qコマンドでgdbを終了します。
(gdb) q
$
おわり
gdbの基本は以上となります。
enjoy your gdb life!