更為復雜C程序的運行時結構
運行環境 win 10 企業版 1809 17763.194,MinGW V3.14 32位,Bundled V3.13.2,Bundled GDB V8.2。
在C語言中,棧的方向是從高地址向低地址延伸,而數組中數據在棧中的存儲方向與此正好相反。字符串拷貝等數組操作是不對數據長度做審核的,如果實際的數據長度超過了棧中預留的空間,就會將棧中其他數據覆蓋,這種現象被稱為“棧溢出”。棧溢出可能導致一個不可預期的錯誤,也可能導致一個精心策劃的執行流程發生改變。可見,是否能夠對自己所寫程序的運行時狀態做到心中有數,是能否寫出高質量、安全代碼的前提保證。
以上兩節介紹的運行時結構都是由C程序所對應的指令,在內存中執行,驅動數據變化而產生的。C程序只有經過編譯,才能生成目標代碼。目標代碼將與指令和全局數據一一對應。編譯的最終目標就是能讓C程序的設計意圖體現在運行時結構中,這也使得編譯的每個階段的中心任務都要為形成運行時結構著想。下一節我們將概述編譯的過程。
1.2 更為復雜C程序的運行時結構(1)
在實際編程過程中會遇到更為復雜的問題。要解決這樣的問題,更加依賴對運行時結構的了解。下面我們來看一個比較復雜的案例,案例的兩個程序分別如下:
#include <stdio.h> #include <string.h> voidfun1() { int m = 10; char num[4]; strcpy(num, "bbbb"); } void fun2() { printf("You were attacked!!!\n"); } int main() { fun1(); printf("over"); return 0; }
#include <stdio.h> #include <string.h> void fun1() { int m = 10; char num[4]; strcpy(num, "bbbbbbbbbbbb\x12\x13\x40\x00"); } void fun2() { printf("You were attacked!!!\n"); } int main() { int address = (int) fun2; printf("%08x\n", address); fun1(); return 0; }
這個案例中的兩個程序在代碼上只有微小的差別,但執行結果卻不同,尤其是左邊的程序,執行結果如下所示:
這些字符顯然是fun2函數被調用時才會輸出的,但fun2這個函數在本程序中沒有被調用過,這樣的輸出結果顯得有些不可思議了,程序執行時到底發生了什麽呢?下面我們一步一步地對比分析這個案例。我們先來看main函數調用fun1函數時的情景,fun1函數執行後的返回地址被壓入棧中,跳轉到fun1函數執行,此時兩邊程序的執行沒有差異,情景如圖1-29所示。
之後保存了main函數棧底的地址值,ebp被騰出來,指向fun1函數的棧底,此時兩邊也沒有差異。情景如圖1-30所示。
m入棧,初始化為10,為num數組開辟了棧空間,此時仍然沒有差異,情景如圖1-31所示。
下面差異產生了。調用strcpy函數,執行的目的是把指定的字符串拷貝到num數組中,指定多少,拷貝多少。我們先來看右邊的程序。該程序會把指定的字符串拷貝給num數組,其長度剛好填滿num數組,情景如圖1-32所示。
再看左邊程序,指定的字符串長度已經超出了num數組的長度,所以在拷貝的時候,會把棧中前面的數據覆蓋掉,包括num的數組、main函數棧底地址值直至fun1函數執行後的返回地址,全部被覆蓋,情景如圖1-33所示。
覆蓋的結果使得fun1函數在返回並恢復現場時出現了問題。
我們先來看右邊的程序,跳轉回main函數,正常恢復,情景如圖1-34所示。
再看左邊的程序,棧底地址值被覆蓋了,ebp會得到一個亂值,不再指向main函數的棧底,另外,由於fun1函數執行後返回地址已經被覆蓋,而且覆蓋的數值正好是fun2函數的起始地址,將這個數據傳遞給eip,那麽eip自然跳轉到fun2函數執行,相當於調用了fun2函數,也就輸出了fun2函數的打印信息。同時,ebp成了亂值,程序最終將產生段錯誤,情景如圖1-35所示。
更為復雜C程序的運行時結構