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等)现已全部完结,但在嵌入式设备中的运用较少。跟着嵌入式设备功能的增强,软件复杂度的提高,对先进调试办法的需求也会益发激烈。