1. 程式人生 > >函式呼叫棧幀的分析

函式呼叫棧幀的分析

先來看一段程式碼

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

void fun(){
    printf("\n\nfun runing\n");
    Sleep(5000);
    printf("fun end\n");
    exit(1);
}

int fun1(int a,int b){
    printf("\n\nfun1 runing\n");
    int *p = &a;
    p--;
    *p = fun;
    p = &a;
    *(
++p) = 0xcccc; int d = 0xdddd; printf("fun1 end\n"); return d; } int main(){ printf("runing\n"); int a = 0xaaaa; int b = 0xbbbb; int c = fun1(a,b); printf("a = %d\nb = %d\nc = &d\n",a,b,c); printf("should be here\n"); }

這段小程式在vs2013中的執行結果:
1

為什麼會是這個結果嘞。按照main函式的邏輯,應該是在呼叫完fun1函式之後返回到main函式中繼續執行之後的程式碼,詭異的是非但沒有回到主函式中反而進入了fun函式。這個現象是很可怕的,如果這個fun函式是一段惡意程式,那後果不敢想象。
要解釋上述現象的原因,就必須說一說函式呼叫原理–棧幀。

一、當可執行程式執行的時候可執行檔案載入到記憶體中,編譯器會為對其分配一段空間,在邏輯上可以分為程式碼段,資料段,堆,棧;
程式碼段:儲存程式文字,指令指標EIP就是指向程式碼段,可讀可執行不可寫
資料段:儲存初始化的全域性變數和靜態變數,可讀可寫不可執行
BSS:未初始化的全域性變數和靜態變數
堆(Heap):動態分配記憶體,向地址增大的方向增長,可讀可寫可執行
棧(Stack):存放區域性變數,函式引數,當前狀態,函式呼叫資訊等,向地址減小的方向增長,非常非常重要,可讀可寫可執行
如圖所示:
2
暫存器
EAX:累加(Accumulator)暫存器,常用於函式返回值
EBX:基址(Base)暫存器,以它為基址訪問記憶體
ECX:計數器(Counter)暫存器,常用作字串和迴圈操作中的計數器
EDX:資料(Data)暫存器,常用於乘除法和I/O指標
ESI:源變址暫存器
DSI:目的變址暫存器
ESP:堆疊(Stack)指標暫存器,指向堆疊頂部
EBP:基址指標暫存器,指向當前堆疊底部
EIP:指令暫存器,指向下一條指令的地址

二、當發生函式呼叫的時候,棧空間中存放的資料是這樣的:
1、呼叫者函式把被調函式所需要的引數按照與被調函式的形參順序相反的順序壓入棧中,即:從右向左依次把被調函式所需要的引數壓入棧;
2、呼叫者函式使用call指令呼叫被調函式,並把call指令的下一條指令的地址當成返回地址壓入棧中(這個壓棧操作隱含在call指令中);
3、在被調函式中,被調函式會先儲存呼叫者函式的棧底地址(push ebp),然後再儲存呼叫者函式的棧頂地址,即:當前被調函式的棧底地址(mov ebp,esp);
4、在被調函式中,從ebp的位置處開始存放被調函式中的區域性變數和臨時變數,並且這些變數的地址按照定義時的順序依次減小,即:這些變數的地址是按照棧的延伸方向排列的,先定義的變數先入棧,後定義的變數後入棧;
所以,發生函式呼叫時,入棧的順序為:
引數N
引數N-1
…..
引數2
引數1
函式返回地址
上一層呼叫函式的EBP/BP
區域性變數1
區域性變數2
….
區域性變數N
函式呼叫棧如下圖所示:
3
解釋:
首先,將呼叫者函式的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<stdio.h>

void fun(int a,int b,int c){
    int x = 0xffffffff;
    int y = 0xeeeeeeee;
    int z = 0xdddddddd;
    int *pa = &a;
    int *pb = &b;
    int *pc = &c;
    printf("\n\n引數c的地址      %x   %x\n", pc,c);
    printf("引數b的地址      %x   %x\n", pb,b);
    printf("引數a的地址      %x   %x\n\n", pa,a);
    printf("\n區域性變數x的地址  %x   %x\n", &x,x);
    printf("區域性變數y的地址  %x   %x\n", &y,y);
    printf("區域性變數z的地址  %x   %x\n", &z,z);
}

int main(){
    int a = 0xaaaaaaaa;
    int b = 0xbbbbbbbb;
    int d = 0xdddddddd;
    int c[10] = { 0 };
    printf("\n\n區域性變數a的地址  %x   %x\n",&a,a);
    printf("區域性變數b的地址  %x   %x\n", &b,b);
    printf("陣列c[9]的地址   %x\n",&c[9]);
    printf("陣列c[0]的地址   %x\n\n", &c[0]);
    fun(a,b,d);
    getchar();
}

執行結果:
4
結果分析:
在main函式中可以看到區域性變數的分佈是按照總高地址向低地址排布,對於陣列來說,首地址在低地址,尾在高地址。然後再main函式中呼叫了fun函式,此時發生函式呼叫棧幀。可以看到函式引數列表中的引數排布是按照從引數c開始到引數a結束,地址依次減小;接下來是函式的返回地址。然後是fun函式的區域性變數,按照先後定義的次序從高地址處開始依次排布。

三、關於堆疊空間利用最核心的一點就是:函式呼叫棧。而要深入理解函式呼叫棧,最重要的兩點就是:棧的結構變化,ebp暫存器的作用。
首先要認識到這樣兩個事實:
1. 一個函式呼叫動作可分解為:零到多個push指令(用於引數入棧),一個call指令。call指令內部其實還暗含了一個將eip返回地址(即call指令下一條指令的地址)壓棧的動作。
2. 幾乎所有本地編譯器都會在每個函式體之前插入類似的指令:push %ebp,mov %esp %ebp。
因此,在程式執行到一個函式的真正函式體的時候,已經有以下資料壓入到堆疊中:零到多個引數,返回地址eip,ebp。
由此得到如下的棧結構(其中引數入棧順序跟呼叫方式有關,這裡以C語言預設的CDECL為例):
5

首先將ebp入棧,然後將棧頂指標esp賦值給ebp。“mov %esp %ebp”這條指令表面上看是用esp把ebp原來的值覆蓋了,其實不然,因為在給ebp賦值之前,原ebp值已經被壓棧(位於棧頂),esp賦值給ebp後,ebp恰好指向棧頂(即被壓棧的原esp的位置)。
此時,ebp暫存器就處在一個非常重要的地位,該暫存器中儲存著棧中的一個地址(原ebp入棧後的棧頂),以該地址為基準,向上(棧底方向)能獲取返回地址,函式呼叫引數值;向下(棧頂方向)能獲取函式區域性變數值;而該地址處又儲存著上一層函式呼叫時的ebp值!!一般而言,ss:[ebp+4]處為返回地址,ss:[ebp+8]處為第一個引數值(最後一個入棧的引數值,此處假設其佔用4位元組記憶體),ss:[ebp-4]處為第一個區域性變數,ss:[ebp]處為上一層ebp值。
由於ebp中的地址總是“上一層函式呼叫時的ebp值”,而在每一層函式呼叫中,都能通過當時的ebp值“向上(棧底方向)能獲取返回地址、函式呼叫引數,向下(棧頂方向)能獲取函式區域性變數值”。如此形成遞迴,直至到達棧底。這就是函式呼叫棧。由此看見,編譯器對於ebp暫存器的使用實在是太精妙了。此外,從當前ebp出發,逐層向上找到所有的ebp是非常容易的。

四、現在回到最開始的程式。應該很清楚出現這種現行的原因了。
在fun函式中,將第一個引數a的地址賦予int* p變數,p–即可獲得函式的返回地址,因此通過*p = fun指令;將返回地址修改為fun的入口地址。讓原本應該返回main函式的程式進入到了fun函式中。在使用p也可以獲得引數b的地址,如果獲得了引數的地址,修改其內容就很簡單啦。通過下面三行程式碼很輕易的糾可以不是用引數b的名稱達到訪問引數b的效果。

int *p = &a;
p++;
*p = 0x12345678;