上一篇笔者讲完了内存管理算法的完整实现,不过差点忘了,直接上这一部分是不是有点不友好,要知道笔者当初写内存算法可是调试得死去活来,奇奇怪怪的问题不断出现。
就比如笔者当初写了一个内存池算法,结果奇葩的事情发生了。如果内存数组的命名不恰当(例如mempool),而且内存的大小定义为800,如果你访问内存后八个字节,程序就会报segmentfault,但是改成mempool1,程序又能跑了,而且这并不能稳定复现,换一个编译器bug就消失了。这当时直接把笔者整懵逼了,以前看到有人说程序删除掉注释就能跑了,笔者内心是一点也不相信,但是自从自己经历了奇怪的bug,现在写程序命名都要小心翼翼了。
为了让读者面对bug时不至于茫然不知所措,笔者觉得有必要介绍一些调试方法。(不仅仅是gdb,也包括keil,由于keil偏图形化,不需要过多介绍,所以笔者会重点讲gdb命令)
gdb调试
本小节笔者会介绍各种gdb的小命令,例如b, s, p, n, list, x/10x,disassemble,bt 等等命令。也介绍栈回溯等技术。
gdb是一个非常强大的工具,但是笔者发现网上没有什么教程讲使用gdb调试嵌入式程序,而且发现很多人对调试技术并不重视,所以笔者觉得有必要讲一讲gdb的使用。
让我们在实战中学习gdb的使用!
笔者修改了内存管理算法,使它成为了一个错误的工程,现在该如何调试它呢?
在HardFault_Handler函数内部打上断点
HardFault_Handler函数是一个非常重要的函数,当我们的单片机出现各种奇怪的问题时,此时单片机就会触发这个中断,然后我们会发现程序一直在这里转圈圈,这其实是官方为了帮助我们debug而设置的一个功能:(断点最好打在while(1)上面)
b命令
b命令的作用就是打上断点,在clion下,读者可以手动点击87行打上断点,也可以使用b 87命令打上断点
添加图片注释,不超过 140 字(可选)
r命令
r命令是运行命令,不过在clion的gdb里面,点击右边那个小虫子,程序就自动运行到断点处了,如果是在linux环境下使用gdb是需要使用r命令运行程序的,但是在clion下是不用的。
添加图片注释,不超过 140 字(可选)
调试错误的内存管理算法
当我们运行错误的内存管理算法的程序时,会发现程序跑进了HardFault_Handler,那么,是哪里导致了问题呢?
寻找案发现场
栈回溯
“ ⽬前的主流CPU架构都是⽤栈来进⾏函数调⽤的,栈上记录了函 数的返回地址,因此通过递归式寻找放在栈上的函数返回地址,便可 以追溯出当前线程的函数调⽤序列,这便是栈回溯(stack backtrace) 的基本原理。通过栈回溯产⽣的函数调⽤信息称为call stack(函数调 ⽤栈)。 栈回溯是记录和探索程序执⾏踪迹的极佳⽅法,使⽤这种⽅法, 可以快速了解程序的运⾏轨迹,看其“从哪⾥来,向哪⾥去”。”
以上解释来自张银奎老师的《软件调试》。
笔者简单解释,就是看进入HardFault_Handler之前是哪些函数在不断嵌套调用。
bt命令
添加图片注释,不超过 140 字(可选)
(话说笔者是不是应该把Sparrow程序全部删除比较好,不过考虑到实际情况,还是先保留,因为笔者将会用这些多余的程序引出调试的原则)
我们现在发现了,在进入HardFault_Handler之前,程序一路嵌套调用,最后在heap_init处发生了错误。
现在该引出调试的两大原则了:问题简化原则和问题追踪原则。
问题简化原则
我们现在已经知道了,是heap_init导致了错误,也就是说,很有可能是我们的内存管理算法出现了问题。为了验证我们的猜想,我们需要单独debug内存管理算法部分:
添加图片注释,不超过 140 字(可选)
在main函数中,笔者删除了sparrow的其他程序,单独debug内存管理算法,让我们看看是不是它引起了错误:
添加图片注释,不超过 140 字(可选)
问题依旧存在!这说明确实是内存管理算法有问题。
案发现场的确认
为了搞清楚是那一行命令导致的问题,我们需要查看跳转到HardFault_Handler前程序在执行那一行程序,为此,我们可以借助堆栈指针。
arm cm3架构的单片机采用的是双堆栈,也就是有两个stack指针(psp和msp),分别在不同场合使用。
我们需要查看寄存器R14(LR)的值。如果R14(LR) = 0xFFFFFFE9,继续查看MSP(主堆栈指针)的值,如果R14(LR) = 0xFFFFFFFD,继续查看PSP(进程栈指针)的值。
info registers 命令
使用该命令,可以帮助我们快速查看arm所有寄存器的值:
添加图片注释,不超过 140 字(可选)
x/10x $msp命令
笔者的lr是0xFFFFFFE9,所以需要查看msp后面的的内存。
x/10x $msp命令会从$msp寄存器的值开始,检查并显示接下来10个内存单元的内容,每个单元的值以十六进制格式显示:
添加图片注释,不超过 140 字(可选)
看着这些十六进制的数字不要慌,想一想我们的程序地址一般是从哪里开头的?一般不是在0x08后面吗?所以我们只需要查看0x080开头的地址的内容即可。
list命令
0x08000281处的程序:
我们已经知道了错误是在heap_init内存发生的,因此我们还需要进一步查看。
添加图片注释,不超过 140 字(可选)
0x08000218处:
添加图片注释,不超过 140 字(可选)
disassemble 命令
如果读者懂汇编语言,也可以查看汇编程序:
添加图片注释,不超过 140 字(可选)
观察程序执行发现具体问题
问题追踪原则
s和n命令
到现在,我们找到了案发现场,但是如果案发现场也仅仅是受害者呢?比如一个函数的错误执行导致修改了某个指针,案发现场的程序对这个指针进行了解引用,这个时候该怎么办呢?
此时我们需要重新梳理程序的执行,遵守问题追踪原则,使用s和n命令一步步执行程序,找出问题。
s命令:
一行行执行程序,遇到函数会进入函数内部继续一行行执行命令。
n命令:
一行行执行程序,但是遇到函数不会进入函数内部,而是执行完这一行后转到下一行。
使用s命令,我们来到了malloc内部:
添加图片注释,不超过 140 字(可选)
继续使用s命令,我们进入到了heap_init函数内部:
其实到这里读者应该都看得出了,笔者并没有给start_heap传递allheap这片内存的地址,导致程序出错了。
添加图片注释,不超过 140 字(可选)
修改了之后程序就能继续跑了:
添加图片注释,不超过 140 字(可选)
keil调试
使用keil调试是一样的,keil只要点击这个放大镜即可进入调试。
keil的右边会自动显示各个寄存器的值:(笔者很久没用keil了,没找到有bug的工程。随便打开的一个工程,凑合着看吧(><))
添加图片注释,不超过 140 字(可选)
调试方法是一样的:我们需要查看寄存器R14(LR)的值。如果R14(LR) = 0xFFFFFFE9,继续查看MSP(主堆栈指针)的值,如果R14(LR) = 0xFFFFFFFD,继续查看PSP(进程栈指针)的值。
假设此时LR是0xFFFFFFE9,我们把0x200004F4输入右下角的Memory1这里:
添加图片注释,不超过 140 字(可选)
现在我们已经获得了出错程序的地址,我们打开这个窗口,右键然后点击Show Disassembly at Address:
添加图片注释,不超过 140 字(可选)
输入地址:
添加图片注释,不超过 140 字(可选)
现在我们就可以找到出错的程序现场!
总结
笔者介绍了如何使用gdb进行嵌入式调试,并且使用错误的内存管理算法作为案例,带领读者一点点找出问题所在,希望读者能够学有所得!
本章的程序地址:skaiui2/SKRTOS_sparrow at memory (github.com)
读者可以下载后跟着文章进行调试。地址中的heapmem.c和heapmem.h是笔者进行了简单的修改后的内存管理算法。它可以作为一个库被广泛使用在嵌入式单片机程序中,并且比通用的c语言malloc效率更高,执行时间也相对固定,能够提高程序的性能。