棧:?在函數調用時,第一個進棧的是主函數中函數調用后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
當發生函數調用的時候,棧空間中存放的數據是這樣的:
1、調用者函數把被調函數所需要的參數按照與被調函數的形參順序相反的順序壓入棧中,即:從右向左依次把被調函數所需要的參數壓入棧;
2、調用者函數使用call指令調用被調函數,并把call指令的下一條指令的地址當成返回地址壓入棧中(這個壓棧操作隱含在call指令中);
3、在被調函數中,被調函數會先保存調用者函數的棧底地址(push ebp),然后再保存調用者函數的棧頂地址,即:當前被調函數的棧底地址(mov ebp,esp);
4、在被調函數中,從ebp的位置處開始存放被調函數中的局部變量和臨時變量,并且這些變量的地址按照定義時的順序依次減小,即:這些變量的地址是按照棧的延伸方向排列的,先定義的變量先入棧,后定義的變量后入棧;
所以,發生函數調用時,入棧的順序為:
參數N
參數N-1
參數N-2
.....
參數3
參數2
參數1
函數返回地址
上一層調用函數的EBP/BP
局部變量1
局部變量2
....
局部變量N
函數調用棧如下圖所示:
解釋:? //EBP 基址指針,是保存調用者函數的地址,總是指向函數棧棧底,ESP被調函數的指針,總是指向函數棧棧頂。
首 先,將調用者函數的EBP入棧(pushebp),然后將調用者函數的棧頂指針ESP賦值給被調函數的EBP(作為被調函數的棧底,movebp,esp),此時,EBP寄存器處于一個非常重要的位置,該寄存器中存放著一個地址(原EBP入棧后的棧頂),以該地址為基準,向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數的局部變量值,而該地址處又存放著上一層函數調用時的EBP值;
一般規律,SS:[ebp+4]處為被調函數的返回地址,SS:[EBP+8]處為傳遞給被調函數的第一個參數(最后一個入棧的參數,此處假設其占用4字節內存)的值,SS:[EBP-4]處為被調函數中的第一個局部變量,SS:[EBP]處為上一層EBP值;由于EBP中的地址處總是"上一層函數調用時的EBP值",而在每一層函數調用中,都能通過當時的EBP值"向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取被調函數的局部變量值";
如此遞歸,就形成了函數調用棧;
Eg函數內局部變量布局示例:
[plain]?view plain
?copy#include?<stdio.h>??#include?<string.h>??struct?C??{????int?a;????int?b;????int?c;??};??int?test2(int?x,?int?y,?int?z)??{????printf("hello,test2\n");????return?0;??}??int?test(int?x,?int?y,?int?z)??{????int?a?=?1;????int?b?=?2;????int?c?=?3;????struct?C?st;????printf("addr?x?=?%u\n",(unsigned?int)(&x));????printf("addr?y?=?%u\n",(unsigned?int)(&y));????printf("addr?z?=?%u\n",(unsigned?int)(&z));????printf("addr?a?=?%u\n",(unsigned?int)(&a));????printf("addr?b?=?%u\n",(unsigned?int)(&b));????printf("addr?c?=?%u\n",(unsigned?int)(&c));????printf("addr?st?=?%u\n",(unsigned?int)(&st));????printf("addr?st.a?=?%u\n",(unsigned?int)(&st.a));????printf("addr?st.b?=?%u\n",(unsigned?int)(&st.b));????printf("addr?st.c?=?%u\n",(unsigned?int)(&st.c));????return?0;??}?int?main(int?argc,?char**?argv)??{????int?x?=?1;????int?y?=?2;????int?z?=?3;????test(x,y,z);????printf("x?=?%d;?y?=?%d;?z?=?%d;\n",?x,y,z);????memset(&y,?0,?8);????printf("x?=?%d;?y?=?%d;?z?=?%d;\n",?x,y,z);????return?0;??}??
打印輸出如下:
[plain]?view plain
?copyaddr?x?=?3220024704??addr?y?=?3220024708??addr?z?=?3220024712??addr?a?=?3220024684??addr?b?=?3220024680??addr?c?=?3220024676??addr?st?=?3220024664??addr?st.a?=?3220024664??addr?st.b?=?3220024668??addr?st.c?=?3220024672??x?=?1;?y?=?2;?z?=?3;??x?=?0;?y?=?0;?z?=?3;??
局部變量在棧中布局示意圖:
該圖中的局部變量都是在該示例中定義的:
?這個圖片中反映的是一個典型的函數調用棧的內存布局;
訪問函數的局部變量和訪問函數參數的區別:
局部變量總是通過將ebp減去偏移量來訪問,函數參數總是通過將ebp加上偏移量來訪問。對于32位變量而言,第一個局部變量位于ebp-4,第二個位于ebp-8,以此類推,32位局部變量在棧中形成一個逆序數組;第一個函數參數位于ebp+8,第二個位于ebp+12,以此類推,32位函數參數在棧中形成一個正序數組。
Eg、研究函數調用過程:
[plain]?view plain
?copy#include?<stdio.h>????int?bar(int?c,int?d)??{??????????int?e=c+d;??????????return?e;??}????int?foo(int?a,int?b)??{??????????return?bar(a,b);??}????int?main(int?argc,int?argv)??{??????????foo(2,3);??????????return?0;??}??
上面是一個很簡單的函數調用過程,整個程序的執行過程是main調用foo,foo調用bar。
//查看反匯編文件(要查看編譯后的匯編代碼,其實還有一種辦法是gcc -S text_stack.c,這樣只生成匯編代碼text_stack.s,而不生成二進制的目標文件。)
[plain]?view plain
?copyroot@wangye:/home/wangye#?gcc?text_stack.c?-g??root@wangye:/home/wangye#?objdump?-dS?a.out???
反匯編結果很長,下面只列出我們關心的部分。
[plain]?view plain
?copy08048394?<bar>:??#include?<stdio.h>????int?bar(int?c,int?d)??{???8048394:???55??????????????????????push???%ebp???8048395:???89?e5???????????????????mov????%esp,%ebp???8048397:???83?ec?10????????????????sub????$0x10,%esp??????int?e=c+d;???804839a:???8b?45?0c????????????????mov????0xc(%ebp),%eax???804839d:???8b?55?08????????????????mov????0x8(%ebp),%edx???80483a0:???8d?04?02????????????????lea????(%edx,%eax,1),%eax???80483a3:???89?45?fc????????????????mov????%eax,-0x4(%ebp)??????return?e;???80483a6:???8b?45?fc????????????????mov????-0x4(%ebp),%eax??}???80483a9:???c9??????????????????????leave?????80483aa:???c3??????????????????????ret????????080483ab?<foo>:????int?foo(int?a,int?b)??{???80483ab:???55??????????????????????push???%ebp???80483ac:???89?e5???????????????????mov????%esp,%ebp???80483ae:???83?ec?08????????????????sub????$0x8,%esp??????return?bar(a,b);???80483b1:???8b?45?0c????????????????mov????0xc(%ebp),%eax???80483b4:???89?44?24?04?????????????mov????%eax,0x4(%esp)???80483b8:???8b?45?08????????????????mov????0x8(%ebp),%eax???80483bb:???89?04?24????????????????mov????%eax,(%esp)???80483be:???e8?d1?ff?ff?ff??????????call???8048394?<bar>??}???80483c3:???c9??????????????????????leave?????80483c4:???c3??????????????????????ret????????080483c5?<main>:????int?main(int?argc,int?argv)??{???80483c5:???55??????????????????????push???%ebp???80483c6:???89?e5???????????????????mov????%esp,%ebp???80483c8:???83?ec?08????????????????sub????$0x8,%esp??????foo(2,3);???80483cb:???c7?44?24?04?03?00?00????movl???$0x3,0x4(%esp)???80483d2:???00????80483d3:???c7?04?24?02?00?00?00????movl???$0x2,(%esp)???80483da:???e8?cc?ff?ff?ff??????????call???80483ab?<foo>??????return?0;???80483df:???b8?00?00?00?00??????????mov????$0x0,%eax??}??
//我們用gdb跟蹤程序的執行,直到bar函數中的int e = c + d;語句執行完畢準備返回時,這時在gdb中打印函數棧幀。
[plain]?view plain
?copywangye@wangye:~$?gdb?text_stack???GNU?gdb?(GDB)?7.0.1-debian??Copyright?(C)?2009?Free?Software?Foundation,?Inc.??License?GPLv3+:?GNU?GPL?version?3?or?later?<http://gnu.org/licenses/gpl.html>??This?is?free?software:?you?are?free?to?change?and?redistribute?it.??There?is?NO?WARRANTY,?to?the?extent?permitted?by?law.??Type?"show?copying"??and?"show?warranty"?for?details.??This?GDB?was?configured?as?"i486-linux-gnu".??For?bug?reporting?instructions,?please?see:??<http://www.gnu.org/software/gdb/bugs/>...??Reading?symbols?from?/home/wangye/text_stack...done.??(gdb)?start??Temporary?breakpoint?1?at?0x80483cb:?file?text_stack.c,?line?16.??Starting?program:?/home/wangye/text_stack?????Temporary?breakpoint?1,?main?(argc=1,?argv=-1073744732)?at?text_stack.c:16??16??????foo(2,3);??(gdb)?s??foo?(a=2,?b=3)?at?text_stack.c:11??11??????return?bar(a,b);??(gdb)?s??bar?(c=2,?d=3)?at?text_stack.c:5??5???????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),%eax??0x0804839d?<bar+9>:???mov????0x8(%ebp),%edx??0x080483a0?<bar+12>:??lea????(%edx,%eax,1),%eax??0x080483a3?<bar+15>:??mov????%eax,-0x4(%ebp)??0x080483a6?<bar+18>:??mov????-0x4(%ebp),%eax??0x080483a9?<bar+21>:??leave????0x080483aa?<bar+22>:??ret??????End?of?assembler?dump.??(gdb)?si??0x0804839d??5???????int?e=c+d;??(gdb)?si??0x080483a0??5???????int?e=c+d;??(gdb)?si??0x080483a3??5???????int?e=c+d;??(gdb)?si??6???????return?e;??(gdb)?si??7???}??(gdb)?bt??#0??bar?(c=2,?d=3)?at?text_stack.c:7??#1??0x080483c3?in?foo?(a=2,?b=3)?at?text_stack.c:11??#2??0x080483df?in?main?(argc=1,?argv=-1073744732)?at?text_stack.c:16??(gdb)?info?re??record?????registers????(gdb)?info?regi??eax????????????0x5??5??ecx????????????0x4c2f5d43???1278172483??edx????????????0x2??2??ebx????????????0xb7fcaff4???-1208176652??esp????????????0xbffff3c8???0xbffff3c8??ebp????????????0xbffff3d8???0xbffff3d8??esi????????????0x0??0??edi????????????0x0??0??eip????????????0x80483a9????0x80483a9?<bar+21>??eflags?????????0x282????[?SF?IF?]??cs?????????????0x73?115??ss?????????????0x7b?123??ds?????????????0x7b?123??es?????????????0x7b?123??fs?????????????0x0??0??gs?????????????0x33?51??(gdb)?info?regi??eax????????????0x5??5??ecx????????????0x4c2f5d43???1278172483??edx????????????0x2??2??ebx????????????0xb7fcaff4???-1208176652??esp????????????0xbffff3c8???0xbffff3c8??ebp????????????0xbffff3d8???0xbffff3d8??esi????????????0x0??0??edi????????????0x0??0??eip????????????0x80483a9????0x80483a9?<bar+21>??eflags?????????0x282????[?SF?IF?]??cs?????????????0x73?115??ss?????????????0x7b?123??ds?????????????0x7b?123??es?????????????0x7b?123??fs?????????????0x0??0??gs?????????????0x33?51??(gdb)?x/20?$esp??0xbffff3c8:?-1073744904?134513689???-1208175868?5??0xbffff3d8:?-1073744920?134513603???2???3??0xbffff3e8:?-1073744904?134513631???2???3??0xbffff3f8:?-1073744776?-1209406298?1???-1073744732??0xbffff408:?-1073744724?-1208084392?-1073744800?-1??
這里我們又用了幾個新的gdb命令,簡單解釋一下:info registers
可以顯示所有寄存器的當前值。在
gdb
中表示寄存器名時前面要加個
$
,例如
p $esp
可以打印
esp
寄存器的值,在上例中
esp
寄存器的值是0xbffff3c8,所以
x/20 $esp
命令查看內存中從0xbffff3c8 地址開始的20個32位數。在執行程序時,操作系統為進程分配一塊棧空間來保存函數棧幀,
esp
寄存器總是指向棧頂,在x86平臺上這個棧是從高地址向低地址增長的,我們知道每次調用一個函數都要分配一個棧幀來保存參數和局部變量,現在我們詳細分析這些數據在棧空間的布局,根據
gdb
的輸出結果圖示如下:
圖中每個小方格表示4個字節的內存單元,例如b: 3這個小方格占的內存地址是0xbffff3f4~0xbffff3f7,把地址寫在每個小方格的下邊界線上,是為了強調該地址是內存單元的起始地址。我們從main函數的這里開始看起:
[plain]?view plain
?copyfoo(2,3);??80483cb:????c7?44?24?04?03?00?00????movl???$0x3,0x4(%esp)??80483d2:????00???80483d3:????c7?04?24?02?00?00?00????movl???$0x2,(%esp)??80483da:????e8?cc?ff?ff?ff??????????call???80483ab?<foo>??return?0;??80483df:????b8?00?00?00?00??????????mov????$0x0,%eax??
要調用函數foo先要把參數準備好,第二個參數保存在esp+4指向的內存位置,第一個參數保存在esp指向的內存位置,可見參數是從右向左依次壓棧的。然后執行call指令,這個指令有兩個作用:
foo函數調用完之后要返回到call的下一條指令繼續執行,所以把call的下一條指令的地址134513631壓棧,同時把esp的值減4,esp的值現在是0xbffff3ec。
修改程序計數器eip,跳轉到foo函數的開頭執行。
現在看foo函數的匯編代碼:
[plain]?view plain
?copy080483ab?<foo>:????int?foo(int?a,int?b)??{???80483ab:???55??????????????????????push???%ebp???80483ac:???89?e5???????????????????mov????%esp,%ebp???80483ae:???83?ec?08????????????????sub????$0x8,%esp????????
push %ebp指令把ebp寄存器的值壓棧,同時把esp的值減4。esp的值現在是0xbff1c414,下一條指令把這個值傳送給ebp寄存器。這兩條指令合起來是把原來ebp的值保存在棧上,然后又給ebp賦了新值。在每個函數的棧幀中,ebp指向棧底,而esp指向棧頂,在函數執行過程中esp隨著壓棧和出棧操作隨時變化,而ebp是不動的,函數的參數和局部變量都是通過ebp的值加上一個偏移量來訪問,例如foo函數的參數a和b分別通過ebp+8和ebp+12來訪問。所以下面的指令把參數a和b再次壓棧,為調用bar函數做準備,然后把返回地址壓棧,調用bar函數:
[plain]?view plain
?copyreturn?bar(a,b);???80483b1:???8b?45?0c????????????????mov????0xc(%ebp),%eax???80483b4:???89?44?24?04?????????????mov????%eax,0x4(%esp)???80483b8:???8b?45?08????????????????mov????0x8(%ebp),%eax???80483bb:???89?04?24????????????????mov????%eax,(%esp)???80483be:???e8?d1?ff?ff?ff??????????call???8048394?<bar>??}???80483c3:???c9??????????????????????leave?????80483c4:???c3??????????????????????ret????
現在看bar函數的指令:
[plain]?view plain
?copyint?bar(int?c,int?d)??{???8048394:???55??????????????????????push???%ebp???8048395:???89?e5???????????????????mov????%esp,%ebp???8048397:???83?ec?10????????????????sub????$0x10,%esp??????int?e=c+d;???804839a:???8b?45?0c????????????????mov????0xc(%ebp),%eax???804839d:???8b?55?08????????????????mov????0x8(%ebp),%edx???80483a0:???8d?04?02????????????????lea????(%edx,%eax,1),%eax???80483a3:???89?45?fc????????????????mov????%eax,-0x4(%ebp)????????
這次又把foo函數的ebp壓棧保存,然后給ebp賦了新值,指向bar函數棧幀的棧底,通過ebp+8和ebp+12分別可以訪問參數c和d。bar函數還有一個局部變量e,可以通過ebp-4來訪問。所以后面幾條指令的意思是把參數c和d取出來存在寄存器中做加法,計算結果保存在eax寄存器中,再把eax寄存器存回局部變量e的內存單元。
在gdb中可以用bt命令和frame命令查看每層棧幀上的參數和局部變量,現在可以解釋它的工作原理了:如果我當前在bar函數中,我可以通過ebp找到bar函數的參數和局部變量,也可以找到foo函數的ebp保存在棧上的值,有了foo函數的ebp,又可以找到它的參數和局部變量,也可以找到main函數的ebp保存在棧上的值,因此各層函數棧幀通過保存在棧上的ebp的值串起來了。
現在看bar函數的返回指令:
[plain]?view plain
?copyreturn?e;???80483a6:???8b?45?fc????????????????mov????-0x4(%ebp),%eax??}???80483a9:???c9??????????????????????leave?????80483aa:???c3??????????????????????ret???
bar函數有一個int型的返回值,這個返回值是通過eax寄存器傳遞的,所以首先把e的值讀到eax寄存器中。然后執行leave指令,這個指令是函數開頭的push %ebp和mov %esp,%ebp的逆操作:
把ebp的值賦給esp,現在esp的值是0xbffff3d8。
現在esp所指向的棧頂保存著foo函數棧幀的ebp,把這個值恢復給ebp,同時esp增加4,esp的值變成0xbffff3dc。
最后是ret指令,它是call指令的逆操作:
現在esp所指向的棧頂保存著返回地址,把這個值恢復給eip,同時esp增加4,esp的值變成0xbffff3e0。
修改了程序計數器eip,因此跳轉到返回地址0x80483c2繼續執行。
地址0x80483c2處是foo函數的返回指令:
[plain]?view plain
?copy80483c3:????c9??????????????????????leave????80483c4:????c3??????????????????????ret????
重復同樣的過程,又返回到了main函數。注意函數調用和返回過程中的這些規則:
參數壓棧傳遞,并且是從右向左依次壓棧。
ebp總是指向當前棧幀的棧底。
返回值通過eax寄存器傳遞。
這些規則并不是體系結構所強加的,ebp寄存器并不是必須這么用,函數的參數和返回值也不是必須這么傳,只是操作系統和編譯器選擇了以這樣的方式實現C代碼中的函數調用,這稱為CallingConvention,Calling Convention是操作系統二進制接口規范(ABI,Application BinaryInterface)的一部分。
總結
以上是生活随笔為你收集整理的函数调用栈 剖析+图解的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。