函數
將高級語言中定義的函數,被編譯位匯編代碼執行時,會被編譯為一堆指令的集合,用來實現特定的功能,並獲得執行后的結果。如果不關注函數中的具體實現,就可以將一個函數看作一個整體,函數調用過程等同於執行了一個操作,只不過這個操作比較復雜而已。
匯編中實現一個函數可以使用JMP 和 CALL 指令完成。
函數是一堆完成特定功能的指令集,這些指令集同樣需要按照順序依次執行,所以只要知道函數執行的第一條指令的地址(函數的首地址),函數將會依次執行這些指令,完成函數。
計算兩數之和
我們使用C語言實現簡單的加法函數
函數分為帶參函數和無參函數,對於無參函數,直接使用CALL指令跳轉到函數首地址然后開始執行即可,並且CALL指令會將下一行指令地址入棧保存,用於函數結束返回時跳回到原地址。
// 函數體 首地址 指令 00401019 ADD EAX, 4
0040101D RETN // 將棧中的地址 00401038 取出, 賦值給ESI,下次執行回到00401038地址處執行
... ... 00401034 CALL 00401019 // 將下一行指令地址 00401038存入棧空間,然后執行 00401019地址處的函數
00401038 ...
如果函數帶參數,我們使用C語言實現簡單的加法函數為例
int add(int x, int y) { int z = 0; z = x + y; return z; } void main() { int x = 1; int y = 2; int z; z = add(x, y) }
該函數執行時可以分為以下的步驟。
- add函數需要x, y兩個參數至,所以在調用函數前,將參數push 到棧中,為了在函數中使用,多個參數多次執行push即可。 push 1, push 2
- 執行CALL 指令,該指令將下一指令地址存入棧中,並將函數首地址寫入EIP寄存器,下一次將會執行函數體中的指令。
- 函數中的指令開始執行,但是執行我們加法邏輯前,會執行一些操作
- 首先進行堆棧提升,並開啟一段緩沖區空間用於儲存函數中的臨時變量,使用 SUB ESP, 40h指令,將ESP向上偏移40個數據寬度。堆棧提升后,使用EBP位置進行尋址。
- 將三個寄存器中的值寫入棧中,函數執行的過程中會覆蓋寄存器的值,保存在棧中后,函數結束時可以從該處恢復。
- 開始執行函數的主邏輯
- 獲取棧中 x,y的值進行加法操作。通過EBP寄存器中保存的位置,函數參數在棧中的位置分別為,EBP+8和EBP+C(16進制)
- 執行加法操作
- MOV EAX, 0 -- EAX寄存器中的值設置為0
- ADD EAX, ptr ds:[ EBP+8 ] -- EAX寄存器中的值 + 內存地址 EBP +8位置值,結果保存在EAX中
- ADD EAX, ptr ds:[ EBP+C] -- EAX寄存器中的值 + 內存地址 EBP +C 位置值,結果保存在EAX中
- 執行結束后,結果被保存到了EAX寄存器中。
- 函數執行結束,開始恢復堆棧,清除函數棧中的信息,保證函數執行前的堆棧信息和函數執行后的堆棧相同,也就是滿足堆棧平衡。
- 恢復三個寄存器edi, esi, ebx中值,pop edi, pop esi, pop ebx
- 清除緩沖區,這里的清除並不刪除其中的數據,將ESP指針恢復即可,這樣緩沖區空間會被作為未使用區域,新的數據寫入時候,將原來的數據覆蓋。由於ESP提升執行了SUB,所以ADD ESP, 40h即可
- EBP恢復到原EBP位置,此時棧中保存了原EBP中的位置,所以 POP EBP ,將堆棧中的原EBP地址保存到EBP中,EBP地址恢復。
- 下一行指令地址賦值給EPI寄存器。到此為止,函數執行結束,並回到了CALL指令的下一行指令,但是函數參數空間還沒有清除,所以需要在函數外部恢復堆棧平衡。
- CALL指令的下一行,清除函數參數。同樣的,ESP向下移動,ADD EBP, C 即可。
上面是執行過程,匯編代碼為。
// 主程序入口 。。。 push 1 push 2 call 0040107D -- 調用函數 add esp, 8 -- 清楚堆棧中的參數值,恢復堆棧
-- 執行結果在eaxz中,需要使用時,獲取即可 。。。
// 此處為函數首地址為 0040107D push ebp -- 堆棧提升,保存原ebp值,然后將esp賦值給esp mov ebp, esp sub esp, 40h push ebx push esi push edi mov eax, 0 add eax, ptr ds:[ebp+8] add eax, ptr ds:[ebp+c] pop edi pop esi pop ebx add esp, 40h cmp ebp esp -- 比較esp和ebp是否相同,清除棧信息后應該相,否則說明棧中的內容沒有被清除。堆棧不平衡 pop ebp -- 從棧中取出原ebp值,存入ebp即恢復原ebp值 ret
函數執行過程中的堆棧空間是在函數執行時才分配的,這里發生了一次堆棧提升,所以我們總是說函數執行時有獨立的棧空間,也是通過這種方式實現。函數執行前后始終需要保證堆棧平衡。
總結
總結函數的執行過程
- 參數入棧
- 保存當前執行指令的地址,入棧
- 進入函數,
- ESP 和 EBP 分別進行堆棧提升
- sub esp 開辟緩沖區空間,緩沖區空間地址為 EBP + 4 -> ESP
- 三個寄存器值保存到棧中
- 執行函數中內容,EBP為基址,獲取通過+8 +C獲取函數參數,+4位置為函數返回時跳轉的地址
- 函數執行結束:
- 恢復三個寄存器
- add esp恢復 緩沖區,
- ESP和EBP位置恢復
- ret
- 函數外部add ebp 恢復參數空間。
- 在eax中獲取函數返回值即可。