加入收藏 | 设为首页 | 会员中心 | 我要投稿 核心网 (https://www.hxwgxz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 业界 > 正文

Linux内核的栈回溯与妙用

发布时间:2018-11-14 03:19:13 所属栏目:业界 来源:今日头条
导读:1 前言 说起linux内核的栈回溯功能,我想这对每个Linux内核或驱动开发人员来说,太常见了。如下演示的是linux内核崩溃的一个栈回溯打印,有了这个崩溃打印我们能很快定位到在内核哪个函数崩溃,大概在函数什么位置,大大简化了问题排查过程。 网上或多或少

指令接着执行,由B函数跳转到A函数,A函数前三条指令与B函数执行情况类似,重点就三处,A函数栈的第一片内存存储A函数的返回地址,A函数栈的第二片内存存储B函数栈的第二片内存地址,当A函数执行到指令5后,fp寄存器保存的是A函数栈的第二片内存地址,示意图中全部标出。当A函数执行指令6崩溃,怎么栈回溯?

A函数崩溃时,按照上文的分析,fp寄存器保存的数据是A函数栈的第二片内存首地址0X1000。0X1000地址中存储的数据就是B函数的栈地址0x1008(就是B函数的栈的第二片内存),0x1000+4=0X1004地址就是A函数栈的第一片内存,存储的数据是A函数的返回地址0X0030,这个指令地址就是B函数的指令6地址,这样就知道了时B函数调用了A函数。

因为此时已经知道了B函数栈的第二片内存地址,该地址的数据就是C函数栈的第二片内存地址,B函数栈的第一片内存地址中的数据是B函数的返回地址0X0048(C函数的指令6内存地址)。这样就倒着推出函数调用关系:A函数ßB函数ßC函数。

笔者认为,这种情况栈回溯的核心是:每个函数栈的第二片内存地址存储的数据是上一级函数栈的第二片内存地址,每个函数栈的第一片内存地址存储的数据是函数返回地址。只要获取到崩溃函数栈的第二片内存地址(此时就是fp寄存器的数据),就能循环计算出每一级调用的函数。

3.1.1 内核源码分析

如果读者对上一节的演示理解的话,理解下方的源码就比较容易。

  1. arch/arm64/kerneltraps.c 

内核崩溃时,产生异常,内核的异常处理程序自动将崩溃时的CPU寄存器存入struct pt_regs结构体,并传入该函数,相关代码不再列出。这样栈回溯的关键环节就是红色标注的代码,先对frame.fp,frame.sp,frame.pc赋值。

下方进入while循环,先执行unwind_frame(&frame) 找出崩溃过程的每个函数中的汇编指令地址,存入frame.pc(第一次while循环是直接where = frame.pc赋值,这就是当前崩溃函数的崩溃指令地址),下次循环存入where变量,再传入dump_backtrace_entry函数,在该函数中打印诸如[] chrdev_open+0x12/0x4B1 的字符串。

这个打印的其实是在print_ip_sym函数中做的,将ip按照%pS形式打印,就能打印出该函数指令所在的函数,以及相对函数首指令的偏移。栈回溯的重点是在unwind_frame函数。

在正式贴出代码前,先介绍一下栈回溯过程的三个核心CPU寄存器:pc、lr、fp。pc指向运行的汇编指令地址;sp指向函数栈;fp是栈帧指针,不同架构情况不同,但笔者认为它是栈回溯过程中,联系两个有调用关系函数的纽带,下面的分析就能体现出来。

  1. arch/arm64/kernel/stacktrace.c 

首先说明一下,这是arm64位系统,一个long型数据8个字节大小。为了叙述方便,假设内核代码的崩溃函数流程还是 C函数->B函数->A函数,在A函数崩溃,最后在unwind_frame函数中栈回溯。

接着针对代码介绍栈回溯的原理。第一次执行unwind_frame函数时,第二行,frame->fp保存的就是崩溃时CPU的fp寄存器的值,就是A函数栈第二片内存地址,frame->sp = fp + 0x10赋值后,frame->sp就是A函数的栈底地址;frame->fp= *(unsigned long *)(fp)获取的是存储在A函数栈第二片内存中的数据,就是调用A函数的B函数的栈的第二片内存地址;frame->pc = *(unsigned long *)(fp + 8)是获取A函数栈的第一片内存中的数据,就是A函数的返回地址(就是B函数中指令地址),这样就知道了是B函数调用了A函数;经过一次unwind_frame函数调用,就知道了A函数的返回地址和B函数的栈的第二片内存地址,有了B函数栈的第二片内存地址,就能按照上述过程推出B函数的返回地址(C函数的指令地址)和C函数栈的第二片内存地址,这样就知道了时C函数调用了B函数,如此循环,不管有多少级函数调用,都能按照这个规律找出函数调用关系。当然这里的关系是是AßBßC。

为什么栈回溯的原理是这样?首先这个原理笔者都是实际验证过的,细心的读者应该会发现,这个栈回溯的流程跟前文第2节演示的简单栈回溯原理一样。是的,第2节就是笔者按照自己对arm 64位系统栈回溯的理解,用简单的形式表达出来,还附了演示图,这里不了解的读者可以回到第2节分析一下。

3.1.2 arm架构从汇编代码角度解释栈回溯的原理

为了使读者理解的更充分,下文列出一段应用层C语言代码和反汇编后的代码

C代码

汇编代码

分析test_2函数的汇编代码,第一条指令stpx29, x30,[sp,#-16],x29就是fp寄存器,x30就是lr寄存器,指令执行过程:将x30(lr)、x29(fp)寄存器的值随着栈指针sp向下偏移依次入栈,栈指针sp共偏移两次8+8=16个字节(arm 64位系统栈指针sp减一偏移8个字节,并且栈是向下增长,所以指令是-16)。

mov x29,sp 指令就是将栈指针赋予fp寄存器,此时sp就指向test_2函数栈的第二片内存,因为sp偏移了两次,fp寄存器的值就是test_2函数栈的第二片内存地址。

去除不相关的指令,直接从test_2函数跳转到test_1函数开始分析,看test_1函数的第一条指令stp x29, x30,[sp,#-16],首先栈指针sp减一,将x30(lr)寄存器的数据存入test_1函数栈的第一片内存,这就是test_1函数的返回地址,接着栈指针sp减一,将x29(fp)寄存器值入栈,存入test_1函数的第二片内存,此时fp寄存器的值正是test_2函数栈的第二片内存地址,本质就是将test_2函数栈的第二片内存地址存入test_1函数栈的第二片内存中。接着执行mov x29,sp 指令,就是将栈指针sp赋予fp寄存器,此时sp指向test_1函数栈的第二片内存…..

这样就与上一小结的分析一致了, 这里就对arm栈回溯的一般过程,做个较为系统的总结:当C函数跳转的B函数时,先将B函数的返回地址存入B函数栈的第一片内存,然后将C函数栈的第二片内存地址存入B函数栈的第二片内存,接着将B函数栈的第二片内存地址存入fp寄存器,B函数跳转到A函数流程也是这样。

(编辑:核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读