写点什么

LiteOS 调测利器:backtrace 函数原理知多少

发布于: 2021 年 01 月 29 日

摘要:本文将会和读者分享 LiteOS 5.0 版本中 Cortex-M 架构的 backtrace 软件原理及实现。


本文将会和读者分享 LiteOS 5.0 版本中 Cortex-M 架构的 backtrace 软件原理及实现,供大家参考和学习交流。


原理介绍


· 汇编指令的执行流程


图 1 汇编指令的执行顺序


上图 1 所示,ARM 的汇编指令执行分三步:取值(fetch)、译指(decode)、执行(execute),按照流水线的方式执行,即当运行指令节拍 m 时,pc 会指向 n+2 汇编指令地址进行取指令操作,同时会将 n+1 处汇编指令翻译成对应机器码,并执行指令 n。


· 内存中栈的布局


图 2 栈在内存中的布局


LiteOS Cortex-M 架构的栈布局如上图 2,栈区间在内存中位于最末端,程序运行时从内存末端(栈顶)开始进行递减压栈。LiteOS 的内存末端为主栈空间(msp_stack),LiteOS 进入任务前的初始化过程及中断函数调用过程的栈数据保存在此区间内,主栈地址空间往下为任务栈空间(psp_stack),任务栈空间在每个任务被创建时指定,多个任务栈空间依次排列。一个任务中可能包含多个函数,每个函数都有自己的栈空间,称为栈帧。调用函数时,会创建子函数的栈帧,同时将函数入参、局部变量、寄存器入栈。栈帧从高地址向低地址生长。


· 寄存器数据入栈流程


ARM 为了维护栈中的数据设计了两个寄存器,分别为 fp 寄存器(framepointer,帧指针寄存器)和 sp 寄存器(stack pointer,堆栈寄存器)。fp 指向当前函数的父函数的栈帧起始地址, sp 指向当前函数的栈顶。通过对 sp 寄存器的地址进行偏移访问可以得到栈中的数据内容,通过访问 fp 寄存器地址可以得到上一栈帧的起始位置,进而计算出函数的返回地址。由于 Cortex-M 没有 fp 寄存器,若想获得函数入口地址只能通过 sp 地址偏移找到 lr 寄存器(link register,链接寄存器,指向当前函数的返回地址),并结合函数入口的 push 指令计算得出。lr 寄存器会在每次函数调用时压入栈中,用以返回到函数调用前的位置继续执行。函数调用执行流程引用自 Joseph Yiu 的《Cortex-M3 权威指南》,如下图 3 所示。


图 3 函数调用执行流程

如函数调用执行流程所示,程序进入一个子函数后,通常都会使用 push 指令先将寄存器的值压入栈中,执行完业务逻辑后再使用 pop 指令将栈中保存的寄存器数据出栈并按顺序存入对应的寄存器。当程序执行 bl 跳转指令时,pc 中的值为 bl 指令后的第二条指令的地址,减去一条汇编指令的长度后为 bl 后第一条指令的地址,即 lr 值。程序在进入 Fx1 前,bl 或 blx 指令会将此 lr 值保存到 lr 寄存器,并在进入 Fx1 函数时将其压入栈中。例如有如下汇编指令:



当程序执行到地址 0x8007810 时,在 bl 指令跳转到函数 test_div 之前,bl 指令会将此时的 pc 地址(0x8007818)减去一条汇编指令的长度(这里为 4),将计算得到的值 0x8007814(本条指令仅执行到译指,尚未完成全部执行过程,返回后需重新取指)保存到 lr 寄存器。


· 实现思路


根据函数调用执行流程的原理,当程序跳入异常时,传入当前位置 sp 指针,通过对 sp 指针进行循环自增访问操作获取栈中的内容,sp 指向栈顶,循环自增的边界即任务栈的栈底,由于 Cortex-M 使用的 thum-2 指令集,汇编指令长度为 2 字节,因此可通过判断栈中的数据是否两字节对齐及位于代码段区间内筛选出当前栈中的汇编指令地址。并通过判断上一条是否为 bl 指令或 blx 指令(b、bx 指令不将 lr 寄存器入栈,不对其进行处理)对上一条指令进行计算。跳转指令的机器码构成如下图 4 所示:


图 4 thum跳转指令机器码构成

如果为 bl 指令地址(特征码 0xf000),通过该地址中存储的机器码计算出偏移地址(原理见下图 5),从而获得跳转指令目标函数入口地址,如果为 blx 指令(这里为 blx 寄存器 n 指令,其特征码 0x4700),由于目标偏移地址保存在寄存器中,无法通过机器码计算偏移地址,则需要根据被调用帧保存的 lr 地址推算其所在的函数入口地址,直到入口处的 push 指令。


图 5 bl指令偏移地址计算规则


设计实现分析


LiteOS 在运行过程中出现异常时,会自动转入异常处理函数。LiteOS 提供了 backtrace 函数用于跟踪函数的堆栈信息,通过系统注册的异常处理函数来调用 backtrace 函数实现系统异常时自动打印函数的调用栈。


· 设计思路


由于 Cortex-M 架构无 fp 寄存器,sp 寄存器分为 msp 寄存器(用于主栈)和 psp 寄存器(用于任务栈),因此只能通过汇编指令机器码计算及 lr 地址自增查找函数入口处的 push 指令特征码计算函数入口。


· 详细设计


图 6 backtrace代码框架

当调用 Cortex-M 架构的 ArchBackTrace 接口时,该函数会通过 ArchGetSp 获取当前 sp 指针,如果在初始化或中断过程发生异常,sp 指向 msp,在任务中发生异常,sp 指向 psp。将获取的 sp 指针传入 BackTraceWithSp 进行调用栈分析,该函数通过 FindSuitableStack 函数进行栈边界确认,找到合适的任务栈边界或主栈(未区分中断栈及初始化栈)边界。再通过边界值控制循环查找次数,从而确保将对应栈空间内所有栈帧的 lr 地址过滤出来。最后将 lr 地址传入 CalculateTargetAddress 函数计算出 lr 前一条指令(即跳转指令)要跳转到的函数入口地址。


· 代码路径


以上代码在 LiteOS 5.0 版本中已经发布,核心代码路径如下:


https://gitee.com/LiteOS/Lite...


Backtrace 效果演示


· 演示 demo


图 7 除0错误用例函数

演示 demo 设计了一个会导致除 0 错误的函数(如上图图 7),分别在初始化、中断、任务三个场景下调用该函数,将会触发异常并打印相应的信息,观察相应的 fp(此处指函数入口地址,非栈帧寄存器的值)地址是否与实际代码的反汇编地址一致。


可以通过 menuconfig 菜单使能 backtrace 功能,菜单项为:Debug--> Enable Backtrace。同时为避免编译优化造成的影响,还需配置编译优化选项为不优化:Compiler--> Optimize Option --> Optimize None。


· 演示效果


下面所示图中,左图为异常接管打印的日志,右图为反汇编代码。可以看到左图中出现异常的 pc 指令值,对应于右图中的汇编代码为 sdiv r3, r2, r3,即为 test_div 函数中的 int z = a / b 代码行。左图中打印的 backtrace 信息,其 fp 值和右图中的函数入口地址一致。


任务中触发异常:


图 8 backtrace任务演示效果

中断处理函数中触发异常:


图 9 backtrace中断演示效果

初始化函数中触发异常:


图 10 backtrace初始化演示效果


结语


程序异常或崩溃时,通过 backtrace 可以快速定位到问题代码的程序段,是代码调试的必备利器。当与其它工具深度结合时,如与 LiteOS 的 LMS 结合时,会碰撞出更奇妙的火花,甚至可以不用分析汇编代码,直接跳转到出问题的 C 代码行。


对于其它架构,如 LiteOS Cortex-A 的 backtrace 实现会有差异,读者可以参考 arch 目录下其它架构的 backtrace 相应实现。


如果您对 backtrace 有其它疑问或需求,可以在公众号留言或者在社区参与讨论:https://gitee.com/LiteOS/Lite...


本文分享自华为云社区《LiteOS 调测利器之 backtrace 原理剖析》,原文作者:风清扬。 


点击关注,第一时间了解华为云新鲜技术~


发布于: 2021 年 01 月 29 日阅读数: 15
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
LiteOS调测利器:backtrace函数原理知多少