棧幀詳解———函式呼叫原理
前言:我們知道呼叫函式對這個函式傳參時,形參例項化時會形成一份臨時拷貝,在函式返回時這些臨時拷貝又被釋放;那麼呼叫函式時這些引數是如何儲存、被儲存在哪裡?又是如何釋放的呢?在呼叫函式返回時是如何返回的?返回值是通過什麼返回?類似的這些函式呼叫問題都可以通過棧幀原理解釋
下面我通過一些簡單的例項來分析函式呼叫原理------棧幀
首先應該明白,棧是從高地址向低地址延伸的。每個函式的每次呼叫,都有它自己獨立的一個棧幀,這個棧幀中維持著所需要的各種資訊。每個函式都有一份屬於自己的ebp和esp,cpu只有一份ebp和esp,那麼怎樣讓每個函式都有ebp和esp指向棧底和棧頂呢?因為ebp和esp永遠儲存最新當前函式的值
暫存器ebp指向當前的棧幀的底部(高地址)
暫存器esp指向當前的棧幀的頂部(地址地)
PC指標:永遠指向當前執行程式指令的下一條指令
下圖為典型的存取器安排,觀察棧在其中的位置
見上圖,————黑色線指向的是呼叫者函式main()
————藍色線指向的是被呼叫者函式fun()
————紅色線指向的的是函式fun()返回時,恢復到呼叫函式fun()之前的狀態
CPU在重複的執行三個操作:取指令、分析指令、執行指令
取指令通過pc指標,分析程式通過cpu裡的各種指令集
通過下面這個程式碼及反彙編來分析函式呼叫原理:
#include<stdio.h> #include<windows.h> int fun(int x, int y) { int c = 0xcccccccc; return c; } int main() { int a = 0xaaaaaaaa; int b = 0xbbbbbbbb; int ret = fun(a, b); printf("You should runing here!\n"); system("pause"); return 0; }
進入main函式:
形參例項化形成臨時拷貝壓入棧的過程是在呼叫者函式(main函式)內實現的:
b和a依次壓入棧後執行call命令,call命令的作用如下圖所示:
jmp 使Pc指標指向fun函式(地址空間圖上第三條藍線;從上往下數,下面一樣)
呼叫fun函式棧形成過程:
push ebp 首先將esp指標下移,然後把ebp暫存器的內容(main函式的棧底地址)
存到fun函式的當前棧頂(當前esp指向的地址);
mov ebp, esp 把esp的內容賦給ebp;(地址空間圖上第一個藍色線)fun函式的棧底。
sub esp,
二條藍色線)
fun函式返回時的過程:
mov esp, ebp 把esp指向ebp的內容,形成新的棧頂
pop ebp 將棧頂的內容(main:ebp)彈出來放在ebp(地址空間圖上的第一個
紅線),把esp指標上移。
ret 把main:retaddr返回給pc ,esp上移;使pc指向main函式的下一
條指令;(地址空間圖上的第三條紅線)
add esp,8 fun函式返回後a和b就會被釋放,所以這裡給esp加兩個整形的位元組,
使esp恢復到形參例項化前的位置(地址空間圖上第二條紅色線)
fun函式的返回值c是通過eax暫存器返回給main函式的
—————————————————————————————————————————————————————
以上就是一個函式呼叫的整個過程及原理。
下面我們在看幾個程式碼來了解壓入棧的變數之間的地址關係:
1.想一想如何不改變形參y的值,通過形參x來改變y的值呢?
#include<stdio.h>
#include<windows.h>
int fun(int x, int y)
{
printf("修改之前:\nx -> %d\ny -> %d\n", x, y);
int c = 0xcccccccc;
int *p = &x;
p += 1;
*p = 20;
printf("修改之後:\nx -> %d\ny -> %d\n", x, y);
return c;
}
int main()
{
int a = 10;
int b = 10;
int ret = fun(a, b);
printf("You should runing here!\n");
system("pause");
return 0;
}
從地址空間圖裡可以發現,形參例項化是從左往右,所以棧空間裡a的地址低b的地址高,又因為都是整形所以相差四個位元組,a+1便是b的地址
2.那麼被呼叫的函式定義的第一個變數與它的第一個形參之間在棧空間中相差幾個位元組
呢?下面看這個程式碼:
#include<stdio.h>
#include<windows.h>
int fun(int x, int y)
{
int c = 0xffffffff;
printf("%p\n", &c);
printf("%p\n", &x);
printf("%p\n", &y);
return c;
}
int main()
{
int a = 0xaaaaaaaa;
int b = 0xbbbbbbbb;
int ret = fun(a, b);
printf("You should runing here!\n");
system("pause");
return 0;
}
從這個程式碼可以發現fun函式的第一個變數c與形參x之間相差20個位元組,我們從地址空間中可以發現a與fun函式返回地址相鄰,所以相差四個位元組,在vs2013中c與a相差20個位元組,所以c與函式返回地址相差16個位元組,這個和編譯環境有關,不同的環境可能不同。
3.在看一個不通過fun函式呼叫,然後在fun函式裡插入一個bug函式,這是怎樣實現的呢?這裡也是通過上面已知的地址關係進行插入的:
#include<stdio.h>
#include<windows.h>
void *add = NULL;
void bug()
{
int d = 0;
int *p = &d;
p += 4;
*p =(int *)add; //將fun函式原來的返回地址給bug函式,使返回到main函式
printf("bug function\n");
}
int fun(int x, int y)
{
int c = 0xffffffff;
int *p = &x;
p -= 1;
add = *p; //儲存fun函式返回地址
*p = &bug; //篡改fun函式的返回地址,不返回main函式,而返回bug函式
printf("fun function\n");
return c;
}
int main()
{
int a = 0xaaaaaaaa;
int b = 0xbbbbbbbb;
int ret = fun(a, b);
printf("You should runing here!\n");
_asm{ //平衡棧幀
sub esp,4
}
system("pause");
return 0;
}
//因為bug函式不是fun函式直接呼叫的,而是通過修改fun函式的返回時的pc指標,
//使其跳轉到bug函式,所以沒有呼叫call命令,沒有push bug函式返回地址,因此pc指標沒有下移;//
//而在bug返回時,進行了ret,pop使得函式返回地址彈出,pc指標上移。
//所以可以發現bug函式返回到main函式時,pc指標比原來呼叫函式時指標上移了一個指標,因此這裡的_asm彙編程式碼esp-4用來平衡棧