現在,讓我們看看程序調用過程中彙編操作,一個過程調用包括將數據(以過程參數和返回值的形式), 和控制從代碼的一部分傳遞到令一部分。另外,它還必須在進入是爲過程的局部變量分配空間,並在退出時釋放這些空間。
棧幀結構
IA32程序用程序棧來支持過程調用。機器用棧來傳遞過程參數,存儲返回信息,保存寄存器用於以後恢復,以及本地存儲,爲整個過程分配的那部分棧稱爲棧幀(stack frame)。最頂端的棧幀以兩個指針界定,寄存器%ebp爲幀指針,而寄存器%esp爲棧指針。當程序執行時,棧指針是可以移動的,因此絕大多數信息的訪問都是相對於幀指針的。
轉移控制
call指令:其效果是將返回值地址入棧,並跳轉到被調用過程的起始處。返回地址是在程序中緊跟在call後面的那條指令的地址。這樣當被調用函數返回時,執行會從此處繼續。ret指令從棧中彈出地址,並跳轉到這個位置。例如下面代碼:
int accum = 0;
int sum(int x,int y);
int main()
{
return sum(1.3);
}
int sum(int x,int y)
{
int t = x + y;
accum += t;
return t;
}
經過反彙編後,節選處call部分的代碼如下圖所示:
在main函數中我們可以看到,在main函數中,地址爲0x080483dc的call指令調用函數sum,指明瞭棧指針%esp和程序計數器%eip的值。我們可以看到在main函數中,地址0x080483dc的下一個執行地址是是0x080483e1,這一點很重要。call指令的效果是將返回地址0x080483e1壓入棧中,並跳到函數sum的第一條指令,地址爲0x8048394。函數sum繼續執行,直到遇到地址爲0x080483a4的ret指令。這條指令從棧中彈出值0x080483e1,然後跳轉到這個地址,就在調用sum的call函數之後,繼續main函數的執行。
寄存器使用示例
int swap_add(int* xp,int* yp);
int caller()
{
int arg1 = 534;
int arg2 = 1057;
int sum = swap_add(&arg1 , &arg2);
int diff = arg1 - arg2;
retur sum * diff;
}
int swap_add(int* xp,int* yp)
{
int x = * xp;
int y = * yp;
*xp = y;
*yp = x;
return x + y;
}
我們先來看看caller函數調用swap_add正在運行時的棧幀結構。有些指令訪問的棧位置是相對於棧指針%esp的,而另一些的訪問的棧位置是相對於基地址指針%ebp的。
caller:
push1 %ebp
movl %esp,%ebp
sub1 $24, %esp
movl $534,-4(%ebp)
movl $1057,-8(%ebp)
leal -8(%ebp),%eax //compute &arg2
movl %eax,4(%esp)
leal -4(%ebp),%eax //compute &arg1
movl %eax,(%esp)
call swap_add
我們可以看到,在調用swap_add之前,我們用push1指令把%ebp的數據壓入棧,然後爲這個棧分配24字節。caller的幀棧包括局部變量arg1和arg2的存儲,其位置相對於幀指針是-4和-8。這些變量必須存在棧中,因爲我們必須爲他們生成地址。
在這裏我們可以看到分配給棧幀的24個字節中,8個用於局部變量,8個用於向swap_add傳遞參數,還有8個未使用。因爲GCC堅持一個x86編程指導方針,也就是一個函數使用的所有棧空間必須是16字節的整數倍,包括保存%ebp值的4個字節和返回值的4個字節,caller一共使用了32個字節,採用這個規則是爲了保證訪問數據的嚴格對齊。