1. 程式人生 > >Linux下的函式呼叫原理—棧幀

Linux下的函式呼叫原理—棧幀

首先我們先來看一段程式碼

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>


void fun()
{
printf("haha  \n");
sleep(2);
printf("you are done");
sleep(3);
system("reboot");
exit(1);
}
int fun1(int a,int b)
{
int *p=&a;
p--;
*p=fun;
int c=0xcccc;

printf("dao zhe le mei");
return c;
}


int main()
{
printf("begin runn...\n");
int a=0xaaaa;
int b=0xbbbb;
fun1(a,b);
printf("you should run here\n");
return 0;
}


LZ在centos和vs2013下執行,執行結果如下,在輸出幾句資訊後系統直接關閉。

Linux下的執行結果:


vs下的執行結果:

我想這時肯定很多人感覺到很奇怪,我又沒直接呼叫fun()函式,他怎麼自己就執行了呢。其實這裡用到了函式呼叫中棧幀的知識。

#include <stdio.h>
void swap(int *a, int * b)
{
int c;
c = * a;
* a = *b ;
* b = c;
}
int main(void )
{
int a;
int b;
int ret;
a = 16;
b = 64;
ret = 0;
swap(&a, &b);
return ret;
}

我們通過這個例子來了解下函式呼叫過程。


通過這張圖片我們可以發現在每次函式呼叫的過程中函式都首先要將當前執行的地址先壓入棧中,之後才將ebp移動到返回地址的下一位,esp則根據函式的內部元素進行調整。在上面的例子中,*p=&a,--p;這兩句其實是使指標p指向此次函式呼叫結束的返回地址處,我們在之後通過*p=fun重新給這塊空間賦值,所以在此次函式呼叫結束時程式並不會跳到之前函式呼叫前的位置,而是跳轉到我們重新賦值處,所以此時程式會執行fun函式。

為了驗證我們的猜想,我們更改程式為:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>


void fun()
{
printf("haha  \n");
sleep(2);
printf("you are done");
sleep(3);
system("reboot");
exit(1);
}
int fun1(int a,int b)
{
int *p=&b;
p--;

   p--;
*p=fun;
int c=0xcccc;
return c;
}

int main()
{
printf("begin runn...\n");
int a=0xaaaa;
int b=0xbbbb;
fun1(a,b);
printf("you should run here\n");
return 0;
}
程式執行結果為:


很明顯,通過b也可以使程式跳轉到fun()函式處。

總結下函式呼叫的堆疊實現過程:

見下圖,假設函式A呼叫該函式B,我們稱函式A為“呼叫者”,B函式為"被呼叫者“則函式呼叫過程可以這麼描述:
(1)先將呼叫者(A)的堆疊的基址(ebp)入棧,以儲存之前任務的資訊。

(2)然後呼叫者(A)的棧頂指標(esp)的值賦給ebp,作為新的基址(即被呼叫者B的棧底)。

(3)然後在這個基址(被呼叫者B的棧底)上開闢(一般用sub指令)相應的空間用作被呼叫者B的棧空間。

(4)函式B返回後,從當前棧幀的ebp即恢復為呼叫者A的棧頂(esp),使棧頂恢復函式B被呼叫前的位置;然後呼叫者A再從恢復後的棧頂可彈出之前的ebp值(可以)這麼做是因為這個值在函式呼叫的前一步被壓入堆疊)。這樣,ebp和esp就都恢復了呼叫函式B前的位置,也就是恢復函式B 呼叫前的狀態。


寫的比較簡單,在之後的深入學習後再視情況進行補充。