"); //-->
本篇文章是鱼鹰于 2019-04-20 在公众号发表的,几乎每篇原创文章的推荐阅读,鱼鹰都列在上面,但是竟然还是有很多道友没有看过,还在群里问:这个数据莫名其妙被篡改了,但是不知道在哪修改的,这个应该怎么查;由于调用框架太复杂(通过注册方式调用),不知道这个数据会在哪里被使用……
这些问题,都可以由今天介绍的内容解决,所以建议大家仔细阅读并实践一番。
当然,这个方法有一个缺点,就是无法使用离线的方式跟踪数据篡改问题(后面有时间鱼鹰去研究一个非在线调试下的离线定位方式),只能跟踪全局变量,无法跟踪局部变量,还有后面介绍的 printf 打印也不是那么好用,但它在跟踪数据(包括代码,比如有多少个地方直接或间接的调用了该函数)方面绝对是一大利器!!
为了更好的使用 MDK 这个工具,建议大家把《佛祖保佑,永无 BUG,永不修改 | KEIL 调试系列总结篇》这个系列看完。
同时把当初鱼鹰藏的私货《KEIL 调试经验总结》一并看完(当时这份笔记是需要条件才可获得的,现在直接分享了,所以大家不要在私信我要这份笔记了),当然还有一篇干货《BUG 终结者,现场抓获!|颠覆认知》也是解决问题的利器。
相信有了这些笔记,熟练运用并掌握 MDK 这个工具不在话下,遇到问题,你也能通过这些方法快速解决,你值得拥有~~
导读:程序运行过程中,有些数据被莫名修改了,在哪里修改的?又是怎么修改的?这个代码我只想知道是否运行过,或者运行了多少次,但是不想让程序停下来,或者仅打印调试信息,怎么办?当这个变量设置成某个数据后,我想让程序自动暂停下来进行分析,怎么办?
以上问题的所有答案就在本节内容:断点窗口(KEIL)。
本节内容将颠覆你之前对断点调试的认知。这个调试技巧鱼鹰也用了半年多了,当时知道这个调试方法的时候特别兴奋,感觉发现了新大陆。而这个调试技巧也在鱼鹰接手公司项目代码的时候快速解决了不少疑难杂症,而前些天又扩展学习了这个技巧的功能,更是让鱼鹰在学会之后轻松解决了好几个一般调试方法很难解决的 BUG,相信这个技巧也将为鱼鹰之后的开发调试之旅发挥更大的作用。
我们知道常规的断点调试是在想观察哪里的问题时就在对应的代码地址设置断点,并且一旦运行到断点位置会让程序自动暂停运行,这种断点调试功能确实为开发者解决 bug 立下了汗马功劳,但是这种方式有很大的局限性,因为很多时候我们并不需要让程序停下来,而只想知道是否在这段代码运行过,或者说发生问题的位置根本不能停下来,否则就会让整个系统功能出现问题,比如中断处理函数的调试,程序一旦停下了也就失去了所有中断的后续响应;比如两个设备通信,一方采用常规断点的方式调试,肯定会打断正常的通信过程,而这可不是我们想要的,我们只想知道在收到或发送数据后得到环境快照,而并不想让程序停下来。以上这些问题可以采用打印方式解决,但是打印调试也有很多弊端:
以串口为例:
1、你必须添加必要的打印和串口驱动代码,如果你使用 printf 函数,你还得重定向(如果对空间要求高的话,你得知道使用 printf 差不多要占用 1K 大小代码空间)。
2、如果打印效率比较低,常规波特率 9600 和 115200 打印一个字符串耗时可能比较久,那么对于中断频率较高的函数就可能就不适用了。如果你使用 printf 函数,你还得考虑函数是否可重入问题。
3、在代码中引入调试代码有风险,本来程序运行没有问题的,一旦引入调试代码之后可能就出现了问题,这种情况对于拥有丰富开发经验的人来说应该见怪不怪了。原因就在于打印输出时间太久,打乱了程序运行的节奏(而这也是我推荐使用 ITM 调试的一个原因,因为它的输出效率比串口要高得多),或者打印函数本身有问题,也会导致程序运行出现问题。
4、调试完毕之后,你必须把对应的调试代码删除(不管是删除代码还是使用宏,都要进行这一步),不然会影响运行效率。而人是健忘的(也不能说健忘,可能只是因为专注于 BUG 本身,容易忘记其它细枝末节,而解决 bug 之后的欣喜更可能忘记后续处理工作了)这个时候你可以尝试用 #warnning。但是这一步还是必不可少。
而以上问题的解决方案就是 KEIL 的断点调试窗口!
首先打开数据观察点的窗口:
快捷键是 Ctrl + B。
可以看到如下窗口:
当然你也可以通过下面这种方式打开并设置:
从这里你会发现,其实这个窗口就是用来管理你设置的断点的。平常使用的设置断点方法只是其中的一种特例罢了。
首先要知道的就是,调试器支持的断点数量是有限的,具体有多少视情况而定,一旦 KEIL 警告你设置断点太多,那么就要删除一些断点了:
常规用法
1、代码位置运行次数
有些时候我们想知道某些代码的运行次数,比如进入中断处理函数的次数,寻常的断点设置方式必然会让程序停止在中断程序中,但有些时候我们并不希望它停下来。这个时候,你只需要打开该窗口,找到已有的对应断点位置,双击之后就可以看到类似下面的窗口:
此时,你将 Count 的值设置的尽可能大一些,那么就可以让程序运行多次之后才停止。
比如我们设置 Count 的值为 100 次,那么必须在该代码位置运行 100 次才会让程序暂停。当你设置完后点击【Define】后,就会询问你是否需要重新定义,你选择“是”即可。
这样你的断点变成了这样:
后面的 count=100 表示剩余运行次数为 100,运行 100 次后将停止程序。前面的 00 代表断点号,E 代表这是一个执行断点,0x080016B0 代表代码地址,后面的是源码位置。
当这个断点位置运行了 2 次,重新打开该窗口(刷新数据),发现这个数变成了 98,从而可以推算出,已经运行了多少了。如果说你想让这段代码运行 2 次后停止,那么你只需要一开始设置 Count 的值为 2 即可。
2、数据访问
有些时候我们需要知道一些变量会在哪里被访问,那么你可以设置该变量的访问条件。比如鱼鹰想知道 emOspery 变量会在哪里被读取?那么你只需设置如下:
定义之后就是这样:
因为 Count 值设置为 1,所以每一次读取 emOspery 的操作都将使程序停止。比如这段代码:
还有后面的打印函数也使用 emOsprey 变量,所以也会导致程序运行停止。可能你会感到奇怪,为什么 emOsprey++这样的操作也会涉及到读取?事实上你理解了 CPU 寄存器存在的意义也就明白了。
而当你设置为写(Write)访问时,你会发现从复位程序开始运行后,程序会停止在某个地方,这是为什么?当你知道全局变量会在进入 main 函数之前被初始化时,你也就明白为什么了。
在这里我们选择使用 Objects 访问,即按整个变量对象进行访问,上面的 emOsprey 变量实际上是 uint16_t,所以 len 为 2,即字节大小。也就说,如果你设置为 Objects 访问,那么它会根据实际的情况设置访问范围。
为了更好的说明这一点,我构造一个结构体。
这个结构体大小可以看出是 6 个字节。
然后设置访问该结构体的条件:
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。