1、認識棧幀linux
先來看一段神奇的代碼:windows
(windows下,代碼以下)ide
#include<stdio.h> #include<stdlib.h> #include<Windows.h> void fun() { printf("You Are Done\n"); Sleep(2000); printf("Suppose The Computer Will Shut Down~~~~\n"); //上面這行若是換成system("reboot")之類的呢? Sleep(2000); exit(1); } int fun1(int a, int b) { int *p = &a; p--; *p = fun; int c = 0xcccc; return c; } int main() { printf("begin run...\n"); int a = 0; int b = 1; fun1(a, b); printf("you should run here\n"); return 0; }
執行結果爲:函數
linux下:工具
win下:spa
結果彷佛都和咱們預期的不同啊3d
按理說,程序從main開始執行指針
中間調用fun1函數調試
調用完畢後應該繼續執行下面的printform
而後輸出:
you should run here
而實際上,程序最終卻進入了fun函數
之因此這樣,是由於棧幀的緣故。
若是你對上面發生的事情感到好奇,能夠接着往下看
2、原理解釋
關於棧幀,百度百科是這樣解釋的:
C語言中,每一個棧幀對應着一個未運行完的函數。棧幀中保存了該函數的返回地址和局部變量。
也就是說,上面的代碼,在內存方面能夠這樣理解:
簡單解釋一下,
咱們知道C語言中函數中定義的變量是在棧上開闢的,這張圖片就表示棧內存,
其地址從上往下表示從大到小
main函數中,前後將a b 入棧,
而後調用fun1(a, b)
圖片中的這個fun1() 其實不許確,它應該是 返回地址
這個 返回地址 就是表示:執行fun1(a, b)完畢後,應該返回到這個地方接着執行main函數中剩下的代碼。
此外,在fun1() 和 b 之間,還應該存放一個東西:棧指針ebp(圖上沒有表示出來)
而後參數 a b 是局部變量,也分別入棧,
藉助調試工具,能夠看到,a b 的地址分別爲圖中所示
而後定義一個指針p指向a
接下來p--,這時p指向的地址爲0x0018fc20,也就是 剛剛說的 返回地址
這時候,應該能發現,返回地址已經變了, 變成了fun的地址
也就是說,當執行完fun1()後,程序並不會返回到 main函數中調用它的地方,而是接着調用fun函數
這就致使程序難以想象地進入了咱們沒有預想到的地方,調用了咱們本不想調用的函數,
並且因爲這個返回地址的丟失,在調用完畢fun後,程序也會由於找不到返回地址而掛掉。
(我在代碼中執行了 exit(1); 這句話強行終止了程序)
以上就是代碼的原理解釋了。
接下來,利用剛剛所get到的棧幀方面的知識,能夠作一個事情:
3、修改b的值
要求:不要直接修改a、b變量,而經過棧幀,實現修改a、b變量的值
代碼:
void fun1(int a, int b) { int *p = &a; p -= 2; int ReturnAddr = *p; //返回值 //修改main中的a p = ReturnAddr - 4 * 2; *p = 11111; //修改main中的b p = ReturnAddr - 4 * 5; *p = 12345; } int main() { printf("begin run...\n"); int a = 0; int b = 1; fun1(a, b); printf("you should run here\n"); printf("%d\n", a); printf("%d\n", b); return 0; }
以前畫的內存圖比較簡單,要想實現這個要求,必須進一步瞭解棧幀是怎麼存放的了,下面是比較詳細的棧內存圖:
linux代碼:
運行結果
(gcc 和 vs編譯的程序的棧幀存放規則不一樣)