• 【经验分享】基于Linux命令行编程环境的栈追溯和反汇编技术实践分享




    1 写在前面

    在嵌入式开发中,往往受限于系统(芯片)本身的资源环境,没法像基于PC主机开发那样方便地进行程序debug,尤其当程序出现一些异常的时候,找到有效的方法进行debug,对排查问题非常有帮助。

    在现实编程调试环境中,甚至于根本不可能拿到仿真器或者IDE环境来辅助你做异常分析,这个时候你的排查手段更多的是依赖一些命令行工具,结合编译输出的各种文件,配合代码逻辑做异常分析。其中,比较常用的方法有栈帧追溯和反汇编技术。

    本文将简要介绍一下这两种方法的基础知识,需要对大家有所帮助;通过本文的阅读,你讲会了解到以下知识点:

    • 代码构建时可能输出的各种文件介绍
    • N种常见常用的命令行工具
    • 栈帧追溯的基本原理和操作方法
    • 反汇编的基本操作方法
    • 汇编代码的基本阅读技巧

    PS:本次分享源自团队neibu的现场分享,所以部分深入的内容不便公开,部分文字也不可能百分百还原现场的讲解,感兴趣的可以私下交流。

    2 代码构建系统简介

    2.1 常见的构建系统

    有传统的Make、CMake,也有新主流的scons、xmake的,我个人观点,构建功能无所谓绝对的优劣,主要看各自团队的掌握情况以及需求程序,大可不必迫切追新,基础原理都是一样,万变不离其中,学精通一个就差不多了,其他的只是语法、命令、函数不一样而已。

    image-20220531211720422

    2.2 编译构建生成的文件简介

    GCC 编译器为例,

    下面是业内常见叫法的文件:

    • .d文件 :.d文件的内容是参与编译的C源码文件
    • .i文件 : .i 文件的内容是C源码文件被执行 预处理 之后的代码文本(可读的文本文件
    • .s文件 : .s 文件的内容是预处理之后的文件(.i文件)经过 编译 之后,生成的 汇编代码 文件 (可读的文本文件
    • .o文件 : .o 文件的内容是编译器对汇编文件进行汇编处理后,生成的 目标文件,(不可读的二进制文件);在 GCC 编译器中,.o文件实际也是一个 elf 文件;
    • .map文件:map文件就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件,里面包含函数大小,入口地址等一些重要信息;
    • .elf文件 :.elf文件(Executable and Linkable Format),是Linux平台是的可执行文件,里面包含代码段和数据段,以及可能包含的调试信息等等;
    • .hex文件 :Intel HEX文件是由一行行符合Intel HEX文件格式的文本所构成的ASCII文本文件;
    • .bin文件 : 纯二进制文件,一般用做烧录flash的固件文件;
    • .a文件 :静态库文件,通常是原厂把不希望公开的代码打包成静态库的形式开放给第三方使用;
    • .mk文件:即为Makefile文件,里面遵循Makefile的标准语法。

    下面几个文件后缀,是我们自己约定的文件:

    .cmd文件 :.cmd是命令文件,里面保存的是一些流程需要用到文本命令;(在构建时直接把相关命令打包好,放在这个文件里)

    .dmp文件:.dmp是反汇编的输出文件,用于保存反汇编得到的汇编的代码。

    2.3 几个常用的命令行工具

    以下几个Linux 命令行工具,常常用于分析上述文件,用好它们一定能让你的工作事半功倍。

    • vim : Linux下文本编辑神器
    • grep :关键字检索工具,搜索log,搜索代码的神器
    • find :查找指定类似文件的神器
    • file :分析文件类型的神器
    • size :分析程序大小的神器
    • readelf :分析elf文件的神器(object文件、so文件、elf可执行文件,本质都是elf文件)
    • nm : 分析静态库(.a)文件的神器
    • objdump :反汇编神器 (下文详讲
    • addr2line :栈追溯神器 (下文详讲

    3 基于命令行工具的栈追溯

    3.1 栈追溯技术简介

    栈追溯技术,一般来说,就是利用 处理器异常 时的现场信息(主要是寄存器信息),把函数的调用栈关系捋出来,从而还原出代码调用的异常路径,辅助排查异常问题的一种技术。

    3.2 栈追溯技术的基础知识

    3.2.1 ARM寄存器简介

    ARM有37个寄存器,31个通用寄存器,6个状态寄存器:

    这里写图片描述

    主要介绍如下:

    不分组通用寄存器R0-R7:这意味着在所有处理器模式下,访问的都是同一个物理寄存器。不分组寄存器没有被系统用于特别的用途,任何可采用通用寄存器的应用场合都可以使用未分组寄存器;

    • R0-R3:函数调用的入参和出参;
    • R4-R7:函数执行的中间变量;
    • 规则由 ATPCS(即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准))描述。

    分组寄存器R8-R12:处理器保留做特殊通途;

    寄存器R13通常做堆栈指针SP;

    寄存器R14用作子程序链接寄存器(Link Register-LR),也称为LR,指向函数的返回地址

    寄存器R15被用作 程序计数器,也称为PC。

    3.2.2 RISC-V寄存器简介

    以32位RISC-V处理器为例,它的寄存器描述如下表所示:

    在这里插入图片描述

    3.2.3 函数调用的栈描述示意图及栈追溯的基本原理

    函数调用的示意图如下所示:

    img img

    在排查栈追溯相关的问题中,我们主要关注的寄存器是:PC寄存器和LR寄存器,核心思路就是把LR地址串起来,本质上就还原了函数调用的层次关系。

    3.3 基于命令行addr2line的栈追溯实践

    某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
    ============================================
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个build.sh是我们的构建编译的前端shell脚本,用于接收参数输入,内容不方便公开(你懂的),但是如何调起addr2line可以简单说一下,里面的脚本核心就是调用如下:

    addr2line_cmd="$backtrace_cmd 0x$addr -e $backtrace_elf_file -f -C -s -p"
    
    • 1

    其中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
    然后把多个地址的输出组起来,把调用层次关系捋一下,就是是从上往下调用了。

    比如有一段异常代码如下:

    image-20220531205019833

    运行之后,异常现场出现了:

    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
    ==========
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们先试下x-task任务的 栈追溯:

    image-20220531205147912

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

    image-20220531205243648

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

    image-20220523190104810

    果然跟我们上面特意写的异常代码是吻合的。

    所以啊,也不能太一味地信任栈帧,有时直接对PC指针进行addr2line也很好使。

    4 基于命令行objdump的反汇编

    4.1 反汇编技术简介

    所谓的反汇编技术 就是执行与编译器编译C源码代码相关的流程,属于逆向分析的一种。

    简单来说,如下:

    程序汇编:将C源代码翻译成汇编代码,进而翻译成适用于处理器运行的二进制指令;

    程序反汇编:将目标文件换成会汇编代码,再由汇编代码尽可能地还原出具备可读性的高级语言代码(比如C代码)。

    • 基于elf文件的反汇编:如果elf文件中带有调试信息,基本可以做到 汇编代码与C代码一一对应;
    • 基于bin文件的反汇编:一般来说,不太可能之间翻译成C代码,但可以直接翻译成汇编代码;汇编代码到C代码,需要借助一些工具,比如IDA工具

    4.2 反汇编技术的基础知识

    要想熟练掌握反汇编技术,除了基本的反汇编操作方法外,剩下的就是了解对应处理器平台的汇编指令集。

    由于不同的处理器的汇编指令集都不一样,甚至于一种处理器的不同版本对应的指令集也不尽相同(比如ARM处理器),所以只能是学以致用,一边学习,一边应用。

    比如开源的 RISC-V指令集

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxfZnqNG-1656603216369)(https://s2.loli.net/2022/05/23/lYJdz9xf3VWEkrK.png)]

    一大堆指令规范文档,慢慢啃吧。

    4.3 基于objdump命令行的反汇编实践

    某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 ]
    
    • 1
    • 2
    • 3
    • 4
    • 5

    查找一个函数:自己实现的函数 softap_decrypt_password 分析C代码和汇编代码。

    image-20220531210940752

    查找一个 原厂提供的接口函数(打包在静态库里面的函数)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
    
    • 1

    其中xxx.elf为编译输出的elf文件,注意,如果想在反汇编的代码中看到C代码与汇编代码的对应关系,记得把GCC的-g选项打开;

    由于objdump是直接输出到控制台的,所以需要重定义输出到某个文件中,随便取一个名字即可。

    看固件的代码大小,反汇编的时候可长可短,完成之后,打开xxx.dmp文件就可以看到反汇编之后的汇编代码了。

    5 Q & A

    • map文件中可以看到局部变量的地址吗? 【答案:看不到】

    • 执行backtrace操作的时候,有时候会下面的提示,是不是出什么问题了?

      backtrace[03] 01000000 ?? ??:0

    • 既然能反汇编,那岂不是没有源码秘密可言?那我不是就可以看到原厂封装的那些库文件的源代码了?【你觉得呢?】

    • 静态库里面能看到结构体是怎么定义的吗?【答案:看不到】

    • 快速阅读汇编代码及了解其大致的实现逻辑,有什么技巧可言?【你觉得呢?】

    6 参考链接

    7 更多分享

    架构师李肯

    一个专注于嵌入式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主页和专栏:

    【CSDN主页:架构师李肯】

    【RT-Thread主页:架构师李肯】

    【C/C++语言编程专栏】

    【GCC专栏】

    【信息安全专栏】

    【RT-Thread开发笔记】

    【freeRTOS开发笔记】

    有问题的话,可以跟我讨论,知无不答,谢谢大家。

  • 相关阅读:
    4、StyleGAN系列
    力扣-路径总和问题
    Flink CDC-SQL Server CDC配置及DataStream API实现代码...可实现监控采集一个数据库的多个表
    阿里云通义千问14B模型开源!性能超越Llama2等同等尺寸模型
    10.5号作业
    JavaScript重点知识总结一
    最新版WPS 2023 加载Zotero方法
    springboot毕设项目大学校园公益回收平台设计与实现 9kwh9(java+VUE+Mybatis+Maven+Mysql)
    CTF工具PDF隐写神器wbStego4open安装和详细使用方法
    数据分析Pandas专栏---第三章<Pandas合并list和字典>
  • 原文地址:https://blog.csdn.net/szullc/article/details/125550164