GDB查看堆棧局部變量
“參數從右到左入棧”,“局部變量在棧上分配空間”,聽的耳朵都起繭子了。最近做項目涉及C和匯編互相調用,寫代碼的時候才發現沒真正弄明白。自己寫了個最簡單的函數,用gdb跟蹤了調用過程,才多少懂了一點。
參考資料:
http://blog.csdn.net/liigo/archive/2006/12/23/1456938.aspx
http://blog.csdn.net/eno_rez/archive/2008/03/08/2158682.aspx
int add(int x, int y)
{
int a = 0;
a = x;
a += y;
return a;
}
int main(int argc, char *argv[])
{
int x, y, result;
x = 0x12;
y = 0x34;
result = add(x, y);
return 0;
}
編譯:(Fedora6, gcc 4.1.2)
[test]$ gcc -g -Wall -o stack stack.c
反匯編:
這里的匯編的格式是AT&T匯編,它的格式和我們熟悉的匯編格式不太一樣,尤其要注意源操作數和目的操作數的順序是反過來的
[test]$ objdump -d stack > stack.dump
[test]$ cat stack.dump
......
08048354 :
8048354: 55 push %ebp ;保存調用者的幀指針
8048355: 89 e5 mov %esp,%ebp ;把當前的棧指針作為本函數的幀指針
8048357: 83 ec 10 sub $0x10,%esp ;調整棧指針,為局部變量保留空間
804835a: c7 45 fc 00 00 00 00 movl $0x0,0xfffffffc(%ebp) ;把a置0。ebp-4的位置是第一個局部變量
8048361: 8b 45 08 mov 0x8(%ebp),%eax ;把參數x保存到eax。ebp+8的位置是最后一個入棧的參數,也就是第一個參數
8048364: 89 45 fc mov %eax,0xfffffffc(%ebp) ;把eax賦值給變量a
8048367: 8b 45 0c mov 0xc(%ebp),%eax ;把參數y保存到eax。ebp+C的位置是倒數第二個入棧的參數,也就是第二個參數
804836a: 01 45 fc add %eax,0xfffffffc(%ebp) ;a+=y
804836d: 8b 45 fc mov 0xfffffffc(%ebp),%eax ;把a的值作為返回值,保存到eax
8048370: c9 leave
8048371: c3 ret
08048372 :
8048372: 8d 4c 24 04 lea 0x4(%esp),%ecx ;????
8048376: 83 e4 f0 and $0xfffffff0,%esp ;把棧指針16字節對齊
8048379: ff 71 fc pushl 0xfffffffc(%ecx) ;????
804837c: 55 push %ebp ;保存調用者的幀指針
804837d: 89 e5 mov %esp,%ebp ;把當前的棧指針作為本函數的幀指針
804837f: 51 push %ecx ;????
8048380: 83 ec 18 sub $0x18,%esp ;調整棧指針,為局部變量保留空間
8048383: c7 45 f0 12 00 00 00 movl $0x12,0xfffffff0(%ebp) ;x=0x12。ebp-16是局部變量x
804838a: c7 45 f4 34 00 00 00 movl $0x34,0xfffffff4(%ebp) ;y=0x34。ebp-12是局部變量y
8048391: 8b 45 f4 mov 0xfffffff4(%ebp),%eax ;y保存到eax
8048394: 89 44 24 04 mov %eax,0x4(%esp) ;y作為最右邊的參數首先入棧
8048398: 8b 45 f0 mov 0xfffffff0(%ebp),%eax ;x保存到eax
804839b: 89 04 24 mov %eax,(%esp) ;x第二個入棧
804839e: e8 b1 ff ff ff call 8048354 ;調用add
80483a3: 89 45 f8 mov %eax,0xfffffff8(%ebp) ;把保存在eax的add的返回值,賦值給位於ebp-8的第三個局部變量result。注意這條指令的地址,就是add的返回地址
80483a6: b8 00 00 00 00 mov $0x0,%eax ;0作為main的返回值,保存到eax
80483ab: 83 c4 18 add $0x18,%esp ;恢復棧指針,也就是討論stdcall和cdecl的時候總要提到的“調用者清棧”
80483ae: 59 pop %ecx ;
80483af: 5d pop %ebp ;
80483b0: 8d 61 fc lea 0xfffffffc(%ecx),%esp ;
80483b3: c3 ret
80483b4: 90 nop
......
有一點值得注意的是main在調用add之前把參數壓棧的過程。
它用的不是push指令,而是另一種方法。
在main入口調整棧指針的時候,也就是位於8048380的這條指令 sub $0x18,%esp
不但象通常函數都要做的那樣給局部變量預留了空間,還順便把調用add的兩個參數的空間也預留出來了。
然后把參數壓棧的時候,用的是mov指令。
我不太明白這種方法有什么好處。
另外一個不明白的就是main入口的四條指令8048372、8048376、8048379、804837f,還有與之對應的main返回之前的指令。
貌似main對esp要求16字節對齊,所以先把原來的esp壓棧,然后強行把esp的低4位清0。等到返回之前再從棧里恢復原來的esp
准備工作都做好了,現在開始gdb
對gdb不太熟悉的同學要注意一點,stepi命令執行之后顯示出來的源代碼行或者指令地址,都是即將執行的指令,而不是剛剛執行完的指令。
我在每個stepi后面都加了注釋,就是剛執行過的指令。
[test]$ gdb -q stack
(gdb) break main
Breakpoint 1 at 0x8048383: file stack.c, line 11.
gdb並沒有把斷點設置在main的第一條指令,而是設置在了調整棧指針為局部變量保留空間之后
(gdb) run
Starting program: /home/brookmill/test/stack
Breakpoint 1, main () at stack.c:11
11 x = 0x12;
(gdb) stepi // 注釋: movl $0x12,0xfffffff0(%ebp)
12 y = 0x34;
(gdb) stepi // 注釋: movl $0x34,0xfffffff4(%ebp)
13 result = add(x, y);
(gdb) info registers esp
esp 0xbf8df8ac 0xbf8df8ac
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0xbf8df8d8 0x080483e9
0xbf8df8b0: 0x001ca8d5 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
這就是傳說中的棧。在main准備調用add之前,先看看這里有些什么東東
0xbf8df8c8(ebp)保存的是上一層函數的幀指針:0xbf8df938,距離這里有112字節
0xbf8df8cc(ebp+4)保存的是main的返回地址0x001b4dec
0xbf8df8b8(ebp-16)是局部變量x,已經賦值0x12;
0xbf8df8bc(ebp-12)是局部變量y,已經賦值0x34;
0xbf8df8c0(ebp-8)是局部變量result。值得注意的是,因為我們沒有給result賦值,這里是一個不確定的值。局部變量如果不顯式的初始化,初始值不一定是0。
現在開始調用add
(gdb) stepi // 注釋: mov 0xfffffff4(%ebp),%eax
0x08048394 13 result = add(x, y);
(gdb) stepi // 注釋: mov %eax,0x4(%esp)
0x08048398 13 result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0xbf8df8d8 0x080483e9
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
y首先被壓棧,在0xbf8df8b0
(gdb) stepi // 注釋: mov 0xfffffff0(%ebp),%eax
0x0804839b 13 result = add(x, y);
(gdb) stepi // 注釋: mov %eax,(%esp)
0x0804839e 13 result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0xbf8df8d8 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
x第二個進棧,在0xbf8df8ac
(gdb) stepi // 注釋: call 8048354
add (x=18, y=52) at stack.c:2
2 {
剛剛執行了call指令,現在我們進入了add函數
(gdb) info registers esp
esp 0xbf8df8a8 0xbf8df8a8
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
現在esp指向0xbf8df8a8,這里保存的是add函數的返回地址,它是由call指令壓棧的。
(gdb) stepi // 注釋: push %ebp
0x08048355 2 {
(gdb) stepi // 注釋: mov %esp,%ebp
0x08048357 2 {
(gdb) stepi // 注釋: sub $0x10,%esp
3 int a = 0;
(gdb) info registers esp
esp 0xbf8df894 0xbf8df894
(gdb) info registers ebp
ebp 0xbf8df8a4 0xbf8df8a4
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x002daff4 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
剛剛執行完的3條指令是函數入口的定式。
現在我們可以看到,main的棧還是原樣,向下增長之后就是add的棧。
0xbf8df8a4(ebp)保存的是上層函數main的幀指針
0xbf8df8a8(ebp+4)保存的是返回地址
0xbf8df8ac(ebp+8)保存的是最后一個入棧的參數x
0xbf8df8b0(ebp+C)保存的是倒數第二個入棧的參數y
0xbf8df8a0(ebp-4)保存的是局部變量a,現在是一個不確定值
接下來add函數就真正開始干活了
(gdb) stepi // 注釋: movl $0x0,0xfffffffc(%ebp)
4 a = x;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000000 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
可以看到a被置0了
(gdb) stepi // 注釋: mov 0x8(%ebp),%eax
0x08048364 4 a = x;
(gdb) stepi // 注釋: mov %eax,0xfffffffc(%ebp)
5 a += y;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000012 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
參數x(ebp+8)的值通過eax賦值給了局部變量a(ebp-4)
(gdb) stepi // 注釋: mov 0xc(%ebp),%eax
0x0804836a 5 a += y;
(gdb) stepi // 注釋: add %eax,0xfffffffc(%ebp)
6 return a;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000046 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
參數y(ebp+C)的值通過eax加到了局部變量a(ebp-4)
現在要從add返回了。返回之前把局部變量a(ebp-4)保存到eax用作返回值
(gdb) stepi // 注釋: mov 0xfffffffc(%ebp),%eax
7 }
(gdb) stepi // 注釋: leave
0x08048371 in add (x=1686688, y=134513616) at stack.c:7
7 }
(gdb) stepi // 注釋: ret
0x080483a3 in main () at stack.c:13
13 result = add(x, y);
現在我們回到了main,棧現在是這樣的
(gdb) info registers esp
esp 0xbf8df8ac 0xbf8df8ac
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000046 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
可以看到,esp和ebp都已經恢復到了調用add之前的值。
但是,調用add的兩個參數還在棧里(0xbf8df8ac、0xbf8df8b0,都在esp以上)。
也就是說,被調用的函數add沒有把它們從棧上清出去,需要調用方main來清理。這就是著名的“調用者清棧”,cdecl調用方式的特點之一。
(gdb) stepi // 注釋: mov %eax,0xfffffff8(%ebp)
14 return 0;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000046 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x00000046 0xbf8df8e0 0xbf8df938 0x001b4dec
從eax得到函數add的返回值,賦值給了局部變量result(ebp-8)
(gdb) stepi // 注釋: mov $0x0,%eax ;把eax置0作為main的返回值
15 }
(gdb) stepi // 注釋: add $0x18,%esp ; 調用者清棧
0x080483ae 15 }
(gdb) continue
Continuing.
Program exited normally.
(gdb) quit
[test]$