由于不是科班出生,又是自學(xué)開發(fā),對(duì)很多方面的知識(shí)都是只知其然而不知其所以然。加上最近公司事情不多,剛好乘此機(jī)會(huì)把長(zhǎng)毛了小本本又翻了出來,希望能每天學(xué)習(xí)一點(diǎn)點(diǎn),每天進(jìn)步一點(diǎn)點(diǎn)。
之前沒有接觸過底層方面,所以這篇文章,會(huì)通過菜鳥視角,從基礎(chǔ)概念到實(shí)戰(zhàn)演練,一步步的揭開函數(shù)調(diào)用背后,寄存器,堆棧都干了些什么。
1. 棧區(qū)(stack)
高地址向低地址生長(zhǎng)的一塊連續(xù)的內(nèi)存區(qū)域,所以棧頂?shù)刂泛蜅5淖畲笕萘慷际窍到y(tǒng)預(yù)先規(guī)定好的;
編譯器自動(dòng)管理;
方式類似數(shù)據(jù)結(jié)構(gòu)中的棧,后入先出(LIFO);
每個(gè)進(jìn)程在用戶態(tài)對(duì)應(yīng)一個(gè)調(diào)用棧結(jié)構(gòu);
存放函數(shù)參數(shù)和返回值,函數(shù)局部變量(不包括 static 聲明的變量,它們存放在靜態(tài)變量區(qū));
高效快速,但大小限制,數(shù)據(jù)不靈活(支持?jǐn)?shù)據(jù)類型有限,一般是整型,指針,浮點(diǎn)型等系統(tǒng)直接支持的數(shù)據(jù)類型);
2. 棧幀(stack frame)
函數(shù)調(diào)用經(jīng)常是嵌套的,在同一時(shí)刻,堆棧中會(huì)有多個(gè)函數(shù)的信息,每個(gè)未完成運(yùn)行的函數(shù)占用一個(gè)獨(dú)立連續(xù)區(qū)域(包含這個(gè)函數(shù)涉及的參數(shù),局部變量,返回地址等相關(guān)信息),稱為棧幀。
當(dāng)調(diào)用函數(shù)時(shí),就要壓入一個(gè)新的棧幀,發(fā)起調(diào)用函數(shù)的棧幀成為調(diào)用者棧幀,被調(diào)用函數(shù)的棧幀則稱為當(dāng)前棧幀(rsp 和 rbp 之間的內(nèi)存空間);被調(diào)用的函數(shù)運(yùn)行結(jié)束后回收棧幀,回到調(diào)用者棧幀。這一過程都是自動(dòng)的,由系統(tǒng)分配與銷毀,無需手動(dòng)調(diào)度。
3. 寄存器 (register)
x86-64中,所有寄存器都是64位,相對(duì)32位的x86來說,標(biāo)識(shí)符發(fā)生了變化,比如:從原來的
%ebp變成了%rbp。為了向后兼容性,%ebp依然可以使用,不過指向了%rbp的低32位。

rip 指令地址寄存器,用來存儲(chǔ) CPU 即將要執(zhí)行的指令地址。每次 CPU 執(zhí)行完相應(yīng)的匯編指令之后,rip 寄存器的值就會(huì)自行累加;rip 無法直接賦值,call, ret, jmp 等指令可以修改 rip。
rbp ?;刂芳拇嫫鳎4娈?dāng)前幀的棧底地址。
rsp 棧指針寄存器,保存當(dāng)前棧頂。
棧幀中,最重要的是幀指針 rbp 和棧指針 rsp,有了這兩個(gè)指針,我們就可以刻畫一個(gè)完整的棧幀。
3.1 寄存器保存慣例
調(diào)用者棧幀需要寄存器暫存數(shù)據(jù),被調(diào)用者棧幀也需要寄存器暫存數(shù)據(jù)。
如果調(diào)用者使用了 rbx,那被調(diào)用者就需要在使用之前把 rbx 保存起來,然后在返回調(diào)用者棧幀之前,恢復(fù) rbx。遵循該使用規(guī)則的寄存器就是被調(diào)用者保存寄存器,對(duì)于調(diào)用者來說, rbx 就是非易失的。
調(diào)用者使用 r10 存儲(chǔ)局部變量,為了能在子函數(shù)調(diào)用后還能使用 r10,調(diào)用者把 r10 先保存起來,然后在子函數(shù)返回之后,再恢復(fù) r10。遵循該使用規(guī)則的寄存器就是調(diào)用者保存寄存器,對(duì)于調(diào)用者來說, r10 就是易失的。
4. 函數(shù)調(diào)用棧
4.1 參數(shù)入棧
參數(shù)從右向左依次入棧(支持可變參數(shù))。
x86-64 中,有 6 個(gè)寄存器來存儲(chǔ)參數(shù),多于 6 個(gè)參數(shù),依然還是通過入棧實(shí)現(xiàn)。
4.2 返回地址入棧
實(shí)際代碼中我們是看不到 push rip 這句的;
它是包含在 call 指令之中的 call function = push rip + jmp function
4.3 代碼區(qū)跳轉(zhuǎn)
它是包含在 call 指令之中的 call function = push rip + jmp function
4.4 棧幀調(diào)整
- 將調(diào)用幀的
push %rbp入棧。 - 切換棧幀到當(dāng)前棧幀
movq %rsp, %rbp。 - 抬高棧頂,分配臨時(shí)數(shù)據(jù)區(qū)
subq &xx, %rsp。
5 實(shí)例測(cè)試
Xcode 新建工程,main.c 文件:
#include <stdio.h> //line 9
char* get_memory(char *a, char *b) {
char p[]="hello world";
return p;
}
int main(int argc, const char * argv[]) {
// insert code here...
char* str = NULL;
char* a = "good";
int b = 3;
float d = 12345.67890;
str = get_memory("h", "w");
printf("%s",str);
return 0;
}
5.1 實(shí)例分析
選中 main.c 文件,x86-64環(huán)境 Product -> Perform Action -> Assemble 'main.c'。
生成的代碼中會(huì)有很多 . 開頭的,例如 .loc,.section 等等,這些都是匯編器需要的,我們可以直接忽略,這篇文章對(duì)這些指令做了一些說明,清除掉它們和相關(guān)注釋后我們重點(diǎn)關(guān)注 main 函數(shù):
.section __TEXT,__text,regular,pure_instructions //.section __TEXT 只讀和可執(zhí)行的代碼段
.macosx_version_min 10, 12
.file 1 ".../Project/test" ".../Project/test/test/main.c"
.globl _get_memory //`_get_memory` 是一個(gè)外部符號(hào)(Symbol),對(duì)于二進(jìn)制文件外部可見。
.p2align 4, 0x90 //指出了后面代碼的對(duì)齊方式。在我們的代碼中,后面的代碼會(huì)按照 16(2^4) 字節(jié)對(duì)齊,如果需要的話,用 0x90 補(bǔ)齊。
_get_memory: ## @get_memory
Lfunc_begin0:
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
subq $48, %rsp
leaq -20(%rbp), %rax
movq ___stack_chk_guard@GOTPCREL(%rip), %rcx
movq (%rcx), %rcx
movq %rcx, -8(%rbp)
movq %rdi, -32(%rbp)
movq %rsi, -40(%rbp)
Ltmp3:
##DEBUG_VALUE: get_memory:p <- %RAX
movq L_get_memory.p(%rip), %rcx
movq %rcx, -20(%rbp)
movl L_get_memory.p+8(%rip), %edx
movl %edx, -12(%rbp)
movq ___stack_chk_guard@GOTPCREL(%rip), %rcx
movq (%rcx), %rcx
movq -8(%rbp), %rsi
cmpq %rsi, %rcx
movq %rax, -48(%rbp) ## 8-byte Spill
Ltmp4:
##DEBUG_VALUE: get_memory:p <- [%RBP+-48]
jne LBB0_2 //如果 rsi 和 rcx 不相等,那么就跳轉(zhuǎn)到 LBB0_2
## BB#1:
##DEBUG_VALUE: get_memory:p <- [%RBP+-48]
movq -48(%rbp), %rax ## 8-byte Reload
addq $48, %rsp
popq %rbp
retq
LBB0_2:
##DEBUG_VALUE: get_memory:p <- [%RBP+-48]
callq ___stack_chk_fail
Ltmp5:
Lfunc_end0:
.cfi_endproc
.section __TEXT,__literal4,4byte_literals
.p2align 2
LCPI1_0:
.long 1178658487 ## float 12345.6787
.section __TEXT,__text,regular,pure_instructions
.globl _main
.p2align 4, 0x90
_main: ## @main
Lfunc_begin1:
.cfi_startproc //函數(shù)開始標(biāo)識(shí),用于初始化某些內(nèi)部數(shù)據(jù)結(jié)構(gòu)
## BB#0:
pushq %rbp //保存調(diào)用者的棧幀基址--控制鏈
Ltmp6:
.cfi_def_cfa_offset 16 //此處距離 CFA 16 字節(jié)(用的 rsp 計(jì)算)
Ltmp7:
.cfi_offset %rbp, -16 //rbp 的值,保存在距離 CFA 16 字節(jié)處
movq %rsp, %rbp //設(shè)置新的棧幀基址
Ltmp8:
.cfi_def_cfa_register %rbp //修改計(jì)算 CFA 所用的寄存器,設(shè)成 rbp
subq $48, %rsp //分配臨時(shí)數(shù)據(jù)區(qū)
leaq L_.str.1(%rip), %rax //將 L_.str.1 的指針加載到 rax 寄存器 "h"
leaq L_.str.2(%rip), %rcx //將 L_.str.2 的指針加載到 rcx 寄存器 "w"
movss LCPI1_0(%rip), %xmm0 ## xmm0 = mem[0],zero,zero,zero //將 LCPI1_0 的單精度值加載到 xmm0 寄存器的低雙字
leaq L_.str(%rip), %rdx //將 L_.str 的指針加載到 rdx 寄存器 "good"
movl $0, -4(%rbp)
movl %edi, -8(%rbp) //將第一個(gè)參數(shù)(int argc)的值存在 rbp 低位偏移8字節(jié)
movq %rsi, -16(%rbp) //將第二個(gè)參數(shù)(char *argv[])的值存在 rbp 低位偏移16字節(jié)
Ltmp9:
movq $0, -24(%rbp) //第一個(gè)變量(char* str)值為0,存在 rbp 低位偏移24字節(jié)
movq %rdx, -32(%rbp) //第二個(gè)變量(char* a)值為 rdx 的值(即前面 L_.str 的指針),存在 rbp 低位偏移32字節(jié)
movl $3, -36(%rbp) //第三個(gè)變量(int b)值為3,存在 rbp 低位偏移36字節(jié)
movss %xmm0, -40(%rbp) //第四個(gè)變量(float d)值為 xmm0 的值,存在 rbp 低位偏移40字節(jié)
movq %rax, %rdi //將 get_memory 函數(shù)第一個(gè)參數(shù)值(之前存在 rax 寄存器的指針)設(shè)置到寄存器 edi
movq %rcx, %rsi //將 get_memory 函數(shù)第二個(gè)參數(shù)值(之前存在 rcx 寄存器的指針)設(shè)置到寄存器 rsi
callq _get_memory //調(diào)用 get_memory 函數(shù)
leaq L_.str.3(%rip), %rdi //將 printf 函數(shù)第一個(gè)參數(shù)(L_.str.3 的指針)加載到 rdi 寄存器中 "%s"
movq %rax, -24(%rbp) //將 get_memory 返回值設(shè)置給前面初始化過的第一個(gè)變量
movq -24(%rbp), %rsi //將 printf 函數(shù)第第二個(gè)參數(shù)(char* str)設(shè)置到寄存器 rsi
movb $0, %al //printf 是可變參數(shù)函數(shù),ABI 調(diào)用約定指定,將會(huì)把使用來存儲(chǔ)參數(shù)的寄存器數(shù)量存儲(chǔ)在寄存器 al 中,這里是 0
callq _printf //調(diào)用 printf 函數(shù)
xorl %r8d, %r8d //清 0 r8d 寄存器
movl %eax, -44(%rbp) ## 4-byte Spill //printf 的返回值存在 rbp 低位偏移44字節(jié)
movl %r8d, %eax //清 0 eax 低32位
addq $48, %rsp //堆棧指針 rsp 上移 48 字節(jié)
popq %rbp //之前存儲(chǔ)至 rbp 中的值彈出
retq
Ltmp10:
Lfunc_end1:
.cfi_endproc //與開始時(shí)的 .cfi_startproc 對(duì)應(yīng),結(jié)束
.section __TEXT,__cstring,cstring_literals
L_get_memory.p: ## @get_memory.p
.asciz "hello world"
L_.str: ## @.str
.asciz "good"
L_.str.1: ## @.str.1
.asciz "h"
L_.str.2: ## @.str.2
.asciz "w"
L_.str.3: ## @.str.3
.asciz "%s"
5.2 棧圖

5.3 說明
CFI 是調(diào)用框架指令(Call Frame Information)縮寫,提供的調(diào)用框架信息, 為實(shí)現(xiàn)堆?;乩@(stack unwiding)或異常處理(exception handling)提供了方便。
.cfi_startproc用于函數(shù)開始,.cfi_endproc用于函數(shù)結(jié)束,兩者配套使用。-
.cfi_def_cfa_offset 16指令表示此處(rsp)距離 CFA 16 字節(jié)。CFA(Canonical Frame Address)是標(biāo)準(zhǔn)框架地址,指調(diào)用者棧幀中調(diào)用點(diǎn)處的棧指針值。
.cfi_def_cfa_offset offsetmodifies a rule for computing CFA(Canonical Frame Address). Register remains the same, but offset is new. Note that it is the absolute offset that will be added to a defined register to compute CFA address. -
.cfi_offset %rbp, -16指令表示rbp的值保存在距離CFA16 字節(jié)。rbp是被調(diào)用者保存寄存器,按照慣例,被調(diào)者在使用之前要保存起來。 -
.cfi_offset register %rbp指令表示,從這里開始,使用rbp作為計(jì)算CFA的基址寄存器:- 在這之前的
cfi_def_cfa_offset 16用的是rsp; - 前一條指令
movq %rsp, %rbp已經(jīng)將rsp設(shè)置為新的rbp。
- 在這之前的
-
movl $3, -36(%rbp):-
%用于直接尋址寄存器,$表示立即數(shù)。movl $3, %rbp表示把立即數(shù) 3 存到rbp中。 -
()用于內(nèi)存間接尋址,movl $3, (%rbp)表示將立即數(shù) 3 保存到rbp所指向的內(nèi)存地址中。 -
-36(%rbp)表示先找到rbp所指向地址,再 -36 后所得到的地址。
-
-
浮點(diǎn)數(shù)存儲(chǔ)方式
LCPI1_0: .long 1178658487 ## float 12345.6787 ... movss LCPI1_0(%rip), %xmm0 ...單精度浮點(diǎn)數(shù)的存儲(chǔ)方式:
| 1位符號(hào)數(shù) | 8位指數(shù) | 23位尾數(shù) |-
12345.67890被轉(zhuǎn)成了了十進(jìn)制數(shù)1178658487; - 我們把
1178658487轉(zhuǎn)換成二進(jìn)制0 10001100 10000001110011010110111; - 第一位符號(hào)位
0表示正數(shù); - 第二位到第九位轉(zhuǎn)為十進(jìn)制
140; - 實(shí)際指數(shù) =
140 - 127 = 13;由于指數(shù)需要表示正負(fù)兩種數(shù)據(jù),IEEE標(biāo)準(zhǔn)規(guī)定單精度指數(shù)以127為分割線,實(shí)際存儲(chǔ)的數(shù)據(jù)是指數(shù)加127所得結(jié)果,127為高位為零,后7位為1所得,其他雙精度也以此方式計(jì)算。
6. 尾數(shù)加上1.為1.10000001110011010110111擴(kuò)大2 ^ 23 次方為110000001110011010110111十進(jìn)制12641975;
7. 12641975 / 2 ^ (23 - 13(步驟5算出的指數(shù))) = 12641975 / 1024 = 12345.6787109375;
-
5.4 刨根
接下來,我們直接深入內(nèi)存,來驗(yàn)證一下我們上面是否在一本正經(jīng)的胡說八道。
Xcode 中勾選 Debug -> Debug Workflow -> Always Show Disassembly 后,在 main 方法打斷點(diǎn),就能進(jìn)入?yún)R編調(diào)試界面。

-
All的位置,默認(rèn)選項(xiàng)是auto,看不到寄存器狀態(tài)。 - 也可以用
register read指令查看寄存器狀態(tài)。
直接在 callq 0x100000c90 ; get_memory at main.c:11 位置,檢查進(jìn)入 get_memory 方法之前的寄存器和棧,是否和我們上面的棧圖吻合。
息欄可以看到各寄存器中保存的信息,可以直接看到 rbp 指向的地址 0x00007fff5fbff6e0。
subq $0x30, %rsp 這句將棧抬高了 48 字節(jié),所以我們查看內(nèi)存的時(shí)候要從 0x7FFF5FBFF6B0 開始。
上方菜單欄 Debug -> Debug Workflow -> View Memory 打開內(nèi)存調(diào)試界面。

為了方便查看,我將圖中的內(nèi)存,按照我們之前的棧圖進(jìn)行了調(diào)整:
0x7fff5fbff6e0 rbp
0x7fff5fbff6dc 00 00 00 00 0 -4(%rbp)
0x7fff5fbff6d8 01 00 00 00 1 -8(%rbp)
0x7fff5fbff6d0 00 F7 BF 5F FF 7F 00 00 00007FFF5FBFF700 -16(%rbp)
0x7fff5fbff6c8 00 00 00 00 00 00 00 00 0 -24(%rbp)
0x7fff5fbff6c0 5C 0F 00 00 01 00 00 00 0000000100000F5C -32(%rbp)
0x7fff5fbff6bc 03 00 00 00 3 -36(%rbp)
0x7fff5fbff6b8 B7 E6 40 46 12345.67890 -40(%rbp)
0x7fff5fbff6b4 00 00 00 00 0 -44(%rbp)
0x7fff5fbff6b0 00 00 00 00 0 -48(%rbp)
根據(jù)棧圖 -32(%rbp) 位置應(yīng)該存放的是 "good" 的內(nèi)存地址,同樣的步驟直接查看地址 0x0000000100000F5C:

Xcode 已經(jīng)幫我們標(biāo)出來16進(jìn)制對(duì)應(yīng)的ASCII可顯示內(nèi)容了,當(dāng)然也可以到這里對(duì)照驗(yàn)證一下。