之前在使用linux的时候,无论使用gdb还是kdb,都可以在程序运行时进行debug,可以很方便的看到各种调用栈、变量等,还可以随时切换线程观察某个线程执行的状态。甚至在程序执行异常时,还能产生corefile文件,事后使用gdb就又可以查看异常时的现场,能查看到当时程序执行时各种变量、内存、寄存器等,有这些信息就可以让推导任何问题都有了可能,所以玩意儿可以说是非常的方便开发者调试分析问题。
但是,在嵌入式环境中,一般的芯片跑个rtos,他并没有linux那么完备的功能和机制,甚至受限于厂家的实力和软件生态压根就没啥调试手段,这就导致程序往往在跑出异常时要么根据printf反复复现,要么根据经验直接断定问题,有经验当然是好事,但是大多数时候对于大多数人根本就没啥经验,这就导致调试困难重重。
嵌入式开发中,一般都提供仿真器调试功能,如jtag等等,于是便有了一种调试模式:gdb+openocd。openocd指的是debug server一类型的东西,它运行之后可以监听某个tcp端口,然后我们就可以使用gdb远程连接到此端口上,然后可以happy的使用各种gdb命令了。当然,这里的gdb功能相比linux可能有点缩水,但是大多数的功能都是可以的,已经非常方便了。大多数的IDE所谓的在线调试,其实也就是使用gdb远程模式这套机制,套在了前端ui上了而已,让人使用变得简单,不用手敲各种命令了。但是,我发现有个比较麻烦的事情,那就是使用rtos时,gdb并不能像linux中那样使用直接观察、切换线程等,默认只能观察到当前执行的位置。
因为当前freertos比较流行,我搜了一圈,网上倒是有些什么项目、IDE的插件之类的可以让这种功能实现,但是它是收费的!我在想为啥freertos为啥没人写个咋使用gdb分析调试freertos任务状态的文章呢,于是本文就来探讨一下这种方法,下面分使用调试器实时分析、不使用调试器分析异常现场两类进行。
一、使用调试器实时分析
如,arm contex-m3,调试器可以使用jlink,debugserver可以使用openocd,gdb可以使用arm gcc工具链中的,程序对应的elf文件为xxx.elf。当本地openocd启动后,默认是监听tcp 3333端口,那么可以使用下述命令进行gdb连接:
gdb xxx.elf target remote :3333
此时就会gdb就会远程连上进入调试模式了,可以使用诸如bt、print、b等命令。接下来我们尝试分析如何显示freertos的各个task的运行状态。
先来一张m3核的栈示意图:
那么,只要找到某个task的栈,按顺序取出这些东西填入对应寄存器,那么就可以敲一下bt命令查看到这个task的状态了,对于freertos而言,每个task都有一个对应的TCB,所以找到这个TCB即可等于找到任务栈。以task1_tcb表示一个task的TCB举例(因为某些显示缘故这里我把换行符写反表示,下同不再解释):
set $taskName = task1_tcb->pcTaskName set $pxTopOfStack = task1_tcb->pxTopOfStack printf "task name: %s/r/n", $taskName set $sp = $pxTopOfStack + 16 set $psr = $pxTopOfStack[15] set $pc = $pxTopOfStack[14] set $lr = $pxTopOfStack[13] set $r12 = $pxTopOfStack[12] set $r3 = $pxTopOfStack[11] set $r2 = $pxTopOfStack[10] set $r1 = $pxTopOfStack[9] set $r0 = $pxTopOfStack[8] set $r11 = $pxTopOfStack[7] set $r10 = $pxTopOfStack[6] set $r9 = $pxTopOfStack[5] set $r8 = $pxTopOfStack[4] set $r7 = $pxTopOfStack[3] set $r6 = $pxTopOfStack[2] set $r5 = $pxTopOfStack[1] set $r4 = $pxTopOfStack[0] bt
那么在gdb中就可以显示出这个task的状态了,可以使用bt full等进一步观察到更多信息。当然,当知道栈指针之后,就可以自行推导更多层的调用关系了,按上述只要填入这几组寄存器,就可以使用bt显示了。
我们的目的并不是显示这么一个任务的状态,而是需要看一下全部的,所以我们可以对上述的例子进行拓展。首先我们可以参考freertos中的vTaskList接口怎么查看当前有多少个task,由此我们得知可以通过5个链表:
就绪的任务 :pxReadyTasksLists 推迟执行的任务 :pxDelayedTaskList、pxOverflowDelayedTaskList 已删除待回收资源的任务 :xTasksWaitingTermination 挂起的任务 :xSuspendedTaskList
进行遍历即可找到全部的task。
为了更方便实现我们的目的,这里介绍gdb的两条命令:define和document。define相当于自定义一个自己的命令,document相当于为自定义的命令添加一个help显示的说明,这些东西都算作user-defined类命令。说到这里,再提一下其实大佬们为gdb提供了非常强大的拓展功能,除了像这里提到的这样的命令,甚至还可以使用python进行更多功能的定制,建议感兴趣的朋友可以点此直接阅读gdb的文档学习更多知识(可直接观看第23章Extending GDB)。
所以接下来,我就直接编写一条自己的命令,用来显示freertos的各任务栈:
define dump_task_bt set $taskName = ((TCB_t *)$arg0)->pcTaskName set $pxTopOfStack = ((TCB_t *)$arg0)->pxTopOfStack printf "task name: %s/r/n", $taskName set $sp = $pxTopOfStack + 16 set $psr = $pxTopOfStack[15] set $pc = $pxTopOfStack[14] set $lr = $pxTopOfStack[13] set $r12 = $pxTopOfStack[12] set $r3 = $pxTopOfStack[11] set $r2 = $pxTopOfStack[10] set $r1 = $pxTopOfStack[9] set $r0 = $pxTopOfStack[8] set $r11 = $pxTopOfStack[7] set $r10 = $pxTopOfStack[6] set $r9 = $pxTopOfStack[5] set $r8 = $pxTopOfStack[4] set $r7 = $pxTopOfStack[3] set $r6 = $pxTopOfStack[2] set $r5 = $pxTopOfStack[1] set $r4 = $pxTopOfStack[0] bt end define iter_task_list set $itemNum = ((List_t *)$arg0)->uxNumberOfItems set $index = ((List_t *)$arg0)->pxIndex set $next = (struct xLIST_ITEM *)($index->pxNext) while $itemNum if $next->pvOwner != 0 dump_task_bt $next->pvOwner set $itemNum = $itemNum - 1 end set $next = (struct xLIST_ITEM *)($next->pxNext) printf "/r/n" end end define mybt printf "mybt bgein/r/n" set $spTemp = $sp set $psrTemp = $psr set $pcTemp = $pc set $lrTemp = $lr set $r12Temp = $r12 set $r3Temp = $r3 set $r2Temp = $r2 set $r1Temp = $r1 set $r0Temp = $r0 set $r11Temp = $r11 set $r10Temp = $r10 set $r9Temp = $r9 set $r8Temp = $r8 set $r7Temp = $r7 set $r6Temp = $r6 set $r5Temp = $r5 set $r4Temp = $r4 if pxCurrentTCB != 0 printf "Current task name: %s/r/n", ((TCB_t *)pxCurrentTCB)->pcTaskName end printf "Current stack:/r/n" bt set $uxTopPriority = 0 while $uxTopPriority < (sizeof(pxReadyTasksLists) / sizeof(List_t)) set $item = &pxReadyTasksLists[ $uxTopPriority ] iter_task_list $item set $uxTopPriority = $uxTopPriority + 1 end iter_task_list pxDelayedTaskList iter_task_list pxOverflowDelayedTaskList iter_task_list &xTasksWaitingTermination iter_task_list &xSuspendedTaskList set $sp = $spTemp set $psr = $psrTemp set $pc = $pcTemp set $lr = $lrTemp set $r12 = $r12Temp set $r3 = $r3Temp set $r2 = $r2Temp set $r1 = $r1Temp set $r0 = $r0Temp set $r11 = $r11Temp set $r10 = $r10Temp set $r9 = $r9Temp set $r8 = $r8Temp set $r7 = $r7Temp set $r6 = $r6Temp set $r5 = $r5Temp set $r4 = $r4Temp printf "mybt end/r/n" end document mybt get all freertos task usage: mybt end
这个可以保存为一个文件,如mybt.gdb,可以在gdb使用source mybt.gdb引入,然后就可以敲mybt命令了,也可以敲help mybt查看帮助信息。当然,可以把这个文件改名为.gdbinit,放在当前执行gdb的目录处,启动gdb这个文件就会被自动加载(也即gdb的autoload特性)。
有了这个,其它的诸如单独切换到某一个任务命令也就很好实现了,可以仿照我上面的自行探索了,我这里就不再分析。所以添加更多的调试命令和功能,是非常容易是,只要你想到了,就可以搞。
二、不使用调试器分析异常现场
上面写的是使用调试器在线仿真时候的用法,但是更多的时候,我们往往是在没有接仿真器,程序就遇到异常了。那么能怎么对这个进行分析其实是更具有实用性的。
这里,我觉得可以仿照linux下使用的gdb+corefile机制来做,这个流程大概如下:
1. 程序进入异常流程
2. 利用串口等导出完整ram数据和当前寄存器中的值(也即ramdump)
3. 使用gdb分析ramdump
其实,使用这个ramdump进行分析,世面上有很多的工具已经可以做了,例如trace32,只受限于该cpu有没有被支持。
也可以制作为coredump文件,就可以直接使用gdb xxx.elf coredump进行分析了,例如esp-idf,受限于当前gdb是否支持corefile和需要知道corefile的格式才能制作。
所以,这里我觉得另辟蹊径,完全绕开上面那两种用法进行分析,这样就无需另找解析分析工具,也无须了解corefile格式,让其更具通用性。
那就是,写一个伪debug server,可称之为gdb stub,由gdb stub读取解析ramdump文件,当gdb连接之后,仅需要支持受限的一些命令,让其能读取内存、寄存器读写即可,这样就可以完全复用上面我们编写的自定义命令那样的调试方式了。相比之下也就不需要调试器,只需要了解一些当前cpu常识和GDB Remote Serial Protocol协议就可以实现。
为了节省开发功夫,在github上搜索了一下,运气真不错竟然发现了一个适合的工程mini-gdbstub,此工程只需要适配自己的cpu结构即可使用,非常的nice。
这里我就不再详细分析代码了,以一款国产小众cpu“平头哥ck804”为测试对象,制作了一个适合ck804的gdbstub。当程序跑出异常后,导出设备ramdump环境,运行gdbstub加载ramdump文件,使用gdb成功连接之后,就可以时使用上述的gdb调试方法分析离线的freertos任务栈情况了。
有需要参考的朋友,可点此查看mini-gdbstub-ck804代码。
同时,也提供一个编写的ramdump导出小工具 ramdump_tool.7z :