棧幀與調用慣例——函數是如何被調用的?

原文: http://nullwy.me/2018/01/stac...
若是以爲個人文章對你有用,請隨意讚揚

棧與棧幀

要想知道函數是怎麼被調用的,須要瞭解棧幀和調用慣例相關知識。俞甲子2009 的「第10章 內存: 棧與堆」對相關概念有很好的介紹。本文是對相關知識的學習筆記。html

棧與棧幀佈局

附註,棧幀之間的劃分邊界,其實有兩種不同說法。在有些資料中 [wikipedia; 俞甲子2009 ],callee 參數被劃分在 callee 棧幀,但在 Intel 官方一些權威文檔中 Intel ASDM, [Vol.1, Ch.6; Intel X86-psABI ],callee 參數被劃分在 caller 棧幀。git

調用慣例

調用慣例,規定如下內容:(1) 函數參數的傳遞順序和方式;(2) 棧的清理方式;(3) 名稱修飾(name mangling)。常見的 x86 調用慣例列表有:cdecl(C 語言默認)、stdcall(Win32 API 標準)、fastcall、pascal。這些調用慣例以下下表所示(更加全面的列表參見 wikipedia):github

調用慣例 棧幀清理 參數傳遞 名稱修飾
cdel 調用者 caller 從右至左入棧 RTL 下劃線+函數名,如 _sum
stdcall 被調用者 callee 從右至左入棧 RTL 下劃線+函數名+@+參數字節數,如 _sum@8
fastcall 被調用者 callee 頭兩參數存入寄存器 ECX 和 EDX,其他參數從右至左入棧 RTL @+函數名+@+參數字節數,如 @sum@8
pascal 被調用者 callee 從左至右入棧 LTR 較爲複雜,參見 pascal 文檔

下面舉例說明,cdecl 和 stdcall 兩種調用慣例。shell

cdecl 調用慣例

cdecl,調用者負責清理堆棧(caller clean-up),參數從右至左(Right-to-Left,RTL)壓入棧。舉例說明 [ref1 ref2 ]:macos

// cdecl 調用慣例
int __cdecl sum(int a, int b) {
    return a + b;
}
// 調用
int c = sum(2, 3);

編譯器生成的等價彙編代碼:sass

; 調用者清理堆棧(caller clean-up),參數 RTL 入棧
push 3
push 2
call _sum      ; 將返回地址壓入棧, 同時 sum 的地址裝入 eip
add  esp, 8    ; 清理堆棧, 兩個參數佔用 8 字節
; sum 函數等價彙編代碼
; // function prolog
push ebp  
mov  ebp, esp
; // return a + b;
mov  eax, [ebp + 12] 
add  eax, [ebp + 8]  ; 返回值規定保存在 eax
; // function epilog
mov  esp, ebp        ; 設置棧頂 esp
pop  ebp             ; 恢復 old ebp
ret                  ; 將棧中保存的返回地址裝入 eip

stdcall 調用慣例

stdcall,被調用者負責清理堆棧(callee clean-up),參數從右至左(Right-to-Left,RTL)壓入棧。舉例說明:函數

// stdcall 調用慣例
int __stdcall sum(int a, int b) {
    return a + b;
}
// 調用
int c = sum(2, 3);

編譯器生成的等價彙編代碼:工具

; 被調用者清理堆棧(callee clean-up),參數 RTL 入棧
push 3
push 2
call _sum@8      ; 將返回地址壓入棧, 同時 sum 的地址裝入 eip
; sum 函數等價彙編代碼
; // function prolog
push ebp  
mov  ebp, esp
; // return a + b;
mov  eax, [ebp + 12] 
add  eax, [ebp + 8]  ; 返回值規定保存在 eax
; // function epilog
mov  esp, ebp        ; 設置棧頂 esp
pop  ebp             ; 恢復 old ebp
ret  8               ; 清理堆棧,並將棧中保存的返回地址裝入 eip

gcc 彙編代碼

hello1.c 文件內容以下:佈局

int __cdecl sum(int a, int b) {
    return a + b;
}

int main() {
    sum(1, 2);
    sum(3, 4);
    return 0;
}

生成彙編代碼:學習

$ gcc -m32 -S -masm=intel hello1.c -o hello1.s
$ gcc -m32 hello1.s -o hello
$ ./hello || echo $?
0

生成的 hello1.s,內容以下:

.section  __TEXT,__text,regular,pure_instructions
  .macosx_version_min 10, 12
  .intel_syntax noprefix
  .globl  _sum
  .p2align  4, 0x90
_sum:                                   ## @sum
## BB#0:
  push  ebp
  mov  ebp, esp
  sub  esp, 8                       ; 預先分配 8 字節棧空間,保存 2 個佈局變量
  mov  eax, dword ptr [ebp + 12]    ; 堆棧中讀取參數 2
  mov  ecx, dword ptr [ebp + 8]     ; 堆棧中讀取參數 1
  mov  dword ptr [ebp - 4], ecx     ; 佈局變量 1
  mov  dword ptr [ebp - 8], eax     ; 佈局變量 2
  mov  eax, dword ptr [ebp - 4]
  add  eax, dword ptr [ebp - 8]
  add  esp, 8                       ; 清理 8 字節棧空間
  pop  ebp
  ret

  .globl  _main
  .p2align  4, 0x90
_main:                                  ## @main
## BB#0:
  push  ebp
  mov  ebp, esp
  sub  esp, 40                     ; 預先分配 40 字節棧空間
  mov  eax, 1
  mov  ecx, 2
  mov  dword ptr [ebp - 4], 0
  mov  dword ptr [esp], 1
  mov  dword ptr [esp + 4], 2
  mov  dword ptr [ebp - 8], eax ## 4-byte Spill
  mov  dword ptr [ebp - 12], ecx ## 4-byte Spill
  call  _sum
  mov  ecx, 3
  mov  edx, 4
  mov  dword ptr [esp], 3
  mov  dword ptr [esp + 4], 4
  mov  dword ptr [ebp - 16], eax ## 4-byte Spill
  mov  dword ptr [ebp - 20], ecx ## 4-byte Spill
  mov  dword ptr [ebp - 24], edx ## 4-byte Spill
  call  _sum
  xor  ecx, ecx
  mov  dword ptr [ebp - 28], eax ## 4-byte Spill
  mov  eax, ecx
  add  esp, 40                  ; 清理 40 字節棧空間
  pop  ebp
  ret

.subsections_via_symbols

GCC 生成的彙編代碼並無使用 push 而是經過 sub esp, 40 直接預先分配棧空間,而後使用 mov 指令將參數寫進棧中,清理棧使用 add esp, 40。邏輯上,仍是符合 cdecl 調用慣例,調用者負責清理堆棧(caller clean-up),參數從右至左(Right-to-Left,RTL)壓入棧。這樣作的好處是,若是同時屢次調用 sum,清理棧空間的指令,只須要最後的時候調用一次就能夠了。統一使用 sub espadd esp 去操做 esp 值,避免 push 指令操做 esp。

如今再來看看,stdcall 調用慣例下,GCC 生成的彙編代碼。把 sum 函數改成 __stdcall,運行下面的命令:

$ gcc -m32 -S -masm=intel hello2.c -o hello2.s
$ diff -C1 hello1.s hello2.s
*** hello1.s    Thu Feb 05 22:43:59 2018
--- hello2.s    Thu Feb 05 22:45:24 2018
***************
*** 18,20 ****
    pop  ebp
!   ret
  
--- 18,20 ----
    pop  ebp
!   ret  8
  
***************
*** 35,36 ****
--- 35,37 ----
    call  _sum
+   sub  esp, 8
    mov  ecx, 3
***************
*** 43,44 ****
--- 44,46 ----
    call  _sum
+   sub  esp, 8
    xor  ecx, ecx

反彙編代碼

反彙編 objdumpgdb/lldb,或者商業工具使用,IDA Pro 或者 Hopper Disassembler [wiki ]

objdump 反彙編

$ gobjdump -d -Mintel hello1                # 使用 GNU objdump
$ objdump -d -x86-asm-syntax=intel hello1   # 使用 llvm-objdump
hello1:    file format Mach-O 32-bit i386

Disassembly of section __TEXT,__text:
__text:
    1f30:    55     push    ebp
    1f31:    89 e5     mov    ebp, esp
    1f33:    83 ec 08     sub    esp, 8
    1f36:    8b 45 0c     mov    eax, dword ptr [ebp + 12]
    1f39:    8b 4d 08     mov    ecx, dword ptr [ebp + 8]
    1f3c:    89 4d fc     mov    dword ptr [ebp - 4], ecx
    1f3f:    89 45 f8     mov    dword ptr [ebp - 8], eax
    1f42:    8b 45 fc     mov    eax, dword ptr [ebp - 4]
    1f45:    03 45 f8     add    eax, dword ptr [ebp - 8]
    1f48:    83 c4 08     add    esp, 8
    1f4b:    5d     pop    ebp
    1f4c:    c3     ret
    1f4d:    0f 1f 00     nop    dword ptr [eax]
    1f50:    55     push    ebp
    1f51:    89 e5     mov    ebp, esp
    1f53:    83 ec 28     sub    esp, 40
    1f56:    b8 01 00 00 00     mov    eax, 1
    1f5b:    b9 02 00 00 00     mov    ecx, 2
    1f60:    c7 45 fc 00 00 00 00     mov    dword ptr [ebp - 4], 0
    1f67:    c7 04 24 01 00 00 00     mov    dword ptr [esp], 1
    1f6e:    c7 44 24 04 02 00 00 00     mov    dword ptr [esp + 4], 2
    1f76:    89 45 f8     mov    dword ptr [ebp - 8], eax
    1f79:    89 4d f4     mov    dword ptr [ebp - 12], ecx
    1f7c:    e8 af ff ff ff     call    -81 <_sum>
    1f81:    b9 03 00 00 00     mov    ecx, 3
    1f86:    ba 04 00 00 00     mov    edx, 4
    1f8b:    c7 04 24 03 00 00 00     mov    dword ptr [esp], 3
    1f92:    c7 44 24 04 04 00 00 00     mov    dword ptr [esp + 4], 4
    1f9a:    89 45 f0     mov    dword ptr [ebp - 16], eax
    1f9d:    89 4d ec     mov    dword ptr [ebp - 20], ecx
    1fa0:    89 55 e8     mov    dword ptr [ebp - 24], edx
    1fa3:    e8 88 ff ff ff     call    -120 <_sum>
    1fa8:    31 c9     xor    ecx, ecx
    1faa:    89 45 e4     mov    dword ptr [ebp - 28], eax
    1fad:    89 c8     mov    eax, ecx
    1faf:    83 c4 28     add    esp, 40
    1fb2:    5d     pop    ebp
    1fb3:    c3     ret

_sum:
    1f30:    55     push    ebp
    1f31:    89 e5     mov    ebp, esp
    1f33:    83 ec 08     sub    esp, 8
    1f36:    8b 45 0c     mov    eax, dword ptr [ebp + 12]
    1f39:    8b 4d 08     mov    ecx, dword ptr [ebp + 8]
    1f3c:    89 4d fc     mov    dword ptr [ebp - 4], ecx
    1f3f:    89 45 f8     mov    dword ptr [ebp - 8], eax
    1f42:    8b 45 fc     mov    eax, dword ptr [ebp - 4]
    1f45:    03 45 f8     add    eax, dword ptr [ebp - 8]
    1f48:    83 c4 08     add    esp, 8
    1f4b:    5d     pop    ebp
    1f4c:    c3     ret
    1f4d:    0f 1f 00     nop    dword ptr [eax]

_main:
    1f50:    55     push    ebp
    1f51:    89 e5     mov    ebp, esp
    1f53:    83 ec 28     sub    esp, 40
    1f56:    b8 01 00 00 00     mov    eax, 1
    1f5b:    b9 02 00 00 00     mov    ecx, 2
    1f60:    c7 45 fc 00 00 00 00     mov    dword ptr [ebp - 4], 0
    1f67:    c7 04 24 01 00 00 00     mov    dword ptr [esp], 1
    1f6e:    c7 44 24 04 02 00 00 00     mov    dword ptr [esp + 4], 2
    1f76:    89 45 f8     mov    dword ptr [ebp - 8], eax
    1f79:    89 4d f4     mov    dword ptr [ebp - 12], ecx
    1f7c:    e8 af ff ff ff     call    -81 <_sum>
    1f81:    b9 03 00 00 00     mov    ecx, 3
    1f86:    ba 04 00 00 00     mov    edx, 4
    1f8b:    c7 04 24 03 00 00 00     mov    dword ptr [esp], 3
    1f92:    c7 44 24 04 04 00 00 00     mov    dword ptr [esp + 4], 4
    1f9a:    89 45 f0     mov    dword ptr [ebp - 16], eax
    1f9d:    89 4d ec     mov    dword ptr [ebp - 20], ecx
    1fa0:    89 55 e8     mov    dword ptr [ebp - 24], edx
    1fa3:    e8 88 ff ff ff     call    -120 <_sum>
    1fa8:    31 c9     xor    ecx, ecx
    1faa:    89 45 e4     mov    dword ptr [ebp - 28], eax
    1fad:    89 c8     mov    eax, ecx
    1faf:    83 c4 28     add    esp, 40
    1fb2:    5d     pop    ebp
    1fb3:    c3     ret

lldb 反彙編

使用 lldb 反彙編:

$ lldb hello1
(lldb) target create "hello1"
Current executable set to 'hello1' (i386).
(lldb) settings set target.x86-disassembly-flavor intel
(lldb) disassemble --name main
hello1`main:
hello1[0x1f50] <+0>:  push   ebp
hello1[0x1f51] <+1>:  mov    ebp, esp
hello1[0x1f53] <+3>:  sub    esp, 0x28
hello1[0x1f56] <+6>:  mov    eax, 0x1
hello1[0x1f5b] <+11>: mov    ecx, 0x2
hello1[0x1f60] <+16>: mov    dword ptr [ebp - 0x4], 0x0
hello1[0x1f67] <+23>: mov    dword ptr [esp], 0x1
hello1[0x1f6e] <+30>: mov    dword ptr [esp + 0x4], 0x2
hello1[0x1f76] <+38>: mov    dword ptr [ebp - 0x8], eax
hello1[0x1f79] <+41>: mov    dword ptr [ebp - 0xc], ecx
hello1[0x1f7c] <+44>: call   0x1f30                    ; sum
hello1[0x1f81] <+49>: mov    ecx, 0x3
hello1[0x1f86] <+54>: mov    edx, 0x4
hello1[0x1f8b] <+59>: mov    dword ptr [esp], 0x3
hello1[0x1f92] <+66>: mov    dword ptr [esp + 0x4], 0x4
hello1[0x1f9a] <+74>: mov    dword ptr [ebp - 0x10], eax
hello1[0x1f9d] <+77>: mov    dword ptr [ebp - 0x14], ecx
hello1[0x1fa0] <+80>: mov    dword ptr [ebp - 0x18], edx
hello1[0x1fa3] <+83>: call   0x1f30                    ; sum
hello1[0x1fa8] <+88>: xor    ecx, ecx
hello1[0x1faa] <+90>: mov    dword ptr [ebp - 0x1c], eax
hello1[0x1fad] <+93>: mov    eax, ecx
hello1[0x1faf] <+95>: add    esp, 0x28
hello1[0x1fb2] <+98>: pop    ebp
hello1[0x1fb3] <+99>: ret

(lldb) disassemble --name sum
hello1`sum:
hello1[0x1f30] <+0>:  push   ebp
hello1[0x1f31] <+1>:  mov    ebp, esp
hello1[0x1f33] <+3>:  sub    esp, 0x8
hello1[0x1f36] <+6>:  mov    eax, dword ptr [ebp + 0xc]
hello1[0x1f39] <+9>:  mov    ecx, dword ptr [ebp + 0x8]
hello1[0x1f3c] <+12>: mov    dword ptr [ebp - 0x4], ecx
hello1[0x1f3f] <+15>: mov    dword ptr [ebp - 0x8], eax
hello1[0x1f42] <+18>: mov    eax, dword ptr [ebp - 0x4]
hello1[0x1f45] <+21>: add    eax, dword ptr [ebp - 0x8]
hello1[0x1f48] <+24>: add    esp, 0x8
hello1[0x1f4b] <+27>: pop    ebp
hello1[0x1f4c] <+28>: ret
hello1[0x1f4d] <+29>: nop    dword ptr [eax]

參考資料

  1. 連接、裝載與庫,俞甲子,2009:第10章 內存: 棧與堆,豆瓣
  2. IDA Pro權威指南,Eagle,第2版2011:6.2.1 調用約定,豆瓣
  3. 2001-09 Calling Conventions Demystified https://www.codeproject.com/A...
  4. Calling Conventions https://en.wikibooks.org/wiki...
  5. https://en.wikipedia.org/wiki...
相關文章
相關標籤/搜索