1. 程式人生 > >dump_stack()函式分析

dump_stack()函式分析

簡介

當核心出現比較嚴重的錯誤時,例如發生Oops錯誤或者核心認為系統執行狀態異常,核心就會打印出當前程序的棧回溯資訊,其中包含當前執行程式碼的位置以及相鄰的指令、產生錯誤的原因、關鍵暫存器的值以及函式呼叫關係等資訊,這些資訊對於除錯核心錯誤非常有用。

列印函式呼叫關係的函式就是dump_stack(),該函式不僅可以用在系統出問題的時候,我們在除錯核心的時候,可以通過dump_stack()函式的列印資訊更方便的瞭解核心程式碼執行流程。 dump_stack()函式的實現和系統結構緊密相關,本文介紹ARM體系中dump_stack()函式的實現。該函式定義在arch/arm/kernel/traps.c檔案中,呼叫dump_stack()函式不需要新增標頭檔案,基本上在核心程式碼任何地方都可以直接使用該函式。

相關基本知識

讀者需要了解一些ARM彙編的基本知識。在講程式碼之前,我先簡單說說核心中函式呼叫的一般過程。

關鍵暫存器介紹:

暫存器 含義
r0-r3 用作函式傳參,例如函式A呼叫函式B,如果A需要向B傳遞引數,則將引數放到暫存器r0-r3中,如果引數個數大於4,則需要借用函式的棧空間。
r4-r11 變數暫存器,在函式中可以用來儲存臨時變數。
r9(SB) 靜態基址暫存器。
r10(SL) 棧界限暫存器。
r11(FP) 幀指標暫存器,通常用來訪問函式棧,幀指標指向函式棧中的某個位置。
r12(IP) 內部過程呼叫暫存暫存器。
r13(SP) 棧指標暫存器,用來指向函式棧的棧頂。
r14(LR) 連結暫存器,通常用來儲存函式的返回地址。
r15(PC) 程式計數器,指向程式碼段中下一條將要執行的指令,不過由於流水線的作用,PC會指向將要執行的指令的下一條指令。

核心中的函式棧

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

            mov   ip, sp
            stmfd sp!, {r0 - r3} (可選的)
            stmfd sp!, {..., fp, ip, lr, pc}
            ……
  • 1
  • 2
  • 3
  • 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()主要是呼叫了下面的函式

c_backtrace(fp, mode);
  • 1

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

@ 定義幾個區域性變數
#define frame   r4
#define sv_fp   r5
#define sv_pc   r6
#define mask    r7
#define offset  r8

@ 當前處於dump_backtrace函式的棧中
ENTRY(c_backtrace)
        stmfd   sp!, {r4 - r8, lr}  @ 將r4-r8和lr壓入棧中,我們要使用r4-r8,所以備份一下原來的值。sp指向最後壓入的資料
        movs    frame, r0   @ frame=r0。r0為傳入的第一個引數,即fp暫存器的值
        beq no_frame        @ 如果frame為0,則退出

        tst r1, #0x10       @ 26 or 32-bit mode? 判斷r1的bit4是否為0
        moveq   mask, #0xfc000003   @ mask for 26-bit 如果是,即r1=0x10,則mask=0xfc000003,即pc地址只有低26bit有效,且末兩位為0
        movne   mask, #0        @ mask for 32-bit 如果不是,即r1!=0x10,則mask=0

        @ 下面是一段和該函式無關的程式碼,用來計算pc預取指的偏移,一般pc是指向下兩條指令,所以offset一般等於8
1:      stmfd   sp!, {pc}       @ 儲存pc的值到棧中,sp指向pc。
        ldr r0, [sp], #4        @ r0=sp的值,即剛剛存的pc的值(將要執行的指令),sp=sp+4即還原sp
        adr r1, 1b              @ r1 = 標號1的地址,即指令 stmfd sp!, {pc} 的地址
        sub offset, r0, r1      @ offset=r0-r1,即pc實際指向的指令和讀取pc的指令之間的偏移

/*
 * Stack frame layout:
 *             optionally saved caller registers (r4 - r10)
 *             saved fp
 *             saved sp
 *             saved lr
 *    frame => saved pc     @ frame即上面的fp,每個函式的fp都指向這個位置
 *             optionally saved arguments (r0 - r3)
 * saved sp => <next word>
 *
 * Functions start with the following code sequence:
 *                  mov   ip, sp
 *                  stmfd sp!, {r0 - r3} (optional)
 * corrected pc =>  stmfd sp!, {..., fp, ip, lr, pc} //將pc壓棧的指令
 */
 @ 函式主流程:開始查詢並列印呼叫者函式
for_each_frame: tst frame, mask     @ Check for address exceptions
        bne no_frame

        @ 由sv_pc找到將pc壓棧的那條指令,因為這條指令在程式碼段中的位置有特殊性,可用於定位函式入口。
1001:       ldr sv_pc, [frame, #0]      @ 獲取儲存在callee棧裡的sv_pc,它指向callee的程式碼段的某個位置
1002:       ldr sv_fp, [frame, #-12]    @ get saved fp,這個fp就是caller的fp,指向caller的棧中某個位置

        sub sv_pc, sv_pc, offset    @ sv_pc減去offset,找到將pc壓棧的那條指令,即上面註釋提到的corrected pc。
        bic sv_pc, sv_pc, mask      @ mask PC/LR for the mode 清除sv_pc中mask為1的位,例如,mask=0x4,則清除sv_pc的bit2。

        @ 定位函式的第一條指令,即函式入口地址
1003:       ldr r2, [sv_pc, #-4]    @ if stmfd sp!, {args} exists, 如果在函式最開始壓入了r0-r3
        ldr r3, .Ldsi+4             @ adjust saved 'pc' back one. r3 = 0xe92d0000 >> 10
        teq r3, r2, lsr #10         @ 比較stmfd指令機器碼是否相同(不關注是否儲存r0-r9),目的是判斷是否為stmfd指令
        subne   r0, sv_pc, #4       @ allow for mov: 如果sv_pc前面只有mov   ip, sp
        subeq   r0, sv_pc, #8       @ allow for mov + stmia: 如果sv_pc前面有兩條指令
        @ 至此,r0為callee函式的第一條指令的地址,即callee函式的入口地址

        @ 列印r0地址對應的符號名,傳給dump_backtrace_entry三個引數:
        @ r0:函式入口地址,
        @ r1:返回值即caller中的地址,
        @ r2:callee的fp
        ldr r1, [frame, #-4]    @ get saved lr
        mov r2, frame
        bic r1, r1, mask        @ mask PC/LR for the mode
        bl  dump_backtrace_entry

        @ 列印儲存在棧裡的暫存器,這跟棧回溯沒關係,本文中不太關心
        ldr r1, [sv_pc, #-4]    @ if stmfd sp!, {args} exists, sv_pc前一條指令是否是stmfd指令
        ldr r3, .Ldsi+4
        teq r3, r1, lsr #10 
        ldreq   r0, [frame, #-8]    @ get sp。frame-8指向儲存的IP暫存器,由於mov   ip, sp,所以caller的sp=ip
                                    @ 所以r0=caller的棧的低地址。
        subeq   r0, r0, #4      @ point at the last arg. r0+4就是callee的棧的高地址。
                                @ 由於引數的壓棧順序為r3,r2,r1,r0,所以這裡棧頂實際上是最後一個引數。
        bleq    .Ldumpstm       @ dump saved registers

        @ 列印儲存在棧裡的暫存器,這跟棧回溯沒關係,本文中不太關心
1004:       ldr r1, [sv_pc, #0]     @ if stmfd sp!, {..., fp, ip, lr, pc}
        ldr r3, .Ldsi       @ instruction exists, 如果指令為frame指向的指令為stmfd sp!, {..., fp, ip, lr, pc}
        teq r3, r1, lsr #10
        subeq   r0, frame, #16 @ 跳過fp, ip, lr, pc,即找到儲存的r4-r10
        bleq    .Ldumpstm       @ dump saved registers,打印出來r4-r10

        @ 對儲存在當前函式棧中的caller的fp做合法性檢查
        teq sv_fp, #0       @ zero saved fp means 判斷獲取的caller的fp的值
        beq no_frame        @ no further frames   如果caller fp=0,則停止迴圈

        @ 更新frame變數指向caller函式棧的位置,將上面註釋中的Stack frame layout
        cmp sv_fp, frame        @ sv_fp-frame
        mov frame, sv_fp        @ frame=sv_fp
        bhi for_each_frame      @ cmp的結果,如果frame<sv_fp,即當前fp小於caller的fp,則繼續迴圈
        @ 這時frame指向caller棧的fp,由於函式中不會修改fp的值,所以這個fp肯定是指向caller儲存的pc的位置的。

1006:       adr r0, .Lbad       @ 否則就列印bad frame提示
        mov r1, frame
        bl  printk
no_frame:   ldmfd   sp!, {r4 - r8, pc}
ENDPROC(c_backtrace)
@ c_backtrace函式結束。

        @ 將上面的程式碼放到__ex_table異常表中。其中1001b ... 1006b是指上面的1001-1006標號。
        .section __ex_table,"a"
        .align  3
        .long   1001b, 1006b
        .long   1002b, 1006b
        .long   1003b, 1006b
        .long   1004b, 1006b
        .previous

#define instr r4
#define reg   r5
#define stack r6

@ 列印暫存器值
.Ldumpstm:  stmfd   sp!, {instr, reg, stack, r7, lr}
        mov stack, r0
        mov instr, r1
        mov reg, #10
        mov r7, #0
1:      mov r3, #1
        tst instr, r3, lsl reg
        beq 2f
        add r7, r7, #1
        teq r7, #6
        moveq   r7, #1
        moveq   r1, #'\n'
        movne   r1, #' '
        ldr r3, [stack], #-4
        mov r2, reg
        adr r0, .Lfp
        bl  printk
2:      subs    reg, reg, #1
        bpl 1b
        teq r7, #0
        adrne   r0, .Lcr
        blne    printk
        ldmfd   sp!, {instr, reg, stack, r7, pc}

.Lfp:       .asciz  "%cr%d:%08x"
.Lcr:       .asciz  "\n"
.Lbad:      .asciz  "Backtrace aborted due to bad frame pointer <%p>\n"
        .align
.Ldsi:  
        @ 用來判斷是否是stmfd sp!指令,並且引數包含fp, ip, lr, pc,不包含r10
        .word   0xe92dd800 >> 10    @ stmfd sp!, {... fp, ip, lr, pc}
        @ 用來判斷是否是stmfd sp!指令,並且引數不包含r10, fp, ip, lr, pc
        .word   0xe92d0000 >> 10    @ stmfd sp!, {}
  • 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
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
					<link href="https://csdnimg.cn/release/phoenix/mdeditor/markdown_views-778f64ae39.css" rel="stylesheet">
            </div>