Linux内核的栈回溯与妙用
笔者在研究过内核栈回溯功能后,不禁发问,为什么不能用同样的方法对应用程序的崩溃栈回溯呢?不管是内核空间,应用空间,程序的指令是一样的,无非是地址有差异,函数入栈出栈原理是一样的。栈回溯的入口,arm架构是获取崩溃线程/进程的pc、fp、lr寄存器值,mips架构是获取pc、ra、sp寄存器值,有了这些值就能按照各自的回溯规律,实现栈回溯。从理论上来说,完全是可以实现的。 4.1 .1 arm架构应用程序栈回溯的实现 当应用程序发生崩溃,与内核一样,系统自动将崩溃时所有的CPU寄存器存入struct pt_regs结构,一般崩溃入口函数是do_page_fault,又因为是应用程序崩溃,所以是__do_user_fault函数,这里直接分析__do_user_fault。 在该函数中,tsk就是崩溃的线程,struct pt_regs *regs就指向线程/进程崩溃时的CPU寄存器结构。regs->[29]就是fp寄存器,regs->[30]是lr寄存器, regs->pc的意义很直观。现在有了崩溃应用线程/进程当时的fp、sp、lr寄存器,就能栈回溯了,完全仿照内核dump_backtrace的方法,请看笔者写在user_thread_ dump_backtrace函数中的演示代码。 与内核栈回溯原理一致,打印崩溃过程每个函数的指令地址,然后在应用程序的反汇编文件中查找,就能找到该指令处于的函数,如果不理解,请看文章前方讲解的内核栈回溯代码与原理。请注意,这不是笔者项目实际用的栈回溯代码,实际的改动完善了很多,这只是演示原理的示例代码。 还有一点就是,笔者在3.1.3节提到的,假如崩溃的函数中没有调用其他函数,那上述栈回溯就会有问题,就不会打印第二级函数,解决方法讲的也有,解决的代码这里就不再列出了。 4.1 .2 mips架构应用程序栈回溯的实现 mips 架构不仅内核栈回溯的代码比arm复杂,应用程序的栈回溯更复杂,还有未知bug,即便这样,还是讲解一下具体的解决思路,最后讲一下存在的问题。 先简单回顾一下内核栈回溯的原理,首先根据崩溃函数的pc值,运用内核kallsyms模块,计算出该函数的指令首地址,然后从指令首地址开始分析,找出类似addiu sp,sp,-24和sw ra,20(sp)指令,前者可以找到该函数的栈大小,栈指针sp加上这个数值,就知道上一级函数的栈顶地址(崩溃时sp指向崩溃函数的栈顶);后者知道函数返回地址在该函数栈中存储的地址,从该地址就能获取该函数的返回地址,就是上一级函数的指令地址,也就知道了上一级函数是哪个(同样使用内核kallsyms模块)。 知道了上一级函数的指令地址和栈顶地址,按照同样方法,就能知道再上一级的函数……. 问题来了,内核有kallsyms模块记录了每个函数的首地址和函数名字,函数还是顺序排布。应用程序并没有kallsyms模块,即便知道了崩溃函数的pc值,也无法按照同样的方法找到崩溃函数的指令首地址,真的没有方法?其实还有一个最简单的方法。先列出一段一个应用程序函数的汇编代码,如下所示,与内核态的有小的差别。 现在假如从0X4006a4地址处取指,运行后崩溃了。崩溃发生时,能像arm架构一样获取崩溃前的CPU寄存器值,最重要就是pc、sp、ra值。 pc值就是0X4006a4,然后令一个unsigned long型指针指向该内存地址0X4006a4,每次减一,并取出该地址的指令数据分析,这样肯定能分析到addiu sp,sp,-32 和sw ra,28(sp)指令,我想看到这里,读者应该可以清楚方法了。没错,就是以崩溃时pc值作为基地址,每次减1并从对应地址取出指令分析,直到分析出久违的addiu sp,sp,-32 和sw ra,28(sp)类似指令,再结合崩溃时的栈指针值sp,就能计算出该函数的返回地址和上一级函数的栈顶地址。后续的方法,就与内核栈回溯的过程一致了。下方列出演示的代码。 为了一致性,应用程序栈回溯的函数还是采用名字user_thread_ dump_backtrace。 如上就是mips应用程序栈回溯的示例代码,只是一个演示,笔者实际使用的代码要复杂太多。读者使用时,要基于这个基本原理,多调试,才能应对各种情况,笔者前后调试几周才稳定。由于这个方法并不是标准的,实际使用时还是会出现误报函数现象,分析了发生误报的汇编代码及C代码,发现当函数代码复杂时,函数的汇编指令会变得非常复杂,会出现相似指令等等,读者实际调试时就会发现。这个mips应用程序栈回溯的方法,可以应对大部分崩溃情况,但是有误报的可能,优化的空间非常大,这点请读者注意。 4.2 应用程序double free 内核栈回溯 double free是在C库层发生的,正常情况内核无能为力,但是笔者研究过后,发现照样可以实现对发生double free应用进程的栈回溯。 以arm架构为例,doublefree C库层的代码,大体原理是,当检测到double free(本人实验时,一片malloc分配的内存free两次就会发生),就会执行kill系统调用函数,向出问题的进程发送SIGABRT信号,既然是系统调用,从用户空间进入内核空间时,就会将应用进程用户空间运行时的CPU寄存器pc、sp、lr等保存到进程的内核栈中,发送信号内核必然执行send_signal函数。 在该函数中,使用struct pt_regs *regs = task_pt_regs(current)方法就能从当前进程内核栈中获取进入内核空间前,用户空间运行指令的pc、sp、fp等CPU寄存器值,有了这些值,就能按照用户空间进程崩溃栈回溯方法一样,对double free的进程栈回溯了。比如,A函数double free,A函数->C库函数1-> C库函数2->C库函数3(检测到double free并发送SIGABRT信号,执行系统调用进入内核空间发送信号)。回溯的结果是:C库函数3 ß C库函数2 ß C库函数1ß A函数。 源码不再列出,相信读者理解的话是可以自己开发的。其中task_pt_regs函数的使用,需要读者对进程内核栈有一定的了解。 笔者有个理解,当获取某个进程运行指令某一时间点的CPU寄存器pc、lr、fp的值,就能对该进程进行栈回溯。 4.3 内核发生死循环sysrq无效时栈回溯的应用 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |