×
单片机 > 单片机程序设计 > 详情

嵌入式ARM平台调试方法的讨论

发布时间:2020-08-28 发布时间:
|
1. 当前使用的调试方法
通常情况下我们直接使用JTAG进行嵌入式设备的调试和开发。此方式最简单和直接,且功能强大,能够随时中断处理器,检查程序状态。但是此方式也有缺点:无法长时间跟踪程序的执行情况,对于客户处一些难复现的死机问题很难处理,基本只能依靠静态代码分析。且金融POS来说,由于防拆机制的存在,编写应用时没有办法直接使用JTAG进行调试。因此我们讨论几种新的辅助调试方法。
 
2. 几种新的调试方法
2.1. 打印寄存器信息
此种方法是最简单的辅助调试方法。在需要打印调试信息的地方加入一个打印函数(或串口打印或屏幕打印)。在程序出错时可以打印当前所有寄存器的数据。这样可以根据PC或LR的值得出当前正在运行的函数和上一个运行的函数,进一步通过编译器输出的Listing文件还可以得到当前和上一个函数C源代码中的行号。更进一步可以编写一个PC应用辅助进行错误分析。
 
2.2. 打印调用栈
集成调用栈打印比上一种方式能提供更多的信息,在出错时除了当前寄存器的数值,还可以输出完整的调用堆栈。经过实际验证发现,我司目前使用的keil环境下的c编译器默认没有启用frame_pointer机制。即没有一个寄存器指定栈帧开始的位置,这样就无法通过简单的代码实现调用栈的回溯。解决方法是:修改编译选项,在编译时添加参数“--use_frame_pointer”。这样生成的汇编代码会在寄存器R11中保存frame_pointer,也就可以使用简单的代码实现调用栈的回溯和输出。由于嵌入式设备中的运行代码中并没有存储调试信息,因此种方法输出的调用栈就是地址,需要结合map文件或listing文件将其转化为c函数名和行号。同样也可以编写PC软件辅助调试信息的解析和显示。
 
2.3. 完整栈转储
完整栈转储有比上一种调试方式更高级,使用此种调试方式时应该在设备内部的SPI Flash中开辟出一块固定的存储区域,在程序出错时可以将全部栈数据保存进Flash中。在合适时机(下次开机时或出错的时候直接输出)将保存的栈输出。这样可以结合编译器生成的Listing文件和map文件进行堆栈的分析。由于Listing文件中有每个函数使用栈的大小信息,因此不启用frame_pointer也可以进行调用栈的分析,同时还能还原局部变量的数值。此种方式还有一个巨大的优势,对于程序跑飞的情况,可以从栈底开始正向分析调用栈,这样在堆栈破坏不是太严重的情况下,能够大致找到程序跑飞之前执行的函数,可以很大程度缩小分析跑飞问题时关注函数代码的范围,方便更快找到问题。
 
2.4. 完整内存转储
此种方法是辅助调试的终极大招,由于嵌入式设备的内存普遍比较小,在KB级别。因此可以在出错时将整个内存保存进设备内部的SPI Flash中,在合适时进行输出,在PC端进行分析。分析得到的数据除了上述所有内容,还可以知道所有全局变量的数值。
此种方法除了以上所述,一定还有更多分析使用方法,受限于我的知识范围,当前仅能想到这些分析方法。欢迎其他同学提出更多的内存转储使用方法。
 
3. 进行错误处理的时机
刚才在描述调试方式的时候,仅提到在“程序出错时”进行错误处理。实际使用时是程序出错的时机一般有两个:
各种异常处理函数中。对于非法地址指针访问,对齐问题,权限问题,以及在程序跑飞时一般都会触发硬件异常。因此在异常处理函数中进行错误处理是十分自然的。
对于软件死循环的情形,根据程序架构的不同,检测有多种情形:对于某些不开启抢占的多任务环境,可以利用看门狗机制,单独使用一个线程喂狗,如果有某个线程死锁,会造成喂狗线程得不到调度,因此就可以触发看门口中断,在中断中打印当前线程的调用栈即可发现死锁问题。对于单线程运行的前后台系统,可以在每次大循环的最后进行喂狗,如果狗叫则打印堆栈也可以起到同样的效果。对于开启抢占的多任务环境(比如我司售饭机的情况),暂没有想到什么方法能够进行通用的死循环检测。因此只能自行根据代码逻辑在循环中增加喂狗机制和看门口配合使用上述方法发现死锁。
 
4. 新调试方法的运行原理
上述文字描述了各种辅助调试方法的优缺点和实际,最关键的原理问题并没有介绍,这里我们简单描述一下。
 
4.1. 栈的作用
栈是实现C语言函数调用的基石。对于每一次C语言的函数调用,汇编代码执行的流程基本上是这样的:
1、调用者将调用子函数时需要的参数放入寄存器或压入堆栈(根据参数数量和大小而定);
2、调用者将返回地址放入LR寄存器,然后跳转到子函数处开始执行。
3、子函数在栈中备份用到的寄存器(用于退出前恢复其原内容,包括LR和通用寄存器),并在栈中开辟空间(用于局部变量或返回值)。
4、子函数完成自己的功能,恢复之前寄存器的数值(第三步备份的寄存器)并返回调用者。
因此对于每一级函数调用,C语言编译器都会在栈中生成一个固定的结构。这个结构就是传说中的“栈帧”。
 
4.2. 栈的结构
一图胜千言,如上结构是ARMv5的栈帧结构,对于现在我司常用的ARMv7 M系列而言,结构有点不同,但是还是可以解释如何使用栈来实现函数调用和参数、返回值的传递的。
 
4.3. 关于frame pointer
如上图所示,在函数执行的过程中除了SP固定指示当前的栈顶之外,还有一个FP指针,固定指定栈帧的起始位置。通过FP指针,我们就可以像遍历链表一样回溯整个调用堆栈。
但是对于FP指针的使用,在新的v7系统山是可选的,且默认情况下编译器不适用FP指针,而是根据SP寄存器间接的计算存储在栈中数据的位置。且由于每个函数使用的寄存器数量不同,使用栈的大小不同,因此根据SP查找栈帧起始位置就必须结合汇编代码。因此在不使用FP的情况下,要实现栈的回溯必须依赖对反汇编代码的分析(自行计算每个函数中对栈的使用,然后计算下一层函数的栈帧的偏移),因此就无法在设备端直接进行了。
启用栈帧时针对Cortext-M4处理器,armcc生成的代码:
编译器使用r11保存frame pointer,栈中保存有frame pointer。
;;;209    void GPIO_EnableOpenDrain(GPIO_PortEnum Port, uint32_t Pins)
0002ae  e92d4810          PUSH     {r4,r11,lr}
;;;210    {
0002b2  f10d0b08          ADD      r11,sp,#8
0002b6  4602              MOV      r2,r0
;;;211        PORT_MemMapPtr PortBase;
;;;212        int i;
;;;213
;;;214        PortBase = g_PortBase[Port];
0002b8  4c57              LDR      r4,|L1.1048
0002ba  f8543022          LDR      r3,[r4,r2,LSL #2]
;;;215        for (i = 0; i < 32; ++i){
0002be  2000              MOVS     r0,#0
0002c0  e00a              B        |L1.728
L1.706
;;;216            if (Pins & (0x01 << i)){
0002c2  2401              MOVS     r4,#1
0002c4  4084              LSLS     r4,r4,r0
0002c6  400c              ANDS     r4,r4,r1
0002c8  b12c              CBZ      r4,|L1.726
;;;217                PortBase->PCR[i] |= PORT_PDD_OPEN_DRAIN_ENABLE;
0002ca  f8534020          LDR      r4,[r3,r0,LSL #2]
0002ce  f0440420          ORR      r4,r4,#0x20
0002d2  f8434020          STR      r4,[r3,r0,LSL #2]
L1.726
0002d6  1c40              ADDS     r0,r0,#1              ;215
L1.728
0002d8  2820              CMP      r0,#0x20              ;215
0002da  dbf2              BLT      |L1.706
;;;218            }
;;;219        }
;;;220
;;;221        return;
0002dc  46dd              MOV      sp,r11
0002de  b082              SUB      sp,sp,#8
;;;222    }
0002e0  e8bd8810          POP      {r4,r11,pc}
;;;223
ENDP
不启用栈帧时针对Cortext-M4处理器,armcc生成的代码:
栈中仅有备份的通用寄存器和返回地址,并没有FP。
;;;209    void GPIO_EnableOpenDrain(GPIO_PortEnum Port, uint32_t Pins)
00022e  b510              PUSH     {r4,lr}
;;;210    {
000230  4602              MOV      r2,r0
;;;211        PORT_MemMapPtr PortBase;
;;;212        int i;
;;;213
;;;214        PortBase = g_PortBase[Port];
000232  4c4e              LDR      r4,|L1.876
000234  f8543022          LDR      r3,[r4,r2,LSL #2]
;;;215        for (i = 0; i < 32; ++i){
000238  2000              MOVS     r0,#0
00023a  e00a              B        |L1.594
L1.572
;;;216            if (Pins & (0x01 << i)){
00023c  2401              MOVS     r4,#1
00023e  4084              LSLS     r4,r4,r0
000240  400c              ANDS     r4,r4,r1
000242  b12c              CBZ      r4,|L1.592
;;;217                PortBase->PCR[i] |= PORT_PDD_OPEN_DRAIN_ENABLE;
000244  f8534020          LDR      r4,[r3,r0,LSL #2]
000248  f0440420          ORR      r4,r4,#0x20
00024c  f8434020          STR      r4,[r3,r0,LSL #2]
L1.592
000250  1c40              ADDS     r0,r0,#1              ;215
L1.594
000252  2820              CMP      r0,#0x20              ;215
000254  dbf2              BLT      |L1.572
;;;218            }
;;;219        }
;;;220
;;;221        return;
;;;222    }
000256  bd10              POP      {r4,pc}
;;;223
ENDP
 
5. 在实际项目中的应用情况
当前几种新调试方法中,第一种“出错时打印寄存器信息”已经在现有设备中得到应用。其余调试方法,经过初步的调研是可行的,但是项目进度和实现难度的综合考量,暂没有在实践中投入使用。但如果项目时间允许,我们会将实验上述集中调试方式。
 
Plus,最后补充一句,如上这些调试方式在当今程序的操作系统上(Linux、Windows等)已经悉数实现,但在嵌入式设备中的应用较少。随着嵌入式设备性能的增强,软件复杂度的提升,对先进调试方式的需求也会愈发强烈。
关键字:嵌入式  ARM平台  调试方法 

『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
51单片机中断源的扩展方法