百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

LiteOS调测利器之backtrace原理剖析

yuyutoo 2025-02-19 14:27 4 浏览 0 评论

转自 LiteOS物联网操作系统 公众号 文章。

微信搜索关注该公众号:LiteOS物联网操作系统


编者按:在LiteOS大揭秘系列,我们和读者们分享了《你一定不知道,LiteOS竟偷偷隐藏了这些调测功能》

文中介绍了LiteOS提供的调测功能,并对“异常接管”功能在发生异常时打印的日志做了详细说明,其中打印的函数调用栈回溯信息(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函数时将其压入栈中。例如有如下汇编指令:

    800780e:  6078        str  r0, [r7, #4]
    8007810:??f7ff?ffe0???bl??80077d4?
    8007814:??f7f9?fe68???bl??80014e8?

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


    • 设计思路


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

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


    如果为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/LiteOS/blob/master/arch/arm/cortex_m/src/fault.c


    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/LiteOS/issues

    相关推荐

    微软Win10/Win11版Copilot上线:支持OpenAI o3推理模型

    IT之家4月3日消息,科技媒体WindowsLatest昨日(4月2日)发布博文,报道称Windows10、Windows11新版Copilot应用已摘掉Beta帽...

    WinForm 双屏幕应用开发:原理、实现与优化

    在当今的软件开发领域,多屏幕显示技术的应用越来越广泛。对于WinForm应用程序来说,能够支持双屏幕显示不仅可以提升用户体验,还能满足一些特定场景下的业务需求,比如在演示、监控或者多任务处理等场景...

    推荐一个使用 C# 开发的 Windows10 磁贴美化小工具

    ...

    OpenJDK 8 安装(openjdk 8 windows)

    通常OpenJDK8和11都能互相编译和通用。我们建议使用11,但是如果你使用JDK8的话也是没有问题的。建议配置使用OpenJDK,不建议使用OracleJDK,主要是因为版...

    基于 Linux 快速部署 OpenConnect VPN 服务(ocserv 实战指南)

    一、前言在如今远程办公和内网穿透需求日益增长的背景下,搭建一套安全、稳定、高效的VPN系统显得尤为重要。OpenConnectServer(ocserv)是一个开源、高性能的VPN服务端软件...

    巧妙设置让Edge浏览器更好用(edge怎么设置好用)

    虽然现在新版本的Edge浏览器已经推出,但是毕竟还处于测试的状态中。而Win10系统里面自带的老版Edge浏览器,却越来越不被人重视。其实我们只需要根据实际情况对老版本的Edge浏览器进行一些简单的设...

    WPF做一个漂亮的登录界面(wpf页面设计)

    ...

    微软开源博客工具Open Live Writer更新:多项Bug修复

    OpenLiveWriter前身是WindowsLiveWriter,是微软WindowsLive系列软件之一,曾经是博主们非常喜爱的一款所见即所得博文编辑工具,支持离线保存,还支持图像编辑...

    基于OpenVINO的在线设计和虚拟试穿 | OPENAIGC大赛企业组优秀作品

    在第二届拯救者杯OPENAIGC开发者大赛中,涌现出一批技术突出、创意卓越的作品。为了让这些优秀项目被更多人看到,我们特意开设了优秀作品报道专栏,旨在展示其独特之处和开发者的精彩故事。...

    C#开源免费的Windows右键菜单管理工具

    ...

    Windows10或11中隐藏的功能,用它再也不用担心电脑中病毒!

    ...

    Python open函数详解(python open函数源码)

    演示环境,操作系统:Win1021H2(64bit);Python解释器:3.8.10。open是Python的一个内置函数,一般用于本地文件的读写操作。用法如下。my_file=open(fi...

    Windows 11 安装 Docker Desktop(Windows 11 安装助手 Windows 易升 关系)

    ...

    Windows 11 新版发布:屏幕亮度自适应控制,小组件界面重新设计!

    ...

    世界上最好用的Linux发行版之一,OpenSUSE安装及简单体验

    背景之前无意在论坛里看到openSUSE的Linux发行版,被称为世界上最好用的Linux发行版之一(阔怕),一直想体验一下,于是这期做一个安装和简单体验教程吧。...

    取消回复欢迎 发表评论: