linux内核中打印栈回溯信息 - dump_stack()函数分析【转】

简介:

转自: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}
            ……
AI 代码解读
  • 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);
AI 代码解读
  • 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, #'\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!, {}
AI 代码解读

 

 










本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sky-heaven/p/6297675.html ,如需转载请自行联系原作者
目录
打赏
0
0
0
0
64
分享
相关文章
Linux 内核源码分析---proc 文件系统
`proc`文件系统是Linux内核中一个灵活而强大的工具,提供了一个与内核数据结构交互的接口。通过本文的分析,我们深入探讨了 `proc`文件系统的实现原理,包括其初始化、文件的创建与操作、动态内容生成等方面。通过对这些内容的理解,开发者可以更好地利用 `proc`文件系统来监控和调试内核,同时也为系统管理提供了便利的工具。
50 16
|
20天前
|
Linux系统中如何查看CPU信息
本文介绍了查看CPU核心信息的方法,包括使用`lscpu`命令和读取`/proc/cpuinfo`文件。`lscpu`能快速提供逻辑CPU数量、物理核心数、插槽数等基本信息;而`/proc/cpuinfo`则包含更详细的配置数据,如核心ID和处理器编号。此外,还介绍了如何通过`lscpu`和`dmidecode`命令获取CPU型号、制造商及序列号,并解释了CPU频率与缓存大小的相关信息。最后,详细解析了`lscpu`命令输出的各项参数含义,帮助用户更好地理解CPU的具体配置。
58 8
|
1月前
|
linux中的目录操作函数
本文详细介绍了Linux系统编程中常用的目录操作函数,包括创建目录、删除目录、读取目录内容、遍历目录树以及获取和修改目录属性。这些函数是进行文件系统操作的基础,通过示例代码展示了其具体用法。希望本文能帮助您更好地理解和应用这些目录操作函数,提高系统编程的效率和能力。
178 26
Linux系统查看操作系统版本信息、CPU信息、模块信息
在Linux系统中,常用命令可帮助用户查看操作系统版本、CPU信息和模块信息
204 23
Intel Linux 内核测试套件-LKVS介绍 | 龙蜥大讲堂104期
《Intel Linux内核测试套件-LKVS介绍》(龙蜥大讲堂104期)主要介绍了LKVS的定义、使用方法、测试范围、典型案例及其优势。LKVS是轻量级、低耦合且高代码覆盖率的测试工具,涵盖20多个硬件和内核属性,已开源并集成到多个社区CICD系统中。课程详细讲解了如何使用LKVS进行CPU、电源管理和安全特性(如TDX、CET)的测试,并展示了其在实际应用中的价值。
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
158 15
浅入分析Linux
Linux 操作系统必须完成的两个主要目的 与硬件部分交互, 为包含在硬件平台上的所有底层可编程部件提供服务 为运行在计算机系统上的应用程序(即所谓的用户空间)提供执行环境 一些操作系统运行所有的用户程序都直接与硬件部分进行交互, 比如典型的MS-DOS。
1037 0
|
1天前
|
linux命令详细说明以及案例
本文介绍了常用的 Linux 命令及其详细说明和示例,包括:`ls`(列出目录内容)、`cd`(更改目录)、`rm` 和 `mv`(删除与移动文件)、`grep`(搜索文本)、`cat`(显示文件内容)以及 `chmod`(更改文件权限)。每个命令均配有功能描述、选项说明及实际案例,帮助用户更好地掌握 Linux 命令的使用方法。
79 56
|
4天前
|
Linux基础:文件和目录类命令分析。
总的来说,这些基础命令,像是Linux中藏匿的小矮人,每一次我们使用他们,他们就把我们的指令准确的传递给Linux,让我们的指令变为现实。所以,现在就开始你的Linux之旅,挥动你的命令之剑,探索这个充满神秘而又奇妙的世界吧!
48 19
|
19天前
|
Linux 常用文件查看命令
`cat` 命令用于连接文件并打印到标准输出,适用于快速查看和合并文本文件内容。常用示例包括:`cat file1.txt` 查看单个文件,`cat file1.txt file2.txt` 合并多个文件,`cat &gt; filename` 创建新文件,`cat &gt;&gt; filename` 追加内容。`more` 和 `less` 命令用于分页查看文件,`tail` 命令则用于查看文件末尾内容,支持实时追踪日志更新,如 `tail -f file.log`。
45 5
Linux 常用文件查看命令