如何使用gdb调试freertos并显示各任务信息
时间:2023年11月01日 人气:...

之前在使用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. 1. 程序进入异常流程

  2. 2. 利用串口等导出完整ram数据和当前寄存器中的值(也即ramdump)

  3. 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 :

热门评论