すごく基本的なことですが、今まで知らなかったのでメモ。
GDBで関数にブレークポイントを設定した際、機械語レベルではどこで実行を止めてくれるのかを知りませんでした。
呼び出し元のcall直前?call直後?ベースポインタpushしてから?局所変数の領域確保してから?
結論から言うと、 局所変数の領域確保してから
でした。
(つまり呼び出された際に最低限必要なスタックフレームの処理までは実行される)
実際の動作確認
言語(規格)はC99です。
環境
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11)
Linux Vega 4.4.0-131-generic #157-Ubuntu SMP Thu Jul 12 15:51:36 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
確認用のコード
#include <stdio.h>
int func(int a, int b){
int c;
c = a + b;
return(2 * c);
}
int main(void){
int n;
n = func(1, 2);
printf("%d\n", n);
return 0;
}
x86
コンパイル
gcc -m32 -g -O0 -std=c99 -fno-stack-protector test.c
gdbで確認
salacia@Vega:~/tmp/gdb$ gdb a.out
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 a.out...done.
(gdb) b func
Breakpoint 1 at 0x8048411: file test.c, line 5.
(gdb) r
Starting program: /home/salacia/tmp/gdb/a.out
Breakpoint 1, func (a=1, b=2) at test.c:5
5 c = a + b;
(gdb) p $eip
$1 = (void (*)()) 0x8048411 <func+6>
止まったのは 0x8048411
のアドレスに配置された命令。(この前の命令までは実行されてる)
前後の命令を見てみる。
salacia@Vega:~/tmp/gdb$ objdump -d -M intel a.out
a.out: file format elf32-i386
============================= 省略 =============================
0804840b <func>:
804840b: 55 push ebp
804840c: 89 e5 mov ebp,esp
804840e: 83 ec 10 sub esp,0x10
8048411: 8b 55 08 mov edx,DWORD PTR [ebp+0x8]
8048414: 8b 45 0c mov eax,DWORD PTR [ebp+0xc]
8048417: 01 d0 add eax,edx
8048419: 89 45 fc mov DWORD PTR [ebp-0x4],eax
804841c: 8b 45 fc mov eax,DWORD PTR [ebp-0x4]
804841f: 01 c0 add eax,eax
8048421: c9 leave
8048422: c3 ret
804840b : ベースポインタを保存
804840c : スタックポインタを新たなベースポインタに設定する。
804840e : espを減算し、局所変数用の領域を確保する
8048411 : (ここで止まった)演算のためレジスタに引数をコピー、関数内の処理はここから
というわけで、x86では関数にブレークポイントを設定すると、 スタックフレームの処理までは実行される
という事がわかりました。
おまけ(x64)
x64アセンブラ触ったことないので、どんな結果が出るか全くわからないけど、自分なりに調べてみた。
もし間違ってたら、ご指摘いただけると嬉しいです。
コンパイル
gcc -g -O0 -std=c99 -fno-stack-protector test.c
gdbでの確認
salacia@Vega:~/tmp/gdb$ gdb a.out
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 a.out...done.
(gdb) b func
Breakpoint 1 at 0x400530: file test.c, line 5.
(gdb) r
Starting program: /home/salacia/tmp/gdb/a.out
Breakpoint 1, func (a=1, b=2) at test.c:5
5 c = a + b;
(gdb) p $rip
$1 = (void (*)()) 0x400530 <func+10>
止まったのは 0x400530
のアドレスにある命令のところ。
このアドレスの前後の命令を見てみる。
salacia@Vega:~/tmp/gdb$ objdump -d -M intel a.out
a.out: file format elf64-x86-64
============================= 省略 =============================
0000000000400526 <func>:
400526: 55 push rbp
400527: 48 89 e5 mov rbp,rsp
40052a: 89 7d ec mov DWORD PTR [rbp-0x14],edi
40052d: 89 75 e8 mov DWORD PTR [rbp-0x18],esi
400530: 8b 55 ec mov edx,DWORD PTR [rbp-0x14]
400533: 8b 45 e8 mov eax,DWORD PTR [rbp-0x18]
400536: 01 d0 add eax,edx
400538: 89 45 fc mov DWORD PTR [rbp-0x4],eax
40053b: 8b 45 fc mov eax,DWORD PTR [rbp-0x4]
40053e: 01 c0 add eax,eax
400540: 5d pop rbp
400541: c3 ret
まず、ベースポインタの保存と、新たなベースポインタを設定している。
次がおそらく引数の処理の部分である。
Linuxのx64の呼び出し規約(System V ABI)では、スタックの前にレジスタで引数を渡そうとする。
今回は引数が4byteの引数が2つなので、呼び出し元では edi
, esi
に、値を格納している。
これらの値を、スタックフレームにコピーしてきている( 40052a
と 40052d
の命令)。
ここまでは実行して、ブレークポイントに差し掛かる。
x86と違って、スタックポインタの減算などがないが、
x64の場合はここまでが、スタックフレームの処理という扱い?だと思って良いのだろうか?
と、ここで終わってしまうのは気持ち悪いので、もう少し試す。
どこまでが、スタックフレームの処理なのか調べようと、特に何もしない関数を見てみた。
salacia@Vega:~/tmp/gdb$ cat func.c
int func(int a, int b){
int c;
return 0;
}
salacia@Vega:~/tmp/gdb$ objdump -d -M intel func.o
func.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <func>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
a: b8 00 00 00 00 mov eax,0x0
f: 5d pop rbp
10: c3 ret
x86と違って、スタックポインタの減算はしないらしい。
そして、やはりスタックフレームに引数をコピーしてきている。
その後は、eax
に戻り地を入れているのですでに関数内の処理に入っている。
スタックポインタを減算しておかないと、pushが出来ない気がするのだが、ローカル変数が少ないので、スタック操作が不要ということなのだろうか。
というわけで、今度はスタックを使わせてみた。
x64では引数の数が多い場合などレジスタで渡せなくなると、スタックを使うようになるので、引数の多い関数を呼んでみた。
int func2(int a, int b, int c, int d, int e, int f, int g){
return 0;
}
int func(int a, int b){
int c = 0x00;
int d = 0x11;
int e = 0x22;
int f = 0x33;
int g = 0x44;
int h = 0x55;
int i = 0x66;
func2(a, b, c, d, e, f, g);
return 0;
}
salacia@Vega:~/tmp/gdb$ objdump -d -M intel func.o
func.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <func2>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 89 7d fc mov DWORD PTR [rbp-0x4],edi
7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi
a: 89 55 f4 mov DWORD PTR [rbp-0xc],edx
d: 89 4d f0 mov DWORD PTR [rbp-0x10],ecx
10: 44 89 45 ec mov DWORD PTR [rbp-0x14],r8d
14: 44 89 4d e8 mov DWORD PTR [rbp-0x18],r9d
18: b8 00 00 00 00 mov eax,0x0
1d: 5d pop rbp
1e: c3 ret
000000000000001f <func>:
1f: 55 push rbp
20: 48 89 e5 mov rbp,rsp
23: 48 83 ec 28 sub rsp,0x28
27: 89 7d dc mov DWORD PTR [rbp-0x24],edi
2a: 89 75 d8 mov DWORD PTR [rbp-0x28],esi
2d: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
34: c7 45 f8 11 00 00 00 mov DWORD PTR [rbp-0x8],0x11
3b: c7 45 f4 22 00 00 00 mov DWORD PTR [rbp-0xc],0x22
42: c7 45 f0 33 00 00 00 mov DWORD PTR [rbp-0x10],0x33
49: c7 45 ec 44 00 00 00 mov DWORD PTR [rbp-0x14],0x44
50: c7 45 e8 55 00 00 00 mov DWORD PTR [rbp-0x18],0x55
57: c7 45 e4 66 00 00 00 mov DWORD PTR [rbp-0x1c],0x66
5e: 44 8b 4d f0 mov r9d,DWORD PTR [rbp-0x10]
62: 44 8b 45 f4 mov r8d,DWORD PTR [rbp-0xc]
66: 8b 4d f8 mov ecx,DWORD PTR [rbp-0x8]
69: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
6c: 8b 75 d8 mov esi,DWORD PTR [rbp-0x28]
6f: 8b 45 dc mov eax,DWORD PTR [rbp-0x24]
72: 8b 7d ec mov edi,DWORD PTR [rbp-0x14]
75: 57 push rdi
76: 89 c7 mov edi,eax
78: e8 00 00 00 00 call 7d <func+0x5e>
7d: 48 83 c4 08 add rsp,0x8
81: b8 00 00 00 00 mov eax,0x0
86: c9 leave
87: c3 ret
関数内でスタック操作を行う場合は、スタックポインタの減算(0x23
の命令)を行うようだ。
関数によっては命令数が少なくなるので、x86よりも最適化されていると言えそう。
では、このスタックを使うようになった関数をブレークポイントとして設定するとどこで止まるのだろうか。
#include <stdio.h>
int func2(int a, int b, int c, int d, int e, int f, int g){
return 0;
}
int func(int a, int b){
int c = 0x00;
int d = 0x11;
int e = 0x22;
int f = 0x33;
int g = 0x44;
int h = 0x55;
int i = 0x66;
func2(a, b, c, d, e, f, g);
return 0;
}
int main(void){
int n;
n = func(1, 2);
printf("%d\n", n);
return 0;
}
salacia@Vega:~/tmp/gdb$ gdb a.out
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 a.out...done.
(gdb) b func
Breakpoint 1 at 0x400553: file test.c, line 8.
(gdb) r
Starting program: /home/salacia/tmp/gdb/a.out
Breakpoint 1, func (a=1, b=2) at test.c:8
8 int c = 0x00;
(gdb) p $rip
$1 = (void (*)()) 0x400553 <func+14>
0x400553
の前後を見る。
0000000000400545 <func>:
400545: 55 push rbp
400546: 48 89 e5 mov rbp,rsp
400549: 48 83 ec 28 sub rsp,0x28
40054d: 89 7d dc mov DWORD PTR [rbp-0x24],edi
400550: 89 75 d8 mov DWORD PTR [rbp-0x28],esi
400553: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
40055a: c7 45 f8 11 00 00 00 mov DWORD PTR [rbp-0x8],0x11
400561: c7 45 f4 22 00 00 00 mov DWORD PTR [rbp-0xc],0x22
400568: c7 45 f0 33 00 00 00 mov DWORD PTR [rbp-0x10],0x33
40056f: c7 45 ec 44 00 00 00 mov DWORD PTR [rbp-0x14],0x44
400576: c7 45 e8 55 00 00 00 mov DWORD PTR [rbp-0x18],0x55
40057d: c7 45 e4 66 00 00 00 mov DWORD PTR [rbp-0x1c],0x66
400584: 44 8b 4d f0 mov r9d,DWORD PTR [rbp-0x10]
400588: 44 8b 45 f4 mov r8d,DWORD PTR [rbp-0xc]
40058c: 8b 4d f8 mov ecx,DWORD PTR [rbp-0x8]
40058f: 8b 55 fc mov edx,DWORD PTR [rbp-0x4]
400592: 8b 75 d8 mov esi,DWORD PTR [rbp-0x28]
400595: 8b 45 dc mov eax,DWORD PTR [rbp-0x24]
400598: 8b 7d ec mov edi,DWORD PTR [rbp-0x14]
40059b: 57 push rdi
40059c: 89 c7 mov edi,eax
40059e: e8 83 ff ff ff call 400526 <func2>
4005a3: 48 83 c4 08 add rsp,0x8
4005a7: b8 00 00 00 00 mov eax,0x0
4005ac: c9 leave
4005ad: c3 ret
引数をスタックフレーム内にコピーしてくるところまでは実行して止まったようだ。
ここからはローカル変数に初期値の代入の処理が始まっている。
x64では、以下がスタックフレームの処理で、gdbで関数にブレークポイントを張った場合はここまで実行する
- ベースポインタの保存と、新しいベースポインタの設定
- ローカル変数の領域確保(やらない場合もある)
- 引数をスタックフレーム内にコピー
とても勉強になりました。
(おまけが本編だった)