棧由棧幀組成,每一個棧幀對應於一個(未執行完的)函數。接下來咱們經過講解棧幀的佈局、造成和消亡來理解棧幀在函數調用時是如何起做用的。小程序
棧幀的佈局圖10.7所示是一個簡單的測試程序,用於幫助咱們瞭解棧幀。app
embedded/code/application/stackframe/main.c函數
00001: #include <stdio.h>佈局
00002:測試
00003: //lint -e530 -e123ui
00004:spa
00005: void tail (int _param)3d
00006: {指針
00007: int local = 0;code
00008: int reg_esp, reg_ebp;
00009:
00010: asm volatile(
00011: // get EBP
00012: "movl %%ebp, %0 \n"
00013: // get ESP
00014: "movl %%esp, %1 \n"
00015: : "=r" (reg_ebp), "=r" (reg_esp)
00016: );
00017: printf ("tail (): EBP = %x\n", reg_ebp);
00018: printf ("tail (): ESP = %x\n", reg_esp);
00019: printf ("tail (): (EBP) = %x\n", *(int *)reg_ebp);
00020: printf ("tail (): return address = %x\n", *(((int *)reg_ebp + 1)));
00021: printf ("tail (): &local = %p\n", &local);
00022: printf ("tail (): ®_esp = %p\n", ®_esp);
00023: printf ("tail (): ®_ebp = %p\n", ®_ebp);
00024: printf ("tail (): &_param = %p\n", &_param);
00025: }
00026:
00027: int middle (int _p0, int _p1, int _p2)
00028: {
00029: int reg_esp, reg_ebp;
00030:
00031: asm volatile(
00032: // get EBP
00033: "movl %%ebp, %0 \n"
00034: // get ESP
00035: "movl %%esp, %1 \n"
00036: : "=r" (reg_ebp), "=r" (reg_esp)
00037: );
00038: tail (_p0);
00039: printf ("middle (): EBP = %x\n", reg_ebp);
00040: printf ("middle (): ESP = %x\n", reg_esp);
00041: printf ("middle (): (EBP) = %x\n", *(int *)reg_ebp);
00042: printf ("middle (): return address = %x\n", *(((int *)reg_ebp + 1)));
00043: printf ("middle (): ®_esp = %p\n", ®_esp);
00044: printf ("middle (): ®_ebp = %p\n", ®_ebp);
00045: printf ("middle (): &_p0 = %p\n", &_p0);
00046: printf ("middle (): &_p1 = %p\n", &_p1);
00047: printf ("middle (): &_p2 = %p\n", &_p2);
00048: return 1;
00049: }
00050:
00051: int main ()
00052: {
00053: int reg_esp, reg_ebp;
00054: int local = middle (1, 2, 3);
00055:
00056: asm volatile(
00057: // get EBP
00058: "movl %%ebp, %0 \n"
00059: // get ESP
00060: "movl %%esp, %1 \n"
00061: : "=r" (reg_ebp), "=r" (reg_esp)
00062: );
00063: printf ("main (): EBP = %x\n", reg_ebp);
00064: printf ("main (): ESP = %x\n", reg_esp);
00065: printf ("main (): (EBP) = %x\n", *(int *)reg_ebp);
00066: printf ("main (): return address = %x\n", *(((int *)reg_ebp + 1)));
00067: printf ("main (): ®_esp = %p\n", ®_esp);
00068: printf ("main (): ®_ebp = %p\n", ®_ebp);
00069: printf ("main (): &local = %p\n", &local);
00070: return 0;
00071: }
圖10.7
這個小程序的每一個函數中都嵌入了彙編代碼,以便得到各函數運行時刻ESP和EBP寄存器的值。另外,每個函數中都打印出了EBP寄存器所指向內存地址處的值,以及位於其後的函數返回地址,這樣作的緣由後面還會細講。圖10.8顯示了這一程序的編譯和運行結果。
yunli.blog.51CTO.com /embedded/build
$ make
yunli.blog.51CTO.com /embedded/build
$ ./release/stackframe.exe
tail (): EBP = 22cd08
tail (): ESP = 22ccf0
tail (): (EBP) = 22cd28
tail (): return address = 40120b
tail (): &local = 0x22cd04
tail (): ®_esp = 0x22cd00
tail (): ®_ebp = 0x22ccfc
tail (): &_param = 0x22cd10
middle (): EBP = 22cd28
middle (): ESP = 22cd10
middle (): (EBP) = 22cd58
middle (): return address = 401302
middle (): ®_esp = 0x22cd24
middle (): ®_ebp = 0x22cd20
middle (): &_p0 = 0x22cd30
middle (): &_p1 = 0x22cd34
middle (): &_p2 = 0x22cd38
main (): EBP = 22cd58
main (): ESP = 22cd30
main (): (EBP) = 22cd98
main (): return address = 61006e73
main (): ®_esp = 0x22cd50
main (): ®_ebp = 0x22cd4c
main (): &local = 0x22cd48
圖10.8
爲 了更好地理解輸出結果中各數據間的關係,咱們將其轉化爲圖,如圖10.9所示。圖的左邊還示例說明了棧的增加方向和棧的內存地址。黑色的箭頭和寄存器名錶 示當前棧幀,不然用灰色表示。圖中表示的是站在tail()函數內所看到的棧佈局,其中完整地示例說明了tail()和middle()兩個函數的棧幀結 構,以及main()函數的一部分。
在一般情形下,每一個函數都有本身的棧幀。各棧幀中存在一個域用於存放前一個調用函數的棧幀基址,經過這個 域將全部調用與被調用函數的棧幀以鏈表的形式連在一塊兒。棧幀的這種組織結構說明了爲何函數調用級數越多,所佔用的棧空間也越大,也解釋了爲何在嵌入式 軟件開發中咱們須要當心使用遞歸函數。
棧幀的造成爲了方便講解,咱們還得獲取圖10.7所示的示例程序所對應的彙編代碼片斷,如圖10.10所示。圖中刪除了tail()函數彙編代碼的中間部分,而只保留了頭和尾用於建立和刪除棧幀的內容。在彙編代碼中,最左邊列出了指令在內存中的地址,在接下來說解棧幀中的返回地址(return address)信息時,其所指的內容就是指這一地址。
yunli.blog.51CTO.com /embedded/build
$ objdump -d ./release/stackframe.exe > stackframe.txt
yunli.blog.51CTO.com /embedded/build
$ vi stackframe.txt
00401130 <_tail>:
401130: 55 push %ebp
401131: 89 e5 mov %esp,%ebp
401133: 83 ec 18 sub $0x18,%esp
401136: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
40113d: 89 ea mov %ebp,%edx
40113f: 89 e0 mov %esp,%eax
401141: 89 55 f4 mov %edx,-0xc(%ebp)
401144: 89 54 24 04 mov %edx,0x4(%esp)
401148: 89 45 f8 mov %eax,-0x8(%ebp)
40114b: c7 04 24 a0 20 40 00 movl $0x4020a0,(%esp)
…… 顯示結果有刪減 ……
4011e1: c9 leave
4011e2: c3 ret
004011f0 <_middle>:
4011f0: 55 push %ebp
4011f1: 89 e5 mov %esp,%ebp
4011f3: 83 ec 18 sub $0x18,%esp
4011f6: 89 e8 mov %ebp,%eax
4011f8: 89 e2 mov %esp,%edx
4011fa: 89 45 f8 mov %eax,-0x8(%ebp)
4011fd: 8b 45 08 mov 0x8(%ebp),%eax
401200: 89 55 fc mov %edx,-0x4(%ebp)
401203: 89 04 24 mov %eax,(%esp)
401206: e8 25 ff ff ff call 401130 <_tail>
40120b: 8b 45 f8 mov -0x8(%ebp),%eax
40120e: c7 04 24 44 21 40 00 movl $0x402144,(%esp)
401215: 89 44 24 04 mov %eax,0x4(%esp)
401219: e8 da 01 00 00 call 4013f8 <_printf>
…… 顯示結果有刪減 ……
圖10.10
如今假設程序運行在main()剛調用middle()函數的時刻,讓咱們看一看棧佈局是如何發生變化的。程序一進入middle()函數所運行的第一條指令位於內存地址4011f0處,在運行這一指令以前的棧結構如圖10.11所示。此時的EBP仍是指向main()函數棧幀的頭部,而ESP所指向的內存中所存放的是程序返回到main()函數的指令位置,後面分析middle()函數對tail()函數的調用時還將涉及這一點。
內存地址4011f0~4011f3的指令的做用就是造成middle()函數的棧幀。第一條指令(位於內存地址4011f0處)是將調用函數(即main()函數,middle()是被調用函數)的棧幀基址保存到棧上,這條指令是一個壓棧操做。正是各函數內的這一操做,使得全部的棧幀連在了一塊兒成爲一條鏈。
第二條指令(位於內存地址4011f1處)將ESP寄存器的值賦值給EBP寄存器,也就是說,此時的ESP寄存器中保存的是middle()函數的棧幀基址。請注意,基址並無將用於保存返回地址的空間包含在內。
第三條指令(位於內存地址4011f3處)對ESP進行一個減操做,即將ESP向低地址處移動24個字節(對應於十六進制的0x18),移動24個字節的目地是爲了在棧上騰出空間來存放局部變量和本函數需調用函數的傳入參數。顯然,函數內局部變量越大,則所減的數值就越大。
運行完了上面的三條指令之後,middle()函數的棧幀就造成了,如圖10.12所示。圖中還示例說明了middle()函數內局部變量reg_esp和reg_ebp在棧幀中的位置。
位於內存地址4011f6和4011f8處的指令是咱們在middle()函數中所嵌入的彙編代碼即用於獲取此時EBP和ESP寄存器的值。4011fa處的指令將EBP寄存器的值放入局部變量reg_ebp中,401200處的指令將ESP寄存器的值放入局部變量reg_esp中。4011fd和401203處的指令將main()函數中傳遞過來的第一個變量_p0的值拷貝到ESP寄存器所指向的內存中,爲調用tail()函數準備參數。此刻的棧空間如圖10.13所示。
位於內存地址401206處的指令是調用tail()函數的指令,這個調用會形成返回地址被壓入到棧中,調用完了這條指令後的棧空間如圖10.14所示。
所壓入棧的返回地址是40120b,從圖10.10中能夠看出這一地址指向的是middle()函數內調用tail()函數的後一條指令,也就是說,當tail()函數返回時將從這一地址處繼續運行程序。這條指令的調用也意味着進入了tail()函數的棧幀,tail()函數也像middle()函數那樣採用相同的「手法」創建本身的棧幀。前面圖10.9所示的內存佈局,正是tail()函數創建了棧幀時的。
棧幀的消亡
下面讓咱們看一看在tail()函數內進行函數返回時棧空間又是如何發生變化的。內存地址4011e1處的leave指令,其功能是將ESP寄存器的值設置爲EBP寄存器的並作一次退棧操做,將退棧操做的內容放入EBP寄存器中。這條指令的功能等價於「mov %ebp, %esp; pop %ebp」,就是將tail()函數所創建的棧幀去掉。這條指令執行完了後的棧佈局與圖10.14徹底同樣。tail()函數的最後是一條返回指令(位於內存地址4011e2處),用於將棧上(即ESP寄存器所指的位置)的內容彈出到PC寄存器中,其效果就是程序返回到了middle()函數的40120b地址處。執行完這條指令後的棧結構與圖10.13是同樣的。
至此,咱們徹底瞭解了棧幀的造成與消亡。實際上,對於每個C函數,編譯器都會生成彙編代碼在進入函數時建立其棧幀,以及從函數返回時將棧幀刪除。在x86的ABI規範中,分別稱這兩部分爲「前言」和「後序」,其大體代碼分別如圖10.15和圖10.16所示。
prologue:
pushl %ebp // 保存上一函數的棧幀指針
movel %esp, %ebp // 設置本函數的棧幀指針
subl $80, %ebp // 分配函數的棧幀空間(會因各函數的局變量大小不一樣而不一樣)
pushl %edi // 保存局部變量寄存器
pushl %esi // 保存局部變量寄存器
pushl %ebx // 保存局部變量寄存器
圖10.15
epilogue:
popl %ebx // 恢復局部變量寄存器
popl %esi // 恢復局部變量寄存器
popl %edi // 恢復局部變量寄存器
leave // 恢復調用這一函數的棧幀指針
ret // 返回到調用這一函數的函數內
圖10.16
在每個函數的「前言」部分存在爲棧幀分配大小的指令(好比圖10.15中的「subl $80, %ebp」),C編譯器會根據函數中所存在的局部變量大小和所調用函數最多參數的個數來決定棧幀的大小。
另外在這兩個圖中分別存在對EDI、ESI和EBX的壓棧及退棧操做。在10.3節中提到,EDI、ESI和EBX是用作局部變量寄存器的。也就是說,若是這三個寄存器器在某函數(稱之爲函數A)中使用了,而在其調用的函數(稱之爲函數B)中也要用到它的話,那麼函數B就必須在使用它們以前將它們保存起來,以便返回到函數A以前能恢復。但若是這兩個函數都沒有使用到這些寄存器,「聰明的」編譯器會作出無須在「前言」中對其壓棧的決定,以便提升程序的執行效率。
因爲函數一旦返回其棧幀就不存在了,正因如此,咱們不能將局部變量的指針做爲函數的返回值。
若是讀者如今回頭看一看圖10.6中的表,相信能更好地理解其含義。