1. 程式人生 > 實用技巧 >彙編基礎四 --函式呼叫與堆疊平衡

彙編基礎四 --函式呼叫與堆疊平衡

函式

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

彙編中實現一個函式可以使用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中獲取函式返回值即可。