打印

教你如何找到导致程序跑飞的指令(一)

[复制链接]
4146|17
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
ifreecoding|  楼主 | 2012-2-29 00:02 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
更多资料请访问我的博客blog.sina.com.cn/ifreecoding


调试嵌入式程序时,你是否遇到过程序跑飞最终导致硬件异常中断的问题?遇到这种问题是否感觉比较难定位?不知道问题出在哪里,没有办法跟踪?尤其是当别人的程序踩了自己的内存,那就只能哭了:(

今天在论坛上看有同学求助这种问题,正好我还算有一点办法,就和大家分享一下。
解决办法非常非常简单,本文将以Aduc7026ARM7内核)和LM3S8962cortex内核,STM32也是cortex内核,同理)为例,讲讲解如何定位此种问题。

先说ARM7内核,cortex内核稍微有一点复杂,后面再说。
ARM7内核有多种工作模式,每种模式下有R0~R15以及CPSR17个寄存器可以使用,有关这些寄存器的细节我就不详细介绍了,详细的介绍请参考“底层工作者手册之嵌入式操作系统内核”中的2.2~2.3节,这里只介绍与本文相关的寄存器。
其中R14又叫做LR寄存器,它被用来保存函数、中断调用时的返回地址,看到了吧,它保存了“返回地址”!这不就是我们需要的么?就这么简单,发生异常中断时,LR寄存器中保存的地址附近就会有导致异常的指令。

接下来我们再先了解一下相关的知识,然后再通过一个例子构造一个指令异常,然后再反推找到产生异常的这条指令,做一个实例演练!
当程序跑飞时,绝大部分情况都会触发硬件异常中断,硬件异常中断的中断服务函数在中断向量表中有定义,我们来看看ARM7的中断向量表,在keil开发环境里(以下例子是在keil环境下介绍的),这个文件一般叫startup.s,如下:
Vectors:        LDR     PC, Reset_Addr
                LDR     PC, Undef_Addr
                LDR     PC, SWI_Addr
                LDR     PC, PAbt_Addr
                LDR     PC, DAbt_Addr
                NOP                            /* Reserved Vector */
                LDR     PC, IRQ_Addr
                LDR     PC, FIQ_Addr

Reset_Addr:     .word   Reset_Handler
Undef_Addr:     .word   ADI_UNDEF_Interrupt_Setup
SWI_Addr:       .word   ADI_SWI_Interrupt_Setup
PAbt_Addr:      .word   ADI_PABORT_Interrupt_Setup
DAbt_Addr:      .word   ADI_DABORT_Interrupt_Setup
IRQ_Addr:       .word   ADI_IRQ_Interrupt_Setup
FIQ_Addr:       .word   ADI_FIQ_Interrupt_Setup
ARM7的中断向量表比较简单,只有7种中断,它把所有正常的中断都放到了SWIIRQFIQ中了,那么本文所介绍的异常情况将会触发UndefPAbt或者DAbt异常中断,至于是哪种就需要看具体的原因了。
指令A    //触发异常
指令B
比如说当指令A无法执行时,它就会触发异常中断,硬件就会自动将这条指令后面的指令的所在地址,也就是指令B的地址保存到LR寄存器中,然后就跳转到与这种异常相关的中断向量表中,假如指令A触发了Undef异常中断,那么硬件就会跳转到中断向量表的第二个中断向量Undef_Addr,从中断向量表可知,这个中断向量对应的中断服务函数就是ADI_UNDEF_Interrupt_Setup,这个函数一般是一个死循环,这样单板就死了,当我们停下程序时,就会发现程序停在了这个函数里面。
我们来看下面这个实例,我把定位过程的每一步都记录下来,一起来看下:
14  S32 main(void)
15  {
16      U8* pucAddr;
17

18      /* 初始化硬件 */
19      DEV_HardwareInit();
20

21      /* 创建任务 */
22      WLX_TaskInit(1, TEST_TestTask1, TEST_GetTaskInitSp(1));
23      WLX_TaskInit(2, TEST_TestTask2, TEST_GetTaskInitSp(2));
24

25

   /**********此指令会触发异常中断**********/
26

   pucAddr = (U8*)0;
27

   *pucAddr = 0;
28

   /****************************************/
29

30

31      /* 开始任务调度 */
32      WLX_TaskStart();
33

34      return 0;
35  }
上面这段测试代码是我在我写的一个小型嵌入式操作系统上改的(有兴趣的话可以访问我的博客O(_)O),只需要关注2627行即可,其余的只是陪衬,以使这段程序看起来稍微复杂一些。这两行指令将0地址清00地址是中断向量表,向这个地址写数据会导致异常的,但——这正是我们所需要的。
然后,为了方便,我们在中断向量表里把上面的3个异常中断向量都修改一下,如下:
Vectors:        LDR     PC, Reset_Addr
                LDR     PC, FaultIsr
                LDR     PC, SWI_Addr
                LDR     PC, FaultIsr
                LDR     PC, FaultIsr
                NOP                            /* Reserved Vector */
                LDR     PC, IRQ_Addr
                LDR     PC, FIQ_Addr
这样,只要发生异常中断就都会进入FaultIsr函数,FaultIsr函数如下:
void FaultIsr()
{
    while(1)

   {
        ;

   }
}
可以看到FaultIsr函数是个死循环,所以当程序发生异常跑飞时就会死在这里了。

准备工作完成,准备实战演练!在这之前还有一点需要注意,那就是最好将编译选项设置为不优化,这样方便我们定位问题。当然,实际情况也许不允许我们这么做,这样的话就需要你有比较高的汇编语言水平了,这不在本文讨论之内,先不管了。我们在这个例子里将编译选项设置为不优化。

我们将上面改动后的代码重新编译,然后加载到单板里,进入仿真状态,然后全速运行,然后再停止运行,我们就可以发现程序死在FaultIsr函数里了,如下图所示:


图1

从图1可以看到程序停在了42行,这与我们的设计是一致的。在图1的左侧显示了此时各个寄存器内的数值,注意到LR寄存器了吧,这里保存的就是返回地址,出错的指令就在这附近。但,还有一点需要注意,FaultIsr函数是C语言函数,它运行时可能会修改LR寄存器,如果是这样的话,那么此时LR寄存器内的数值就不是发生异常时的值了,为解决此问题,我们可以找到FaultIsr函数的起始地址,将断点打在FaultIsr函数的起始地址,这样当异常发生时就会停在断点的地方,也就是FaultIsr函数的起始地址,这样就可以保证LR寄存器的值就是发生异常时的值了。
如果你的汇编语言足够好,那么你可以在图1右上角的汇编窗口里向上找,找到FaultIsr函数的起始地址。另外,我们还可以通过一个简单的方法找到FaultIsr函数的起始地址。我们在keil的选项中选择生成map文件,代码编译后就会生成一个map文件,我们可以从这个文件里找到FaultIsr函数的地址。
使用一个文本编辑器打开这个map文件,然后搜索“FaultIsr”,如下图,我们就找到了FaultIsr函数的起始地址:0x80608。


2

在汇编窗口找到0x80608的地址,打上断点,如下图所示:


3

复位程序,再重新全速跑一遍,我们就会发现程序停在了断点上,这时LR里面的数值就是程序异常时存入的返回地址,通过这个地址差不多就可以找到出错的指令了。
如图3所示,LR的值为0x805ec,我们在汇编窗口里跳到这个地址,如下图所示:



4

ARM7内核有2级流水线,存入LR的地址一般会多+8个字节,因此0x805ec-8=0x805e4,如图4所示,0x805e4地址是一条STRB R2[R3]指令,这条指令的意思是将R2寄存器里的数值保存到R3寄存器所指向的地址(一个字节)内。从图3左侧可以看到R2寄存器的数值为0R3寄存器的数值也为0,那么这条指令的意思就是将0这个数值写入0地址这个字节内,这不是正好对应上述main函数中27行的C指令么?
看到这里我们就应该明白了,向0地址写0,这条C指令有问题,那么这个跑飞的问题也就找到原因了,是不是很简单?

当然,实际情况可能要比上述介绍的情况复杂的多。实际使用的程序几乎都是经过优化的,这样从汇编指令找到C指令就会比较麻烦。还有可能FaultIsr函数的指令或者堆栈被破坏了,那么FaultIsr函数运行都会出问题。还有可能出错的指令不会象27行这么明显,可能是经过了前面很多步骤的积累才在这里触发异常的,最典型的就是别人的程序踩了你的内存,结果错误在你的程序里表现出来了,如果遇到这种情况你就先哭一顿吧。对于这种踩内存的情况也是可以通过这种方法定位的,但这相当复杂,需要从出错点开始到触发异常点为止,这之间所有的堆栈信息,然后从最后的堆栈开始,结合反汇编的代码,从最后一条指令向前推,直到发现问题的根源。这种方法相当于是我们用我们的大脑模拟CPU的反向运行过程,如果程序是经过优化的,那么这个过程就更麻烦了。我准备在“底层工作者手册之嵌入式操作系统内核”6.1节实例讲解一个这种情况(现在是2012.02.28,手册暂时只写到了5.4节)。

好了,先不说这么复杂的了,接着上面的继续说。
有时候出现问题的单板并不在我们手边,问题也许不能复现,那么我们就可以预先在FaultIsr函数里做一个打印功能——将出现异常时的寄存器、堆栈、软件版本号等信息打印出来,编写这样的FaultIsr函数需要注意,FaultIsr函数开始的代码一定要用汇编语言来写,以防止调用FaultIsr函数时的寄存器、堆栈信息被C语言破坏。
如果我们的单板有这样的功能,那么当单板跑死时,一般情况都会向外打印信息,比如上面的例子,就会打印出LR的值为0x805ec。但我们似乎又遇到了一个问题,我们如何知道0x805ec这个地址是哪个函数的?别忘了,我们在一个版本发布时会将软件所有的信息归档(什么?没归档!这样的公司我劝你还是走了吧),根据软件版本号找到出问题的软件的归档文件,取出map文件,利用上面讲述的方法通过map文件我们就可以找到出问题的函数了。再通过软件版本从归档文件中找到这个函数最终编译链接生成的目标文件,一般为.o.axf.elf等文件(必须是静态链接的文件,需要有各种段信息的),不能是binhex等文件,windowslinux等动态链接的文件已经超出了我目前的知识范围,也不再其中。
然后使用objdump程序进行反汇编,将目标文件与objdump程序放到同一个目录,在cmd窗口下进到这个目录,执行下面命令:

objdump -d wanlix.elf >> uncode.txt

这行命令的意思是将wanlix.elf目标程序进行反汇编,反汇编的结果以文本格式存入uncode.txt文本文件。
我们用文本编辑器打开uncode.txt文件,找到0x805ec地址,如下图所示:



5

如图5所示,我们可以看到0x805ec这个地址位于main函数内,我们再对比一下图5和图4中的指令,可以发现它们是相同的,可能写法上会有一些差异,但功能是相同的。

好了,ARM7内核的介绍到此结束,下面介绍cortex内核的,使用STSTM32TILM3S系列的同学们注意了,它们都是cortex内核的,下面的介绍你也许用得上。

由于论坛字数限制,请继续看“教你如何找到导致程序跑飞的指令(二)”,或者下载附件查看,或者登录我博客查看

教你如何找到导致程序跑飞的指令.pdf

276.96 KB

沙发
supreme42| | 2012-2-29 09:33 | 只看该作者
多谢,非常有用

使用特权

评论回复
板凳
lxyppc| | 2012-2-29 09:50 | 只看该作者
这个写得不错,顶一下

使用特权

评论回复
地板
PXJ_520| | 2012-2-29 15:26 | 只看该作者
谢谢LZ的分享

使用特权

评论回复
5
雨辰073| | 2012-2-29 17:21 | 只看该作者
写的蛮好的,支持下!

使用特权

评论回复
6
xsgy123| | 2012-2-29 19:20 | 只看该作者
这个难度不小

使用特权

评论回复
7
dfsa| | 2012-2-29 19:26 | 只看该作者
没看太明白

使用特权

评论回复
8
txcy| | 2012-2-29 19:33 | 只看该作者
这个的确是比较难找

使用特权

评论回复
9
程序匠人| | 2012-2-29 23:03 | 只看该作者
有益的探索。先笑纳了。

建议香斑竹给裤子!

使用特权

评论回复
10
香水城| | 2012-3-1 11:47 | 只看该作者
哈哈,匠人说项,哪有不从的道理,赏裤子!

使用特权

评论回复
11
香水城| | 2012-3-1 12:10 | 只看该作者
顺便把LZ的第二部分的链接也贴出来吧:

教你如何找到导致程序跑飞的指令(二)

使用特权

评论回复
12
ifreecoding|  楼主 | 2012-3-1 13:12 | 只看该作者
看来还是这里有人识货啊,大家的支持才是我的动力
已经在STM32上完成了一个俄罗斯方块的小游戏,基于我写的Mindows操作系统,估计下周就贴出来了,分享全部设计及源代码

使用特权

评论回复
13
elec921| | 2012-3-1 13:28 | 只看该作者
mark

使用特权

评论回复
14
lxyppc| | 2012-3-1 13:58 | 只看该作者
一般简单调试我会这样做
void FaultISR(void){
    static volatile uint32_t f = 1;
    while(f);
}
这样进入Fault之后会手动将f改为0,跳出循环。
在汇编中单步跟踪,中断函数的返回,就能回到“事故”现场

使用特权

评论回复
15
nicholasldf| | 2012-3-1 16:31 | 只看该作者
我以前也是这么想,用的uCOS-II操作系统,异常时,把任务堆栈内容打印出来,看看PC和LR的值,大致定位程序出错在那一段代码。
                    SystemDebug(SYSDBG_USART, "R0  : 0x%08x\n\r", *(p_sp + 0x01));
                    SystemDebug(SYSDBG_USART, "R1  : 0x%08x\n\r", *(p_sp + 0x02));
                    SystemDebug(SYSDBG_USART, "R2  : 0x%08x\n\r", *(p_sp + 0x03));
                    SystemDebug(SYSDBG_USART, "R3  : 0x%08x\n\r", *(p_sp + 0x04));
                    SystemDebug(SYSDBG_USART, "R4  : 0x%08x\n\r", *(p_sp + 0x05));
                    SystemDebug(SYSDBG_USART, "R5  : 0x%08x\n\r", *(p_sp + 0x06));
                    SystemDebug(SYSDBG_USART, "R6  : 0x%08x\n\r", *(p_sp + 0x07));
                    SystemDebug(SYSDBG_USART, "R7  : 0x%08x\n\r", *(p_sp + 0x08));
                    SystemDebug(SYSDBG_USART, "R8  : 0x%08x\n\r", *(p_sp + 0x09));
                    SystemDebug(SYSDBG_USART, "R9  : 0x%08x\n\r", *(p_sp + 0x0A));
                    SystemDebug(SYSDBG_USART, "R10 : 0x%08x\n\r", *(p_sp + 0x0B));
                    SystemDebug(SYSDBG_USART, "R11 : 0x%08x\n\r", *(p_sp + 0x0C));
                    SystemDebug(SYSDBG_USART, "R12 : 0x%08x\n\r", *(p_sp + 0x0D));
                    SystemDebug(SYSDBG_USART, "SP  : 0x%08x\n\r",   p_sp);
                    SystemDebug(SYSDBG_USART, "LR  : 0x%08x\n\r", *(p_sp + 0x0E));
                    SystemDebug(SYSDBG_USART, "PC  : 0x%08x\n\r", *(p_sp + 0x0F));
                    SystemDebug(SYSDBG_USART, "CPSR: 0x%08x\n\r\n\r", *(p_sp + 0x00));

使用特权

评论回复
16
microaue| | 2012-3-1 21:24 | 只看该作者
盗亦有道:lol,一直很看好,哈哈哈

使用特权

评论回复
17
ifreecoding|  楼主 | 2012-3-2 11:18 | 只看该作者
一般简单调试我会这样做
void FaultISR(void){
    static volatile uint32_t f = 1;
    while(f);
}
这样进入Fault之后会手动将f改为0,跳出循环。
在汇编中单步跟踪,中断函数的返回,就能回到“事故”现场 ...
lxyppc 发表于 2012-3-1 13:58


在线调试时这办法不错!!

使用特权

评论回复
18
aaaxmaaa007| | 2013-5-16 22:29 | 只看该作者
非常的好用,谢谢分享!

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2

主题

68

帖子

3

粉丝