• linux内核中打印栈回溯信息


    转自:http://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汇编的基本知识。在讲代码之前,我先简单说说内核中函数调用的一般过程。

    关键寄存器介绍:

    寄存器含义
    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
    • 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
    • 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, #'
    '
            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  "
    "
    .Lbad:      .asciz  "Backtrace aborted due to bad frame pointer <%p>
    "
            .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!, {}
     
     
  • 相关阅读:
    kinect笔记 一 、 配置环境
    WPF 控制键盘鼠标
    EC-R3308CC四核工业主机
    【免费】Station P1极客主机免费试用活动
    【Sublinux】Sublinux固件下载及使用模式
    ROC-RK3399-PC Plus 六核64位高性能主板
    ROC-RK3308B-CC Plus IoT四核64位开源主板
    【集群服务器】BMC基板管理控制器
    NPU算力集成解决方案
    【技术案例】RK3399/RK3399Pro屏幕拼接
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/6297675.html
Copyright © 2020-2023  润新知