在x86的計(jì)算機(jī)系統(tǒng)中憋他,內(nèi)存空間中的棧主要用于保存函數(shù)的參數(shù)于置,返回值,返回地址吧恃,本地變量等。一切的函數(shù)調(diào)用都要將不同的數(shù)據(jù)冒晰、地址壓入或者彈出棧同衣。因此,為了更好地理解函數(shù)的調(diào)用壶运,我們需要先來(lái)看看棧是怎么工作的。
棧是什么浪秘?
簡(jiǎn)單來(lái)說(shuō)蒋情,棧是一種LIFO形式的數(shù)據(jù)結(jié)構(gòu),所有的數(shù)據(jù)都是后進(jìn)先出耸携。這種形式的數(shù)據(jù)結(jié)構(gòu)正好滿足我們調(diào)用函數(shù)的方式:父函數(shù)調(diào)用子函數(shù)棵癣,父函數(shù)在前,子函數(shù)在后夺衍;返回時(shí)狈谊,子函數(shù)先返回,父函數(shù)后返回沟沙。棧支持兩種基本操作河劝,push和pop。push將數(shù)據(jù)壓入棧中矛紫,pop將棧中的數(shù)據(jù)彈出并存儲(chǔ)到指定寄存器或者內(nèi)存中赎瞎。
這里是一個(gè)push操作的例子。假設(shè)我們有一個(gè)棧颊咬,其中黃色部分是已經(jīng)寫(xiě)入數(shù)據(jù)的區(qū)域务甥,綠色部分是還未寫(xiě)入數(shù)據(jù)的區(qū)域。現(xiàn)在我們將0x50壓入棧中:
// 將0x50的壓入棧
push $0x50
我們?cè)賮?lái)看看pop操作的例子:
// 將0x50彈出棧
pop
這里有兩點(diǎn)需要注意的喳篇,第一敞临,上面例子中棧的生長(zhǎng)方向是從高地址到低地址的,這是因?yàn)樵谙挛闹v的棧幀中麸澜,棧就是向下生長(zhǎng)的挺尿,因此這里也用這種形式的棧;第二痰憎,pop操作后票髓,棧中的數(shù)據(jù)并沒(méi)有被清空,只是該數(shù)據(jù)我們無(wú)法直接訪問(wèn)铣耘。有了這些棧的基本知識(shí)洽沟,我們現(xiàn)在可以來(lái)看看在x86-32bit系統(tǒng)下,C語(yǔ)言函數(shù)是如何調(diào)用的了蜗细。
棧幀是什么裆操?
棧幀怒详,也就是stack frame,其本質(zhì)就是一種棧踪区,只是這種棧專門用于保存函數(shù)調(diào)用過(guò)程中的各種信息(參數(shù)昆烁,返回地址,本地變量等)缎岗。棧幀有棧頂和棧底之分静尼,其中棧頂?shù)牡刂纷畹停瑮5椎牡刂纷罡叽矗琒P(棧指針)就是一直指向棧頂?shù)氖竺臁T趚86-32bit中,我們用 %ebp
指向棧底眷细,也就是基址指針拦盹;用 %esp
指向棧頂,也就是棧指針溪椎。下面是一個(gè)棧幀的示意圖:
一般來(lái)說(shuō)普舆,我們將
%ebp
到 %esp
之間區(qū)域當(dāng)做棧幀(也有人認(rèn)為該從函數(shù)參數(shù)開(kāi)始,不過(guò)這不影響分析)校读。并不是整個(gè)椪勇拢空間只有一個(gè)棧幀,每調(diào)用一個(gè)函數(shù)地熄,就會(huì)生成一個(gè)新的棧幀华临。在函數(shù)調(diào)用過(guò)程中,我們將調(diào)用函數(shù)的函數(shù)稱為“調(diào)用者(caller)”端考,將被調(diào)用的函數(shù)稱為“被調(diào)用者(callee)”雅潭。在這個(gè)過(guò)程中,1)“調(diào)用者”需要知道在哪里獲取“被調(diào)用者”返回的值却特;2)“被調(diào)用者”需要知道傳入的參數(shù)在哪里扶供,3)返回的地址在哪里。同時(shí)裂明,我們需要保證在“被調(diào)用者”返回后椿浓,%ebp
, %esp
等寄存器的值應(yīng)該和調(diào)用前一致。因此闽晦,我們需要使用棧來(lái)保存這些數(shù)據(jù)扳碍。
函數(shù)調(diào)用實(shí)例
函數(shù)的調(diào)用
我們直接通過(guò)實(shí)例來(lái)看函數(shù)是如何調(diào)用的。這是一個(gè)有參數(shù)但沒(méi)有調(diào)用任何函數(shù)的簡(jiǎn)單函數(shù)仙蛉,我們假設(shè)它被其他函數(shù)調(diào)用笋敞。
int MyFunction(int x, int y, int z)
{
int a, b, c;
a = 10;
b = 5;
c = 2;
...
}
int TestFunction()
{
int x = 1, y = 2, z = 3;
MyFunction1(1, 2, 3);
...
}
對(duì)于這個(gè)函數(shù),當(dāng)調(diào)用時(shí)荠瘪,MyFunction()
的匯編代碼大致如下:
_MyFunction:
push %ebp ; //保存%ebp的值
movl %esp, $ebp ; //將%esp的值賦給%ebp夯巷,使新的%ebp指向棧頂
movl -12(%esp), %esp ; //分配額外空間給本地變量
movl $10, -4(%ebp) ;
movl $5, -8(%ebp) ;
movl $2, -12(%ebp) ;
光看代碼可能還是不太明白赛惩,我們先來(lái)看看此時(shí)的棧是什么樣的:
此時(shí)調(diào)用者做了兩件事情:第一,將被調(diào)用函數(shù)的參數(shù)按照從右到左的順序壓入棧中趁餐。第二喷兼,將返回地址壓入棧中。這兩件事都是調(diào)用者負(fù)責(zé)的后雷,因此壓入的棧應(yīng)該屬于調(diào)用者的棧幀季惯。我們?cè)賮?lái)看看被調(diào)用者,它也做了兩件事情:第一喷面,將老的(調(diào)用者的)
%ebp
壓入棧星瘾,此時(shí) %esp
指向它。第二惧辈,將 %esp
的值賦給 %ebp
, %ebp
就有了新的值,它也指向存放老 %ebp
的椏拇桑空間盒齿。這時(shí),它成了是函數(shù) MyFunction()
棧幀的棧底困食。這樣边翁,我們就保存了“調(diào)用者”函數(shù)的 %ebp
,并且建立了一個(gè)新的棧幀硕盹。
只要這步弄明白了符匾,下面的操作就好理解了。在 %ebp
更新后瘩例,我們先分配一塊0x12字節(jié)的空間用于存放本地變量啊胶,這步一般都是用 sub
或者 mov
指令實(shí)現(xiàn)。在這里使用的是 movl
垛贤。通過(guò)使用 mov
配合 -4(%ebp)
, -8(%ebp)
和 -12(%ebp)
我們便可以給 a
, b
和 c
賦值了焰坪。
函數(shù)的返回
上面講的都是函數(shù)的調(diào)用過(guò)程,我們現(xiàn)在來(lái)看看函數(shù)是如何返回的聘惦。從下面這個(gè)例子我們可以看出某饰,和調(diào)用函數(shù)時(shí)正好相反。當(dāng)函數(shù)完成自己的任務(wù)后善绎,它會(huì)將 %esp
移到 %ebp
處黔漂,然后再?gòu)棾雠f的 %ebp
的值到 %ebp
。這樣禀酱,%ebp
就恢復(fù)到了函數(shù)調(diào)用前的狀態(tài)了炬守。
int MyFunction( int x, int y, int z )
{
int a, int b, int c;
...
return;
}
其匯編大致如下:
_MyFunction:
push %ebp
movl %esp, %ebp
movl -12(%esp), %esp
...
mov %ebp, %esp
pop %ebp
ret
我們注意到最后有一個(gè) ret
指令,這個(gè)指令相當(dāng)于 pop + jum
比勉。它首先將數(shù)據(jù)(返回地址)彈出棧并保存到 %eip
中劳较,然后處理器根據(jù)這個(gè)地址無(wú)條件地跳到相應(yīng)位置獲取新的指令驹止。
總結(jié)
到這里,C函數(shù)的調(diào)用過(guò)程就基本講完了观蜗。函數(shù)的調(diào)用其實(shí)不難臊恋,只要搞懂了如何保存以及還原 %ebp
和 %esp
,就能明白函數(shù)是如何通過(guò)棧幀進(jìn)行調(diào)用和返回的了墓捻。希望這篇文章對(duì)你有幫助抖仅!
引用
在我學(xué)習(xí)棧幀以及寫(xiě)這篇文章的過(guò)程中,參考了下面這些文章砖第,在這我感謝他們對(duì)我提供的大力的幫助撤卢。如果你對(duì)這些文章感興趣,請(qǐng)?jiān)L問(wèn)以下鏈接:
1. x86 Instruction Set Reference
2. x86 Disassembly/Functions and Stack Frames
3. x86 Assembly Guide