摘要:本文将会和读者分享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 <test_div>
8007814: f7f9 fe68 bl 80014e8 <OsTickStart>

当程序执行到地址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/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。

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

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

LiteOS调测利器:backtrace函数原理知多少的更多相关文章

  1. 物联网打工人必备:LiteOS Studio图形化调测能力

    摘要:本文会给大家介绍下LiteOS Studio的调测的几个知识点,包括: 调测配置,监视变量,反汇编代码同步展示,数值进制切换,跨平台编译调测,Qemu模拟器调测,多核调测,远程设备调测等. 掌握 ...

  2. 用C代码简要模拟实现一下RPC(远程过程调用)并谈谈它在代码调测中的重要应用【转】

    转自:http://blog.csdn.net/stpeace/article/details/44947925 版权声明:本文为博主原创文章,转载时请务必注明本文地址, 禁止用于任何商业用途, 否则 ...

  3. 调测Onvif事件总结解决办法

    主要在调测事件用例的过程中,发现了大量的信息,和未曾碰到的场景和非法错误等信息,先总结解决办法如下: (1)测试过程中发现以前的一个难题解决了,原先在生成soap空间命名的文件中有部分需要下载,离线生 ...

  4. 关于Android真机调测Profiler

    U3D中的Profile也是可以直接在链接安卓设备运行游戏下查看的,导出真机链接U3D的Profile看数据,这样能更好的测试具体原因.   大概看了下官方的做法,看了几张帖子顺带把做法记录下来.   ...

  5. backtrace函数

    1.函数原型 #include <execinfo.h> int backtrace(void **buffer, int size); 该函数获取当前线程的调用堆栈,获取的信息将会被存放 ...

  6. Android真机调测Profiler

    U3D中的Profile也是可以直接在链接安卓设备运行游戏下查看的,导出真机链接U3D的Profile看数据,这样能更好的测试具体原因. 大概看了下官方的做法,看了几张帖子顺带把做法记录下来. 参考: ...

  7. 【经验分享】如何搭建本地MQTT服务器(Windows ),并进行上下行调测

    网上查了很多资料,实际动手的时候踩了很多坑,现在把我的经验分享给大家: 一.安装和启动 使用EMQTT,下载完直接到bin目录下执行emqttd start就可以了,简单方便 下载地址:https:/ ...

  8. [Debug]驱动程序调测方法与技巧

    转自:http://blog.csdn.net/lichangc/article/details/43272457 驱动程序开发的一个重大难点就是不易调试.本文目的就是介绍驱动开发中常用的几种直接和间 ...

  9. 机器学习笔记——模型调参利器 GridSearchCV(网格搜索)参数的说明

    GridSearchCV,它存在的意义就是自动调参,只要把参数输进去,就能给出最优化的结果和参数.但是这个方法适合于小数据集,一旦数据的量级上去了,很难得出结果.这个时候就是需要动脑筋了.数据量比较大 ...

随机推荐

  1. Java学习_Java快速入门

    Java简介 安装完JDK后,需要设置一个JAVA_HOME的环境变量,它指向JDK的安装目录.在Windows下,它是安装目录,类似: C:\Program Files\Java\jdk-15 把J ...

  2. java Stream学习笔记

    1.Stream与io无关. 2.Stream和用普通的循环没有太大区别,甚至时间复杂度更高,代码可读性差(通常代码行数更少). 3.流操作就是把循环要做的任务单独抽取出来,最终通过编译在一起. 来看 ...

  3. ECMAScript概述及浅谈const,let与块级作用域

    ECMAScript可以看作javascript的标准规范,实际上javascript是ECMAScript的一门脚本语言,ECMAScript只提供了最基本的语言JavaScript对ECMAScr ...

  4. Spark Streaming 与Filnk对比分析

    转:https://mp.weixin.qq.com/s/jllAegJMYh_by95FhHt0jA

  5. 使用python做一个IRC在线下载器

    使用python做一个IRC在线下载器 1.开发流程 2.软件流程 3.开始 3.0 准备工作 3.1寻找API接口 3.2 文件模块 3.2.1 选择文件弹窗 3.2.2 提取文件名 3.2.2.1 ...

  6. Wi-Fi 6 与 5G 相比哪个更快?

    随着 iPhone12 的发布,iOS 系统正式开始拥抱 5G,智能手机全面进入了 5G 时代.伴随着各大运营商的 5G 推广以及相关基站建设的如火如荼,5G 成了大家广泛讨论的热门词汇.这样热门的光 ...

  7. 值得推荐的C#不同版本语言特性

    C#语言在不断地版本升级中,为我们提供了很多新的语言特性.其中,有很多使用的语言特性,我觉得在实际开发中用起来很方便,能够简化我们的代码,增强可读性,提高开发效率. 小编不才,在这里给大家整理了一些实 ...

  8. 分贝单位的本质(下半篇),dBm、dBFS、dBV的妙处你想象不到

    上半篇讲到了声音分贝的概念, 对于声音的单位:dB SPL和dB SIL,有兴趣了解并推算的朋友,可以点击以下链接(PC端效果更佳) http://www.sengpielaudio.com/calc ...

  9. python Logger模块单例模式

    前言 提前祝大家过个好年 最近忙于项目,今天抽出点时间写写Blog谈谈昨天遇到的问题 项目最近要收尾了,想把Logger规整一下,因为很多地方都有用到 Python的Logger模块是Python自带 ...

  10. Head First 设计模式 —— 11. 组合 (Composite) 模式

    思考题 我们不仅仅要支持多个菜单,升值还要支持菜单中的菜单.你如何处理这个新的设计需求? P355 [提示]在我们的新设计中,真正需要以下三点: P354 我们需要某种属性结构,可以容纳菜单.子菜单和 ...