1. 函数调用
1.?函數(shù)調(diào)用
我們用下面的代碼來(lái)研究函數(shù)調(diào)用的過(guò)程。
例?19.1.?研究函數(shù)的調(diào)用過(guò)程
int bar(int c, int d) {int e = c + d;return e; }int foo(int a, int b) {return bar(a, b); }int main(void) {foo(2, 3);return 0; }?
如果在編譯時(shí)加上-g選項(xiàng)那么用objdump反匯編時(shí)可以把C代碼和匯編代碼穿插起來(lái)顯示,這樣C代碼和匯編代碼的對(duì)應(yīng)關(guān)系看得更清楚。反匯編的結(jié)果很長(zhǎng),以下只列出我們關(guān)心的部分。
$ gcc main.c -g $ objdump -dS a.out ... 08048394 <bar>: int bar(int c, int d) {8048394: 55 push %ebp8048395: 89 e5 mov %esp,%ebp8048397: 83 ec 10 sub $0x10,%espint e = c + d;804839a: 8b 55 0c mov 0xc(%ebp),%edx804839d: 8b 45 08 mov 0x8(%ebp),%eax80483a0: 01 d0 add %edx,%eax80483a2: 89 45 fc mov %eax,-0x4(%ebp)return e;80483a5: 8b 45 fc mov -0x4(%ebp),%eax }80483a8: c9 leave 80483a9: c3 ret 080483aa <foo>:int foo(int a, int b) {80483aa: 55 push %ebp80483ab: 89 e5 mov %esp,%ebp80483ad: 83 ec 08 sub $0x8,%espreturn bar(a, b);80483b0: 8b 45 0c mov 0xc(%ebp),%eax80483b3: 89 44 24 04 mov %eax,0x4(%esp)80483b7: 8b 45 08 mov 0x8(%ebp),%eax80483ba: 89 04 24 mov %eax,(%esp)80483bd: e8 d2 ff ff ff call 8048394 <bar> }80483c2: c9 leave 80483c3: c3 ret 080483c4 <main>:int main(void) {80483c4: 8d 4c 24 04 lea 0x4(%esp),%ecx80483c8: 83 e4 f0 and $0xfffffff0,%esp80483cb: ff 71 fc pushl -0x4(%ecx)80483ce: 55 push %ebp80483cf: 89 e5 mov %esp,%ebp80483d1: 51 push %ecx80483d2: 83 ec 08 sub $0x8,%espfoo(2, 3);80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)80483dc: 00 80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)80483e4: e8 c1 ff ff ff call 80483aa <foo>return 0;80483e9: b8 00 00 00 00 mov $0x0,%eax }80483ee: 83 c4 08 add $0x8,%esp80483f1: 59 pop %ecx80483f2: 5d pop %ebp80483f3: 8d 61 fc lea -0x4(%ecx),%esp80483f6: c3 ret ...要查看編譯后的匯編代碼,其實(shí)還有一種辦法是gcc -S main.c,這樣只生成匯編代碼main.s,而不生成二進(jìn)制的目標(biāo)文件。
整個(gè)程序的執(zhí)行過(guò)程是main調(diào)用foo,foo調(diào)用bar,我們用gdb跟蹤程序的執(zhí)行,直到bar函數(shù)中的int e = c + d;語(yǔ)句執(zhí)行完畢準(zhǔn)備返回時(shí),這時(shí)在gdb中打印函數(shù)棧幀。
(gdb) start ... main () at main.c:14 14 foo(2, 3); (gdb) s foo (a=2, b=3) at main.c:9 9 return bar(a, b); (gdb) s bar (c=2, d=3) at main.c:3 3 int e = c + d; (gdb) disassemble Dump of assembler code for function bar: 0x08048394 <bar+0>: push %ebp 0x08048395 <bar+1>: mov %esp,%ebp 0x08048397 <bar+3>: sub $0x10,%esp 0x0804839a <bar+6>: mov 0xc(%ebp),%edx 0x0804839d <bar+9>: mov 0x8(%ebp),%eax 0x080483a0 <bar+12>: add %edx,%eax 0x080483a2 <bar+14>: mov %eax,-0x4(%ebp) 0x080483a5 <bar+17>: mov -0x4(%ebp),%eax 0x080483a8 <bar+20>: leave 0x080483a9 <bar+21>: ret End of assembler dump. (gdb) si 0x0804839d 3 int e = c + d; (gdb) si 0x080483a0 3 int e = c + d; (gdb) si 0x080483a2 3 int e = c + d; (gdb) si 4 return e; (gdb) si 5 } (gdb) bt #0 bar (c=2, d=3) at main.c:5 #1 0x080483c2 in foo (a=2, b=3) at main.c:9 #2 0x080483e9 in main () at main.c:14 (gdb) info registers eax 0x5 5 ecx 0xbff1c440 -1074674624 edx 0x3 3 ebx 0xb7fe6ff4 -1208061964 esp 0xbff1c3f4 0xbff1c3f4 ebp 0xbff1c404 0xbff1c404 esi 0x8048410 134513680 edi 0x80482e0 134513376 eip 0x80483a8 0x80483a8 <bar+20> eflags 0x200206 [ PF IF ID ] cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) x/20 $esp 0xbff1c3f4: 0x00000000 0xbff1c6f7 0xb7efbdae 0x00000005 0xbff1c404: 0xbff1c414 0x080483c2 0x00000002 0x00000003 0xbff1c414: 0xbff1c428 0x080483e9 0x00000002 0x00000003 0xbff1c424: 0xbff1c440 0xbff1c498 0xb7ea3685 0x08048410 0xbff1c434: 0x080482e0 0xbff1c498 0xb7ea3685 0x00000001 (gdb)這里又用到幾個(gè)新的gdb命令。disassemble可以反匯編當(dāng)前函數(shù)或者指定的函數(shù),單獨(dú)用disassemble命令是反匯編當(dāng)前函數(shù),如果disassemble命令后面跟函數(shù)名或地址則反匯編指定的函數(shù)。以前我們講過(guò)step命令可以一行代碼一行代碼地單步調(diào)試,而這里用到的si命令可以一條指令一條指令地單步調(diào)試。info registers可以顯示所有寄存器的當(dāng)前值。在gdb中表示寄存器名時(shí)前面要加個(gè)$,例如p $esp可以打印esp寄存器的值,在上例中esp寄存器的值是0xbff1c3f4,所以x/20 $esp命令查看內(nèi)存中從0xbff1c3f4地址開始的20個(gè)32位數(shù)。在執(zhí)行程序時(shí),操作系統(tǒng)為進(jìn)程分配一塊棧空間來(lái)保存函數(shù)棧幀,esp寄存器總是指向棧頂,在x86平臺(tái)上這個(gè)棧是從高地址向低地址增長(zhǎng)的,我們知道每次調(diào)用一個(gè)函數(shù)都要分配一個(gè)棧幀來(lái)保存參數(shù)和局部變量,現(xiàn)在我們?cè)敿?xì)分析這些數(shù)據(jù)在棧空間的布局,根據(jù)gdb的輸出結(jié)果圖示如下
圖?19.1.?函數(shù)棧幀
?
圖中每個(gè)小方格表示4個(gè)字節(jié)的內(nèi)存單元,例如b: 3這個(gè)小方格占的內(nèi)存地址是0xbff1c420~0xbff1c423,我把地址寫在每個(gè)小方格的下邊界線上,是為了強(qiáng)調(diào)該地址是內(nèi)存單元的起始地址。我們從main函數(shù)的這里開始看起:
foo(2, 3);80483d5: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)80483dc: 00 80483dd: c7 04 24 02 00 00 00 movl $0x2,(%esp)80483e4: e8 c1 ff ff ff call 80483aa <foo>return 0;80483e9: b8 00 00 00 00 mov $0x0,%eax要調(diào)用函數(shù)foo先要把參數(shù)準(zhǔn)備好,第二個(gè)參數(shù)保存在esp+4指向的內(nèi)存位置,第一個(gè)參數(shù)保存在esp指向的內(nèi)存位置,可見參數(shù)是從右向左依次壓棧的。然后執(zhí)行call指令,這個(gè)指令有兩個(gè)作用:
foo函數(shù)調(diào)用完之后要返回到call的下一條指令繼續(xù)執(zhí)行,所以把call的下一條指令的地址0x80483e9壓棧,同時(shí)把esp的值減4,esp的值現(xiàn)在是0xbff1c418。
修改程序計(jì)數(shù)器eip,跳轉(zhuǎn)到foo函數(shù)的開頭執(zhí)行。
現(xiàn)在看foo函數(shù)的匯編代碼:
int foo(int a, int b) {80483aa: 55 push %ebp80483ab: 89 e5 mov %esp,%ebp80483ad: 83 ec 08 sub $0x8,%esppush %ebp指令把ebp寄存器的值壓棧,同時(shí)把esp的值減4。esp的值現(xiàn)在是0xbff1c414,下一條指令把這個(gè)值傳送給ebp寄存器。這兩條指令合起來(lái)是把原來(lái)ebp的值保存在棧上,然后又給ebp賦了新值。在每個(gè)函數(shù)的棧幀中,ebp指向棧底,而esp指向棧頂,在函數(shù)執(zhí)行過(guò)程中esp隨著壓棧和出棧操作隨時(shí)變化,而ebp是不動(dòng)的,函數(shù)的參數(shù)和局部變量都是通過(guò)ebp的值加上一個(gè)偏移量來(lái)訪問(wèn),例如foo函數(shù)的參數(shù)a和b分別通過(guò)ebp+8和ebp+12來(lái)訪問(wèn)。所以下面的指令把參數(shù)a和b再次壓棧,為調(diào)用bar函數(shù)做準(zhǔn)備,然后把返回地址壓棧,調(diào)用bar函數(shù):
return bar(a, b);80483b0: 8b 45 0c mov 0xc(%ebp),%eax80483b3: 89 44 24 04 mov %eax,0x4(%esp)80483b7: 8b 45 08 mov 0x8(%ebp),%eax80483ba: 89 04 24 mov %eax,(%esp)80483bd: e8 d2 ff ff ff call 8048394 <bar>現(xiàn)在看bar函數(shù)的指令:
int bar(int c, int d) {8048394: 55 push %ebp8048395: 89 e5 mov %esp,%ebp8048397: 83 ec 10 sub $0x10,%espint e = c + d;804839a: 8b 55 0c mov 0xc(%ebp),%edx804839d: 8b 45 08 mov 0x8(%ebp),%eax80483a0: 01 d0 add %edx,%eax80483a2: 89 45 fc mov %eax,-0x4(%ebp)這次又把foo函數(shù)的ebp壓棧保存,然后給ebp賦了新值,指向bar函數(shù)棧幀的棧底,通過(guò)ebp+8和ebp+12分別可以訪問(wèn)參數(shù)c和d。bar函數(shù)還有一個(gè)局部變量e,可以通過(guò)ebp-4來(lái)訪問(wèn)。所以后面幾條指令的意思是把參數(shù)c和d取出來(lái)存在寄存器中做加法,計(jì)算結(jié)果保存在eax寄存器中,再把eax寄存器存回局部變量e的內(nèi)存單元。
在gdb中可以用bt命令和frame命令查看每層棧幀上的參數(shù)和局部變量,現(xiàn)在可以解釋它的工作原理了:如果我當(dāng)前在bar函數(shù)中,我可以通過(guò)ebp找到bar函數(shù)的參數(shù)和局部變量,也可以找到foo函數(shù)的ebp保存在棧上的值,有了foo函數(shù)的ebp,又可以找到它的參數(shù)和局部變量,也可以找到main函數(shù)的ebp保存在棧上的值,因此各層函數(shù)棧幀通過(guò)保存在棧上的ebp的值串起來(lái)了。
現(xiàn)在看bar函數(shù)的返回指令:
return e;80483a5: 8b 45 fc mov -0x4(%ebp),%eax }80483a8: c9 leave 80483a9: c3 retbar函數(shù)有一個(gè)int型的返回值,這個(gè)返回值是通過(guò)eax寄存器傳遞的,所以首先把e的值讀到eax寄存器中。然后執(zhí)行l(wèi)eave指令,這個(gè)指令是函數(shù)開頭的push %ebp和mov %esp,%ebp的逆操作:
把ebp的值賦給esp,現(xiàn)在esp的值是0xbff1c404。
現(xiàn)在esp所指向的棧頂保存著foo函數(shù)棧幀的ebp,把這個(gè)值恢復(fù)給ebp,同時(shí)esp增加4,esp的值變成0xbff1c408。
最后是ret指令,它是call指令的逆操作:
現(xiàn)在esp所指向的棧頂保存著返回地址,把這個(gè)值恢復(fù)給eip,同時(shí)esp增加4,esp的值變成0xbff1c40c。
修改了程序計(jì)數(shù)器eip,因此跳轉(zhuǎn)到返回地址0x80483c2繼續(xù)執(zhí)行。
地址0x80483c2處是foo函數(shù)的返回指令:
80483c2: c9 leave 80483c3: c3 ret重復(fù)同樣的過(guò)程,又返回到了main函數(shù)。注意函數(shù)調(diào)用和返回過(guò)程中的這些規(guī)則:
參數(shù)壓棧傳遞,并且是從右向左依次壓棧。
ebp總是指向當(dāng)前棧幀的棧底。
返回值通過(guò)eax寄存器傳遞。
這些規(guī)則并不是體系結(jié)構(gòu)所強(qiáng)加的,ebp寄存器并不是必須這么用,函數(shù)的參數(shù)和返回值也不是必須這么傳,只是操作系統(tǒng)和編譯器選擇了以這樣的方式實(shí)現(xiàn)C代碼中的函數(shù)調(diào)用,這稱為Calling Convention,Calling Convention是操作系統(tǒng)二進(jìn)制接口規(guī)范(ABI,Application Binary Interface)的一部分。
1、Old Style C風(fēng)格的函數(shù)聲明可以不指定參數(shù)個(gè)數(shù)和類型,這樣編譯器不會(huì)對(duì)函數(shù)調(diào)用做檢查,那么如果調(diào)用時(shí)的參數(shù)類型不對(duì)或者參數(shù)個(gè)數(shù)不對(duì)會(huì)怎么樣呢?比如把本節(jié)的例子改成這樣: int foo(); int bar();int main(void) {foo(2, 3, 4);return 0; }int foo(int a, int b) {return bar(a); }int bar(int c, int d) {int e = c + d;return e; }main函數(shù)調(diào)用foo時(shí)多傳了一個(gè)參數(shù),那么參數(shù)a和b分別取什么值?多的參數(shù)怎么辦?foo調(diào)用bar時(shí)少傳了一個(gè)參數(shù),那么參數(shù)d的值從哪里取得?請(qǐng)讀者利用反匯編和gdb自己分析一下。我們?cè)倏匆粋€(gè)參數(shù)類型不符的例子:
#include <stdio.h>int main(void) {void foo();char c = 60;foo(c);return 0; }void foo(double d) {printf("%f\n", d); }打印結(jié)果是多少?如果把聲明void foo();改成void foo(double);,打印結(jié)果又是多少?
Linux內(nèi)核為每個(gè)新進(jìn)程指定的棧空間的起始地址都會(huì)有些不同,所以每次運(yùn)行這個(gè)程序得到的地址都不一樣,但通常都是0xbf??????這樣一個(gè)地址
轉(zhuǎn)載自(http://learn.akae.cn/media/ch19s01.html)
轉(zhuǎn)載于:https://www.cnblogs.com/Fightingbirds/archive/2012/12/30/2839902.html
總結(jié)