參考資料:(感謝liigo和eno_rez兩位作者)
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 = 0×12;
- y = 0×34;
- 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 <add>:
8048354: 55 push %ebp ;保存調用者的幀指針
8048355: 89 e5 mov %esp,%ebp ;把當前的棧指針作為本函數的幀指針
8048357: 83 ec 10 sub $0×10,%esp ;調整棧指針,為局部變量保留空間
804835a: c7 45 fc 00 00 00 00 movl $0×0,0xfffffffc(%ebp) ;把a置0。ebp-4的位置是第一個局部變量
8048361: 8b 45 08 mov 0×8(%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 <main>:
8048372: 8d 4c 24 04 lea 0×4(%esp),%ecx ;????
8048376: 83 e4 f0 and $0xfffffff0,%esp ;把棧指針16字節(jié)對齊
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 $0×18,%esp ;調整棧指針,為局部變量保留空間
8048383: c7 45 f0 12 00 00 00 movl $0×12,0xfffffff0(%ebp) ;x=0×12。ebp-16是局部變量x
804838a: c7 45 f4 34 00 00 00 movl $0×34,0xfffffff4(%ebp) ;y=0×34。ebp-12是局部變量y
8048391: 8b 45 f4 mov 0xfffffff4(%ebp),%eax ;y保存到eax
8048394: 89 44 24 04 mov %eax,0×4(%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> ;調用add
80483a3: 89 45 f8 mov %eax,0xfffffff8(%ebp) ;把保存在eax的add的返回值,賦值給位于ebp-8的第三個局部變量result。注意這條指令的地址,就是add的返回地址
80483a6: b8 00 00 00 00 mov $0×0,%eax ;0作為main的返回值,保存到eax
80483ab: 83 c4 18 add $0×18,%esp ;恢復棧指針,也就是討論stdcall和cdecl的時候總要提到的“調用者清?!?br> 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 $0×18,%esp
不但象通常函數都要做的那樣給局部變量預留了空間,還順便把調用add的兩個參數的空間也預留出來了。
然后把參數壓棧的時候,用的是mov指令。
我不太明白這種方法有什么好處。
另外一個不明白的就是main入口的四條指令8048372、8048376、8048379、804837f,還有與之對應的main返回之前的指令。
貌似main對esp要求16字節(jié)對齊,所以先把原來的esp壓棧,然后強行把esp的低4位清0。等到返回之前再從棧里恢復原來的esp
對gdb不太熟悉的同學要注意一點,stepi命令執(zhí)行之后顯示出來的源代碼行或者指令地址,都是即將執(zhí)行的指令,而不是剛剛執(zhí)行完的指令。
我在每個stepi后面都加了注釋,就是剛執(zhí)行過的指令。
[test]$ gdb -q stack
(gdb) break main
Breakpoint 1 at 0×8048383: file stack.c, line 11.
gdb并沒有把斷點設置在main的第一條指令,而是設置在了調整棧指針為局部變量保留空間之后
(gdb) run
Starting program: /home/brookmill/test/stack
Breakpoint 1, main () at stack.c:11
11 x = 0×12;
(gdb) stepi // 注釋: movl $0×12,0xfffffff0(%ebp)
12 y = 0×34;
(gdb) stepi // 注釋: movl $0×34,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: 0×002daff4 0×002d9220 0xbf8df8d8 0×080483e9
0xbf8df8b0: 0×001ca8d5 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
這就是傳說中的棧。在main準備調用add之前,先看看這里有些什么東東
0xbf8df8c8(ebp)保存的是上一層函數的幀指針:0xbf8df938,距離這里有112字節(jié)
0xbf8df8cc(ebp+4)保存的是main的返回地址0×001b4dec
0xbf8df8b8(ebp-16)是局部變量x,已經賦值0×12;
0xbf8df8bc(ebp-12)是局部變量y,已經賦值0×34;
0xbf8df8c0(ebp-8)是局部變量result。值得注意的是,因為我們沒有給result賦值,這里是一個不確定的值。局部變量如果不顯式的初始化,初始值不一定是0。
現在開始調用add
(gdb) stepi // 注釋: mov 0xfffffff4(%ebp),%eax
0×08048394 13 result = add(x, y);
(gdb) stepi // 注釋: mov %eax,0×4(%esp)
0×08048398 13 result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0×002daff4 0×002d9220 0xbf8df8d8 0×080483e9
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
y首先被壓棧,在0xbf8df8b0
(gdb) stepi // 注釋: mov 0xfffffff0(%ebp),%eax (gdb) stepi // 注釋: call 8048354 <add>
0×0804839b 13 result = add(x, y);
(gdb) stepi // 注釋: mov %eax,(%esp)
0×0804839e 13 result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0×002daff4 0×002d9220 0xbf8df8d8 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
x第二個進棧,在0xbf8df8ac
add (x=18, y=52) at stack.c:2
2 {
(gdb) info registers esp
esp 0xbf8df8a8 0xbf8df8a8
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0×002daff4 0×002d9220 0×080483a3 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
現在esp指向0xbf8df8a8,這里保存的是add函數的返回地址,它是由call指令壓棧的。
(gdb) stepi // 注釋: push %ebp
0×08048355 2 {
(gdb) stepi // 注釋: mov %esp,%ebp
0×08048357 2 {
(gdb) stepi // 注釋: sub $0×10,%esp
3 int a = 0;
(gdb) info registers esp
esp 0xbf8df894 0xbf8df894
(gdb) info registers ebp
ebp 0xbf8df8a4 0xbf8df8a4
(gdb) x/16 0xbf8df890
0xbf8df890: 0×00000000 0×08049574 0xbf8df8a8 0×08048245
0xbf8df8a0: 0×002daff4 0xbf8df8c8 0×080483a3 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
剛剛執(zhí)行完的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 $0×0,0xfffffffc(%ebp)
4 a = x;
(gdb) x/16 0xbf8df890
0xbf8df890: 0×00000000 0×08049574 0xbf8df8a8 0×08048245
0xbf8df8a0: 0×00000000 0xbf8df8c8 0×080483a3 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
可以看到a被置0了
(gdb) stepi // 注釋: mov 0×8(%ebp),%eax
0×08048364 4 a = x;
(gdb) stepi // 注釋: mov %eax,0xfffffffc(%ebp)
5 a += y;
(gdb) x/16 0xbf8df890
0xbf8df890: 0×00000000 0×08049574 0xbf8df8a8 0×08048245
0xbf8df8a0: 0×00000012 0xbf8df8c8 0×080483a3 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
參數x(ebp+8)的值通過eax賦值給了局部變量a(ebp-4)
(gdb) stepi // 注釋: mov 0xc(%ebp),%eax
0×0804836a 5 a += y;
(gdb) stepi // 注釋: add %eax,0xfffffffc(%ebp)
6 return a;
(gdb) x/16 0xbf8df890
0xbf8df890: 0×00000000 0×08049574 0xbf8df8a8 0×08048245
0xbf8df8a0: 0×00000046 0xbf8df8c8 0×080483a3 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
參數y(ebp+C)的值通過eax加到了局部變量a(ebp-4)
現在要從add返回了。返回之前把局部變量a(ebp-4)保存到eax用作返回值
(gdb) stepi // 注釋: mov 0xfffffffc(%ebp),%eax
7 }
(gdb) stepi // 注釋: leave
0×08048371 in add (x=1686688, y=134513616) at stack.c:7
7 }
(gdb) stepi // 注釋: ret
0×080483a3 in main () at stack.c:13
13 result = add(x, y);
現在我們回到了main,?,F在是這樣的
(gdb) info registers esp
esp 0xbf8df8ac 0xbf8df8ac
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/16 0xbf8df890
0xbf8df890: 0×00000000 0×08049574 0xbf8df8a8 0×08048245
0xbf8df8a0: 0×00000046 0xbf8df8c8 0×080483a3 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×001903d0 0xbf8df8e0 0xbf8df938 0×001b4dec
可以看到,esp和ebp都已經恢復到了調用add之前的值。
但是,調用add的兩個參數還在棧里(0xbf8df8ac、0xbf8df8b0,都在esp以上)。
也就是說,被調用的函數add沒有把它們從棧上清出去,需要調用方main來清理。這就是著名的“調用者清棧”,cdecl調用方式的特點之一。
(gdb) stepi // 注釋: mov %eax,0xfffffff8(%ebp)
14 return 0;
(gdb) x/16 0xbf8df890
0xbf8df890: 0×00000000 0×08049574 0xbf8df8a8 0×08048245
0xbf8df8a0: 0×00000046 0xbf8df8c8 0×080483a3 0×00000012
0xbf8df8b0: 0×00000034 0xbf8df96c 0×00000012 0×00000034
0xbf8df8c0: 0×00000046 0xbf8df8e0 0xbf8df938 0×001b4dec
從eax得到函數add的返回值,賦值給了局部變量result(ebp-8)
(gdb) stepi // 注釋: mov $0×0,%eax ;把eax置0作為main的返回值
15 }
(gdb) stepi // 注釋: add $0×18,%esp ; 調用者清棧
0×080483ae 15 }
(gdb) continue
Continuing.
Program exited normally.
(gdb) quit
[test]$