skaiuijing

上一篇笔者讲完了内存管理算法的完整实现,不过差点忘了,直接上这一部分是不是有点不友好,要知道笔者当初写内存算法可是调试得死去活来,奇奇怪怪的问题不断出现。

就比如笔者当初写了一个内存池算法,结果奇葩的事情发生了。如果内存数组的命名不恰当(例如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命令打上断点

image-20241015173729155image-20241015173729155

r命令

r命令是运行命令,不过在clion的gdb里面,点击右边那个小虫子,程序就自动运行到断点处了,如果是在linux环境下使用gdb是需要使用r命令运行程序的,但是在clion下是不用的。

image-20241015182042756image-20241015182042756

调试错误的内存管理算法

当我们运行错误的内存管理算法的程序时,会发现程序跑进了HardFault_Handler,那么,是哪里导致了问题呢?

寻找案发现场

栈回溯

“ ⽬前的主流CPU架构都是⽤栈来进⾏函数调⽤的,栈上记录了函 数的返回地址,因此通过递归式寻找放在栈上的函数返回地址,便可 以追溯出当前线程的函数调⽤序列,这便是栈回溯(stack backtrace) 的基本原理。通过栈回溯产⽣的函数调⽤信息称为call stack(函数调 ⽤栈)。 栈回溯是记录和探索程序执⾏踪迹的极佳⽅法,使⽤这种⽅法, 可以快速了解程序的运⾏轨迹,看其“从哪⾥来,向哪⾥去”。”

以上解释来自张银奎老师的《软件调试》。

笔者简单解释,就是看进入HardFault_Handler之前是哪些函数在不断嵌套调用。

bt命令

image-20241015174035948image-20241015174035948

(话说笔者是不是应该把Sparrow程序全部删除比较好,不过考虑到实际情况,还是先保留,因为笔者将会用这些多余的程序引出调试的原则)

我们现在发现了,在进入HardFault_Handler之前,程序一路嵌套调用,最后在heap_init处发生了错误。

现在该引出调试的两大原则了:问题简化原则和问题追踪原则。

问题简化原则

我们现在已经知道了,是heap_init导致了错误,也就是说,很有可能是我们的内存管理算法出现了问题。为了验证我们的猜想,我们需要单独debug内存管理算法部分:

image-20241015175305507image-20241015175305507

在main函数中,笔者删除了sparrow的其他程序,单独debug内存管理算法,让我们看看是不是它引起了错误:

image-20241015175325096image-20241015175325096

问题依旧存在!这说明确实是内存管理算法有问题。

案发现场的确认

为了搞清楚是那一行命令导致的问题,我们需要查看跳转到HardFault_Handler前程序在执行那一行程序,为此,我们可以借助堆栈指针。

arm cm3架构的单片机采用的是双堆栈,也就是有两个stack指针(psp和msp),分别在不同场合使用。

我们需要查看寄存器R14(LR)的值。如果R14(LR) = 0xFFFFFFE9,继续查看MSP(主堆栈指针)的值,如果R14(LR) = 0xFFFFFFFD,继续查看PSP(进程栈指针)的值。

info registers 命令

使用该命令,可以帮助我们快速查看arm所有寄存器的值:

image-20241015175954345image-20241015175954345

x/10x $msp命令

笔者的lr是0xFFFFFFE9,所以需要查看msp后面的的内存。

x/10x $msp命令会从$msp寄存器的值开始,检查并显示接下来10个内存单元的内容,每个单元的值以十六进制格式显示:

image-20241015180311110image-20241015180311110

看着这些十六进制的数字不要慌,想一想我们的程序地址一般是从哪里开头的?一般不是在0x08后面吗?所以我们只需要查看0x080开头的地址的内容即可。

list命令

0x08000281处的程序:

我们已经知道了错误是在heap_init内存发生的,因此我们还需要进一步查看。

image-20241015180548669image-20241015180548669

0x08000218处:

image-20241015180710571image-20241015180710571

disassemble 命令

如果读者懂汇编语言,也可以查看汇编程序:

image-20241015180840733image-20241015180840733

观察程序执行发现具体问题

问题追踪原则

s和n命令

到现在,我们找到了案发现场,但是如果案发现场也仅仅是受害者呢?比如一个函数的错误执行导致修改了某个指针,案发现场的程序对这个指针进行了解引用,这个时候该怎么办呢?

此时我们需要重新梳理程序的执行,遵守问题追踪原则,使用s和n命令一步步执行程序,找出问题。

s命令:

一行行执行程序,遇到函数会进入函数内部继续一行行执行命令。

n命令:

一行行执行程序,但是遇到函数不会进入函数内部,而是执行完这一行后转到下一行。

使用s命令,我们来到了malloc内部:

image-20241015182303612image-20241015182303612

继续使用s命令,我们进入到了heap_init函数内部:

其实到这里读者应该都看得出了,笔者并没有给start_heap传递allheap这片内存的地址,导致程序出错了。

image-20241015182510219image-20241015182510219

p命令

p,也就是print,该命令可以帮助我们查看变量的值,我们先查看start_heap的值,看看它是不是有意义的地址:

imgimg

显然,它并不是,让我们再看看内存块的起始地址:

imgimg

可以看出,确实是start_heap没有赋给它内存块的地址导致出错的。

修改了之后程序就能继续跑了:

image-20241015182649393image-20241015182649393

keil调试

使用keil调试是一样的,keil只要点击这个放大镜即可进入调试。

keil的右边会自动显示各个寄存器的值:(笔者很久没用keil了,没找到有bug的工程。随便打开的一个工程,凑合着看吧(><))

image-20241015184149837image-20241015184149837

调试方法是一样的:我们需要查看寄存器R14(LR)的值。如果R14(LR) = 0xFFFFFFE9,继续查看MSP(主堆栈指针)的值,如果R14(LR) = 0xFFFFFFFD,继续查看PSP(进程栈指针)的值。

假设此时LR是0xFFFFFFE9,我们把0x200004F4输入右下角的Memory1这里:

image-20241015184303744image-20241015184303744

现在我们已经获得了出错程序的地址,我们打开这个窗口,右键然后点击Show Disassembly at Address:

image-20241015184652333image-20241015184652333

输入地址:

image-20241015184942357image-20241015184942357

现在我们就可以找到出错的程序现场!

总结

笔者介绍了如何使用gdb进行嵌入式调试,并且使用错误的内存管理算法作为案例,带领读者一点点找出问题所在,希望读者能够学有所得!

本章的程序地址:skaiui2/SKRTOS_sparrow at memory (github.com)

读者可以下载后跟着文章进行调试。其中heapmem.c和heapmem.h是笔者进行了简单的修改后的内存管理算法。它可以作为一个库被广泛使用在嵌入式单片机程序中,并且比通用的c语言malloc效率更高,执行时间也相对固定,能够提高程序的性能。