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

Linux内核的栈回溯与妙用

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

当A函数中崩溃时,先从fp寄存器中获取A函数栈的第二片内存地址,从中取出B函数栈的第二片内存地址,再从A函数栈的第一片内存取出A函数的返回地址,也就是B函数中的指令地址,这样就推导出B函数调用了A函数,同理推导出C函数调用了B函数。

演示的代码很简答,但是这个分析是适用于复杂函数的,已经实际验证过。

3.1.3 arm 内核栈回溯的“bug”

这个不是我危言耸听,是实际测出来的。比如如下代码:

这个函数调用流程在内核崩溃了,内核栈回溯是不会打印上边的b函数,有arm 64系统的读者可以验证一下,我多次验证得出的结论是,如果崩溃的函数没有执行其他函数,就会打乱栈回溯规则,为什么呢?请回头看上一节的代码演示

汇编代码是

可以发现,test_a_函数前两条指令不是stpx29, x30,[sp,#-16]和mov x29,sp,这两条指令可是栈回溯的关键环节。怎么解决呢?仔细分析的话,是可以解决的。

一般情况,函数崩溃,fp寄存器保存的数据是当前函数栈的第二片内存地址,当前函数栈的第一片内存地址保存的是函数返回地址,从该地址取出的数据与lr寄存器的数据应是一致的,因为lr寄存器保存的也是函数返回地址,如果不相同,说明该函数中没有执行stp x29, x30,[sp,#-16]指令,此时应使用lr寄存器的值作为函数返回地址,并且此时fp寄存器本身就是上一级函数栈的第二片内存地址,有了这个数据就能按照前文的方法栈回溯了。解决方法就是这样,读者可以仔细体会一下我的分析。

3.2 mips 栈回溯过程

前文说过,mips内核崩溃处理流程是

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

打印崩溃函数流程是在show_backtrace()函数。

3.2.1 mips 架构内核栈回溯原理分析

  1. arch/mips/kernel/ traps.c 

可以发现,与arm架构栈回溯流程基本一致。函数开头是对sp、ra、pc寄存器器赋值,sp和pc与arm架构一致,ra相当于arm架构的lr寄存器,没有arm架构的fp寄存器。print_ip_sym函数就是根据pc值打印形如[] chrdev_open+0x12/0x4B1的字符串,不再介绍。关键还是unwind_stack_by_address函数。mips架构由于没有像arm架构的fp寄存器,导致栈回溯的过程比arm架构复杂很多,为了读者理解方便,决定先从mips架构汇编代码分析,指出与栈回溯有关的指令,推出栈回溯的流程,最后讲解内核代码。

如下是mips架构内核驱动ko文件的 C代码和汇编代码。

C代码

汇编代码

这里说明一下,驱动ko反汇编出来的指令是从0地址开始的,为了叙述方便,笔者加了0x80000000,实际的汇编代码不是这样的。

这里直接介绍根据笔者的分析,总结mips架构内核栈回溯的原理,分析完后再结合源码验证。mips架构没有fp寄存器,假设在test_c函数中0X80000048地址处指令崩溃了,首先利用内核的kallsyms模块,根据崩溃时的指令地址找出该指令是哪个函数的指令,并且找出该指令地址相对函数指令首地址的偏移ofs,在本案例中ofs = 0X10(0X80000048 – 0X80000038 =0X10),这样就能算出test_c函数的指令首地址是 0X80000048 - 0X10 = 0X80000038。然后就从地址0X80000038开始,依次取出每条指令,找到addiu sp,sp,-24 和sw ra,20(sp),内核有标准函数可以判断出这两条指令,下文可以看到。

addiu sp,sp,-24是test_c函数的第一条指令,栈指针向下偏移24个字节,笔者认为是为test_c函数分配栈大小( 24个字节);sw ra,20(sp)指令将test_c函数返回地址存入sp +20 内存地址处,此时sp指向的是test_c函数的栈顶,sp+20就是test_c函数栈的第二片内存,该函数栈大小24字节,一共24/4=6片内存。

根据sw ra,20(sp)指令知道test_c函数返回地址在test_c函数栈的存储位置,取出该地址的数据,就知道是test_a函数的指令地址,当然就知道是test_a函数调用了test_c函数。并根据addiu sp,sp,-24指令知道test_c函数栈总计24字节,因为test_c函数崩溃时,栈指针sp指向test_c函数栈顶,sp+24就是test_a函数的栈顶,因为test_a函数调用了test_c函数,两个函数的栈必是紧挨着的。

按照上述推断,首先知道了test_a函数中的指令地址了,使用内核kallsyms功能就推算出test_a函数的指令首地址,同时也计算出test_a函数的栈顶,就能按照上述规律找出谁调用了test_a函数,以及该函数的栈顶。依次就能找出所有函数调用关系。

关于内核的kallsyms,笔者的理解是:执行过cat /proc/kallsyms命令的读者,应该了解过,该命令会打印内核所有的函数的首地址和函数名称,还有内核编译后生成的System.map文件,记录内核函数、变量的名称与内存地址等等,kallsyms也是记录了这些内容,当执行kallsyms_lookup_size_offset(0X80000048, &size,&ofs)函数,就能根据0X80000048指令地址计算出处于test_c函数,并将相对于test_c函数指令首地址的偏移0X10存入ofs,test_c函数指令总字节数存入size。

笔者没有研究过kallsyms模块,但是可以理解到,内核的所有函数都是按照分配的地址,顺序排布。如果记录了每个函数的首地址和名称,当知道函数的任何一条指令地址,就能在其中搜索比对,找到该指令处于按个函数,计算出函数首地址,该指令的偏移。

3.2.2 mips 架构内核栈回溯核心源码分析

3.2.1详细讲述了mips栈回溯的原理,接着讲解栈回溯的核心函数unwind_stack_by_address。

上述源码已经在关键点做了详细注释,其实就是对3.2.1节栈回溯原理的完善,请读者自己分析,这里不再赘述。但是有一点请注意,就是蓝色注释,这是针对崩溃的函数没有执行其他函数的情况,此时该函数没有类似汇编指令sw ra,20(sp) 将函数返回地址保存到栈中,计算方法就变了,要直接使用ra寄存器的值作为函数返回地址,计算上一级函数栈顶的方法还是一致的,后续栈回溯的方法与前文相同。

4 linux内核栈回溯的应用

文章最开头说过,笔者在实际项目开发过程,已经总结出了3个内核栈回溯的应用:

1 应用程序崩溃,像内核栈回溯一样打印整个崩溃过程,应用函数的调用关系

2 应用程序发生double free,像内核栈回溯一样打印double free过程,应用函数的调用关系

3 内核陷入死循环,sysrq的内核线程栈回溯功能无法发挥作用时,在系统定时钟中断函数中对卡死线程栈回溯,找出卡死位置

下文逐一讲解。

4.1 应用程序崩溃栈回溯

(编辑:晋中站长网)

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

热点阅读