kenkovlog

けんこふたんっオフィシャユブヨグッ
アンッ!アンッ!アンッ!アンッ!

スタックセグメントを確認してみる

簡単なコードをコンパイルしてgdb でメモリの中身(主にスタックセグメント)を見てみる。 これを行う上でのぼくらの心強い味方は

  • gdb
  • objdump
  • hexdump

などっ 使用したマシンのCPU は

model name      : Intel(R) Xeon(R) CPU

なので、 リトルエンディアン であることに注意する。

プログラム

main から関数を呼びだすプログラム。これでスタックセグメントを調査する。

#include <stdio.h>

void func(int a, int b, int c, int d) {
    int flag;
    char buffer[10];
    flag = 6;
    printf("%d\n", a + b + c + d);
}

int main(int argc, char *argv[]) {
    func(0, 1, 2, 3);
    return 0;
}

最適化オプションなしで -g オプション付きでコンパイルする

$ gcc -g -o test test.c

gdb で解析する

ひとまず、breakpoint をfunc 呼びだしの前と後に仕掛ける。

$ gdb -q test
Reading symbols from /home/kenkov/hack/test2...done.
(gdb) list main
5           char buffer[10];
6           flag = 6;
7           printf("%d\n", a + b + c + d);
8       }
9
10      int main(int argc, char *argv[]) {
11          func(0, 1, 2, 3);
12          return 0;
13      }
(gdb) break 11
Breakpoint 1 at 0x40054f: file test2.c, line 11.
(gdb) break func
Breakpoint 2 at 0x400514: file test2.c, line 6.

こうすることで、func 呼びだしの前と、func をcall して 関数プロローグ が終了した後で一時停止する。その状態でスタックを見てみる。

まずはfunc がcall される前の状態。

(gdb) run
Starting program: /home/kenkov/hack/test2
warning: Could not load shared library symbols for linux-vdso.so.1.
Do you need "set solib-search-path" or "set sysroot"?

Breakpoint 1, main (argc=1, argv=0x7fffffffe408) at test2.c:11
11          func(0, 1, 2, 3);
(gdb) i r  rsp rbp rip
rsp            0x7fffffffe2e0   0x7fffffffe2e0
rbp            0x7fffffffe2f0   0x7fffffffe2f0
rip            0x40054f 0x40054f <main+15>
(gdb) disassemble
Dump of assembler code for function main:
   0x0000000000400540 <+0>:     push   rbp
   0x0000000000400541 <+1>:     mov    rbp,rsp
   0x0000000000400544 <+4>:     sub    rsp,0x10
   0x0000000000400548 <+8>:     mov    DWORD PTR [rbp-0x4],edi
   0x000000000040054b <+11>:    mov    QWORD PTR [rbp-0x10],rsi
=> 0x000000000040054f <+15>:    mov    ecx,0x3
   0x0000000000400554 <+20>:    mov    edx,0x2
   0x0000000000400559 <+25>:    mov    esi,0x1
   0x000000000040055e <+30>:    mov    edi,0x0
   0x0000000000400563 <+35>:    call   0x400500 <func>
   0x0000000000400568 <+40>:    mov    eax,0x0
   0x000000000040056d <+45>:    leave
   0x000000000040056e <+46>:    ret
End of assembler dump.

この状態で次に実行される内容を見てみると、 => の行が次に実行される行で、

=> 0x000000000040054f <+15>:    mov    ecx,0x3
   0x0000000000400554 <+20>:    mov    edx,0x2
   0x0000000000400559 <+25>:    mov    esi,0x1
   0x000000000040055e <+30>:    mov    edi,0x0
   0x0000000000400563 <+35>:    call   0x400500 <func>

とあるように、引数を右から順番にレジスタに代入する。 これによりレジスタを介してfunc に引数を渡す。

引数の処理を終えたらfunc をcall する。

(gdb) cont
Continuing.

Breakpoint 2, func (a=0, b=1, c=2, d=3) at test2.c:6
6           flag = 6;
(gdb) i r  rsp rbp rip
rsp            0x7fffffffe2b0   0x7fffffffe2b0
rbp            0x7fffffffe2d0   0x7fffffffe2d0
rip            0x400514 0x400514 <func+20>
gdb) disassemble
Dump of assembler code for function func:
   0x0000000000400500 <+0>:     push   rbp
   0x0000000000400501 <+1>:     mov    rbp,rsp
   0x0000000000400504 <+4>:     sub    rsp,0x20
   0x0000000000400508 <+8>:     mov    DWORD PTR [rbp-0x14],edi
   0x000000000040050b <+11>:    mov    DWORD PTR [rbp-0x18],esi
   0x000000000040050e <+14>:    mov    DWORD PTR [rbp-0x1c],edx
   0x0000000000400511 <+17>:    mov    DWORD PTR [rbp-0x20],ecx
=> 0x0000000000400514 <+20>:    mov    DWORD PTR [rbp-0x4],0x6
   0x000000000040051b <+27>:    mov    eax,DWORD PTR [rbp-0x18]
   0x000000000040051e <+30>:    mov    edx,DWORD PTR [rbp-0x14]
   0x0000000000400521 <+33>:    add    edx,eax
   0x0000000000400523 <+35>:    mov    eax,DWORD PTR [rbp-0x1c]
   0x0000000000400526 <+38>:    add    edx,eax
   0x0000000000400528 <+40>:    mov    eax,DWORD PTR [rbp-0x20]
   0x000000000040052b <+43>:    add    eax,edx
   0x000000000040052d <+45>:    mov    esi,eax
   0x000000000040052f <+47>:    mov    edi,0x4005f4
   0x0000000000400534 <+52>:    mov    eax,0x0
   0x0000000000400539 <+57>:    call   0x4003e0 <printf@plt>
   0x000000000040053e <+62>:    leave
   0x000000000040053f <+63>:    ret
End of assembler dump.

するとこのようになる。まずは関数プロローグを見てみると、

0x0000000000400500 <+0>:     push   rbp
0x0000000000400501 <+1>:     mov    rbp,rsp
0x0000000000400504 <+4>:     sub    rsp,0x20
0x0000000000400508 <+8>:     mov    DWORD PTR [rbp-0x14],edi
0x000000000040050b <+11>:    mov    DWORD PTR [rbp-0x18],esi
0x000000000040050e <+14>:    mov    DWORD PTR [rbp-0x1c],edx
0x0000000000400511 <+17>:    mov    DWORD PTR [rbp-0x20],ecx

となっている。まずは push によってフレームポインタ( rbp )をスタックにプッシュする。 次に現在のフレームポインタを現在のスタックの一番上(つまり rsp )に設定する。 最後にスタックに引数やローカル変数が入るように rsp を調節している。 そして次に続く4 つの mov で、その空いたスペースに引数を代入している。

図でかくと、こんなかんじになっている。

rsp = [rbp-0x20] -> 0x3 (引数 d=3)
      [rbp-0x1c] -> 0x2 (引数 c=2)
      [rbp-0x18] -> 0x1 (引数 b=1)
      [rbp-0x14] -> 0x0 (引数 a=0)
rbp = 0x7fffffffe2d0 -> 0x00007fffffffe2f0 (退避されたフレームポインタ)

さて、実際にこうなっていることをメモリを中をのぞいて確かめてみる。

(gdb) x/12xw 0x7fffffffe2b0
0x7fffffffe2b0: 0x00000003      0x00000002      0x00000001      0x00000000
0x7fffffffe2c0: 0xffffe2fe      0x00007fff      0x00000000      0x00000000
0x7fffffffe2d0: 0xffffe2f0      0x00007fff      0x00400568      0x00000000

となっている。実際になっていることが分かると思う。上の最後の行に注目すると

0x7fffffffe2d0: 0xffffe2f0      0x00007fff      0x00400568      0x00000000

となっていて、 0x7fffffffe2d0 にきちんと 退避されたフレームポインタ が 配置されていることがわかる。リトルエンディアンなので、64bit アドレスは逆順に格納されている:

0xffffe2f0      0x00007fff

ちなみに、上のアドレスを見ると気付くかもしれないが、 戻りアドレス が退避されたフレームポインタの次 に配置されている。 main 関数のcall 付近をもう一度示すと

0x000000000040055e <+30>:    mov    edi,0x0
0x0000000000400563 <+35>:    call   0x400500 <func>
0x0000000000400568 <+40>:    mov    eax,0x0

だが、 戻りアドレスの 0x0000000000400568 が退避されたフレームポインタの後ろについていることがわかる。 したがって先程の図を書き直すと

rsp = [rbp-0x20] -> 0x3 (引数 d=3)
      [rbp-0x1c] -> 0x2 (引数 c=2)
      [rbp-0x18] -> 0x1 (引数 b=1)
      [rbp-0x14] -> 0x0 (引数 a=0)
rbp = 0x7fffffffe2d0 -> 0x00007fffffffe2f0 (退避されたフレームポインタ)
      [rbp+0x8] -> 0x0000000000400568 (戻りアドレス)

となる。このことから、関数をcall する時は

  • まず戻りアドレスをスタックにプッシュ
  • 次にフレームポインタを退避
  • そして引数やローカル変数をプッシュ

という順番で行われていることがわかる。

さて、最後にfunc のローカル変数がどのように格納されるか見てみる。 そのために、変数に値 6 を代入した後にbreakpoint をしかける。

(gdb) break 7
Breakpoint 3 at 0x40051b: file test2.c, line 7.
(gdb) cont
Continuing.

Breakpoint 3, func (a=0, b=1, c=2, d=3) at test2.c:7
7           printf("%d\n", a + b + c + d);
(gdb) disassemble
Dump of assembler code for function func:
   0x0000000000400500 <+0>:     push   rbp
   0x0000000000400501 <+1>:     mov    rbp,rsp
   0x0000000000400504 <+4>:     sub    rsp,0x20
   0x0000000000400508 <+8>:     mov    DWORD PTR [rbp-0x14],edi
   0x000000000040050b <+11>:    mov    DWORD PTR [rbp-0x18],esi
   0x000000000040050e <+14>:    mov    DWORD PTR [rbp-0x1c],edx
   0x0000000000400511 <+17>:    mov    DWORD PTR [rbp-0x20],ecx
   0x0000000000400514 <+20>:    mov    DWORD PTR [rbp-0x4],0x6
=> 0x000000000040051b <+27>:    mov    eax,DWORD PTR [rbp-0x18]
   0x000000000040051e <+30>:    mov    edx,DWORD PTR [rbp-0x14]
   0x0000000000400521 <+33>:    add    edx,eax
   0x0000000000400523 <+35>:    mov    eax,DWORD PTR [rbp-0x1c]
   0x0000000000400526 <+38>:    add    edx,eax
   0x0000000000400528 <+40>:    mov    eax,DWORD PTR [rbp-0x20]
   0x000000000040052b <+43>:    add    eax,edx
   0x000000000040052d <+45>:    mov    esi,eax
   0x000000000040052f <+47>:    mov    edi,0x4005f4
   0x0000000000400534 <+52>:    mov    eax,0x0
   0x0000000000400539 <+57>:    call   0x4003e0 <printf@plt>
   0x000000000040053e <+62>:    leave
   0x000000000040053f <+63>:    ret
End of assembler dump.

アセンブリ言語から分かるように、 0x06

0x0000000000400514 <+20>:    mov    DWORD PTR [rbp-0x4],0x6

とあるように rbp-0x4 に格納される。 int 型なので4bit rbp から減算していることがわかる。 さて、実際に確認してみると

(gdb) x/12xw 0x7fffffffe2b0
0x7fffffffe2b0: 0x00000003      0x00000002      0x00000001      0x00000000
0x7fffffffe2c0: 0xffffe2fe      0x00007fff      0x00000000      0x00000006
0x7fffffffe2d0: 0xffffe2f0      0x00007fff      0x00400568      0x00000000

となっているため、 rbp-0x4 に配置されている。図でかけば

rsp = [rbp-0x20] -> 0x3 (引数 d=3)
      [rbp-0x1c] -> 0x2 (引数 c=2)
      [rbp-0x18] -> 0x1 (引数 b=1)
      [rbp-0x14] -> 0x0 (引数 a=0)
      [rbp-0x4] -> 0x6 (ローカル変数flag=6)
rbp = 0x7fffffffe2d0 -> 0x00007fffffffe2f0 (退避されたフレームポインタ)
      [rbp+0x8] -> 0x0000000000400568 (戻りアドレス)

ひとまず

スタックセグメントについてはこんなところっ次はバッファオーバーフローの勉強をするっ

けんこふたん