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内核崩溃处理流程是
打印崩溃函数流程是在show_backtrace()函数。 3.2.1 mips 架构内核栈回溯原理分析
可以发现,与arm架构栈回溯流程基本一致。函数开头是对sp、ra、pc寄存器器赋值,sp和pc与arm架构一致,ra相当于arm架构的lr寄存器,没有arm架构的fp寄存器。print_ip_sym函数就是根据pc值打印形如[ 如下是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 应用程序崩溃栈回溯 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |