linux內核中打印棧回溯信息 - dump_stack()函數分析


參考文章:

https://blog.csdn.net/jasonchen_gbd/article/details/45585133

簡介

當內核出現比較嚴重的錯誤時,例如發生Oops錯誤或者內核認為系統運行狀態異常,內核就會打印出當前進程的棧回溯信息,其中包含當前執行代碼的位置以及相鄰的指令、產生錯誤的原因、關鍵寄存器的值以及函數調用關系等信息,這些信息對於調試內核錯誤非常有用。

打印函數調用關系的函數就是dump_stack(),該函數不僅可以用在系統出問題的時候,我們在調試內核的時候,可以通過dump_stack()函數的打印信息更方便的了解內核代碼執行流程。
dump_stack()函數的實現和系統結構緊密相關,本文介紹ARM體系中dump_stack()函數的實現。該函數定義在arch/arm/kernel/traps.c文件中,調用dump_stack()函數不需要添加頭文件,基本上在內核代碼任何地方都可以直接使用該函數。

相關基本知識

讀者需要了解一些ARM匯編的基本知識。在講代碼之前,我先簡單說說內核中函數調用的一般過程。

關鍵寄存器介紹:

內核中的函數棧

 內核中,一個函數的代碼最開始的指令都是如下形式:

1             mov   ip, sp
2             stmfd sp!, {r0 - r3} (可選的)
3             stmfd sp!, {..., fp, ip, lr, pc}
4             ……

從其中兩條stmfd(壓棧)指令可以看出,一個函數的函數棧的棧底(高地址)的結構基本是固定的,如下圖:

 

 首先我們約定被調用的函數稱為callee函數,而調用者函數稱為caller函數。

在進行函數調用的回溯時,內核中的dump_stack()函數需要做以下嘗試:

  1. 首先讀取系統中的FP寄存器的值,我們知道幀指針是指向函數棧的某個位置的,所以通過FP的值可以直接找到當前函數的函數棧的地址。
  2. 得到當前函數的代碼段地址,這個很容易,因為當前正在執行的代碼(可通過PC寄存器獲得)就處在函數的代碼段中。在函數棧中保存了一個PC寄存器的備份,通過這個PC寄存器的值可以定位到函數的第一條指令,即函數的入口地址。
  3. 得到當前函數的入口地址后,內核中保存了所有函數地址和函數名的對應關系,所以可以打印出函數名(詳見另一篇博客:內核符號表的查找過程)。
  4. 在當前函數的函數棧中還保存了caller函數的幀指針(FP寄存器的值),所以我們就可以找到caller函數的函數棧的位置。
  5. 繼續執行2-4步,直到某個函數的函數棧中保存的幀指針(FP寄存器的值)為0或非法。

發生函數調用時,函數棧和代碼段的關系如下圖所示:

 

 

dump_stack()函數

接下來我們就來看一下dump_stack()函數的實現。
dump_stack()主要是調用了下面的函數

1 c_backtrace(fp, mode);

兩個參數的含義為:
fp: current進程棧的fp寄存器。
mode: ptrace用到的PSR模式,在這里我們不關心。dump_stack傳入的值為0x10。
這兩個參數分別賦值給r0, r1寄存器傳給c_backtrace()函數。
c_backtrace函數定義如下(arch/arm/lib/backtrace.S):

  1 @ 定義幾個局部變量
  2 #define frame   r4
  3 #define sv_fp   r5
  4 #define sv_pc   r6
  5 #define mask    r7
  6 #define offset  r8
  7 
  8 @ 當前處於dump_backtrace函數的棧中
  9 ENTRY(c_backtrace)
 10         stmfd   sp!, {r4 - r8, lr}  @ 將r4-r8和lr壓入棧中,我們要使用r4-r8,所以備份一下原來的值。sp指向最后壓入的數據
 11         movs    frame, r0   @ frame=r0。r0為傳入的第一個參數,即fp寄存器的值
 12         beq no_frame        @ 如果frame為0,則退出
 13 
 14         tst r1, #0x10       @ 26 or 32-bit mode? 判斷r1的bit4是否為0
 15         moveq   mask, #0xfc000003   @ mask for 26-bit 如果是,即r1=0x10,則mask=0xfc000003,即pc地址只有低26bit有效,且末兩位為0
 16         movne   mask, #0        @ mask for 32-bit 如果不是,即r1!=0x10,則mask=0
 17 
 18         @ 下面是一段和該函數無關的代碼,用來計算pc預取指的偏移,一般pc是指向下兩條指令,所以offset一般等於8
 19 1:      stmfd   sp!, {pc}       @ 存儲pc的值到棧中,sp指向pc。
 20         ldr r0, [sp], #4        @ r0=sp的值,即剛剛存的pc的值(將要執行的指令),sp=sp+4即還原sp
 21         adr r1, 1b              @ r1 = 標號1的地址,即指令 stmfd sp!, {pc} 的地址
 22         sub offset, r0, r1      @ offset=r0-r1,即pc實際指向的指令和讀取pc的指令之間的偏移
 23 
 24 /*
 25  * Stack frame layout:
 26  *             optionally saved caller registers (r4 - r10)
 27  *             saved fp
 28  *             saved sp
 29  *             saved lr
 30  *    frame => saved pc     @ frame即上面的fp,每個函數的fp都指向這個位置
 31  *             optionally saved arguments (r0 - r3)
 32  * saved sp => <next word>
 33  *
 34  * Functions start with the following code sequence:
 35  *                  mov   ip, sp
 36  *                  stmfd sp!, {r0 - r3} (optional)
 37  * corrected pc =>  stmfd sp!, {..., fp, ip, lr, pc} //將pc壓棧的指令
 38  */
 39  @ 函數主流程:開始查找並打印調用者函數
 40 for_each_frame: tst frame, mask     @ Check for address exceptions
 41         bne no_frame
 42 
 43         @ 由sv_pc找到將pc壓棧的那條指令,因為這條指令在代碼段中的位置有特殊性,可用於定位函數入口。
 44 1001:       ldr sv_pc, [frame, #0]      @ 獲取保存在callee棧里的sv_pc,它指向callee的代碼段的某個位置
 45 1002:       ldr sv_fp, [frame, #-12]    @ get saved fp,這個fp就是caller的fp,指向caller的棧中某個位置
 46 
 47         sub sv_pc, sv_pc, offset    @ sv_pc減去offset,找到將pc壓棧的那條指令,即上面注釋提到的corrected pc。
 48         bic sv_pc, sv_pc, mask      @ mask PC/LR for the mode 清除sv_pc中mask為1的位,例如,mask=0x4,則清除sv_pc的bit2。
 49 
 50         @ 定位函數的第一條指令,即函數入口地址
 51 1003:       ldr r2, [sv_pc, #-4]    @ if stmfd sp!, {args} exists, 如果在函數最開始壓入了r0-r3
 52         ldr r3, .Ldsi+4             @ adjust saved 'pc' back one. r3 = 0xe92d0000 >> 10
 53         teq r3, r2, lsr #10         @ 比較stmfd指令機器碼是否相同(不關注是否保存r0-r9),目的是判斷是否為stmfd指令
 54         subne   r0, sv_pc, #4       @ allow for mov: 如果sv_pc前面只有mov   ip, sp
 55         subeq   r0, sv_pc, #8       @ allow for mov + stmia: 如果sv_pc前面有兩條指令
 56         @ 至此,r0為callee函數的第一條指令的地址,即callee函數的入口地址
 57 
 58         @ 打印r0地址對應的符號名,傳給dump_backtrace_entry三個參數:
 59         @ r0:函數入口地址,
 60         @ r1:返回值即caller中的地址,
 61         @ r2:callee的fp
 62         ldr r1, [frame, #-4]    @ get saved lr
 63         mov r2, frame
 64         bic r1, r1, mask        @ mask PC/LR for the mode
 65         bl  dump_backtrace_entry
 66 
 67         @ 打印保存在棧里的寄存器,這跟棧回溯沒關系,本文中不太關心
 68         ldr r1, [sv_pc, #-4]    @ if stmfd sp!, {args} exists, sv_pc前一條指令是否是stmfd指令
 69         ldr r3, .Ldsi+4
 70         teq r3, r1, lsr #10 
 71         ldreq   r0, [frame, #-8]    @ get sp。frame-8指向保存的IP寄存器,由於mov   ip, sp,所以caller的sp=ip
 72                                     @ 所以r0=caller的棧的低地址。
 73         subeq   r0, r0, #4      @ point at the last arg. r0+4就是callee的棧的高地址。
 74                                 @ 由於參數的壓棧順序為r3,r2,r1,r0,所以這里棧頂實際上是最后一個參數。
 75         bleq    .Ldumpstm       @ dump saved registers
 76 
 77         @ 打印保存在棧里的寄存器,這跟棧回溯沒關系,本文中不太關心
 78 1004:       ldr r1, [sv_pc, #0]     @ if stmfd sp!, {..., fp, ip, lr, pc}
 79         ldr r3, .Ldsi       @ instruction exists, 如果指令為frame指向的指令為stmfd sp!, {..., fp, ip, lr, pc}
 80         teq r3, r1, lsr #10
 81         subeq   r0, frame, #16 @ 跳過fp, ip, lr, pc,即找到保存的r4-r10
 82         bleq    .Ldumpstm       @ dump saved registers,打印出來r4-r10
 83 
 84         @ 對保存在當前函數棧中的caller的fp做合法性檢查
 85         teq sv_fp, #0       @ zero saved fp means 判斷獲取的caller的fp的值
 86         beq no_frame        @ no further frames   如果caller fp=0,則停止循環
 87 
 88         @ 更新frame變量指向caller函數棧的位置,將上面注釋中的Stack frame layout
 89         cmp sv_fp, frame        @ sv_fp-frame
 90         mov frame, sv_fp        @ frame=sv_fp
 91         bhi for_each_frame      @ cmp的結果,如果frame<sv_fp,即當前fp小於caller的fp,則繼續循環
 92         @ 這時frame指向caller棧的fp,由於函數中不會修改fp的值,所以這個fp肯定是指向caller保存的pc的位置的。
 93 
 94 1006:       adr r0, .Lbad       @ 否則就打印bad frame提示
 95         mov r1, frame
 96         bl  printk
 97 no_frame:   ldmfd   sp!, {r4 - r8, pc}
 98 ENDPROC(c_backtrace)
 99 @ c_backtrace函數結束。
100 
101         @ 將上面的代碼放到__ex_table異常表中。其中1001b ... 1006b是指上面的1001-1006標號。
102         .section __ex_table,"a"
103         .align  3
104         .long   1001b, 1006b
105         .long   1002b, 1006b
106         .long   1003b, 1006b
107         .long   1004b, 1006b
108         .previous
109 
110 #define instr r4
111 #define reg   r5
112 #define stack r6
113 
114 @ 打印寄存器值
115 .Ldumpstm:  stmfd   sp!, {instr, reg, stack, r7, lr}
116         mov stack, r0
117         mov instr, r1
118         mov reg, #10
119         mov r7, #0
120 1:      mov r3, #1
121         tst instr, r3, lsl reg
122         beq 2f
123         add r7, r7, #1
124         teq r7, #6
125         moveq   r7, #1
126         moveq   r1, #'\n'
127         movne   r1, #' '
128         ldr r3, [stack], #-4
129         mov r2, reg
130         adr r0, .Lfp
131         bl  printk
132 2:      subs    reg, reg, #1
133         bpl 1b
134         teq r7, #0
135         adrne   r0, .Lcr
136         blne    printk
137         ldmfd   sp!, {instr, reg, stack, r7, pc}
138 
139 .Lfp:       .asciz  "%cr%d:%08x"
140 .Lcr:       .asciz  "\n"
141 .Lbad:      .asciz  "Backtrace aborted due to bad frame pointer <%p>\n"
142         .align
143 .Ldsi:  
144         @ 用來判斷是否是stmfd sp!指令,並且參數包含fp, ip, lr, pc,不包含r10
145         .word   0xe92dd800 >> 10    @ stmfd sp!, {... fp, ip, lr, pc}
146         @ 用來判斷是否是stmfd sp!指令,並且參數不包含r10, fp, ip, lr, pc
147         .word   0xe92d0000 >> 10    @ stmfd sp!, {}

 


免責聲明!

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



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