简介

当内核出现比较严重的错误时,例如发生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}

……

从其中两条stmfd(压栈)指令可以看出,一个函数的函数栈的栈底(高地址)的结构基本是固定的,如下图:

首先我们约定被调用的函数称为callee函数,而调用者函数称为caller函数。

在进行函数调用的回溯时,内核中的dump_stack()函数需要做以下尝试:

首先读取系统中的FP寄存器的值,我们知道帧指针是指向函数栈的某个位置的,所以通过FP的值可以直接找到当前函数的函数栈的地址。

得到当前函数的代码段地址,这个很容易,因为当前正在执行的代码(可通过PC寄存器获得)就处在函数的代码段中。在函数栈中保存了一个PC寄存器的备份,通过这个PC寄存器的值可以定位到函数的第一条指令,即函数的入口地址。

得到当前函数的入口地址后,内核中保存了所有函数地址和函数名的对应关系,所以可以打印出函数名(详见另一篇博客:内核符号表的查找过程)。

在当前函数的函数栈中还保存了caller函数的帧指针(FP寄存器的值),所以我们就可以找到caller函数的函数栈的位置。

继续执行2-4步,直到某个函数的函数栈中保存的帧指针(FP寄存器的值)为0或非法。

发生函数调用时,函数栈和代码段的关系如下图所示:

dump_stack()函数

接下来我们就来看一下dump_stack()函数的实现。

dump_stack()主要是调用了下面的函数

c_backtrace(fp, mode);

两个参数的含义为:

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 =>

*

* 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

@ 这时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 \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!, {}

Linux内核出错的栈打印详解,linux内核中打印栈回溯信息 - dump_stack()函数分析相关推荐

  1. linux 内存 参数,linux free命令参数及用法详解(linux查看内存命令)

    linux free命令参数及用法详解(linux查看内存命令) 2019年05月31日 | 萬仟网科技 | 我要评论 free指令会显示内存的使用情况,包括实体内存,虚拟的交换文件内存,共享内存区段 ...

  2. linux中whoami命令的作用是,linux whoami命令参数及用法详解(linux查看登陆用户名)

    linux whoami命令参数及用法详解(linux查看登陆用户名) 2019年05月31日 | 萬仟网科技 | 我要评论 whoami 命令显示登录名.与使用命令 who 并指定 am i 不同, ...

  3. linux中的su-命令的功能,linux su命令参数及用法详解(linux切换用户命令)

    linux su命令参数及用法详解(linux切换用户命令) 发布时间:2012-07-21 12:12:39   作者:佚名   我要评论 su的作用是变更为其它使用者的身份,超级用户除外,需要键入 ...

  4. linux 的中断 命令,linux skill命令参数及用法详解(linux中断进程执行命令)

    linux skill命令参数及用法详解(linux中断进程执行命令) 发布时间:2012-07-21 12:09:37   作者:佚名   我要评论 linux 冻结进程命令 skill 使用详解 ...

  5. linux命令优先级设置,linux renice命令参数及用法详解(linux修改程序运行优先级命令)...

    linux renice命令参数及用法详解(linux修改程序运行优先级命令) 发布时间:2012-07-21 12:45:32   作者:佚名   我要评论 renice指令可重新调整程序执行的优先 ...

  6. c语言 字符串 strncpy,详解c语言中的 strcpy和strncpy字符串函数使用

    详解c语言中的 strcpy和strncpy字符串函数使用 strcpy 和strcnpy函数--字符串复制函数. 1.strcpy函数 函数原型:char *strcpy(char *dst,cha ...

  7. linux中fdisk的参数,Linux fdisk命令参数及用法详解--Linux磁盘分区管理命令fdisk

    fdisk 命令 linux磁盘分区管理 用途:观察硬盘之实体使用情形与分割硬盘用. 使用方法: 一.在 console 上输入 fdisk -l /dev/sda ,观察硬盘之实体使用情形. 二.在 ...

  8. linux c 获取进程p id,详解Linux获取线程的PID(TID、LWP)的几种方式

    在 Linux C/C++ 中通常是通过 pthread 库进行线程级别的操作. 在 pthread 库中有函数: pthread_t pthread_self(void); 它返回一个 pthrea ...

  9. linux 查看网络流量来源_详解Linux查看实时网卡流量的几种方式

    在工作中,我们经常需要查看服务器的实时网卡流量.通常,我们会通过这几种方式查看Linux服务器的实时网卡流量. 1. sar -n DEV 1 2 sar命令包含在sysstat工具包中,提供系统的众 ...

最新文章

  1. “还完花呗,再也不用吃土!”是真的吗?(附代码)
  2. Android底层开发之Audio HAL Android Audio Overview
  3. 数字通信介绍(2)香农与信息论
  4. java中PL层_安装pljava - RuralHunter的个人空间 - OSCHINA - 中文开源技术交流社区
  5. vue 集成 Loading 加载效果
  6. Jmeter使用方法
  7. Nature:新发现挑战神经元作用传统理论 [转自科学网]
  8. Kinect + OpenNI + OpenCV + OpenGL 三维重建
  9. 备战2019秋招之程序员代码面试指南(左程云)C++
  10. Matlab 绘制柱状图并标注对应数字值
  11. Endnote插入Word参考文献国标格式
  12. EveryDay-Shell之ifconfig输出详解“——netspeed.sh网速监控脚本
  13. PADS过孔与安全间距的设置与使用
  14. signal 11 linux,关于运行时Signal 11 Caught的错误
  15. matlab fscanf
  16. 新机如何把机械硬盘中的系统克隆到固态硬盘
  17. 容器技术---(一)Docker
  18. D:/Vitis/export/RF47DR/RF47DRxpfm‘ is invalid. please choose a valid platform.
  19. 华为云IOT Android应用开发详细教程
  20. 淘宝标签单法做好这6步,帮你快速上淘宝首页,提升宝贝流量

热门文章

  1. jvm 参数_6个重要的JVM性能参数
  2. php补充 扩展,php补充安装扩展支持
  3. 吴恩达 matlab,吴恩达机器学习记录--Matlab 一些基本操作
  4. 计算机无法用telnet,为何我的电脑cmd没法使用telnet命令?
  5. python事件循环_简单了解一下事件循环(Event Loop)
  6. Redis的安装以及基本操作简介
  7. 三、Git多人开发:不同人修改了同文件的不同区域如何处理
  8. 二十四、爬取古诗网中的100首古诗文
  9. centos7安装Java
  10. pytorch 实现线性回归