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

Linux内核的栈回溯与妙用

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

假设函数调用过程C->B->A,另外每个函数中只有一个printk打印。这种情况下函数的入栈和unwind段的信息就很规则和简单,这里就以简单的来讲解,便于理解。此时每个函数第一条指令一般是push{r4,lr},这表示将lr和r4寄存器入栈,此时系统会将跟push{r4,lr}指令相关的编码数据0x80a8b0b0存入C函数的unwind段中,0x7fffff10跟偏移有关,但是实际用处不大。0x80a8b0b0分离成0x80,0xa8 ,0xb0又有不同的意义,最重要的是0xa8,表示出栈指令pop {r4 r14},r14就是lr寄存器,与push{r4,lr}入栈指令正好相反。C函数跳转到B函数后,会把B函数的返回地址0xbf004068存入B函数栈。

B函数按照同样的方法执行,当执行到A函数最后,几个函数的栈信息和unwind段信息就如图所示。假设在A函数中崩溃了,会首先根据崩溃的pc值,找到崩溃A函数的unwind段(每个函数的指令地址和unwind段都是对应的,内核有标准的函数可以查找)。如图所示,从地址0xbf00416c的A函数unwind段中取出数据0x80a8b0b0,分析出其中的0xa8,就知道对应的pop {r4 r14}出栈指令,相应就知道函数入栈时执行的是push{r4,lr}指令,其中有两个重要信息,一个是函数入栈时只有lr和r4寄存器入栈,并且函数栈大小是2*4=8个字节,函数崩溃时栈指针sp指向崩溃函数A的栈顶,根据sp就能找到lr寄存器存储在A函数栈的数据0xbf004038,就是崩溃函数的返回地址,上一级函数B的指令地址,而sp+ 2*4就是上一级B函数的栈顶。

知道了B函数的指令地址和栈顶地址,就能根据指令地址找到B函数的unwind段,分析出B函数的入栈指令,按照同样的方法,就能找到C函数的返回地址和栈顶。

这只是几个很简单unwind栈回溯过程的演示,省去了很多细节,读者想研究清楚的话,可以阅读内核arm架构unwind_frame函数实现流程,其中最核心的是在unwind_exec_insn函数,根据0xa8,0xb0这些跟函数入栈过程有关的编码数据,分析入栈过程的详细信息,计算出函数lr寄存器保存在栈中的地址和上一级函数的栈顶地址。

不同的入栈指令在函数的unwind段对应不同的编码,0x80a8b0b0只是其中比较简单的的编码,还有0x80acb0b0,0x80aab0b0等等很多。可以执行 readelf -u .ARM.unwind_idx vmlinux查看内核init段函数的unwind段数据。比如:

这就表示match_dev_by_uuid函数在unwind段编码数据是0x808ab0b0,0xc0008af8是该函数指令首地址。其中有用的是0xa8 ,表示pop {r4,r14}出栈指令,0xb0表示unwind段结束。

为了方便读者分析对应的栈回溯内核源码,这里把关键点列出,并添加必要注释。内核版本3.10.104。

  1. arch/arm/kernel/unwind.c 

2.3 fp和unwind形式栈回溯的比较

上文介绍了两种常用的栈回溯形式的基本原理,并辅助了例子说明。基于fp寄存器的栈回溯和unwind形式的栈回溯,各有优点和缺点。fp形式的栈回溯,基于APCS规范,入栈过程必须要将pc、lr、fp等4个寄存器入栈(其实没必要这样做,只需把lr和fp入栈),并且消耗的入栈指令要多(除了入栈pc、lr、fp等4个寄存器,还得将栈底地址保存到fp),同时还浪费了寄存器,至少fp寄存器是浪费了,不能参与指令数据运算,CPU寄存器是很宝贵的,多一个对加快指令数据运算是有积极意义的。

而unwind形式的栈回溯,就没有这些缺点,仅仅只是将入栈相关的指令的编码保存到unwind段中,不用把无关的寄存器保存到栈中,也不用浪费fp寄存器。

unwind形式栈回溯是有缺点的,首先栈回溯的速度肯定比fp形式栈回溯慢,理解难度要比fp形式大很多,并且,站在开发者角度,使用前还得对每个入栈指令编码,这都是需要工作量的。但是站在使用者角度,这些缺点影响并不大,所以现在有很多arm32系统用的是unwind形式的栈回溯。

3 linux内核栈回溯的原理

当内核崩溃,将会执行异常处理程序,这里以mips架构为例,崩溃函数执行流程是:

  1. do_page_fault()->die()->show_registers()->show_stacktrace()->show_backtrace() 

栈回溯的过程就是在show_backtrace()函数,arm架构最终是在dump_backtrace()函数,内核崩溃处理流程与mips不同。arm架构栈回溯过程相对来说更简单,首先讲解arm架构的栈回溯过程。

不同内核版本,内核代码有差异,本内核版本3.10.104

3.1 arm架构内核栈回溯的分析

内核实际的栈回溯代码还是有点复杂的,在正式讲解代码前,先通过一个简单演示,进一步详细的介绍栈回溯的原理。这次演示是基于fp形式的栈回溯,与上文介绍传统的fp形式栈回溯稍有差异,但是原理是一样的。

下方以伪汇编指令,演示一个完整的函数指令执行与跳转流程:C函数执行B函数,B函数执行A函数,然后A函数发生空指针崩溃。

数执行A函数,然后A函数发生空指针崩溃。

为了帮助读者理解,做一下解释,以C函数的第一条指令为例:

0x00034: C函数返回地址lr入栈指令; C函数指令1

0x00034:表示汇编指令的内存地址,反汇编的读者应该熟悉

C函数返回地址lr入栈指令:表示具体指令的意思,不再用实际汇编指令表示,理解简单

C函数指令1:表示C函数第一条指令,为了引用的简单

其中提到的lr,做过arm内核开发的读者肯定熟悉,是CPU的一个寄存器,存储函数返回地址,,当C函数跳转到B函数时,CPU自动将C函数的指令地址0x00048存入lr寄存器,这表示B函数执行完返回后,CPU将从0x00048地址取指令继续运行(mips架构是ra寄存器,先以arm为例)。

fp寄存器也是arm架构的一个CPU寄存器,英文释义是frame point,中文有称为栈帧寄存器,我们这里用来存储每个函数栈的第2片内存地址(一片内存地址4个字节,这样称呼是为了叙述方便),下方有详细讲解。为了方便读者理解,特画出函数执行过程函数栈数据示意图。

矩形框表示函数栈,初始化全为0,0x1000、0x1004等表示函数栈处于内存的地址,函数栈向下增长。每个函数前两条指令都是入栈指令,每个函数指令执行后只占用两片内存。由于C函数是初始函数,栈回溯过程C函数栈意义不大,就从C函数跳转到B函数指令开始分析。

(编辑:核心网)

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

热点阅读