Call stack

Call stack,意思是調用堆疊,調用堆疊是一個方法列表,按調用順序保存所有在運行期被調用的方法。

簡介

當本次函式調用結束後,局部變數先出棧,然後是參數,最後棧頂指針指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續運行。

當發生函式調用的時候,棧空間中存放的數據是這樣的:
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入棧(push ebp),然後將調用者函式的棧頂指針ESP賦值給被調函式的EBP(作為被調函式的棧底,mov ebp,esp),此時,EBP暫存器處於一個非常重要的位置,該暫存器中存放著一個地址(原EBP入棧後的棧頂),以該地址為基準,向上(棧底方向)能 獲取返回地址、參數值,向下(棧頂方向)能獲取函式的局部變數值,而該地址處又存放著上一層函式調用時的EBP值;
一般而言,SS: [ebp+4]處為被調函式的返回地址,SS:[EBP+8]處為傳遞給被調函式的第一個參數(最後一個入棧的參數,此處假設其占用4位元組記憶體)的 值,SS:[EBP-4]處為被調函式中的第一個局部變數,SS:[EBP]處為上一層EBP值;由於EBP中的地址處總是"上一層函式調用時的EBP 值",而在每一層函式調用中,都能通過當時的EBP值"向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取被調函式的局部變數值";
如此遞歸,就形成了函式調用棧;
函式內局部變數布局示例:
#include
#include
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;
}
列印輸出如下:
addr x = 4288282272
addr y = 4288282276
addr z = 4288282280
addr a = 4288282260
addr b = 4288282256
addr c = 4288282252
addr st = 4288282240
addr st.a = 4288282240
addr st.b = 4288282244
addr st.c = 4288282248
a = 1; b = 2; c = 3;
a = 0; b = 0; c = 3;
示例效果圖:

Call stack Call stack

該圖中的局部變數都是在該示例中定義的;

Call stack Call stack

這個圖片中反映的是一個典型的函式調用棧的記憶體布局;

訪問函式的局部變數和訪問函式參數的區別:
局部變數總是通過將ebp減去偏移量來訪問,函式參數總是通過將ebp加上偏移量來訪問。對於32位變數而言,第一個局部變數位於ebp-4,第二個位於ebp-8,以此類推,32位局部變數在棧中形成一個逆序數組;第一個函式參數位於ebp+8,第二個位於ebp+12,以此類推,32位函式參數在棧中形成一個正序數組。

彙編代碼示例(1):

////////////////////////////////////////////////////////////////////
比如 我們有這樣一個C函式
#include
long test(int a,int b)
{
a = a + 1;
b = b + 100;
return a + b;
}
void main()
{
printf("%d",test(1000,2000));
}
寫成32位彙編就是這樣
;//////////////////////////////////////////////////////////////////////////////////////////////////////
.386
.model flat,stdcall ;這裡我們用stdcall 就是函式參數 壓棧的時候從最後一個開始壓,和被調用函式負責清棧
option casemap:none ;區分大小寫
includelib msvcrt.lib ;這裡是引入類庫 相當於 #include 了
printf PROTO C:DWORD,:VARARG ;這個就是聲明一下我們要用的函式頭,到時候 彙編程式會自動到msvcrt.lib裡面找的了
;:VARARG 表後面的參數不確定 因為C就是這樣的printf(const char *, ...);
;這樣的函式要注意 不是被調用函式負責清棧 因為它本身不知道有多少個參數
;而是有調用者負責清棧 下面會詳細說明
.data
szTextFmt BYTE '%d',0 ;這個是用來類型轉換的,跟C的一樣,字元用位元組類型
a dword 1000 ;假設
b dword 2000 ;處理數值都用雙字 沒有int 跟long 的區別
;/////////////////////////////////////////////////////////////////////////////////////////
.code
_test proc ;A:DWORD,B:DWORD
push ebp
mov ebp,esp
mov eax,dword ptr ss:[ebp+8]
add eax,1
mov edx,dword ptr ss:[ebp+0Ch]
add edx,100
add eax,edx
pop ebp
retn 8
_test endp
_main proc
push dword ptr ds:b ;反彙編我們看到的b就不是b了而是一個[*****]數字 dword ptr 就是我們在ds(數據段)把[*****]
;開始的一個雙字長數值取出來
push dword ptr ds:a ;跟她對應的還有 byte ptr ****就是取一個位元組出來 比如這樣 mov al,byte ptr ds:szTextFmt
;就把 % 取出來 而不包括 d
call _test
push eax ;假設push eax的地址是×××××
push offset szTextFmt
call printf
add esp,8
ret
_main endp
end _main
彙編代碼示例(2):
研究函式的調用過程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;}

如果在編譯時加上-g選項(在第 10 章 gdb講過-g選項),那么用objdump反彙編時可以把C代碼和彙編代碼穿插起來顯示,這樣C代碼和彙編代碼的對應關係看得更清楚。反彙編的結果很長,以下只列出我們關心的部分。

$ gcc main.c -g$ objdump -dS a.out ...08048394 :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 55 0c mov 0xc(%ebp),%edx 804839d: 8b 45 08 mov 0x8(%ebp),%eax 80483a0: 01 d0 add %edx,%eax 80483a2: 89 45 fc mov %eax,-0x4(%ebp) return e; 80483a5: 8b 45 fc mov -0x4(%ebp),%eax} 80483a8: c9 leave 80483a9: c3 ret 080483aa:int foo(int a, int b){ 80483aa: 55 push %ebp 80483ab: 89 e5 mov %esp,%ebp 80483ad: 83 ec 08 sub $0x8,%esp return bar(a, b); 80483b0: 8b 45 0c mov 0xc(%ebp),%eax 80483b3: 89 44 24 04 mov %eax,0x4(%esp) 80483b7: 8b 45 08 mov 0x8(%ebp),%eax 80483ba: 89 04 24 mov %eax,(%esp) 80483bd: e8 d2 ff ff ff call 8048394 } 80483c2: c9 leave 80483c3: c3 ret 080483c4 :int main(void){ 80483c4: 8d 4c 24 04 lea 0x4(%esp),%ecx 80483c8: 83 e4 f0 and $0xfffffff0,%esp 80483cb: ff 71 fc pushl -0x4(%ecx) 80483ce: 55 push %ebp 80483cf: 89 e5 mov %esp,%ebp 80483d1: 51 push %ecx 80483d2: 83 ec 08 sub $0x8,%esp 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 return 0; 80483e9: b8 00 00 00 00 mov $0x0,%eax} 80483ee: 83 c4 08 add $0x8,%esp 80483f1: 59 pop %ecx 80483f2: 5d pop %ebp 80483f3: 8d 61 fc lea -0x4(%ecx),%esp 80483f6: c3 ret ...

要查看編譯後的彙編代碼,其實還有一種辦法是gcc -S main.c,這樣只生成彙編代碼main.s,而不生成二進制的目標檔案。

整個程式的執行過程是main調用foo,foo調用bar,我們用gdb跟蹤程式的執行,直到bar函式中的int e = c + d;語句執行完畢準備返回時,這時在gdb中列印函式棧幀。

(gdb) start...main () at main.c:1414 foo(2, 3);(gdb) sfoo (a=2, b=3) at main.c:99 return bar(a, b);(gdb) sbar (c=2, d=3) at main.c:33 int e = c + d;(gdb) disassemble Dump of assembler code for function bar:0x08048394 : push %ebp0x08048395: mov %esp,%ebp0x08048397 : sub $0x10,%esp0x0804839a : mov 0xc(%ebp),%edx0x0804839d : mov 0x8(%ebp),%eax0x080483a0 : add %edx,%eax0x080483a2: mov %eax,-0x4(%ebp)0x080483a5 : mov -0x4(%ebp),%eax0x080483a8 : leave 0x080483a9 : ret End of assembler dump.(gdb) si0x0804839d 3 int e = c + d;(gdb) si0x080483a0 3 int e = c + d;(gdb) si0x080483a2 3 int e = c + d;(gdb) si4 return e;(gdb) si5 }(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 5ecx 0xbff1c440 -1074674624edx 0x3 3ebx 0xb7fe6ff4 -1208061964esp 0xbff1c3f4 0xbff1c3f4ebp 0xbff1c404 0xbff1c404esi 0x8048410 134513680edi 0x80482e0 134513376eip 0x80483a8 0x80483a8 eflags 0x200206 [ PF IF ID ]cs 0x73 115ss 0x7b 123ds 0x7b 123es 0x7b 123fs 0x0 0gs 0x33 51(gdb) x/20 $esp0xbff1c3f4: 0x00000000 0xbff1c6f7 0xb7efbdae 0x000000050xbff1c404: 0xbff1c414 0x080483c2 0x00000002 0x000000030xbff1c414: 0xbff1c428 0x080483e9 0x00000002 0x000000030xbff1c424: 0xbff1c440 0xbff1c498 0xb7ea3685 0x080484100xbff1c434: 0x080482e0 0xbff1c498 0xb7ea3685 0x00000001(gdb)

這裡又用到幾個新的gdb命令。disassemble可以反彙編當前函式或者指定的函式,單獨用disassemble命令是反彙編當前函式,如果disassemble命令後面跟函式名或地址則反彙編指定的函式。以前我們講過step命令可以一行代碼一行代碼地單步調試,而這裡用到的si命令可以一條指令一條指令地單步調試。info registers可以顯示所有暫存器的當前值。在gdb中表示暫存器名時前面要加個$,例如p $esp可以列印esp暫存器的值,在上例中esp暫存器的值是0xbff1c3f4,所以x/20 $esp命令查看記憶體中從0xbff1c3f4地址開始的20個32位數。在執行程式時,作業系統為進程分配一塊棧空間來保存函式棧幀,esp暫存器總是指向棧頂,在x86平台上這個棧是從高地址向低地址增長的,我們知道每次調用一個函式都要分配一個棧幀來保存參數和局部變數,現在我們詳細分析這些數據在棧空間的布局,

圖中每個小方格表示4個位元組的記憶體單元,例如b: 3這個小方格占的記憶體地址是0xbff1c420~0xbff1c423,我把地址寫在每個小方格的下邊界線上,是為了強調該地址是記憶體單元的起始地址。我們從main函式的這裡開始看起:

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 return 0; 80483e9: b8 00 00 00 00 mov $0x0,%eax

要調用函式foo先要把參數準備好,第二個參數保存在esp+4指向的記憶體位置,第一個參數保存在esp指向的記憶體位置,可見參數是從右向左依次壓棧的。然後執行call指令,這個指令有兩個作用:

foo函式調用完之後要返回到call的下一條指令繼續執行,所以把call的下一條指令的地址0x80483e9壓棧,同時把esp的值減4,esp的值現在是0xbff1c418。

修改程式計數器eip,跳轉到foo函式的開頭執行。

1.

foo函式調用完之後要返回到call的下一條指令繼續執行,所以把call的下一條指令的地址0x80483e9壓棧,同時把esp的值減4,esp的值現在是0xbff1c418。

2.

修改程式計數器eip,跳轉到foo函式的開頭執行。

現在看foo函式的彙編代碼:

說明

int foo(int a, int b){ 80483aa: 55 push %ebp 80483ab: 89 e5 mov %esp,%ebp 80483ad: 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函式:

return bar(a, b); 80483b0: 8b 45 0c mov 0xc(%ebp),%eax 80483b3: 89 44 24 04 mov %eax,0x4(%esp) 80483b7: 8b 45 08 mov 0x8(%ebp),%eax 80483ba: 89 04 24 mov %eax,(%esp) 80483bd: e8 d2 ff ff ff call 8048394

現在看bar函式的指令:

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 55 0c mov 0xc(%ebp),%edx 804839d: 8b 45 08 mov 0x8(%ebp),%eax 80483a0: 01 d0 add %edx,%eax 80483a2: 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函式的返回指令:

return e; 80483a5: 8b 45 fc mov -0x4(%ebp),%eax} 80483a8: c9 leave 80483a9: c3 ret

bar函式有一個int型的返回值,這個返回值是通過eax暫存器傳遞的,所以首先把e的值讀到eax暫存器中。然後執行leave指令,這個指令是函式開頭的push %ebp和mov %esp,%ebp的逆操作:

把ebp的值賦給esp,現在esp的值是0xbff1c404。

現在esp所指向的棧頂保存著foo函式棧幀的ebp,把這個值恢復給ebp,同時esp增加4,esp的值變成0xbff1c408。

1.

把ebp的值賦給esp,現在esp的值是0xbff1c404。

2.

現在esp所指向的棧頂保存著foo函式棧幀的ebp,把這個值恢復給ebp,同時esp增加4,esp的值變成0xbff1c408。

最後是ret指令,它是call指令的逆操作:

現在esp所指向的棧頂保存著返回地址,把這個值恢復給eip,同時esp增加4,esp的值變成0xbff1c40c。

修改了程式計數器eip,因此跳轉到返回地址0x80483c2繼續執行。

1.

現在esp所指向的棧頂保存著返回地址,把這個值恢復給eip,同時esp增加4,esp的值變成0xbff1c40c。

2.

修改了程式計數器eip,因此跳轉到返回地址0x80483c2繼續執行。

地址0x80483c2處是foo函式的返回指令:

80483c2: c9 leave 80483c3: c3 ret

重複同樣的過程,又返回到了main函式。注意函式調用和返回過程中的這些規則:

參數壓棧傳遞,並且是從右向左依次壓棧。

ebp總是指向當前棧幀的棧底。

返回值通過eax暫存器傳遞。

1.

參數壓棧傳遞,並且是從右向左依次壓棧。

2.

ebp總是指向當前棧幀的棧底。

3.

返回值通過eax暫存器傳遞。

這些規則並不是體系結構所強加的,ebp寄 存器並不是必須這么用,函式的參數和返回值也不是必須這么傳,只是作業系統和編譯器選擇了以這樣的方式實現C代碼中的函式調用,這稱為Calling Convention,Calling Convention是作業系統二進制接口規範(ABI,Application Binary Interface)的一部分。

相關詞條

相關搜尋

熱門詞條

聯絡我們