1. 问题描述
最近我发现我已经很久没有做笔记了,但是我收到了很多后台留言和新的粉丝关注,这让我感到很欣慰,因为这说明我的文字还是有用户认真阅读的,这也为我节省了不少重复劳动的时间。奇怪的是,似乎每到夏季的时候,我都会收到很多脑洞问题,其中一些偶发问题在车上复现的难度非常大,令人头疼。尽管在分析这些问题时花费了很多时间和精力,但是我发现深入研究下去还是能收获很多的。
最近在支持BLE KW客户时,我遇到了一个非常隐蔽的问题,这让我重新审视了ARM断点技术。在同一批KW36板子上烧录相同的代码后,大部分板子都可以正常运行,但是有几块板子却莫名其妙地出现了Hard Fault。而且,导致Hard Fault的位置每块板子都不相同。其中有一块KW36板子在执行某个特定的UDS诊断服务时出现了Hard Fault,而且一旦问题出现后,这块板子就一直可以重复复现该问题。我通过之前写过的一篇旧文《KW36 MCU Hard Fault问题查找和破解方法》中介绍的方法,使用Attach方式通过Ozone IDE(一款由Segger推出的强大调试工具)与板子进行了连接,很快找到了导致Hard Fault问题的代码位置(之前的研究确实没有白费,我感到非常欣喜,因为通过之前的研究我练就了解决问题的技巧,终于在一条狗上收到了回报)。
我发现每次出错的位置都在同一行的C代码上。仔细检查了这段代码,发现它只是一个普通的函数调用,检查了堆栈空间后确认没有溢出。那么为什么会出错呢?为什么仅有这几块板子会出错呢?
2. 问题分析:
本着花里胡哨的问题都是隐藏在最朴实无华角落的真理,去对比了下两者的汇编(实际上是真的没啥思路),偶然发现在Attach到问题板分别执行全速运行到出错代码行和单步运行到出错代码行时,两者汇编竟然差异。如下图右侧是正常执行Attach到芯片得到的汇编代码,左边是出错时的汇编代码,发现出问题时汇编竟然出现了DC16和CDP2。DC16对应常量声明,显然DC16不应该在此处出现,CDP2是协处理器数据处理指令,KW36作为一个单核芯片,肯定无协处理器,更谈不上协处理器指令。
考虑到上述的调试操作都是在Attach方式,不存在因为代码下载导致Flash内容被修改的可能,那可能只有两个:IDE解析错误以及代码被意外修改,显然前者是个死胡同,无从查起。于是顺着第二条路Dump芯片内二进制(左侧)和原始二进制文件(右侧)对比发现两者Flash确实有差异。如下图所示,F000的值被修改成了BE00,同时处理CRC校验值也被修改,So什么原因导致Flash被修改了呢?
以前有概念说断点会程序代码做修改,但这个二进制码的修改如何和断点扯上关系呢?于是开始了漫长的Google/Bing/度娘/ARM各种文档里面轮番挖呀挖呀挖,直到挖出Cortex-M3权威指南中提到如下一句”BKPT 指令的机器码是 0xBE00“,顿时豁然开朗,坐实了断点导致Flash内容被修改。
可新的问题来了,断点是如何修改到这段Flash内容的呢?什么类型的断点会修改Flash?断点改了Flash内容为何不会被还原?打断点要注意什么才能保证修改后的Flash内容被正确还原?打软件断点是否会修改Flash内容?Flash断点,硬件断点以及程序断点的区别?芯片明明写着支持2个breakpoint,为何同一个IDE配合不同的调试工具实际上可以打出的断点个数有差异?带着一堆问号继续探寻…
3. 断点类型和实现原理分析
断点的实现原理网上有很多介绍,大致意思都是说在程序运行的某个地方提前设置一个停止操作,CPU运行到该位置之后就会自动暂停,此时开发者就可以查看CPU寄存器的工作状态。可这里的”设置一个停止操作“是如何实现的呢?又意味着什么呢?网上找到的都是基于X86的实现细节介绍,针对ARM的实现细节找了半天也都是语焉不详。发挥绝知此事要躬行的精神(客户需要复现现场),笔者使用JLINK/I-JET+IAR在KW38 demo板子上做了一番测试,得出几个很有意思的结论,完全颠覆了以前对断点的一些认识,此处先抛出结论。
根据IDE和调试器的不同,断点类型也被分为很多种包括硬件断点、软件断点、Flash断点、数据断点、程序断点、条件断点、代码断点以及Trace断点等。从实现原理上来讲,一共有四大类:硬件断点、软件断点、数据断点以及Flash断点,因为程序断点和代码断点从原理上讲可以归属于硬件断点或者Flash断点(具体哪一种取决于控制器支持的硬件断点个数),条件断点属于数据断点,Trace断点是结合Trace工具的程序断点。下面分别深入介绍这四种断点的实现机制:
3.1 硬件断点:
硬件断点需要MCU内部支持,其数量一般是有限的,具体取决于芯片厂商的实现,对应KW38、KW45、S32K1以及S32K3断点支持个数统计如下表所示。从实现原理上讲硬件断点是通过FPB模块来实现的,尽管这块是芯片厂商自行实现的,但是ARM架构已为其分配固定的地址区域,如下图红色方框。所以不论哪个公司的cortex芯片,用户都可以通过该地址查看已设置的硬件断点所在地址。FPB提供了两个功能:断点功能和Flash地址重载, Flash地址重载是其一个高阶功能,此处用到的是其断点功能。当在程序中设置了硬件断点后,该断点处的地址就会被写入到FPB的存储区域,即E000_2000 – E000_3FFF区域,当CPU访问该地址时,因为该地址在FPB中已经注册过,硬件就会自动触发一个断点事件。
芯片型号 | KW38(M0+) | KW45(M33) | S32K1(M4) | S32K3(M7) |
---|---|---|---|---|
硬件断点个数 | 2个 | 8个 | 6个 | 8个 |
3.2 Flash断点:
Flash断点也就是本次问题的罪魁祸首了,也是从原理上来讲最最容易出现问题的断点。其实现原理是在设置Flash断点时,IDE和仿真器会配合修改控制器Flash上的内容,进一步说就是将断点处的指令修改为断点指令0xBE00,相应地就会牵涉到对Flash的擦除和编程操作。由于Flash内容是非易失的,所以有可能在调试器意外断开后对Flash断点处插入的断点指令还保留着,在用户复位直接运行程序可能就会导致程序无法正常运行。
目前市场上各种宣称支持Unlimited Breakpoint无限断点的仿真器采用的就是这个原理,如下截图的JLINK工具。需要提出的是Flash断点不是任意仿真器+IDE的组合都支持的,需要IDE和仿真器配合起来,譬如Ozone/Embeded Studio配合JLINK,IAR需要配合I-JET才能在断点设置时指定使用Flash断点,如果IAR配合JLINK只能选择软件断点(实际使用的其实也是Flash断点,只是标注上不带F标记)。I-JET和JLINK在支持Flash断点的差别主要是对Flash执行修改的策略,譬如一个Sector地址区域的多个Flash断点是执行一次还是多次的Flash操作,以及Flash插入的断点恢复策略等,需要使用时自行体会。
3.3 软件断点:
软件断点最开始主要用于RAM中设置断点,它的实现机制是将RAM中原有的指令替换为一个断点指令,当CPU执行到该指令之后就会自动暂停,这个时候调试器便会将原有的指令放回原来位置。这个过程都是调试器自动实现的,对RAM的更新速度很快,所以用户一般觉察不到。但实际情况是现在很多MCU受限于RAM大小,程序都是存放在Flash区域的,所以当断点大于硬件断点数量后,即便新的断点选成软件断点,很多时候也会被设置为Flash断点,意味着Flash断点有的问题在使用软件断点时也需要注意,这也是为何将软件断点的讲解放在Flash断点之后。
3.4 数据断点:
数据断点又称为条件断点,在ARM内核手册上称为watchpoint断点,是在CPU对特定地址访问时会触发的一种断点,在芯片内部实现上需要DWT和FPB共同配合完成,在设置断点的时候可以设置触发的条件,比如对特定地址的读访问还是写访问。这种断点对于追踪堆栈溢出相关的问题有奇效。数据断点同样需要硬件支持,数量一般也是有限的,如对应KW38、KW45、S32K1以及S32K3数据断点支持个数统计如下表所示。
芯片型号 | KW38(M0+) | KW45(M33) | S32K1(M4) | S32K3(M7) |
---|---|---|---|---|
数据断点个数 | 2个 | 4个 | 4个 | 4个 |
4. 断点实验证明:
为了证明以上观点,笔者在KW38板子上做了一系列的测试如下:
4.1 IAR+I-JET 强制使用2个Flash断点,无硬件断点:
如下截图可以看到,在汇编和memory窗口中的数据都是原始的binary文件,但是通过额外的JLINK同时去Attach上去读取可以看到对应0x16996地址和0x169A0物理地址的数据已经被修改为了0xBE00;此处要特别鸣谢下IAR的Wen hao兄给的启发,确实没想到IAR Memory窗口读取的数据和汇编窗口的指令居然都是假数据。
“
Note: IAR需要配合IAR公司自己的I-JET才能选择Flash断点,如果选择JLINK等其他工具只能设置软件断点;
”
4.2 IAR+JLINK 使用2个硬件断点+1个Flash断点:
如下图1 可以看到,两个硬件断点地址0x1FA2以及0x1FA6都被注册到了FPB寄存器的0xE0002000地址了。要指出的是,此处提到的Flash断点在IAR中显示为软件断点,因为IAR中默认仅支持使用I-JET的工具去打Flash断点,其标志就是会在红点里面显示一个F图标。所以这也就是前文说到的,对于Flash运行的代码软件断点在实现上本质也还是Flash断点。
下图2 可以看到,强制设置为line 38和line40处为硬件断点之后,因为KW38只支持2个硬件断点,main处这个断点就被强制设定为了Flash断点,分别去读取0x1F98, 0x1FA2以及0x1FA6地址的值,可以看到main处断点所在的地址内容被修改为了0xBE00,0x1FA2以及0x1FA6地址是正常的原始数据,存储在硬件断点地址,完全符合预期。
4.3 IAR+JLINK使用2个硬件断点+2个软件断点:
可以看到,line36,line40被强行设置为硬件断点,line38被设置为软件断点,main断点也自动被分配为了软件断点,对比0x1F98, 0x1F9A, 0x1FA2, 0x1FA6地址的内容,只有0x1F98,0x1FA2两个软件断点地址处的内容被修改为了0xBE00,即插入BKPT指令。0x1F9A和0x1FA6两个硬件地址的内容没有被修改,再次证明软件断点对于在Flash调试的控制器,本质上就是Flash断点。
4.4 IAR+JLINK 使用6个软件断点+1个硬件断点:
因为IAR中默认优先使用硬件断点,而当前debug中设置了6个软件断点,所以main出的断点被自动分配为硬件断点。可以看到,全部6个软件断点对应地址区域的值都被修改为了0xBE00,也就是前文提到的软件断点其实也变成了Flash断点,main地址处内容没有发生变化,符合预期。
5. 如何预防Flash断点带来的潜在风险
对于硬件断点和数据断点来说,他们是基于寄存器的实现,脱离Debug状态无法继续生效,不会影响用户代码的脱机运行。但是Flash断点因为牵涉到修改非易失的Flash数据,就有风险导致程序意外被修改无法运行,譬如说突然断电,不合理退出debug状态等。
而且对于该类问题,如果断点断在main函数的主任务上还容易观察到,因为代码很直观的无法运行,而如果断在了某个不常调用的但又会用到的子任务的某个switch case中,就容易埋下隐蔽的bug(本案例遇到的就是这个状况)。还一种情况是实车现场分析过程中,常用的一种方式就是Attach观察分析问题,如果使用到Flash断点(尤其是Flash运行的软件断点),且退出时没注意清除该类型断点,可能会带来新的潜在问题。
设置完Flash断点,对于突然断电这种方式,自然是无解的,只能重新下载代码。而在debug状态时如何退出才是最优雅的方式(还原原本Flash内容)呢?笔者使用IAR+JLINK在KW38 demo板上上做了一系列测试,结论如下;
序号 | 测试条件(此处软件断点使用的是Flash断点) | 运行结果 |
---|---|---|
1 | 使用6个软件断点+突然断电+上电重启: | 无法运行 |
2 | 使用6个软件断点+IDE中正常stop debug后+上电重启: | 无法运行 |
3 | 使用6个软件断点+在breakpoint窗口中Disable 所有Flash断点+上电重启: | 无法运行 |
4 | 使用6个软件断点+在breakpoint窗口中Disable 所有Flash断点+执行一次SW reset+上电重启: | 正常运行 |
5 | IAR+JLINK+KW38 使用6个软件断点+在breakpoint串口中Delete所有Flash断点+上电重启 | 正常运行 |
总的来说,为了避免Flash断点带来的Flash修改后没有恢复的问题,需要在退出Debug模式时,先delete掉所有断点,再去stop debug,或者先在在breakpoint窗口中Disable 所有Flash断点后,再debug状态下执行一次SW reset,让Flash恢复回来,前者的缺点是下次debug时原来的断点都丢失了,需要重新设置,优点是更彻底。
6. 结论:
基于以上分析可以得出几个结论:
-
Flash断点是会修改到Flash内容的,而且IDE和汇编中是体现不出该变化的,比较隐蔽;
-
Flash断点导致Flash内容修改后未恢复的问题需要尤其重视,需要在退出debug前按章节5的两种办法加以处理;
-
软件断点对于非RAM中调试来说,IAR配合Jlink使用时本质上也是Flash断点;
-
不是所有的调试器都支持Flash断点,当设置的断点个数超过硬件断点个数,就会提示无法设置新的断点,例如CMSIS DAP;
硬件断点的支持个数有限,会在调试时默认采用,超过硬件断点个数后的断点JLINK默认会采用Flash断点, IJET提示超过上限,需要选择Flash断点,断点小红点标志带有F标志;
-
IAR+JLINK中JLINK支持Flash断点是JLINK仿真器和其驱动的支持,IAR+IJET支持Flash断点是IAR工具+I-JET硬件的支持,和IAR+JLINK的区别是其可以直接设置Flash类型断点;
-
如果是Flash断点打在了主函数导致重启后代码无法运行倒容易排查,如果是不小心打在了某个不常调用的子函数分支上,而功能测试时覆盖度如果不够,就可能导致埋下一个潜在的隐患;
以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !