摘要:本文将会和读者分享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原理分析》,原文作者:风清扬。 

点击关注,第一工夫理解华为云陈腐技术~