ARM64位CPU下linux crash手动恢复函数调用栈

注:本文转载自知乎:https://zhuanlan.zhihu.com/p/50768351

 

 

AAPCS:ARM Architecture Procedure Call Standard—ARM结构过程调用规范,AAPCS64即为针对64位体系结构的ARM结构过程调用规范,它是《ARM 体系结构的基础标准应用程序二进制接口》 (BSABI) 规范的组成部分。 遵循 AAPCS 编写代码可以确保分别编译和汇编的模块能够协同工作。

 

GCC有一个编译选项用于指定是否要使用 ARM体系结构的过程调用标准 (AAPCS)。AAPCS定义了各寄存器在函数调用过程中的作用、基础类型的长度、以及函数调用的基本准则,包括栈帧的处理、函数的参数传递法则等。所以理解栈帧的组织方式对于理解汇编代码和定位crash这种bug有重要意义。具体针对linux下的AAPCS/AAPCS64栈帧组织详细分析,瓶子哥后面会专门再拿出来专门讨论。这里限于侧重点我们先挑关键点来看AArch64 arm linux的栈帧组织结构的汇编描述如下:

 

FP(x29)寄存器保存栈帧地址,LR(x30)保存当前过程的返回地址。栈是从高地址向低地址生长。下边是一个实际的函数汇编栈帧描述例子,对于ARM64 linux平台下函数的开始和结束的汇编组织基本都是如下:

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就行了。

 

神兵利器之Crash工具

OK,有了法宝,还差什么呢?兵器,是的,猴哥的金箍棒,洪七公的打狗棒,张无忌的屠龙刀无一不说明了打仗得有一件趁手的神兵利器啊,这样才能事倍功半。

 

做AP(如智能手机,平板等)底层开发的工程师都知道,对于crash问题,类似Qualcomm高通,MTK,SAMSUNG等平台ODM厂商的工程师基本都使用TRACE32来进行调试,尤其是使用TRACE32 Simulator软件来进行debug,但是我等三流屌丝哪里买得起TRACE32仿真器嘛,所以也就木有TRACE32 Simulator了。

 

但是码农的世界真的是很好,很多大神们发布了许多的开源调试内核dump文件的调试工具,我们需要再次向这些大神们致敬,有了他们,我等屌丝才不会苦逼挑灯夜战的使用printk来debug问题点,也能让我们码农门有时间为了下半辈子的幸福,在这狼多肉少的世界里去勾兑勾兑妹子们,也有时间享受美好的人生。在这个案件中 我们要用到的神兵利器就是Crash工具。

 

 

 

前面我们已经传授了大家神技能法则,以及一件神兵利器crash,所以是时候检验下我们的成果了。

上面是一个空指针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工具加载内核转储文件即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 tohandle kernel NULL pointer dereference at virtual address 000004a8”

 

我们知道大概可能decon是一个空指针,而decon的值是通过形参传递进来的,根据子函数传参数传递规则,X0应该是保存的decon的地址,那我们在看下X0的值: x0 : 0000000000000000,果然不是一个有效的地址,所以结论就是decon_lpd_block_exit(structdecon_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= *(unsignedlong *)(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= *(unsignedlong *)(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要案之神技能:手动恢复函数调用栈”

终于大结局了,感谢大家的聆听,下课!”

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值