轉自:https://www.codenong.com/cs105961527/
微信公眾號:二進制人生
專注於嵌入式linux開發。問題或建議,請發郵件至hjhvictory@163.com。
更新:2020/04/26。

本文研究的是arm架構的函數幀棧,閱讀者需要有arm匯編基礎,不過本文涉及的匯編指令不是很多。
理論上來說,ARM的15個通用寄存器是通用的,但實際上並非如此,特別是在過程調用的過程中。
以下4個寄存器有特殊用途:
R11:frame pointer,FP寄存器
R12:IP寄存器,用於暫存SP
R13:stack pointer,SP寄存器
R14:link register,LR寄存器
R15:PC寄存器
我們知道每個進程都有自己的棧,實際上每個函數也有自己的棧(盡管這些棧在空間上是連續的,都是在進程的棧上)。而在ARM上,函數的棧幀是由SP寄存器和FP寄存器來界定的。
我們寫一個小程序來觀察下函數的幀棧。
|
1
2 3 4 5 6 7 8 9 10 11 12 13 |
int fun(int a,int b)
{ int c = 1; int d = 2; return 0; } int main(int argc,char **argv) { int a = 0; int b = 1; fun(a,b); } |
畫出幀棧變化:
|
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
高地址 -----
| fp -- main函數也是被人調用的,所以也要保存調用者的fp。 main_fp ----- | lr -- main里調用了fun,會修改了lr,所以lr也要入棧,如果main里沒有調用fun,不會有lr入棧這一步 ----- | 局部變量a main ----- 的棧 | 局部變量b ----- | argc ----- | argv main_sp ----- | main_fp -- 保存main的fp fun_fp ----- | ----- |局部變量c fun的 ----- 棧 |局部變量d ----- | 形參a ----- | 形參b fun_sp ----- |.... 低地址 ------ |
|
1
|
|
fun調用返回前將sp更新為fun_fp,再pop fp,這兩步操作同時還原了main的fp,sp。
局部變量存放於棧中,
調用函數fun,傳遞了兩個形參,本質上是在函數fun的棧里開辟多兩個空間。將main函數的棧里的兩個局部變量傳遞給中間橋梁---寄存器r0和r1,(如果有第三個參數的話,那就是傳給r3,依次類推)
調用fun時會把r0和r1賦值給fun棧相應的位置,完成傳參,這就是為啥傳遞變量無法修改變量值的原因。
編譯:
|
1
|
arm-himix200-linux-gcc main.c
|
反匯編:
|
1
|
arm-himix200-linux-objdump a.out -d
|
反匯編結果如下:
|
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
000103dc <fun>:
103dc: e52db004 push {fp} ; (str fp, [sp, #-4]!) 103e0: e28db000 add fp, sp, #0 ;更新fun函數棧起始地址 103e4: e24dd014 sub sp, sp, #20 ;為fun函數開辟棧 103e8: e50b0010 str r0, [fp, #-16] 103ec: e50b1014 str r1, [fp, #-20] ; 0xffffffec 103f0: e3a03001 mov r3, #1 ;局部變量1 103f4: e50b3008 str r3, [fp, #-8] 103f8: e3a03002 mov r3, #2 103fc: e50b300c str r3, [fp, #-12] ;局部變量2 10400: e3a03000 mov r3, #0 10404: e1a00003 mov r0, r3 10408: e28bd000 add sp, fp, #0 1040c: e49db004 pop {fp} ; (ldr fp, [sp], #4) 10410: e12fff1e bx lr 00010414 <main>: 10414: e92d4800 push {fp, lr} ;保存調用者的fp和lr,因為main后面調用了bl指令,會改變lr,所以需要將lr入棧 10418: e28db004 add fp, sp, #4 ;更新main函數棧起始地址 1041c: e24dd008 sub sp, sp, #8 ;為main函數開辟棧 10420: e3a03000 mov r3, #0 10424: e50b3008 str r3, [fp, #-8] ;局部變量1 10428: e3a03001 mov r3, #1 1042c: e50b300c str r3, [fp, #-12] ;局部變量2 10430: e51b100c ldr r1, [fp, #-12] ;形參1 10434: e51b0008 ldr r0, [fp, #-8] ;形參2 10438: ebffffe7 bl 103dc <fun> ;函數調用 1043c: e3a03000 mov r3, #0 10440: e1a00003 mov r0, r3 ;main函數返回值 10444: e24bd004 sub sp, fp, #4 ;還原調用者的sp 10448: e8bd8800 pop {fp, pc} ;還原調用者的fp,將lr賦值給pc,相當於b lr, |
如果fun里調用了其他函數,那么在一開始除了會將調用者的fp入棧之外,還會將鏈接寄存器lr入棧。鏈接寄存器在調用函數時保存了返回地址(即跳轉指令的下一條指令的地址)。
平常的函數調用就是通過bl跳轉指令來實現,有人可能見過另外一條跳轉指令b。它們兩者的區別是bl是帶返回的跳轉指令,即它跳轉之前會自動把返回地址保存在鏈接寄存器,而b指令沒有這以一功能,那就一去不復返了。
在函數調用結束會執行bx lr指令,完成返回。
在程序執行過程中(通常是發生了某種意外情況而需要進行調試),通過SP和FP所限定的stack frame,就可以得到母函數的SP和FP,從而得到母函數的stack frame(母函數的SP會在函數調用的第一時間壓棧),以此追溯,即可得到所有函數的調用順序,這就是所謂的棧回溯。
所以,假如有面試官問你什么是棧回溯,可以這樣子回答他。
通過上面的簡短程序,我們知道了arm匯編的一些規定:
1、形參通過r0、r1、…寄存器傳遞
2、函數返回值放在r0
3、函數調用通常使用bl指令,函數返回時調用bx lr
4、在被調用的函數里,會將調用者的fp入棧,退出時再將fp還原。如果函數內部會再次調用其他函數,lr寄存器也要入棧。
5、函數調用至少產生7條指令(即調用一個空的無返回值函數):
|
1
2 3 4 5 6 7 8 |
bl 103dc <fun1>
000103dc <fun1>: push {fp} ; (str fp, [sp, #-4]!) add fp, sp, #0 nop ; (mov r0, r0) add sp, fp, #0 pop {fp} ; (ldr fp, [sp], #4) bx lr |
如果是非空函數,還需要為函數開辟棧:
sub sp, sp, #xxx ;為函數開辟棧xxx * 4字節的棧
退出時還原棧:
add sp, fp, #0
fp寄存器的是必要的嗎?單靠sp寄存器來維持入棧和出棧,似乎也是可以的。
所以arm的gcc提供了選項-fomit-frame-pointer,用於在編譯時忽略產生fp操作,這樣編譯出來的程序反匯編之后會少了操作fp的指令,可以優化程序執行速度以減少程序空間。
但這樣的缺陷是出現段錯誤時無法進行棧回溯,所以效率與調試二者不可得兼。
我們在網上看到分析arm幀棧的文章,都引用了這幅圖:

上面這個圖是標准的過程調用下函數的幀棧。我們需要知道一個選項-mapcs-frame,它對所有函數都生成一個遵從ARM程序調用標准的堆棧幀,即使在正確執行代碼無需嚴格這么做時。缺省情況下是“-mno-apcs-frame”。
-mapcs與“-mapcs-frame”相同。
所以只有指定了-mapcs-frame才會在函數調用時將pc、lr、sp、fp一股腦入棧,實際上如果函數里沒有修改這些寄存器,是沒有必要全部入棧的。現在的編譯器出於效率考慮,都會選擇性入棧。
編譯時加了-fomit-frame-pointer選項后,反匯編結果如下:
|
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
00010410 <fun>:
10410: e52de004 push {lr} ; (str lr, [sp, #-4]!) 10414: e24dd014 sub sp, sp, #20 10418: e58d0004 str r0, [sp, #4] 1041c: e58d1000 str r1, [sp] 10420: e3a03001 mov r3, #1 10424: e58d300c str r3, [sp, #12] 10428: e3a03002 mov r3, #2 1042c: e58d3008 str r3, [sp, #8] 10430: e3a00002 mov r0, #2 10434: ebffff9f bl 102b8 <malloc@plt> 10438: e3a03000 mov r3, #0 1043c: e1a00003 mov r0, r3 10440: e28dd014 add sp, sp, #20 10444: e49df004 pop {pc} ; (ldr pc, [sp], #4) 00010448 <main>: 10448: e52de004 push {lr} ; (str lr, [sp, #-4]!) 1044c: e24dd00c sub sp, sp, #12 10450: e3a03000 mov r3, #0 10454: e58d3000 str r3, [sp] 10458: e3a03001 mov r3, #1 1045c: e58d3004 str r3, [sp, #4] 10460: e1a0300d mov r3, sp 10464: e59d1004 ldr r1, [sp, #4] 10468: e1a00003 mov r0, r3 1046c: ebffffe7 bl 10410 <fun> 10470: e3a03000 mov r3, #0 10474: e1a00003 mov r0, r3 10478: e28dd00c add sp, sp, #12 1047c: e49df004 pop {pc} ; (ldr pc, [sp], #4) |
結果自行分析。
有了本文的基礎,我們后面會介紹棧回溯的原理。
