匯編基礎四 --函數調用與堆棧平衡


函數

將高級語言中定義的函數,被編譯位匯編代碼執行時,會被編譯為一堆指令的集合,用來實現特定的功能,並獲得執行后的結果。如果不關注函數中的具體實現,就可以將一個函數看作一個整體,函數調用過程等同於執行了一個操作,只不過這個操作比較復雜而已。

匯編中實現一個函數可以使用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)
}

該函數執行時可以分為以下的步驟。

  1. add函數需要x, y兩個參數至,所以在調用函數前,將參數push 到棧中,為了在函數中使用,多個參數多次執行push即可。 push 1, push  2
  2. 執行CALL 指令,該指令將下一指令地址存入棧中,並將函數首地址寫入EIP寄存器,下一次將會執行函數體中的指令。
  3. 函數中的指令開始執行,但是執行我們加法邏輯前,會執行一些操作
    1. 首先進行堆棧提升,並開啟一段緩沖區空間用於儲存函數中的臨時變量,使用 SUB ESP, 40h指令,將ESP向上偏移40個數據寬度。堆棧提升后,使用EBP位置進行尋址。
    2. 將三個寄存器中的值寫入棧中,函數執行的過程中會覆蓋寄存器的值,保存在棧中后,函數結束時可以從該處恢復。
    3. 開始執行函數的主邏輯
  4. 獲取棧中 x,y的值進行加法操作。通過EBP寄存器中保存的位置,函數參數在棧中的位置分別為,EBP+8和EBP+C(16進制)
  5. 執行加法操作
    1. MOV EAX, 0                            -- EAX寄存器中的值設置為0
    2. ADD EAX, ptr ds:[ EBP+8 ]     -- EAX寄存器中的值 + 內存地址 EBP +8位置值,結果保存在EAX中
    3. ADD EAX, ptr ds:[ EBP+C]   -- EAX寄存器中的值 + 內存地址 EBP +C 位置值,結果保存在EAX中
    4. 執行結束后,結果被保存到了EAX寄存器中。
  6. 函數執行結束,開始恢復堆棧,清除函數棧中的信息,保證函數執行前的堆棧信息和函數執行后的堆棧相同,也就是滿足堆棧平衡。
    1. 恢復三個寄存器edi, esi, ebx中值,pop edi,   pop esi,    pop ebx
    2. 清除緩沖區,這里的清除並不刪除其中的數據,將ESP指針恢復即可,這樣緩沖區空間會被作為未使用區域,新的數據寫入時候,將原來的數據覆蓋。由於ESP提升執行了SUB,所以ADD ESP, 40h即可
    3. EBP恢復到原EBP位置,此時棧中保存了原EBP中的位置,所以 POP EBP ,將堆棧中的原EBP地址保存到EBP中,EBP地址恢復。
    4. 下一行指令地址賦值給EPI寄存器。到此為止,函數執行結束,並回到了CALL指令的下一行指令,但是函數參數空間還沒有清除,所以需要在函數外部恢復堆棧平衡。
    5. 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中獲取函數返回值即可。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM