【应用笔记】TAE32G5800 进阶调试小技巧分享
1 问题背景
作为一名攻城狮,你是否也经常遇到过这种情况,程序正运行着,突然某个变量就被莫名修改了,导致系统就不能按预期的方向继续运行。此时将进行灵魂的拷问:
- 变量何时改变的?
- 变量被改变了多少次才出现的问题?
- 当这个变量被设置成某个数据后,我想让程序暂停下来继续分析,该怎么办?
- 我不想让程序停下来,又没有串口重定位,又想输出一些额外信息,又该怎么办?
以上问题的答案都可以在这篇文章中找到答案。
2 断点简介
BreakSet命令为指定的表达式 ( exp )设置断点。断点是程序地址或表达式,当它们为真时,会停止目标程序的执行或执行指定的命令(“cmd”)。如下图2.1所示,详细的介绍见下面几个小节,会对几个表达式做详细的介绍。
图2.1 断点表达式简介
2.1 exp表达式
是执行期间的一个地址或者一个表达式。
在表达式中,常用到的就是程序变量(符号),程序变量是程序的变量。它们通常被称为符号或符号名称。表达式只支持简单的操作符,&, &&, <, , >=, ==, and !=。符号名称的一般形式如下图2.2所示:
图2.2 exp格式
2.1.1 ExecName
表示在Target-Output-Name of Executable字段选项中定义的应用程序名称 ,必须以双反斜杠 (\\) 开头。
2.1.2 Path
表示源代码文件的路径。用斜杠 (/) 将文件夹与子文件夹分开。必须以斜杠 (/) 结尾。路径可以有绝对或相对符号。
2.1.3Module
表示源模块的名称(*.c、*.cpp)。可以以Path 开头。必须以反斜杠 (\) 开头。可以使用调试命令从命令行窗口查询模块名称:DIR module。
2.1.4 Identifier
表示源代码对象。必须用反斜杠 (\)与Module分隔。当在应用程序中唯一时,可以独立使用并且没有反斜杠。标识符可以是:
- 代码行号。
- 变量(char、int、long、float、double)。
- 复合变量(数组、结构、文件)。
- 函数
- 上述的有意义组合
2.1.5 exp举例
根据以上对exp表达式各个字段的描述,大概知道每个字段所表达的含义。在程序中手动打一个断点,具体的详细信息如下图2.3所示:
图2.3 exp举例
根据以上信息,可以简单看出各个字段所对应的内容:
- ExecName:\\project
- Path:\UserCode/Source/
- Module:user_hrpwm.c
- Identifier:为行号89
从以上得出的信息来看,手动设置的断点都是以行号来识别的。
2.2 cnt表达式
是一个表达式,用于确定在目标程序停止或执行指定命令之前满足断点条件的次数。默认计数值为 1。第一个断点触发后,计数被忽略。
2.3 cmd表达式
此表达式是一个命令参数,如果不设置则默认是“_break_=1”,是在断点触发时执行的 µVision 命令字符串,程序执行不会停止。用户定义函数可用于命令表达式。在函数中,您可以将系统变量 _break_设置为 1 以停止程序执行。未指定命令时,程序执行停止。
当用户设置的exp成立之后,系统过就会暂停,以便用户分析程序。如果不想程序暂停则需要将_break_设置为0。该命令可以搭配printf使用,只能输出字符串,不能搭配变量使用。
而且如果使用命令行输出则必须加\n才能输出到命令窗口中,否则无法正常输出,后面章节会有例程以及注意点对此表达式做介绍。
3 设置断点的几种方式
Keil调试设置断点有3种方式:手动设置断点、观察窗设置断点、命令窗设置断点。下面会对这3种断点的设置方式逐一进行讲解。
3.1 手动设置断点
此种方法是最简单的,也是大家最熟悉的,即在文件的某一行手动设置一个断点。或者使用快捷键F9设置断点。如下图3.1是用此种方式设置的断点。
图3.1 手动设置断点
3.2 观察窗设置断点
即把需要监控的变量加入观察窗中,然后选中该变量,右键选择“Set Access Breakpoint at xxx”之后在断点观察窗中将所需要的信息补全,这个方法设置断点也是比较常用的。如下图3.2是用此种方式设置断点。
图3.2 观察窗设置断点
3.3 命令窗设置断点
首先将命令行的窗口打开,在View->Command Windows,BS就是BreakSet的缩写,设置的时候就和断点的语法一样就行了,使用逗号隔开。因为Identifier这一选项已经说明如果符号唯一则前面的应用程序、路径以及模块都可以省略。命令输入时候会有提示的,会告诉你下一步应该输入的语法。使用命令行设置断点之后按下回车就可以了,在断点观察窗就可以看到详细信息了。如下图3.3所示:
图3.3 命令窗设置断点
3.3.1 命令窗WRITE命令
1、有时候需要监控变量何时被写进去时,可以使用以下命令行(见图3.4)当变量进行写的时候,程序会暂停。
图3.4
2、有时候在调试时,想知道变量被写了多少次才出现异常,可以用下面的命令行(见图3.5)。当变量进行写10次时,程序会暂停。
图3.5
3、有时候在调试时,想知道变量被设置成某个数据后,让程序停下来继续分析,可以用下面的命令行(见图3.6)。当该变量为10时,程序会暂停。
图3.6
4、有时候在调试程序时,既没有串口重定位,也不想让程序停下来,又想输出一些额外信息,可以用下面的命令行(图3.7)。当变量为10时,命令窗口会输出“TmpData”,并且程序不会暂停。效果图下图3.8所示。
图3.7
图3.8 命令行窗口输出
这里需要注意命令行中的cmd命令需要加\n,否则不会输出信息,而\是一个转义字符,需要\\才能输出。
4 命令窗调试实例
4.1变量定位器
前面介绍了一些常用的命令,这里简单拿个实例看看这些调试命令的强大之处。直接步入正题,请问下面这段代码(代码清单4.1),会修改count2的内容吗?
代码清单4.1 count数值赋值
unsigned char count1[10];
unsigned char count2[10];
int main(void){
SystemClock_Config();
unsigned char *pctr = count1;
pctr[10] = 1;
}
很明显,当我们仿真程序的时候,可以发现count2的内容已经被意外修改了,如下图4.1所示:
图4.1 count调试界面
以上这段代码意外的修改了 count 2 中的内容。这在编码一些大型程序的时候就会导致很多意想不到的结果,比较好的情况下只是程序运行逻辑不正常。但是一旦修改到其他重要的内存空间,比如函数指针之类的地方,可能就会导致程序死机。
这种简单的程序一旦出现问题,我们很容易分析是指针使用越界导致了内存的意外篡改。但是,一旦等到程序的体量比较大,光想靠分析程序逻辑来确认到底是哪里意外篡改内存这几乎是不太可能的情况。针对此类情况分享一个比较实用的小技巧。可以在调试界面按下:Ctrl + B。至于这是什么东西,可以自行百度 keil 的 breakpoint 功能(前面几个章节也有简单的介绍)。我这里暂且命名为:变量定位器。它能在变量被修改或者读取的时候停下来。
在正式调试之前简单介绍下Breakpoints窗口的一些字段意思,如下图4.2所示,相应字段的描述对应图中的标号:
- 需要观察的变量。
- 是否监测被读位置?
- 是否监测被写位置?这里表示count2意外篡改的地方。
- 一般选择Object。
图4.2 窗口介绍
其他地方暂时就不需要知道了。有了这些信息后,我们如何确定上面例子中 count2 的内容是在何处被意外修改的?当你知道 count2 的内容肯定不是你自己修改的时候,我们在 breakpoint 中填入一下信息,具体信息如下所示:
图4.3 count2信息
点击Define后,程序会自动在count2被意外写之前停下来。这就相当于我们告诉 keil 调试器,需要在 count2 被 write 的时候自己停下来。然后关闭 breakpoint 窗口点击运行程序,详细效果如下图4.4所示:
图4.4 count2定位
很明显,就算你没有打断点,但是程序也会莫名其妙的停在了第90行。这是为什么呢?因为第88行修改了 count2 的内容。这也就符合我之前的预期。无论哪里修改 count2 的内容,程序都会停下来。这个调试小技巧对于内存的意外篡改定位有很大的帮助。
4.2 表达式定位断点
上面的实例是说内存意外被篡改,用变量定位的方式很快能找到原因,而有时候在一些特殊的场景下无法用变量去定位分析,基于此简单介绍下另外一种常用的调试方式:表达式定位断点。
下面会基于一个实例,介绍用表达式定位断点的方式分析定位问题原因。为了方便后面演示整个分析的过程,用FPGA模拟一个CPU取指异常导致程序异常进haldfault现象。
步入正题,程序在正常工作时,会意外将死区寄存器的DTR的值意外修改,程序中此值是写死的,没有任何地方对此值进行改写。在正常情况下,DTR的值是0xE40134,芯片发送异常后,会将此值改写为0x13BF,导致程序输出异常进haldfault。如下图4.5是DTR寄存器被改写时的寄存器状态。
图4.5 寄存器状态
根据上面的现象,我们没法确定是哪个变量值被意外写进DTR寄存器,只能通过DTR被修改后值0x13BF来定位这个值是来谁给过来的?通过程序调试,程序中没有变量能给出0x13BF这个值,查到这里,基本没有继续查下去的思路了。后面在分析DTR寄存器的状态时突然发现DTR这个寄存器的bit14为保留位(见下图4.6),实际此bit是不存在的,所以在写入0x53BF时,DTR会被错误改写为0x13BF。这里也能说明为什么在程序中没法找到有这个值的变量。
图4.6 DTR寄存器
后通过对0x53BF这个值分析,用表达式定位断点的方式成功定位到此值在哪一行被赋值。调试在命令行输入此命令:BS WRITE g_u16PwmPDutyMax==0x53BF,1
程序会在变量g_u16PwmPDutyMax改为0x53BF时暂停,方便分析。如下图4.7是分析后的截图,下面会对每一步的分析思路进行详细说明。
图4.7 DTR异常
①:触发断点后,运行停在1164行,此时DTR寄存器值已经被修改,因此被断点表达式抓到并halt CPU。
②:分析前一条语句,为将R6的值,写入R11+0x224的地址内,按前一行代码(1163行)目标是往SLV1_CMPCR(0x4003B224)的寄存器写入数值(g_u16PwmPDutyMax)
③:实际此时R11的值为0x4003B010,按汇编语句计算,实际值为0x4003B234,也就是SLV1_DTR寄存器的值,见⑤
④:此时g_u16PwmPDutyMax的值为0x53BF。
⑤:标注SLV1_DTR寄存器的地址(见图4.6)。
⑥:DTR最终值为0x13BF,经过分析,bit14 为保留位,不存在,因此实际只有0x13BF被写入,DTR寄存器值被错误修改。
再对比一下DTR正常时的寄存器状态,如下图是DTR正常时的分析说明。
图4.8 DTR正常
观察正常与异常的截图(图4.7和图4.8),可见问题出在R11通用寄存器上,正常时R11=0x4003B000,异常时R11=0x4003B010
跟踪前面的代码,找到R11的赋值位置(如下图4.9)
图4.9 R11赋值位置
R11是从当前PC指针地址+736地址后读取一个Word数据出来,FLASH内存储的数据为0x4003B000。
因此正常时R11的值正确,异常时R11的bit4异常为1。导致后面数据被错误写入到DTR寄存器内。
以上实例通过表达式定位断点的方式,成功分析出DTR被意外修改的原因。