目录
- 引言
- 友情提示
- 难以复现的Bug之痛
- 寄存器(SP、LR)详解
- SP寄存器:堆栈的指路明灯
- LR寄存器:函数调用与异常处理的桥梁
- 问题分析与解决流程揭秘
- 保存现场
- 分析堆栈数据
○ 堆栈结构 ○ 入栈顺序
- 案例
J-Link
工具
○ 常用命令 ○ 保存RAM数据到本地
- 分析栈基本信息
○ 分析栈结构 ○ 分析入栈顺序
- 分析栈数据
- 结语
引言
友情提示
本文接近三千字,预计阅读时间为10-15分钟。建议您在空闲时细细阅读,享受阅读的乐趣。
难以复现的Bug之痛
你是否曾为那些难以复现的Bug而头疼不已?本文将揭秘一种通过堆栈分析来定位并解决这类问题的神奇方法。
作为一名开发人员,在开发过程中会碰到各式各样的问题,如果能通过一些操作复现问题的,通过对目标板进行调试还能够逐步分析。
但是,如果由于某些原因不能对目标板进行调试,这种情况分析可就比较复杂了。
这不,前一阵子就碰到了一个问题,在调试过程中,怎样都无法复现问题。直到后来才发现一个现象,只有在对目标板上电时才有一定几率复现问题,即频繁的对目标板进行断电-上电测试。
降臣:“你难道不好奇你的功力是从何而来嘛?”
李星云:“不好奇”
降臣:“你难道不好奇为何换心之后 无法压制这突增的内力吗?”
李星云:“不好奇”
降臣:“你难道不好奇为何你只能活半年?”
李星云:“不好奇”
降臣:“你好奇”
李星云:“不好奇”
降臣:“你好奇”
李星云:“不好奇”
如何对这种问题进行分析,你难道不好奇嘛?
寄存器(SP、LR)详解
这里介绍两个寄存器。SP(Stack Pointer)寄存器和LR(Link Register)寄存器是非常重要的寄存器。
SP寄存器:堆栈的指路明灯
SP寄存器用于指向当前堆栈的栈顶位置。在函数调用时,SP寄存器会调整以反映堆栈的变化,确保数据正确地存储和取出。
LR寄存器:函数调用与异常处理的桥梁
LR寄存器有两个作用。
- 函数调用:当一个函数调用另一个函数时,LR寄存器保存当前函数的返回地址。
- 异常处理:当发生异常时,LR寄存器保存异常处理程序的返回地址。
问题分析与解决流程揭秘
保存现场
在处理难以复现的Bug时,保存现场数据是至关重要的一步。这就像是在犯罪现场拍照留证一样,能够为我们后续的分析提供宝贵的线索。
当上电出现死机问题后,插上Jlink,使用J-Link commander
软件连接目标板,然后通过命令将芯片RAM中的区域保存成二进制文件存放到电脑本地。
如果应用程序的版本号不确定,也可以将ROM中的程序保存到本地,操作方法同保存RAM,后面会说到如何保存。
接下来,我们会分析RAM中的数据。
幸运的情况下,可以直接找到最后一次被调用的函数,然后分析某个函数的功能,即可找到问题。而有的情况下就可能还需要根据函数的前后调用关系分析出问题所在。
分析堆栈数据
我之前也不知道如何分析堆栈数据,第一次分析的时候就感觉进了一个迷宫,绕着绕着就把自己绕进去了。
你可以想象一下,拿着一份二进制文件,去分析函数的调用关系,想想就脑壳疼...
分析堆栈时需要知道下面几个知识点,才能正确分析,我接下来会解释一下。
堆栈结构
堆栈结构主要有四种类型,分别是满递减堆栈、满递增堆栈、空递减堆栈和空递增堆栈。
递增/递减是指栈向高地址还是低地址增长,满是指栈指针(SP)总是指向堆栈中的最后一个元素,即最后压入的数据。空是指栈指针(SP)总是指向下一个将要放入数据的空位置。
常用的可能还是满递减堆栈比较多一点。
入栈顺序
因为我们要分析栈中的数据,所以我们通过汇编查看依次有哪些数据入栈,然后分析出当前的LR寄存器中的值。例如下方是一个入栈的汇编指令。我们需要知道入栈的顺序,是从右往左入栈的还是从左往右入栈的。
0x08000644 B570 PUSH {r4-r6,lr}
案例
J-Link Commande
工具
首先需要安装J-Link
软件,去官网https://www.segger.com/downloads/jlink/
下载,这里是一个套件,安装后会有若干个独立的小软件。
我们需要使用的是J-Link Commande
软件。打开软件之后,可以输入?
来查看支持的命令,如下图:
感兴趣的可以研究这些命令,会对自己有所帮助的。
分析栈基本信息
这里需要分析的是栈属于上面说的四种结构的哪一种,以及数据入栈的顺序是如何的。
这里我们需要将ARM仿真器连接目标板进行调试,通过单步调试定位到PUSH
汇编指令中,如下图所示:
当未执行 0x08000E0C B510 PUSH {r4,1r}
入栈操作时,当前的栈指针SP指向0x20000658
地址,并且该地址中值不为空。从而说明当前的SP指向的是最后一个入栈的元素,即可判定为满
堆栈。
随后我们单步执行,让其执行入栈操作,再来看堆栈中的数据,如下图所示:
此时我们看到SP的地址为0x20000650
,执行了入栈操作,SP的地址减小了,从而判定堆栈的生长方向是递减的。根据上述两个判断从而能得出堆栈结构为满递减堆栈
。
接下来我们要判断数据入栈的顺序,这个汇编是将r4
以及lr
寄存器中的值入栈。分析入栈后的数据得知,第一个入栈的数据为0x08000DE1
,刚好是lr
寄存器中的值。我在图片中也标注了入栈数据对应的寄存器。从而可以得出结论,入栈的顺序是从右往左
的,先入栈lr
后入栈r4
。
保存RAM数据到本地
需要执行如下几个步骤,即可连接到目标板。
- 当系统死机后,需要将目标板连接到ARM仿真器。
- 使用管理员的身份打开
J-Link Commande
工具(否则后面保存数据会提示写入文件失败)
connect
命令准备连接目标板- 选择目标芯片型号
- 选择调试接口
- JTAG
- SWD
- cJTAG
- 选择接口速度
- 连接成功提示
SaveBin
命令保存栈数据
SaveBin c:/ram.bin 0x20000000 0x5000
- 我这里是将整个RAM区域的数据保存起来了
这里用到了一个SaveBin
的命令,命令的原型如下:
SaveBin SaveBin <filename>, <addr>, <NumBytes> Save target memory range into binary file.
如下图所示,是我连接目标板到保存RAM数据的所有操作,此时C盘根目录就会出现一个ram.bin
的文件。
分析栈数据
当我们使用jlink连接目标板后,输入命令h
可以看到一些关键信息,如下图:
可以看到SP(R13)= 20000654, PC = 08000E1E, IPSR = 000 (NoException)
,那我们先去看pc
指向的地址是属于哪一个函数的。我们可以直接在汇编窗口中输入地址然后直接跳转过去,如下图所示:
通过定位得知,这个地址是在InitC
函数内,如下图:
而且这个函数刚执行的时候,执行了一次PUSH
操作入栈了三个元素,根据之前的分析,入栈的顺序是从右往左,所以第一个入栈的数据就是LR
,又因为当前的SP指针指向的地址为20000654
,然后去查ram.bin
数据,如下图:
从而可以推断出LR的值为0x08000E13
,这里是PC+1的值,所以函数返回的实际地址为0x08000E12
。然后再在跳转到这个地址,根据上面图片,发现是一个出栈三个元素的指令,同样找到LR实际地址0x08000E02
然后在跳转到这个地址,反发仍然是一个出栈三个元素的出栈指令,同样找到LR实际地址为0x08001046
,再继续跳转到这个地址,发现是一个延时函数了如下图:
至此,就是通过分析栈数据分析函数的调用关系,这里写了一个简单的测试历程,通过按下按键会执行几层函数调用最后进入一个死循环,从而模拟死机的情况。
怎么样,看着是不是感觉特简单,但是在实际的开发过程中,真实情况可能比这复杂百倍。
结语
为了写这篇文章真的下了血本,我买了一个STM32的小开发板以及一个ARM仿真器,这对于原本就不富裕的我来说无疑是雪上加霜。