在Cortex-M上进行事后调试

在Cortex-M上进行事后调试



背景:


我最近参与了消费类电子产品的非典型设备的开发。 似乎没有什么复杂的,一个盒子有时应该退出睡眠模式,向服务器报告并入睡。


很快的实践表明,调试器在与不断进入深度睡眠模式或切断其电源的微控制器一起使用时并没有太大帮助。 基本上,因为处于测试模式的盒子没有调试器,也没有我在附近,所以有时它是越野车。 大约每隔几天一次。


调试UART拧在喷嘴上,我开始在其中输出日志。 变得更加容易,一些问题得以解决。 但是随后发生了断言,这一切都发生了。


就我而言,断言的宏看起来像这样:
#define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0) 

__BKPT(0xAB)是软件断点; 如果断言是在调试时发生的,则调试器将仅停在问题所在的行,这非常方便。


对于某些断言,可以立即弄清楚是什么导致了这些断言-因为日志显示了断言所起作用的文件名和行号。


但是,根据断言,很明显阵列很溢出-更准确地说,是阵列上的临时包装,它检查了出路。 因此,在日志中仅可见文件名“ super_array.h”和其中的行号。 哪些具体的数组尚不清楚。 在周围的日志中,也不清楚。


当然,可以硬着头皮去阅读您的代码,但是我太懒了,那么这篇文章就行不通了。


由于我是使用armcc编译器在uVision Keil 5中编写的,因此仅在其下检查了更多代码。 我还使用了C ++ 11,因为它已经到了2019年,已经到了。


堆栈跟踪


当然,首先想到的是该死的,因为当在普通台式计算机上发生断言时,堆栈跟踪就会输出到控制台,例如KDPV。 从堆栈跟踪中,通常可以了解导致错误的调用顺序。
好吧,所以我也需要隐身步道。 怎么做?


也许如果您抛出异常,他会被演绎吗?


我们抛出一个异常并且不捕获它;我们看到“ SIGABRT”的输出和对_sys_exit的调用。 不搭便车,好吧,不是,我真的很想允许例外。


谷歌搜索其他人的行为。


所有方法都是execinfo.h平台的(不足为奇),对于POSIX下的gcc,有backtrace()execinfo.h 。 Cale没有什么可理解的。 我们掉下了卑鄙的眼泪。 您必须用手爬上堆栈。


我们用手爬上栈


从理论上讲,一切都非常简单。


  1. 当前函数的返回地址在LR寄存器中,堆栈当前顶部的地址(就堆栈中的最后一个元素而言)在SP寄存器中,当前命令的地址在PC寄存器中。
  2. 不知何故,我们找到了当前函数的堆栈帧大小,以这样的距离沿堆栈移动,在那找到上一个函数的返回地址,然后重复该操作,直到逐步遍历堆栈为止。
  3. 我们以某种方式将返回地址与带有源代码的文件中的行号进行匹配。

好的,对于初学者来说,我怎么知道堆栈框架的大小?


默认情况下,在选项上-显然一点都没有,它只是由编译器硬编码为每个函数的“序言”和“结尾”,并分配为为帧分配和释放一部分堆栈的命令。
但是,幸运的是,armcc具有--use_frame_pointer选项,该选项在帧指针下分配R11寄存器-即 指向上一个函数的堆栈框架的指针。 太好了,现在您可以遍历所有堆栈框架。


现在-如何将返回地址与源文件中的字符串匹配?


该死的,再也没有办法。 调试信息不​​会闪存到微控制器中(这并不奇怪,因为它占用了适当的位置)。 Cale还能让她在那儿闪烁吗,我不知道,我找不到。


我们感叹。 因此,诚实的stackrace(使函数名称和行号立即输出到调试输出)将不起作用。 但是您可以显示地址,然后在计算机上将它们与功能和行号进行比较,因为项目中仍然存在调试信息。


但这看起来很可悲,因为您必须解析.map文件,该文件指示每个函数占用的地址范围。 然后使用反汇编代码分别解析文件以找到特定行。 渴望得分。


另外,仔细查看选项--use_frame_pointer的文档, --use_frame_pointer可以看到此页面该页面表明该选项可能导致HardFault随机崩溃。 嗯
好吧,再想一想。


调试器如何执行此操作?


但是调试器以某种方式即使没有frame pointer'a也显示了调用堆栈。 嗯,很明显,IDE掌握了所有调试信息,她很容易比较地址和函数名。 嗯


同时,当崩溃的应用程序生成一个小文件时,同一个Visual Studio也会发生这种情况-minidump,然后您将其提供给Studio,并在崩溃时恢复应用程序的状态。 您可以考虑所有变量,舒适地走在栈上。 再次


但这很简单。 只需要 每天将厚厚的苏维埃连体擦到臀部 使用下降时的值填充堆栈,并且显然可以恢复寄存器的状态。 仅此而已?


同样,将这个想法分解为子任务。


  1. 在微控制器上,您需要遍历堆栈,为此,您需要获取当前的SP值和堆栈开头的地址。
  2. 在微控制器上,您需要显示寄存器的值。
  3. 在IDE中,您需要以某种方式将“ minidump”中的所有值推回堆栈中。 还有寄存器的值。

如何获得SP的当前值?


最好不要在组装机上掠夺双手。 幸运的是,在Cale中,有一个特殊的函数(固有的) __current_sp() 。 Gcc无效,但我不需要。


如何获得堆栈开始的地址? 由于我使用脚本来防止溢出(这是我在此处写的),因此我的堆栈位于单独的链接器节中,我将其称为REGION_STACK
这意味着可以使用名称中带有美元的奇怪变量在链接器中找到他的地址。


通过反复试验,我们选择所需的名称Image$$REGION_STACK$$ZI$$Limit ,检查是否有效。


解说

这是在链接阶段创建的魔术符号,因此严格来说,它不是编译阶段的常量。
要使用它,您需要取消引用:


 extern unsigned int Image$$REGION_STACK$$ZI$$Limit; using MemPointer = const uint32_t *; //   ,   static const auto stack_upper_address = (MemPointer) &( Image$$REGION_STACK$$ZI$$Limit ); 

如果您不想打扰,则只需对堆栈的大小进行硬编码,因为它很少改变。 在最坏的情况下,我们在呼叫堆栈窗口中看到的不是所有呼叫,而是存根。


如何显示寄存器值?


起初,我认为有必要大体上显示所有通用寄存器,我开始迷惑汇编程序,但是很快意识到这是没有意义的。 毕竟,minidump的输出将由我的一个特殊功能完成,在上下文中寄存器值没有意义。


实际上,我们只需要链接寄存器(LR)和程序计数器(PC),该寄存器用于存储当前函数SP的返回地址,而程序计数器已​​经存储了当前命令的地址。


同样,我找不到适用于任何编译器的选项,但是对于Cale仍然有内在函数:LR的__current_pc()和PC的__current_pc()
太好了 仍然需要将小型转储中的所有值压回堆栈,并将寄存器的值压入寄存器。


如何将一个小型转储加载到内存中?


最初,我计划使用LOAD调试器命令,该命令允许您将.hex或.bin文件中的值加载到内存中,但是很快发现LOAD由于某种原因不会将值加载到RAM中。
而且我仍然无法使用此命令来完成寄存器。


好吧,好的,它仍然需要太多手势,将文本转换为bin,将bin转换为十六进制...


幸运的是,Cale拥有一个模拟器,对于该模拟器,您可以使用一些可悲的类似于C的语言编写脚本。 用这种语言,有机会在内存中写东西! _WDWORD ,有一些特殊功能,例如_WDWORD_WBYTE 。 我们将所有想法收集在一个堆中,并获得这样的代码。


所有代码:
 #define USER_ASSERT( statement ) \ do \ { \ if(! (statement) ) \ { \ DEBUG_PRINTF_ERROR( "Assertion on line %d in file %s!\n", \ __LINE__, __FILE__ ); \ \ print_minidump(); \ __disable_irq(); \ while(1) \ { \ __BKPT(0xAB); \ if(0) \ break; \ } \ } \ } while(0) //   ,    //   ,         scatter- extern unsigned int Image$$REGION_STACK$$ZI$$Limit; void print_minidump() { //   - armcc  arm-clang #if __CC_ARM || ( (__ARMCC_VERSION) && (__ARMCC_VERSION >= 6010050)) using MemPointer = const uint32_t *; //   ,   static const auto stack_upper_address = (MemPointer) &(Image$$REGION_STACK$$ZI$$Limit ); //      , ..      //  SP  stack_upper_address auto LR = __return_address(); auto PC = __current_pc(); auto SP = __current_sp(); auto i = 0; DEBUG_PRINTF("\nCopy the following function for simulator to .ini-file, \n" "start fresh debug session in simulator and call __load_minidump() from command window.\n" "You should be able to see the call stack in CallStack window\n\n"); DEBUG_PRINTF("func void __load_minidump() {\n "); for( MemPointer stack = (MemPointer)SP; stack <= stack_upper_address; stack++ ) { DEBUG_PRINTF("_WDWORD (0x%p, 0x%08x); ", stack, *stack ); //         if( i == 1 ) { DEBUG_PRINTF("\n "); i=0; } else { i++; } } DEBUG_PRINTF("\n LR = 0x%08x;", LR ); DEBUG_PRINTF("\n PC = 0x%08x;", PC ); DEBUG_PRINTF("\n SP = 0x%08x;", SP ); DEBUG_PRINTF("\n}\n"); #endif } 

要加载小型转储,我们需要创建一个.ini文件,将__load_minidump函数复制到其中,将该文件添加到自动运行__load_minidump Project -> Options for Target -> Debug然后将此.ini文件写入Use Simulator部分的“ Initialization file”部分中。


现在,我们只是在模拟器上进行调试,而无需开始调试,请在命令窗口中调用__load_minidump()函数。
瞧,我们在保存PC的那一行print_minidump函数。 在Callstack + Locals窗口中,您可以看到调用堆栈。


注意事项:

该函数在开头特别用两个下划线命名,因为如果模拟脚本中的函数或变量的名称与项目代码中的名称意外地重合,则Cale将无法调用它。 C ++标准禁止在开头使用带有两个下划线的名称,因此降低了匹配名称的可能性。


原则上就是这样。 据我所能证实的,小型转储既可用于常规函数,也可用于中断处理程序。 它是否适用于setjmp/longjmpalloca各种变态-我不知道,因为我不练习变态。


我对发生的事感到很高兴; 很少的代码,开销-断言的宏略有膨胀。 在这种情况下,解析堆栈的所有无聊工作都落在它所属的IDE的肩膀上。


然后我用Google搜索了一下,发现gcc和gdb- CrashCatcher也有类似的情况


我知道我没有发明任何新东西,但是我找不到能产生类似结果的现成配方。 如果他们告诉我可以做得更好,我将不胜感激。

Source: https://habr.com/ru/post/zh-CN452704/


All Articles