先看一個最簡單的程序:
點擊(此處)折疊或打開
- /*test.c*/
- #include <stdio.h>
- int foo1(int m,int n,int p)
- {
- int x = m + n + p;
- return x;
- }
- int main(int argc,char** argv)
- {
- int x,y,z,result;
- x=11;
- y=22;
- z=33;
- result = foo1(x,y,z);
- printf("result=%d\n",result);
- return 0;
- }
點擊(此處)折疊或打開
- .file "test.c"
- .text
- .globl foo1
- .type foo1, @function
- foo1:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movl 12(%ebp), %eax
- movl 8(%ebp), %edx
- leal (%edx,%eax), %eax
- addl 16(%ebp), %eax
- movl %eax, -4(%ebp)
- movl -4(%ebp), %eax
- leave
- ret
- .size foo1, .-foo1
- .section .rodata
- .LC0:
- .string "result=%d\n"
- .text
- .globl main
- .type main, @function
- main:
- pushl %ebp
- movl %esp, %ebp
- andl $-16, %esp
- subl $32, %esp
- movl $11, 16(%esp)
- movl $22, 20(%esp)
- movl $33, 24(%esp)
- movl 24(%esp), %eax
- movl %eax, 8(%esp)
- movl 20(%esp), %eax
- movl %eax, 4(%esp)
- movl 16(%esp), %eax
- movl %eax, (%esp)
- call foo1
- movl %eax, 28(%esp)
- movl $.LC0, %eax
- movl 28(%esp), %edx
- movl %edx, 4(%esp)
- movl %eax, (%esp)
- call printf
- movl $0, %eax
- leave
- ret
- .size main, .-main
- .ident "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
- .section .note.GNU-stack,"",@progbits
[root@maple 1]# gcc -g -o test test.s [root@maple 1]# objdump -D test >testbin [root@maple 1]# vi testbin //… 省略部分不相關代碼 80483c0: ff d0 call *%eax 80483c2: c9 leave 80483c3: c3 ret
080483c4 : 80483c4: 55 push %ebp 80483c5: 89 e5 mov %esp,%ebp 80483c7: 83 ec 10 sub $0x10,%esp 80483ca: 8b 45 0c mov 0xc(%ebp),%eax 80483cd: 8b 55 08 mov 0x8(%ebp),%edx 80483d0: 8d 04 02 lea (%edx,%eax,1),%eax 80483d3: 03 45 10 add 0x10(%ebp),%eax 80483d6: 89 45 fc mov %eax,-0x4(%ebp) 80483d9: 8b 45 fc mov -0x4(%ebp),%eax 80483dc: c9 leave 80483dd: c3 ret
080483de :80483de: 55 push %ebp 80483df: 89 e5 mov %esp,%ebp 80483e1: 83 e4 f0 and $0xfffffff0,%esp 80483e4: 83 ec 20 sub $0x20,%esp 80483e7: c7 44 24 10 0b 00 00 movl $0xb,0x10(%esp) 80483ee: 00 80483ef: c7 44 24 14 16 00 00 movl $0x16,0x14(%esp) 80483f6: 00 80483f7: c7 44 24 18 21 00 00 movl $0x21,0x18(%esp) 80483fe: 00 80483ff: 8b 44 24 18 mov 0x18(%esp),%eax 8048403: 89 44 24 08 mov %eax,0x8(%esp) 8048407: 8b 44 24 14 mov 0x14(%esp),%eax 804840b: 89 44 24 04 mov %eax,0x4(%esp) 804840f: 8b 44 24 10 mov 0x10(%esp),%eax 8048413: 89 04 24 mov %eax,(%esp) 8048416: e8 a9 ff ff ff call 80483c4 804841b: 89 44 24 1c mov %eax,0x1c(%esp) 804841f: b8 04 85 04 08 mov $0x8048504,%eax 8048424: 8b 54 24 1c mov 0x1c(%esp),%edx 8048428: 89 54 24 04 mov %edx,0x4(%esp) 804842c: 89 04 24 mov %eax,(%esp) 804842f: e8 c0 fe ff ff call 80482f4 8048434: b8 00 00 00 00 mov $0x0,%eax 8048439: c9 leave 804843a: c3 ret 804843b: 90 nop 804843c: 90 nop //… 省略部分不相關代碼 |
用GDB調試可執行程序test:

在main函數第一條指令執行前我們看一下進程test的棧空間布局。因為我們最終的可執行程序是通過glibc庫啟動的,在main的第一條指令運行前,其實還有很多故事的,這里就不展開了,以后有時間再細究,這里只要記住一點:main函數執行前,其進程空間的棧里已經有了相當多的數據。我的系統里此時棧頂指針esp的值是0xbffff63c,棧基址指針ebp的值0xbffff6b8,指令寄存器eip的值是0x80483de正好是下一條馬上即將執行的指令,即main函數內的第一條指令“push %ebp”。那么此時,test進程的棧空間布局大致如下:

點擊(此處)折疊或打開
- 25 pushl %ebp //將原來ebp的值0xbffff6b8如棧,esp自動增長4字節
- 26 movl %esp, %ebp //用ebp保存當前時刻esp的值
- 27 andl $-16, %esp //內存地址對其,可以忽略不計




然后main函數里的變量x,y,z的值放到棧上,就是接下來的三條指令:
點擊(此處)折疊或打開
- 29 movl $11, 16(%esp)
- 30 movl $22, 20(%esp)
- 31 movl $33, 24(%esp)
這是三條寄存器間接尋址指令,將立即數11,22,33分別放到esp寄存器所指向的地址0xbffff610向高位分別偏移16、20、24個字節處的內存單元里,最后結果如下:

注意:這三條指令並沒有改變esp寄存器的值。
接下來main函數里就要為了調用foo1函數而做准備了。由於mov指令的兩個操作數不能都是內存地址,所以要將x,y和z的值傳遞給foo1函數,則必須借助通用寄存器來完成,這里我們看到eax承擔了這樣的任務:
點擊(此處)折疊或打開
- 32 movl 24(%esp), %eax
- 33 movl %eax, 8(%esp)
- 34 movl 20(%esp), %eax
- 35 movl %eax, 4(%esp)
- 36 movl 16(%esp), %eax
- 37 movl %eax, (%esp)

當foo1函數所需要的所有輸入參數都已經按正確的順序入棧后,緊接着就需要調用call指令來執行foo1函數的代碼了。前面的博文說過,call指令執行時分兩步:首先會將call指令的下一條指令(movl %eax,28(%esp))的地址(0x0804841b)壓入棧,然后跳轉到函數foo1入口處開始執行。當第38條指令“call foo1”執行完后,棧空間布局如下:

點擊(此處)折疊或打開
- 3 .globl foo1
- 4 .type foo1, @function
- 5 foo1:
- 6 pushl %ebp
- 7 movl %esp, %ebp
- 8 subl $16, %esp
- 9 movl 12(%ebp), %eax
- 10 movl 8(%ebp), %edx
- 11 leal (%edx,%eax), %eax
- 12 addl 16(%ebp), %eax
- 13 movl %eax, -4(%ebp)
- 14 movl -4(%ebp), %eax
- 15 leave
- 16 ret
- 17 .size foo1, .-foo1



因為我們foo1()函數的C代碼中,最終計算結果是保存到foo1()里的局部變量x里,最后用return語句將x的值通過eax寄存器返回到mian函數里,所以我們看到接下來的第13、14條指令有些“多此一舉”。這足以說明gcc人家還是相當嚴謹的,C源代碼的函數里如果有給局部變量賦值的語句,生成匯編代碼時確實會在棧上為本地變量預留的空間里的正確位置為其賦值。當然gcc還有不同級別的優化技術來提高程序的執行效率,這個不屬於本文所討論的東西。讓我們繼續,當第13、14條指令執行完后,棧布局如下:


我們發現,雖然棧頂從0xbffff5f8移動到0xbffff60c了,但棧上的數據依然存在。也就是說,此時你通過esp-8依舊可以訪問foo1函數里的局部變量x的值。當然,這也是說得通的,因為函數此時還沒有返回。我們看棧布局可以知道當前的棧頂0xbffff60c處存放的是下一條即將執行的指令的地址,對照反匯編結果可以看到這正是main函數里的第18條指令(在整個匯編源文件test.s里的行號是39)“movl %eax, 28(%esp)”。leave指令其實完成了兩個任務:
1、將棧上為函數預留的空間“收回”;
2、恢復ebp;
也就是說leave指令等價於下面兩條指令,你將leave替換成它們編譯運行,結果還是對的:
點擊(此處)折疊或打開
- movl %ebp,%esp
- popl %ebp
前面我們也說過,ret指令會自動到棧上去pop數據,相當於執行了“popl %eip”,會使esp增大4字節。所以當執行完第16條指令ret后,esp從0xbffff60c增長到0xbffff610處,棧空間結構如下:


點擊(此處)折疊或打開
- 40 movl $.LC0, %eax
- 41 movl 28(%esp), %edx
- 42 movl %edx, 4(%esp)
- 43 movl %eax, (%esp)



所以,最后關於C的函數調用,我們可以總結一下:
1 、函數輸入參數的入棧順序是函數原型中形參從右至左的原則;
2 、匯編語言里調用函數通常情況下 都用 call指令來完成 ;
3、匯編語言里的函數大部分情況下都符合以下的 函數模板:
點擊(此處)折疊或打開
- .globl fun_name
- .type fun_name, @function
- fun_name:
- pushl %ebp
- movl %esp, %ebp
- <函數主體代碼>
- leave
- ret
如果我們有個函數原型:int funtest(int x,int y int z char* ptr),在匯編層面,當調用它時棧的布局結構一般是下面這個樣子:

而有些資料上將ebp指向函數返回地址的地方,這是不對的。正常情況下應該是ebp指向old ebp才對,這樣函數末尾的leave和ret指令才可以正常工作。