在嵌入式开发中,往往受限于系统(芯片)本身的资源环境,没法像基于PC主机开发那样方便地进行程序debug,尤其当程序出现一些异常的时候,找到有效的方法进行debug,对排查问题非常有帮助。
在现实编程调试环境中,甚至于根本不可能拿到仿真器或者IDE环境来辅助你做异常分析,这个时候你的排查手段更多的是依赖一些命令行工具,结合编译输出的各种文件,配合代码逻辑做异常分析。其中,比较常用的方法有栈帧追溯和反汇编技术。
本文将简要介绍一下这两种方法的基础知识,需要对大家有所帮助;通过本文的阅读,你讲会了解到以下知识点:
PS:本次分享源自团队neibu的现场分享,所以部分深入的内容不便公开,部分文字也不可能百分百还原现场的讲解,感兴趣的可以私下交流。
有传统的Make、CMake,也有新主流的scons、xmake的,我个人观点,构建功能无所谓绝对的优劣,主要看各自团队的掌握情况以及需求程序,大可不必迫切追新,基础原理都是一样,万变不离其中,学精通一个就差不多了,其他的只是语法、命令、函数不一样而已。

以 GCC 编译器为例,
下面是业内常见叫法的文件:
下面几个文件后缀,是我们自己约定的文件:
.cmd文件 :.cmd是命令文件,里面保存的是一些流程需要用到文本命令;(在构建时直接把相关命令打包好,放在这个文件里)
.dmp文件:.dmp是反汇编的输出文件,用于保存反汇编得到的汇编的代码。
以下几个Linux 命令行工具,常常用于分析上述文件,用好它们一定能让你的工作事半功倍。
栈追溯技术,一般来说,就是利用 处理器异常 时的现场信息(主要是寄存器信息),把函数的调用栈关系捋出来,从而还原出代码调用的异常路径,辅助排查异常问题的一种技术。
ARM有37个寄存器,31个通用寄存器,6个状态寄存器:

主要介绍如下:
不分组通用寄存器R0-R7:这意味着在所有处理器模式下,访问的都是同一个物理寄存器。不分组寄存器没有被系统用于特别的用途,任何可采用通用寄存器的应用场合都可以使用未分组寄存器;
分组寄存器R8-R12:处理器保留做特殊通途;
寄存器R13通常做堆栈指针SP;
寄存器R14用作子程序链接寄存器(Link Register-LR),也称为LR,指向函数的返回地址;
寄存器R15被用作 程序计数器,也称为PC。
以32位RISC-V处理器为例,它的寄存器描述如下表所示:

函数调用的示意图如下所示:
在排查栈追溯相关的问题中,我们主要关注的寄存器是:PC寄存器和LR寄存器,核心思路就是把LR地址串起来,本质上就还原了函数调用的层次关系。
以 某RISC-V芯片平台 为例,实践一下:
recan@ubuntu:~/build$ ./build.sh backtrace 23037250 2303660e 23038c2a 2300e45a 2300e070 2300e344
elf file : /home/recan/ccc.elf
================ Backtrace =================
| backtrace[05] 2300e344 protocol_handle_task at protocol.c:416 (discriminator 1)
| backtrace[04] 2300e070 protocol_parse at protocol.c:102
| backtrace[03] 2300e45a read_round_buf at roundbuf.c:131
| backtrace[02] 23038c2a mutex_lock at osal.c:91
| backtrace[01] 2303660e xQueueSemaphoreTake at queue.c:1517
V backtrace[00] 23037250 xTaskGetSchedulerState at tasks.c:4039
============================================
这个build.sh是我们的构建编译的前端shell脚本,用于接收参数输入,内容不方便公开(你懂的),但是如何调起addr2line可以简单说一下,里面的脚本核心就是调用如下:
addr2line_cmd="$backtrace_cmd 0x$addr -e $backtrace_elf_file -f -C -s -p"
其中addr就填脚本传入的那些栈帧地址,比如23037250,backtrace_elf_file就填你编译固件输出的elf文件。
还有就是这个 addr2line,一般交叉编译工具链里面都有,跟xxx-gcc是在一起的,比如我这里是:riscv64_unkown_elf_gcc10.2.0/Linux64/bin/riscv64-unknown-elf-addr2line
得到的输出应该是类似于这样:23037250 xTaskGetSchedulerState at tasks.c:4039
然后把多个地址的输出组起来,把调用层次关系捋一下,就是是从上往下调用了。
比如有一段异常代码如下:

运行之后,异常现场出现了:
Exception Entry--->>>
mcause 38000005, mepc 23002880, mtval 00000008, tasksp 420242d0
Current task sp data:
RA:2300c8b2, mstatus:80007880
A0:00000000 A1:4201d7c0 A2:4201d810 A3:230b1580 A4:4201d811 A5:65707974 A6:65707974 A7:4201d811
T0:00000020 T1:4200f000 T2:00002d25 T3:4200f000 T4:4200f000 T5:00000000 T6:5baba208
S0:42024358 S1:0000000e S2:230b06cc S3:230b1000 S4:42010254 S5:00004007 S6:00002710 S7:230b1000
S8:230b1000 S9:230b1000 S10:4201cd00 S11:230b1000
Exception code: 5
msg: Load access fault
!!!maybe crash task name:x-task
==========
Task name:x-task
Backtrace: 23037e54 23037f0e 42023f88
==========
我们先试下x-task任务的 栈追溯:

我们回过头来,单传入 PC 指针 进行栈追溯看看:(mepc 23002880)

看样子,有点东西,我们找到这个C文件的对应行数:

果然跟我们上面特意写的异常代码是吻合的。
所以啊,也不能太一味地信任栈帧,有时直接对PC指针进行addr2line也很好使。
所谓的反汇编技术 就是执行与编译器编译C源码代码相关的流程,属于逆向分析的一种。
简单来说,如下:
程序汇编:将C源代码翻译成汇编代码,进而翻译成适用于处理器运行的二进制指令;
程序反汇编:将目标文件换成会汇编代码,再由汇编代码尽可能地还原出具备可读性的高级语言代码(比如C代码)。
要想熟练掌握反汇编技术,除了基本的反汇编操作方法外,剩下的就是了解对应处理器平台的汇编指令集。
由于不同的处理器的汇编指令集都不一样,甚至于一种处理器的不同版本对应的指令集也不尽相同(比如ARM处理器),所以只能是学以致用,一边学习,一边应用。
比如开源的 RISC-V指令集 :
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxfZnqNG-1656603216369)(https://s2.loli.net/2022/05/23/lYJdz9xf3VWEkrK.png)]
一大堆指令规范文档,慢慢啃吧。
以 某ARM架构的处理器 为例:
recan@ubuntu:~/build$ ./build.sh objdump
elf file : ./binary/bbb.elf
objdump ... please wait some time ...
objdump done, please check dump file with the following cmd:
[ vi ./binary/bbb.dmp ]
查找一个函数:自己实现的函数 softap_decrypt_password 分析C代码和汇编代码。

查找一个 原厂提供的接口函数(打包在静态库里面的函数)rwip_aes_encrypt 分析其汇编代码:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XSsCVBGo-1656603216371)(https://s2.loli.net/2022/05/31/TnvG1iwDPmCWAta.png)]
**如果我的程序不是基于我的构建流程编译出来的,能进行反汇编吗?**答案是肯定可以的,只要芯片平台和编译工具用的是同一个。
下面我把objdump的命令参数公开一下:
arm-none-eabi-objdump -l -d -x -s -S xxx.elf > xxx.dmp
其中xxx.elf为编译输出的elf文件,注意,如果想在反汇编的代码中看到C代码与汇编代码的对应关系,记得把GCC的-g选项打开;
由于objdump是直接输出到控制台的,所以需要重定义输出到某个文件中,随便取一个名字即可。
看固件的代码大小,反汇编的时候可长可短,完成之后,打开xxx.dmp文件就可以看到反汇编之后的汇编代码了。
map文件中可以看到局部变量的地址吗? 【答案:看不到】
执行backtrace操作的时候,有时候会下面的提示,是不是出什么问题了?
backtrace[03] 01000000 ?? ??:0
既然能反汇编,那岂不是没有源码秘密可言?那我不是就可以看到原厂封装的那些库文件的源代码了?【你觉得呢?】
静态库里面能看到结构体是怎么定义的吗?【答案:看不到】
快速阅读汇编代码及了解其大致的实现逻辑,有什么技巧可言?【你觉得呢?】
一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验,深耕IoT领域多年,熟知IoT领域的业务发展,深度掌握IoT领域的相关技术栈,包括但不限于主流RTOS内核的实现及其移植、硬件驱动移植开发、网络通讯协议开发、编译构建原理及其实现、底层汇编及编译原理、编译优化及代码重构、主流IoT云平台的对接、嵌入式IoT系统的架构设计等等。拥有多项IoT领域的发明专利,热衷于技术分享,有多年撰写技术博客的经验积累,连续多月获得RT-Thread官方技术社区原创技术博文优秀奖,荣获CSDN博客专家、CSDN物联网领域优质创作者、2021年度CSDN&RT-Thread技术社区之星、RT-Thread官方嵌入式开源社区认证专家、RT-Thread 2021年度论坛之星TOP4、华为云云享专家(嵌入式物联网架构设计师)等荣誉。坚信【知识改变命运,技术改变世界】!
欢迎关注我的github仓库01workstation,日常分享一些开发笔记和项目实战,欢迎指正问题。
同时也非常欢迎关注我的CSDN主页和专栏:
有问题的话,可以跟我讨论,知无不答,谢谢大家。