原文: 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,調用者負責清理堆棧(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,被調用者負責清理堆棧(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
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 esp
和 add 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
反彙編 objdump
、gdb
/lldb
,或者商業工具使用,IDA Pro 或者 Hopper Disassembler [wiki ]
$ 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 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]