title: arm平台的調用棧回溯(backtrace)
date: 2018-09-19 16:07:47
tags:
介紹
arm平台的調用棧與x86平台的調用棧大致相同,稍微有些區別,主要在於棧幀的壓棧內容和傳參方式不同。在arm平台的不同程序,采用的編譯選項不同,程序運行期間的棧幀也會不同。有些工具在對arm的調用棧回溯時,可能會遇到無法回溯的情況。例如gdb在使用bt查看core dump文件調用棧時,有時會出現Backtrace stoped
的情況,有可能就是棧空間的壓棧順序導致的。當工具無法回溯時,就需要人工結合匯編代碼對棧進行回溯,或者使用unwind進行回溯。
arm棧幀結構

通常情況下,arm的調用棧大致結構與x86相同,都是從高地址向低地址擴張。上圖是其中一種內存分布。
pc, lr, sp, fp是處理器的寄存器,其含義如下:
- pc, program counter,程序計數器。程序當前運行的指令會放入到pc寄存器中
- fp, 即frame pointer,幀指針。通常指向一個函數的棧幀底部,表示一個函數棧的開始位置。
- sp, stack pointer,棧頂指針。指向當前棧空間的頂部位置,當進行push和pop時會一起移動。
- lr, link register。在進行函數調用時,會將函數返回后要執行的下一條指令放入lr中,對應x86架構下的返回地址。
調用棧從高地址向低地址增長,當函數調用時,分別將分別將pc, lr, ip和 fp寄存器壓入棧中,然后移動sp指針,為當前程序開辟棧空間。
arm官方手冊描述如下:
一個arm程序,在任一時刻都存在十五個通用寄存器,這取決於當前的處理器模式。 它們分別是 r0-r12、sp、lr。
sp(或 r13)是堆棧指針。 C 和 C++ 編譯器始終將 sp 用作堆棧指針。 在 Thumb-2 中,sp 被嚴格定義為堆棧指針,因此許多對堆棧操作無用而又使用了 sp 的指令會產生不可預測的結果。 建議您不要將 sp 用作通用寄存器。
在用戶模式下,lr(或 r14)用作鏈接寄存器 (lr),用於存儲調用子例程時的返回地址。 如果返回地址存儲在堆棧上,則也可將 r14 用作通用寄存器。
在異常處理模式下,lr 存放異常的返回地址;如果在一個異常內執行了子例程調用,則 lr 存放子例程的返回地址。如果返回地址存儲在堆棧上,則可將 lr 用作通用寄存器。
除了官方手冊中描述的sp,lr寄存器,通常r12還會作為fp寄存器。fp寄存器對於程序的運行沒有幫助,主要用於對棧幀的回溯。因為sp時刻指向的棧頂,通過fp得知上一個棧幀的起始位置。
上圖的調用棧對應的匯編代碼如下。
- 8514行將當前的sp保存在ip中(ip只是個通用寄存器,用來在函數間分析和調用時暫存數據,通常為r12);
- 8518行將4個寄存器從右向左依次壓棧。
- 851c行將保存的ip減4,得到當前被調用函數的fp地址,即指向棧里的pc位置。
- 8520行將sp減8,為棧空間開辟出8個字節的大小,用於存放局部便令。
00008514 <func1>:
8514: e1a0c00d mov ip, sp
8518: e92dd800 push {fp, ip, lr, pc}
851c: e24cb004 sub fp, ip, #4
8520: e24dd008 sub sp, sp, #8
8524: e3a03000 mov r3, #0
8528: e50b3010 str r3, [fp, #-16]
852c: e30805dc movw r0, #34268 ; 0x85dc
8530: e3400000 movt r0, #0
8534: ebffff9d bl 83b0 <puts@plt>
8538: e51b3010 ldr r3, [fp, #-16]
853c: e12fff33 blx r3
8540: e3a03000 mov r3, #0
8544: e1a00003 mov r0, r3
8548: e24bd00c sub sp, fp, #12
854c: e89da800 ldm sp, {fp, sp, pc}
-mapcs-frame編譯選項
在第一節中,程序壓棧的寄存器有{fp, ip, lr, pc} 4個,這是在gcc帶有-mapcs-frame的編譯選項下編譯出來的。而gcc默認情況下的參數為mno-apcs-frame。關於該選項,gcc的手冊描述為,
Generate a stack frame that is compliant with the ARM Procedure Call Standard for all functions, even if this is not strictly necessary for correct execution of the code. Specifying -fomit-frame-pointer with this option causes the stack frames not to be generated for leaf functions. The default is -mno-apcs-frame. This option is deprecated.
也就是說,該編譯選項會產生(push {fp, ip, lr, pc}),保證棧幀的格式。如果沒有-mapcs-frame,則不保證幀格式和當前幀格式,GCC生成的指令可能會發生各種變化。在AAPCS發布之后[附錄1],1993年的APCS就已經太舊了,所以
在gcc5.0之后,該選項已經被廢棄。gcc5.0的更新記錄寫到:
The options -mapcs, -mapcs-frame, -mtpcs-frame and -mtpcs-leaf-frame which are only applicable to the old ABI have been deprecated.
至於該參數在將來是否會被gcc移除,那就不知道了。
將第一節中的程序重新使用默認編譯選項,用4.7版本的gcc編譯,結果如下。這時,fp還在,調用棧push了fp和lr到棧空間,新的fp指向了lr在棧中的位置。
00008514 <func1>:
8514: e92d4800 push {fp, lr}
8518: e28db004 add fp, sp, #4
851c: e24dd008 sub sp, sp, #8
8520: e3a03000 mov r3, #0
8524: e50b3008 str r3, [fp, #-8]
8528: e30805d4 movw r0, #34260 ; 0x85d4
852c: e3400000 movt r0, #0
8530: ebffff9e bl 83b0 <puts@plt>
8534: e51b3008 ldr r3, [fp, #-8]
8538: e12fff33 blx r3
853c: e3a03000 mov r3, #0
8540: e1a00003 mov r0, r3
8544: e24bd004 sub sp, fp, #4
8548: e8bd8800 pop {fp, pc}
0000854c <main>:
854c: e92d4800 push {fp, lr}
8550: e28db004 add fp, sp, #4
8554: ebffffee bl 8514 <func1>
8558: e1a00003 mov r0, r3
855c: e8bd8800 pop {fp, pc}
使用gcc-7.3默認選項編譯結果如下,fp已經不在了,雖然這里仍然可能通過r7得知上個棧幀的位置,但是已經沒法使用fp獲取棧幀了。此時是不保證棧幀保存在棧中的。所以依賴棧幀內容進行恢復已經非常不可靠。那么既然無法依賴fp,那該怎么進行棧幀回溯呢,gnu說使用unwind方法回溯,這節暫時不會介紹unwind方法。
000103c8 <func1>:
103c8: b580 push {r7, lr}
103ca: b082 sub sp, #8
103cc: af00 add r7, sp, #0
103ce: 2300 movs r3, #0
103d0: 607b str r3, [r7, #4]
103d2: f240 4048 movw r0, #1096 ; 0x448
103d6: f2c0 0001 movt r0, #1
103da: f7ff ef7e blx 102d8 <puts@plt>
103de: 687b ldr r3, [r7, #4]
103e0: 4798 blx r3
103e2: 2300 movs r3, #0
103e4: 4618 mov r0, r3
103e6: 3708 adds r7, #8
103e8: 46bd mov sp, r7
103ea: bd80 pop {r7, pc}
000103ec <main>:
103ec: b580 push {r7, lr}
103ee: af00 add r7, sp, #0
103f0: f7ff ffea bl 103c8 <func1>
103f4: 2300 movs r3, #0
103f6: 4618 mov r0, r3
103f8: bd80 pop {r7, pc}
使用棧幀進行回溯
這一節使用gcc4.7版本,默認編譯選項編譯出來的程序,演示調用棧回溯。該編譯選項下,壓棧的寄存器為{fp, lr}。
下邊的內容是一段core dump中的寄存器和調用棧,本節將對這段內容進行回溯。
Reg: r9, Val = 0xf7578000; Reg: r10, Val = 0x00000001;
Reg: fp, Val = 0x827d3104; Reg: ip, Val = 0xf7578ae0;
Reg: sp, Val = 0x827d30e0; Reg: lr, Val = 0xf7549990;
Reg: pc, Val = 0xf7548c20; Reg: cpsr, Val = 0x60000210;
0x827d30e0: 0x00000031 0x827d31a0 0x00000001 0xd5dff060
0x827d30f0: 0xd5e0e6b1 0xd5dec134 0xf7578000 0xf7577c40
0x827d3100: 0x827d313c 0xf7549990
0x827d3140: 0x00000000 0xd5dec104 0xf7568514 0x00000002
0x827d3150: 0xd5dec104 0xf7577c40 0xf7577c38 0xd5de9224
0x827d3160: 0x827d31a0 0xf757a084 0xf7577c40 0xd5df6dd4
0x827d3170: 0x827d3194 0x00000001 0xd5e0e678 0xd5dec104
0x827d3180: 0xd5de9224 0xf7568548 0x00000000 0xf7568550
- 當前sp地址為0x827d30e0,fp地址為0x827d3104,從而得知當前函數frame0的棧幀。fp指向的地址0x827d3104為frame1的lr,0x827d3100為上一個棧幀的fp。
0x827d30e0: 0x00000031 0x827d31a0 0x00000001 0xd5dff060
0x827d30f0: 0xd5e0e6b1 0xd5dec134 0xf7578000 0xf7577c40
0x827d3100: 0x827d313c(fp) 0xf7549990(lr)
- 從frame0的fp地址0x827d313c可知,frame1的調用棧起始地址,去掉frame0的內容,得到frame1的棧幀。
0x827d312c 0xf7530c14
0x827d3110: 0xd5dff060 0x0000002c 0xd5e0e6b1 0xd5e0e6b1
0x827d3120: 0x00000001 0xd5e0e6b1 0xd5dff060 0xd5dec134
0x827d3130: 0xf7578000 0xf7577c40 0x827d3194(fp) 0xf754ad0c(lr)
- 依次類推,依次得到frame2、frame3...的棧幀。
當匯編代碼的函數調用使用push {fp, ip, lr, pc}
時,則上一個棧幀的fp2在當前棧幀的(fp - #4)位置。棧幀的回溯要結合程序的匯編代碼具體分析,有可能程序並不使用fp指針,也有可能棧中根本沒有保存fp。
unwind方法回溯
TODO
附錄1-函數調用標准縮略語
- PCS Procedure Call Standard.
- AAPCS Procedure Call Standard for the ARM Architecture (this standard).
- APCS ARM Procedure Call Standard (obsolete).
- TPCS Thumb Procedure Call Standard (obsolete).
- ATPCS ARM-Thumb Procedure Call Standard (precursor to this standar