4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GDBで関数にブレークポイントを設定すると、どこで止まるのか(x86)

Last updated at Posted at 2019-09-11

すごく基本的なことですが、今まで知らなかったのでメモ。

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 に、値を格納している。
これらの値を、スタックフレームにコピーしてきている( 40052a40052d の命令)。

ここまでは実行して、ブレークポイントに差し掛かる。
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で関数にブレークポイントを張った場合はここまで実行する

  • ベースポインタの保存と、新しいベースポインタの設定
  • ローカル変数の領域確保(やらない場合もある)
  • 引数をスタックフレーム内にコピー

とても勉強になりました。
(おまけが本編だった)

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?