首页 > 代码库 > 破获ARM64位CPU下linux crash要案之神技能:手动恢复函数调用栈
破获ARM64位CPU下linux crash要案之神技能:手动恢复函数调用栈
“情况是在不断地变化,要使自己的思想适应新的情况,就得学习”
--伟大主席毛爷爷
引言
前不久老王的一位刚入职的同事小马在调试基于三星平台的7420 SOC(ARM64位处理器芯片)Android驱动的时候遇到了一个crash问题,但是panic出来后没有打印出backtrace(函数的调用关系),后来老王通过分析ARM64 linux的函数调用关系以及AAPCS64,通过手动获取调用堆栈的方式很快定位和解决了此问题。突然老王才意识到,早在2013年苹果发布了iPhone 5s,其做为第一款应用了ARMv8架构的64位处理器的手机以来,各大手机厂商也在64位处理器的道路上摩拳擦掌,你苹果既然带头弄个64位,那么大家也就弄64位了,不然外行以为64位厉害(似乎苹果总能在某些方面引领科技潮流啊),随后高通发布了骁龙410 64位SOC,MTK 的MT6732,华为的麒麟620等,现在的移动AP处理器来说基本都已经鸟枪换炮成64位CPU了,似乎手机厂商在搞PPT的时候,如果不宣讲处理器是64位的话,码农门表示他都没法混下去了。但不管厂商们上不上64位处理器,我们码农的生活还是要继续的,老王的课还是要继续的,言归正传,对于这类没有正常显示backtrace的ARM64位体系结构的linux crash case,该如何分析解决,如何能够有一套行之有效的破案手法呢?
资深的码农们都知道,如果要定位分析PANIC/OOPS等这类crash问题,有backtrace的话无疑对于我们快速定位和分析异常问题有很大的帮助(当然那种我等小白一眼就能发现的错误以及本身有正常的backtrace的话,我们当然不需要恢复堆栈,大家直接可以洗洗睡了哈)。那么问题来了,对于ARM64位SOC来说,开发过程中遇到了linux crash后没有打印出backtrace的话,该如何手动恢复呢?是否还是和32位CPU体系结构相同的方法呢?答案是否定的。
根据ARM官方的手册,对于AArch64的AAPCS有别于ARM的AAPCS,因此堆栈的组织调用方式也有差异,所以对于在64bit平台上遇到的没有backtrace的crash问题,我们需要根据AAPCS64的规则来手动恢复堆栈,所谓磨刀不误砍柴工,心急吃不了热豆腐,那么问题来了,“狗蛋,你知道对于今天我们要讲的这个神技能,我们都需要掌握哪些基础理论知识才能够愉快的实践呢?”
“呃,这个嘛,就是,那个,恩,###@!#¥%……%&*……,老师,狗蛋我不知道耶,但是”
“好吧,看你们大家一脸懵逼的样子,老师我还是继续吧,前面说的这些AArch64,AAPCS64,Backtrace等名词,大家不懂没有关系,这节课就是针对这些知识点为大家扫盲及答疑解惑的。”
OK,码农大讲堂正式开始上课。
今天针对这个ARM64位体系构下调试linux crash问题,如果没有打印出函数的调用堆栈关系,要进行手动栈回溯的话,需要大家掌握以下知识点:
1. 栈回溯的认知
2. 基于ARM64AAPCS64栈帧的组织方式
3. 神兵利器之Crash工具
4. 庖丁解牛之实操手动恢复堆栈
1. 栈回溯的认知
我们先正面瞅一瞅啥是栈回溯呢?辩证唯物主义认识论认为,实践决定认识,实践是认识的基础,所以我们先上干货道具
图:Crash 现场一瞥
基于一个普遍的debug crash逻辑认知,我们在开发过程中用遇到linux kernel crash的期望log基本如上图所示:
箭头1:标识真凶的作案工具是哪种类型?一个NULL pointer、soft/hard lockup 、deadlock或者其他类型的作案凶器。从这里我们基本能看出这个刑事案件的类型,心里大致有一个分析案情的方向。
箭头2/3:案发现场时刻留下的重要分析线索,这些线索保留下了犯罪作案时刻具体行凶的位置(PC指针),逃跑方向(LR链接寄存器),罪犯的特征(pstate状态标识)等。以及各个现场留下的物证(31个64位通用寄存器x0-x30),通过它们我们可以找到案发现场的第一手有用资料,分析这个案件就是从这些证据开始切入。
箭头4:这个很重要,我们都知道,真凶作案肯定都是一环一环的精心策划准备的,比如在家里准备好作案凶器(NULL pointer),通过水路进入案发现场 ,期间可能去过厕所等等,箭头4的堆栈就像是公众场合的摄像头拍摄的视频保存在硬盘里面一样用来保存这些有用的信息点,只要我们拿到罪犯的嫌疑罪证,再加上行之有效的刑侦手段,就能很快的破案。
箭头5:就是我们这节课的最终目的,由于上面第4点保存的罪证信息比较零散没有逻辑,我们需要通过一定的技术刑侦手段把这些零散的证据拼凑并分析出真凶完整的作案流程,最后抓住真凶。
但有时候由于各种原因函数的回溯调用关系(如上箭头5)没有打印出来,或者堆栈被破坏掉了,如果要很容易的分析crash问题,我们就需要这节课总结的神技能啦。
资深的老司机门都知道,对于解决crash问题我们主要是想得到函数的一个调用栈顺序,这样我们才能定位和分析问题,那有同学就表示不理解,难道不是知道最后的一个函数栈的现场不就可以分析了吗,即使不要什么函数调用关系,应该也可以分析定位出问题吧? 非也,请看以下大屏幕:
图:函数的一个调用关系
假设真凶最后是拿了一个被称为空指针的炸yao包炸掉了fun_die这个工厂(crash),那么我们只知道fun_die的现场是没有用的,因为根据现场的信息我们仅仅知道元凶是拿的炸yao包并且在fun_die这里点燃了炸yao包,但是元凶携带的炸yao包(NULL Pointer)最终是从哪条路来呢,空路1?还是陆路2?亦或是水路3?有可能陆路和空陆都是正常的行人拜访fun_die这个城市,而元凶是带着炸yao包从水路进入到fun_die这个城市的,所以我们要抓住真凶,就必须借助前面箭头4保存的信息通过逻辑分析来找到炸yao包的源头,一举破获整个团伙,抓住真凶,即我们需要真正的找到出问题的函数调用路径,如上的水路三:fun1->fun2->fun_die这个调用,查到NULL Pointer最终是从fun1传递过来的,而funb1->fun_die,funa1->funa2->funa3->fun_die这两个调用路径都是其他时候的正常调用,不是我们需要关注的。通过堆栈里面的指令层层回溯,从而找到函数的调用关系就是我们这节课的目的。
2. 基于AAPCS64栈帧的组织方式
那么什么是AAPCS64,和我们的堆栈调用有嘛关系呢?
老王只想说,太有关系了,没有它我们还真只能在这谈谈人生,唠唠家常呢。
AAPCS:ARM ArchitectureProcedure Call Standard—ARM结构过程调用规范,AAPCS64即为针对64位体系结构的ARM结构过程调用规范,它是《ARM 体系结构的基础标准应用程序二进制接口》 (BSABI) 规范的组成部分。 遵循 AAPCS 编写代码可以确保分别编译和汇编的模块能够协同工作。GCC有一个编译选项用于指定是否要使用 ARM 体系结构的过程调用标准 (AAPCS)。AAPCS定义了各寄存器在函数调用过程中的作用、基础类型的长度、以及函数调用的基本准则,包括栈帧的处理、函数的参数传递法则等。所以理解栈帧的组织方式对于理解汇编代码和定位crash这种bug有重要意义。具体针对linux下的AAPCS/AAPCS64栈帧组织详细分析,老王后面会第一时间在我的公众号:码农职场加油站(coder51up)推出,敬请关注。这里限于侧重点我们先挑关键点来看,AArch64 arm linux的栈帧组织结构的汇编描述如下:
FP(x29)寄存器保存栈帧地址,LR(x30)保存当前过程的返回地址。栈是从高地址向低地址生长。
下边是一个实际的函数汇编栈帧描述例子,对于ARM64函数的开始和结束的汇编组织基本都是如下:
fun1函数以及fun1函数的反汇编代码,汇编的开头2处指令和结尾2处指令即为函数为了保存上下文做的寄存器LR,FP的压栈和出栈操作。
从上图的规律我们可以得出ARM64下栈帧布局如下
从上面的ARM64栈帧的组织结构我们可以得出有用的信息:
1. arm64的lr,fp放在栈顶。
2. arm64中当前fp和sp相同,都是栈顶指针。
3. 函数返回时,arm64先将栈中的lr值放入当前lr,再ret 。使用gcc编译选项-fomit-frame-pointer,可以使arm64不使用fp寄存器,这时栈帧稍有变化,栈不在保存fp,局部变量寻址过程也不使用该寄存器 。
4. 在caller调用callee的时候,先预留一段栈帧给本函数进行参数传递保存,寄存器压栈等:[sp,#-32]!,即sub sp ,sp,#32。callee在函数调用的时候会把caller的FP,LR压栈到本函数FP/SP指向的栈帧中:stp x29,x30,[sp]。,这样就能方便的进行栈回溯。
因此说了这么多知识点,我们最终得出的手动恢复函数堆栈所需要的神技能。
1)根据callee的FP找到caller的FP,也即找到调用者的栈帧。这样通过FP的层层回溯就能把整个函数的调用栈帧找到。
2)根据本函数栈帧保存的LR来间接获取PC,从而根据符号表得到具体的函数名(ARM64没有进行PC的压栈,因此我们没法直接使用PC地址来获取入口函数名)。那么关键问题来了,PC如何间接获取呢?我们知道在函数调用的时候我们是通过跳转指令B或者BL来进行函数调用的,在跳转的同时ARM会自动保存函数的返回地址到LR,也即下一条指令的入口地址,函数调用的时候进行LR压栈,函数返回的时候LR出栈,从而保证正确执行程序返回后的后续指令。如下图所示在执行400570地址处的BL跳转指令的同时,LR的值更新为400574地址。而ARM/ARM64的指令编码长度是32位4字节,也就是说我们知道LR的值,通过LR指向的地址-4字节偏移就可以得到PC值即被调用函数callee的入口地址,再通过符号表即可得到此入口地址对应的函数名。
所有上面的各种知识点,最终是为了引出这两条法则。当然我们能够手动恢复堆栈是有一个前提,栈的内容是完整保存的,但如果栈的内容被冲刷干净或被破话了,我们大家连毛都看不到。所以有时候有必要开启栈保护,至少你还能找到栈顶的函数,gcc有相关的参数: -fstack-protector 和 -fstack-protector-all,强烈建议开启,在kernel源码的顶层Makefile里面通常有如下配置:
所以我们只需要在配置内核的时候选择CONFIG_CC_STACKPROTECTOR_REGULAR/CONFIG_CC_STACKPROTECTOR_STRONG就行了。
3. 神兵利器之Crash工具
OK,有了法宝,还差什么呢?兵器,是的,猴哥的金箍棒,洪七公的打狗棒,张无忌的屠龙刀无一不说明了打仗得有一件趁手的神兵利器啊,这样才能事倍功半。做AP(如智能手机,平板等)开发的驱动工程师都知道,对于crash问题,类似QUALCOMM,MTK,SAMSUNG等平台ODM厂商的工程师基本都使用TRACE32来进行调试,尤其是使用TRACE32 Simulator软件来进行debug,但是我等三流屌丝哪里买得起TRACE32仿真器嘛,所以也就木有TRACE32 Simulator了。但是程序员的世界真的是很好,很多大神们发布了许多的开源调试内核dump文件的调试工具,我们需要再次向这些大神们致敬,有了他们,我等屌丝才不会苦逼挑灯夜战的使用printk来debug问题点,也能让我们码农门有时间为了下半辈子的幸福,在这狼多肉少的世界里去勾兑勾兑妹子们,也有时间享受美好的人生。在这个案件中我们要用到的神兵利器就是Crash工具。
图:crash工具一瞥
老王这么多年来基本都是使用的它,可谓帮助我破获了无数的要案大案。而对于我们破获的这类要案,主要需要用到crash的 RD,SYM,DIS,STRUCT命令,仅此而已。详细的crash实操我也会第一时间在我的公众号:码农职场加油站(coder51up)推出,敬请关注。
4. 庖丁解牛之实操手动恢复堆栈
OK,有了制胜法宝,我们还差最后一步:实践是检验真理的唯一标准。是时候配合神兵利器来验证下这个法宝的有效性了。来,再一次上道具,噢,这次不是道具了,是时候该show下码农门工作中真实大案了。
上面是一个空指针crash,可以看出内核没有打印出函数的backtrace。而堆栈现场是完整的,因此我们可以通过手动来恢复函数的调用堆栈。
再次回顾下破案的两条法则:
1)根据callee的FP找到caller的FP,也即找到调用者的栈帧。这样通过FP的层层回溯就能把整个函数的调用路径(栈帧)找到。
2)根据本函数栈帧保存的LR来间接获取PC,从而根据符号表得到具体的函数名,在调用子函数的时候,LR指向子函数的返回的下一条指令,通过LR指向的地址-4字节偏移就得到了 被调用函数callee的入口地址,再通过符号表即可得到此入口地址对应的函数名。
程序员的世界当然要用程序语言来代言我们自己,所以让我们再一次用码农的语言来描述下我们的结论吧。
假设我们需要恢复的堆栈调用为:func1->func2->fun_die。
提问:
已知:当前现场fun_die的 FP(die)/SP(die)/LR(die)/PC(die)。
求解:调用fun_die发生crash的函数调用路径。
解答:
栈推导法则描述:
继续看上面的crash案例:
图Crash现场寄存器
使用crash工具加载内核转储文件即ramdump文件:
1)根据PC获取指向当前挂掉的函数代码现场:
pc : [<ffffffc000343068>]
decon_lpd_block_exit对应的汇编如下:
第一现场已经获取到:ffffffc000343068 (T)decon_lpd_block_exit+12。
由于对于这个问题比较简单,其实我们仅仅通过分析第一现场就能知道大致问题是哪里导致的了。代码是死在decon_lpd_block_exit+12, 也即上图红色箭头标识的汇编ldr w3, [x0,#1192]。这条指令是什么意思呢? 我们先看看对应的源码吧。
通过对应源码再结合汇编,我等码农还是可以猜出个5,6层的汇编含义了。在这里,老王就先直接给出这段代码的汇编解释:
注:arm linux汇编会将内联函数(inline)直接在主代码中展开,不会进行子函数调用。所以decon_lpd_block(decon)直接展开为if(!decon->id) atomic_inc(&decon->lpd_block_cnt),不进行bl调用。在看上面的黄色汇编: ldr w3, [x0,#1192],1192是什么意思呢?其实它就是decon的成员id偏移值。有时候结构体成员比较多,我们很难数清成员的偏移值是多少,这里我们使用crash利器就灰常方便,直接使用-o参数,像下面这个结构体总共占用了3019000个字节,要是我们人为计算后面几个变量的偏移值,我想大家跳楼的心都要有了。所以crash这个神兵利器很不错吧。
从上图我们也得出[x0,#1192]中的1192就是id的偏移位置,也进一步验证了这句代码是在取值(decon->id)。而结合“Unable to handle kernelNULL pointer dereference at virtual address 000004a8”我们知道大概可能decon是一个空指针,而decon的值是通过形参传递进来的,根据子函数传参数传递规则,X0应该是保存的decon的地址,那我们在看下X0的值: x0 :0000000000000000,果然不是一个有效的地址,所以结论就是decon_lpd_block_exit(struct decon_device *decon)传递了一个空指针。因为代码里面有很多函数调用了decon_lpd_block_exit函数,所以进一步的我们需要找到是最终在哪个调用路径上传递了NULL的 decon指针。
根据前面的法则:“在调用子函数的时候,LR是指向子函数返回的下一条指令”,那我们来验证下这个法则是否正确呢?
通过pc值我们已经找到了第一个栈帧调用。那我们看下lr(0xffffffc000333f4c)是否就是调用decon_lpd_block_exit函数返回的主函数的下一条指令的地址呢?
LR指向dsim_read_data+64,根据法则不出意外的话 LR-4=0xffffffc000333f48 地址处应该对应于decon_lpd_block_exit子函数的调用指令。everybody,it’s show time now!:
怎么样?出现这个结果是不是很鸡冻啊,有木有一种成就感,有木有想拥抱老师?有木有?到此为止,看来牛老师讲的还算是良心干货啊。
我想,后面的故事就很轻松了。
我们继续推导:
2)根据第一现场的FP来获取PC。
OK,我们现在在把法则搬出来依葫芦画瓢:
FP =x29 = ffffffc0b611b970
根据FP得到caller的PC:
PC= *(unsigned long *)(FP+ 8) - 4 =*(unsigned long *)LR- 4=*(unsigned long *)ffffffc000333f48
第二现场已经获取到:ffffffc000333f48 (T) dsim_read_data+0x3c
3)根据第一现场的FP来回溯caller的调用栈帧。
FP =x29 = ffffffc0b611b970
根据FP得到caller1的栈帧基指:
FP1 = *FP= ffffffc0b611b980
PC1= *(unsigned long *)(FP1+ 8) - 4 =*(unsigned long *)ffffffc0b611b988 - 4
PC1= *(unsigned long*)ffffffc0b611b988 - 4 = ffffffc00033432c
第三现场已经获取到:ffffffc00033432c (t) dsim_read_test+0x20
FP2= *FP1=*(unsigned long*)ffffffc0b611b980
PC2= *(unsigned long *)(FP2+ 8) - 4 =*(unsigned long *)ffffffc0b611b9f8- 4
PC2= *(unsigned long*)ffffffc00033450c- 4 = ffffffc000334508
第四现场已经获取到:ffffffc000334508 (t)dsim_runtime_resume+0xac.
以此类推:通过callee栈帧的FP得到caller的栈帧FP’。然后根据AAPCS64的栈帧组织结构得到caller的PC’,然后通过crash工具结合符号表得到caller的函数名。直到FP的值为0,表示没有更多的调用栈帧而到栈底。
由此得出整个函数的backtrace如下:
到此,我们就完成了整个backtrace的恢复调用。在结合强大的crash工具和源码分析,相信破案已经是手到擒来了,是不是有那么一丝丝的鸡冻呢。
通过本节课多个知识点的回顾和学习,老王有足够的理由相信,你可以在职场上和你一样的菜鸟们面前显摆一下(当然老王还是那句话,低调),因为无疑这个案子将使你在crash debug的知识技能点上超越一般的码农,通过在实际的工作再运用此中的技能来解决实际的问题,相信你在码农的道路上又前进了一大步。
“好了,最后老王再啰嗦一句很重要的话:everybody,本节课介绍,下课!”
破获ARM64位CPU下linux crash要案之神技能:手动恢复函数调用栈