利用反彙編手段解析C語言函數

1、問題的提出

函數是 C語言中的重要概念。利用好函數能夠充分利用系統庫的功能寫出模塊獨立、易於維護和修改的程序。函數並不是 C 語言獨有的概念,其他語言中的方法、過程等本質上都是函數。

2、解決方法

在《微機原理》 課程介紹了堆棧、彙編語言等必要的相關知識之後,通過在高級語言開發環境下反彙編C 語言程序代碼,使得學生通過分析彙編代碼來理解函數調用中的堆棧變化,可以在實踐中理解高級語言和低級語言的底層映射關係,理解函數調用的實質。本文通過在 Visual C++6.0 下反彙編一個 32 位 C語言程序的部分代碼來解析解釋函數調用的具體過程。

3、函數調用過程

函數調用過程主要由參數傳遞、地址跳轉、局部變量分配和賦初值、執行函數體,結果返回等幾個步驟組成[1]。

3.1、參數傳遞及函數跳轉

參數由實參傳遞給形參。在底層實現上,即是實參按照函數調用規定壓入堆棧。參數傳遞完成後就通過CALL指令由當前程序跳轉到子程序處。

3.2、局部變量分配並賦值

函 數的"{"被認為是分配局部變量空間的時機。在彙編層面局部變量分配體現為堆棧中以 EBP 寄存器為基址向低地址端分配的一個連續區域,通過 EBP 寄存器的相對尋址方式來尋址函數內的局部變量。由於堆棧增長的方向是高地址端到低地址端,因此函數中先定義的局部變量地址較大,後定義的變量地址逐漸變小,相鄰定義的變量其地址一定相鄰[2]。由於全局數據和局部數據定義在不用的數據區而並不與局部變量相鄰,根據程序局部性原理,相鄰的數據會被緩存,因此對相同的運算,局部變量作為操作數的運算效率就可能高於有全局變量參與的運算。同時,局部變量分配和回收只需要移動堆棧指針ESP,因此效率最高。

3.3、尋址函數的參數

參數存放在以 EBP 為基址的高地址端。對參數的訪問同樣是通過EBP 寄存器相對尋址操作來實現。

3.4、執行函數體內的語句

函數內和具體功能相關的語句被轉化成一系列彙編語句。

3.5、返回值

return 語句將返回值返回到主調函數。在底層,參數是通過 EAX 寄存器或 EDX 寄存器傳遞給主調函數。

3.6、返回主調函數

函數的"}"被解釋為函數體已經執行完。遇到"}"時,會將堆棧中的局部變量、程序中壓入堆棧的寄存器的值全部彈出,將之前 CALL指令執行時壓入堆棧的函數返回地址彈到指令指針寄存器 EIP,從而返回到主調函數。

3.7、堆棧平衡

堆棧平衡指的是將函數調用前壓入堆棧的參數彈出堆棧,使堆棧恢復到其調用前的狀態[3]。由於函數調用完成後,參數就是無用的數據了,因此需要將其移出堆棧。

在 C語言中不需要進行堆棧平衡。而在彙編層面上卻根據調用約定來確定由主調函數或是被調函數完成堆棧平衡。


C語言函數調用堆棧常見形式如圖 1 所示[4]:

利用反彙編手段解析C語言函數


參數由主調函數壓入堆棧,CALL 指令將函數返回地址入棧。進入子函數後,需要保存 EBP 原值、分配局部變量空間、保存寄存器初始值。函數內通過"EBP-位移量"方式訪問局部變量,通過"EBP+位移量"方式訪問參數[5]。

每發生一次函數調用,就會在堆棧中建立一個棧幀,棧幀在函數調用後釋放。但是系統的堆棧資源有限,因此如果函數調用(如遞歸調用)層數過多,則可能發生堆棧溢出錯誤。


4.反彙編代碼分析

以下將函數 function 的調用相關代碼在VisualC++6.0 Debug模式反彙編,通過對彙編代碼的分析揭示函數調用的關鍵點和細節。完整的 C語言程序代碼如圖 2 所示:

利用反彙編手段解析C語言函數


Function(i,&j)語句的反彙編代碼如圖 3 所示:

利用反彙編手段解析C語言函數


先 找到主函數中的局部變量 i,j(其在堆棧中位置為 EBP- 8和 EBP- 4),將其壓入堆棧。Visual C/C++的編譯器對 C 語言程序的默認函數約定為 _cdecl[6]。此參數入棧約定為自右向左,並且對函數名前加"_"修飾符。先將 j 的地址壓入堆棧,後將 i 的值壓入堆

棧。通過 call 指令調用函數。從 Call 指令可見 fuction函數編譯後加了"_"修飾符。Call 指令執行時自動將函數的返回地址入棧,之後轉到 function 定義處開始執行此函數。

對funciton函數的"{"的反彙編結果如圖 4 所示:


利用反彙編手段解析C語言函數


在函數內,遇到"{"時分配局部空間,並用值"0xCCH"進行初始化。未在定義時初始化的局部變量其初值就與"0xCCH"相關。因此 int 類型變量由於佔四個字節,其初值為 - 858993460(0xCCCCC-CCCH);兩個連續的 0xCCH 對應漢字"燙"字,因此當

以字符形式顯示函數內未初始化的變量時會顯示為"燙燙…";指針類型變量就指向了地址為 0xCCCC-CCH 的內存。由此在調試模式下能很容易發現未初始化的變量。

堆棧基本的存儲單位為四字節,對於小於四字節的數據按四字節對齊方式分配空間。因此 char 類型變量 ch 雖然數據本身需要兩個字節,也分配了四個字節空間。array 字節數組分配空間時每個字符佔一個字節,不夠四個字符時按四字節對齊存放。因此局部變量

空間總數為 40H+4+4×2+4=50H。局部變量 ch 的地址為 EBP- 4,a、b 的地址分別為 EBP- 8 ,EBP- 0CH,array數組的地址為 EBP- 10h。函數左括號右括號間的所有的語句反彙編結果如圖 5 所示:

利用反彙編手段解析C語言函數


若變量有初值,則反彙編就會為其生成一條 Mov指令為其賦值。對於沒有初值的變量其每個字節都為0xCCH。對於字符數組,情況稍微複雜一些。字符串常量"abc"被存放在全局數據區中。當需要引用其值對數組進行初始化時,實際是將全局數據拷貝到堆棧中的

局部數組 array裡。由於寄存器是 32 位,每次最多隻能賦值 4 個字符,因此對數組賦初值的語句反彙編後可能產生一至多條彙編語句。對數組內容的訪通過[ "EBP+ 數組首地址 + 偏移量]的寄存器間址來完成,因此局部數組初始化費時但訪問時的效率高。

在函數內訪問局部變量和參數通過 [EBP + 位移量 /- 位移量]來完成。函數返回值被放到 EAX 寄存器中供主調函數使用。

可見,在彙編層面上,函數內部並不存儲局部變量,局部變量只有當函數調用發生時才會在棧上為函數分配空間。因此當函數調用後返回局部變量的值是錯誤的。


遇到函數"}"時的操作如圖 6 所示:

利用反彙編手段解析C語言函數


將寄存器 EDI、ESI、EBX 恢復原值;將 ESP 調回到 EBP 處;將 EBP原值彈出。此時 ESP 指向函數返回地址。執行出棧指令,將函數的返回地址彈入 EIP 寄存器返回到主調函數。此時堆棧中只殘留有調用函數時壓入的參數還沒有清理。

主調函數中的堆棧平衡語句如圖 7 所示:


利用反彙編手段解析C語言函數


根據 _cdecl 約定,需要由主調函數完成堆棧平衡。主調函數根據壓入堆棧的參數的數目 2 和參數大小,利用指令 add ESP,8 將參數全部彈出。此時堆棧就恢復到其調用前的狀態。一個完整的函數調用過程完成。



分享到:


相關文章: