参考文章:
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()函数需要做以下尝试:
发生函数调用时,函数栈和代码段的关系如下图所示:
接下来我们就来看一下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!, {}