arm架構函數幀棧分析【轉】


轉自: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)

結果自行分析。

 

 

有了本文的基礎,我們后面會介紹棧回溯的原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM