资讯动态

A33平台GDB+KGDB内核调试环境搭建与实战经验

发布时间:2026/10/14 3:40:43 来源:尧图企业网站定制
上周我手头那块A33方案板又出问题了一个DMA搬运的驱动在压力测试下频繁崩溃printk加了几十行日志每次都是走到一半系统就死掉最后一条输出永远停在某个描述符地址上。折腾了两三天我决定不再靠猜把之前一直没认真搞的GDBKGDB内核调试环境正式搭起来。这篇文章就是那次完整搭建的记录。A33是一颗四核Cortex-A7芯片ARMv7-A架构常见于平板、工控和物联网方案配套BSP很多还是Linux 3.4的老内核。在这个平台上把GDBKGDB环境跑通之后调试内核模块的爽快程度真的不是printk能比的——你可以直接下断点、单步、看变量、改内存整个内核在你面前裸奔。如果你也在用A33或者其他Cortex-A7平台建议把这套流程完整走一遍少走我踩过的坑。1. 为什么非要在A33上搭KGDBprintk排障的尽头1.1 A33平台的内核调试痛点A33这颗芯片性能不算强四核Cortex-A7频率一般跑在1.2GHz左右但它在成本和功耗上有明显优势所以很多量产品都在用。然而随之而来的问题是配套的内核源码往往停留在3.4.xBSP定制量大驱动质量参差不齐你在调试时面对的都是非常具体的问题中断处理函数里数据错乱、DMA描述符被异常改写、某个锁的持有时间过长导致看门狗复位。这些问题的共同特征是printk只能证明代码走到了这里但无法解释为什么状态变成这样。更麻烦的是很多打印本身会改变时序加多了log之后问题反而不复现。我之前排查一个中断丢失问题怀疑是对方驱动关中断时间太长想确认中断号是否正确printk打了一堆结果每次都能跑拿掉log就必现。这种时候就需要一个真正能停下来看现场的调试手段。1.2 KGDB的工作机制与适用边界KGDB是Linux内核自带的一种调试方案它本质上是在内核代码里内置了一个调试stub通过串口实现GDB的远程串行协议。你在主机上运行适用于ARM的GDB客户端执行target remote /dev/ttyUSB0GDB把命令打包成协议数据发过去KGDB stub收到后就让目标板上的内核停止执行然后把寄存器、内存、调用栈等信息回传给你。和其他调试手段对比KGDB的优势是零硬件成本只需要一个串口就能调试整个内核不需要仿真器。但它的边界也很明确无法调试内核启动早期代码因为在KGDB stub自身初始化之前它根本没机会响应你的连接请求另外它调试的是活着的内核断点和单步会暂停整机所有CPU这对实时性敏感的场景是有影响的。还有一点要记住KGDB是软件断点会往目标内存写入BKPT指令所以在某些代码路径上比如关中断的临界区里踩断点可能触发意想不到的时序问题。2. 硬件接线与串口基础先把“眼睛”装好2.1 需要哪些东西怎么接线搭建这套环境硬件上其实很朴素。一块A33开发板、一个USB转TTL串口模块、三根杜邦线就够了。串口模块建议选CP2102或者CH340的A33的调试串口是3.3V TTL电平千万不要用5V的模块硬接轻则乱码重则烧引脚。接线只有三根开发板串口的TXD接模块的RXD开发板的RXD接模块的TXD最后把GND接GND。注意串口是要交叉接的很多新手第一次接反结果终端里什么都看不到。我之前就犯过这个错以为是板子没启动其实只是TXD和RXD接反了。2.2 确认调试串口编号与主机识别A33公板的调试串口编号并不是统一的有的方案是ttyS0有的映射到了ttyS2具体要看原理图和BSP里的设备树或者平台配置。一个简单的确认方法是看U-Boot启动时打印用的是哪个串口设备一般和内核console是同一个。板子启动后在串口终端里执行dmesg | grep tty cat /proc/tty/driver/serial如果U-Boot用的是UART0内核里通常是ttyS0但不同BSP对串口子系统的配置差异很大别想当然必须亲眼确认。主机这头串口模块插上USB后一般会识别成/dev/ttyUSB0没出现的话先查线、查驱动ls /dev/ttyUSB* dmesg | grep -i ch341 # 或者 grep cp210x2.3 主机串口工具与通信参数确认设备节点后先用串口工具把链路打通。我习惯用picocom参数固定为115200、8N1、无流控picocom -b 115200 /dev/ttyUSB0通电后如果能正常看到U-Boot日志和内核启动日志说明串口本身没问题。这一步是整个KGDB环境的地基串口都不通后面一切免谈。另外记一下minicom也行但后面连GDB的时候必须先把这些串口工具退掉否则串口设备被占用GDB打不开。这个细节我在踩坑部分还会再提。3. 内核配置与编译细节KGDB不是勾几个选项那么简单3.1 必开与必关的内核配置项KGDB能不能用首先取决于内核配置。进入A33的内核源码目录执行make ARCHarm menuconfig在Kernel hacking这个菜单下找到KGDB相关选项。以Linux 3.4.x为例下面这些配置必须打开CONFIG_DEBUG_KERNELy CONFIG_DEBUG_INFOy CONFIG_FRAME_POINTERy CONFIG_KALLSYMSy CONFIG_KGDBy CONFIG_KGDB_SERIAL_CONSOLEy CONFIG_MAGIC_SYSRQyCONFIG_DEBUG_INFO是给vmlinux生成DWARF调试信息用的没有它GDB就是睁眼瞎所有函数名、变量名、行号都对不上。CONFIG_FRAME_POINTER保证调用栈回溯是准确的内核调试场景下强烈建议打开。CONFIG_KALLSYMS让内核自带符号表后面调试模块、查看内核symbol都靠它。CONFIG_MAGIC_SYSRQ则是为了用SysRq机制手动触发进入KGDB这个后面细说。同样重要的是有个选项必须关掉CONFIG_DEBUG_RODATAn这个选项默认在某些BSP里是打开的作用是把内核代码段设为只读。听起来很安全但对KGDB来说是致命的——GDB要往代码段写BKPT指令来下断点写不进去就永远打不上断点报Cannot access memory。所以无论如何都要把它关掉。3.2 交叉编译与vmlinux符号文件配置完成后用配套的交叉工具链编译。A33是ARMv7-A一般用arm-linux-gnueabihf-前缀的工具链版本建议和BSP原始构建工具链保持一致这一点我后面会专门说版本不一致会带来符号错位的坑。make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j4编译完会得到两个关键产物arch/arm/boot/zImage和根目录下的vmlinux。前者用于烧录或引导系统后者才是GDB调试用的符号文件。不要用zImage直接喂给GDB它没有完整的调试符号一定要用ELF格式的vmlinux。烧录内核之后主机上的GDB加载的vmlinux必须和板子上实际运行的内核完全对应否则断点地址全是错的表现就是函数名能显示但行为完全不对。3.3 SMP四核对KGDB的影响A33是四核KGDB是支持SMP的。断点命中时内核会通过IPI把其他CPU也拉停下来整个系统进入调试状态。这本来是个好特性但在老内核上也是不少问题的温床比如调试期间softlockup误报、CPU hotplug交互异常等。如果你在调试过程中发现各种奇怪现象第一步可以先在bootargs里加maxcpus1强制单核启动排除SMP的干扰把核心问题定位清楚再放开多核跑。4. U-Boot启动参数与进入调试的两种姿势4.1 bootargs里加kgdboc和kgdbwaitKGDB需要知道用哪个串口、什么波特率来通信这通过内核启动参数kgdboc指定。kgdboc是KGDB Over Console的缩写它指定使用哪一个串口作为调试通道。在U-Boot里这样设置setenv bootargs consolettyS0,115200 root/dev/mmcblk0p1 rw kgdbocttyS0,115200 kgdbwait saveenv boot注意kgdboc填的串口设备名要和你实际确认的调试串口一致。比如你的板子调试串口是ttyS2那console和kgdboc都要写成ttyS2。kgdbwait参数的意思是内核启动到KGDB初始化完成后先停在原地等待主机上的GDB连接连上之后再继续跑。这是最常用也最稳妥的联调方式因为你能从系统第一刻开始调试。4.2 sysrq手动切入日常调试推荐kgdbwait适合从头调试启动流程但如果系统已经稳定跑起来你想等出问题的时候再切入更推荐用SysRq键不需要每次启动都等着被连接。只要内核开了CONFIG_MAGIC_SYSRQ在串口终端里执行echo g /proc/sysrq-trigger这个g就是GDB的缩写内核收到后会在当前状态停下来等待主机GDB连接。这个方式的优点是你可以在系统正常运行时待命一旦发现异常行为比如打印告警、卡在某个状态再手动切入保留了现场。缺点就是如果系统已经彻底死掉连SysRq都处理不了了那就只能重启板子并用kgdbwait去复现问题。4.3 gdb连接前的检查清单每次连不上先别怀疑GDB按清单逐项排查能省非常多时间串口工具是否占用设备退出minicom或picocom否则GDB无法打开/dev/ttyUSB0。波特率是否一致KGDB那边用115200主机GDB打开串口时也要用115200。设备名是否对host侧是/dev/ttyUSB0目标板KGDB用的是ttyS0还是ttyS2。vmlinux是否对应重新编译过内核后有没有重新烧录、GDB加载的是不是最新的vmlinux。串口模块电平是否正确3.3V TTL模块接5V就是乱码。目标板是否已经停下来等待连接kgdbwait模式会有明确的打印SysRq模式内核是运行态GDB连上后再被中断。5. GDB远端连接实操下断点、看现场、临时改内核5.1 建立remote连接目标板停在KGDB等待状态后主机上打开一个新的终端进入vmlinux所在目录启动GDBarm-linux-gnueabihf-gdb vmlinux然后在GDB内部连接串口(gdb) target remote /dev/ttyUSB0 Remote debugging using /dev/ttyUSB0如果连接成功GDB会停在某个地址处往往是在kgdb_breakpoint或kgdb_initial_breakpoint附近你可以马上看到当前函数、行号等信息。到这一步整个KGDB环境就算真正跑通了。5.2 常用命令断点、继续运行、调用栈与寄存器连接之后的第一件事通常是下断点。比如我想在do_fork处停下看系统在什么场景下创建了新进程(gdb) b do_fork Breakpoint 1 at 0xc00e2a40: file kernel/fork.c, line 1892. (gdb) c Continuing.内核继续运行一旦命中do_forkGDB会立即停下并提示命中。这个时候可以看的就多了(gdb) bt # 查看完整调用栈 (gdb) info registers # 查看当前CPU寄存器 (gdb) p parent # 打印变量 (gdb) x/20wx 0xc0101234 # 查看某段内存的十六进制内容关于单步要特别提醒内核代码里尽量少用step逐行跑尤其不要在函数边界附近step。以内核态的行话来说你根本不知道下一步会不会踩到调度器或者中断返回路径单步到一半系统状态就乱了。更稳妥的做法是下断点、continue到感兴趣的位置、用bt看栈、用p看变量必要时用finish跑完当前函数而不是从头到尾单步。5.3 修改内存和变量KGDB的干预能力GDB不仅能看还能改。比如你在中断处理函数里发现某个标志变量被错误清零想验证是不是它导致后续逻辑跳过分支可以直接(gdb) set var flags 0x1 (gdb) c有时候调试死锁你想看看如果暂时绕过一个锁会怎样也能用set var临时改条件。更硬核的是直接改内存比如往某个设备寄存器对应的MMIO地址写值当然这么做要非常小心设备寄存器不是随便碰的。写内核代码段则需要前面说的CONFIG_DEBUG_RODATAn否则GDB会报无法写内存。这里还有一个经验GDB的断点是软件断点会改写目标代码地址的内容KGDB会负责在继续执行前提GDB把原指令写回来。但如果中途GDB异常退出或者断点设置后系统挂了再复位代码段可能残留BKPT指令需要重新烧录或重启恢复。所以联调时改内核代码段之前心里要有数是改写式的别在关键路径上乱设一堆断点。5.4 关于CtrlC中断的用法与局限在gdb客户端里如果目标板上的内核正在运行理论上你可以按CtrlC发一个break信号让KGDB把内核停下来。实测在A33部分BSP上并不会每次都生效因为KGDB处理break的条件依赖串口中断状态和KGDB stub是否正确注册了接收逻辑。所以我平时的习惯是要么提前用kgdbwait进入等待要么在已知的必经函数上设断点很少依赖CtrlC去抢断。如果你真想用它建议先把softlockup相关的看门狗参数关掉不然CPU停下来太久系统的watchdog先跳出来了。6. 联调实测一次驱动中断问题排查给自己看6.1 问题背景与断点选择我那次DMA问题表象是某个网卡驱动的接收中断动不动不触发报文堆积在环形缓冲区里。传统做法是在中断处理函数入口加printk但中断频率太高printk一开就刷屏不开又看不到现场。搭好KGDB后我直接在驱动的中断处理函数上下断点(gdb) b dm9000_rx_interrupt # 假设驱动名实际以符号为准 (gdb) c命中后先看寄存器里的中断状态寄存器确认硬件到底有没有触发这路中断。这一步就直接把CPU没收到中断和收到了但驱动没处理两种情况区分开了。6.2 用bt看上下文用x看环形缓冲区确认硬件中断确实到了之后下一步是看驱动当前的读指针和写指针。用p命令打印关键变量再用x/16wx查看环形缓冲区的当前内容。当时我发现读指针停在某一项上而写指针已经往前绕了一圈明显是上一次处理时中断函数中途返回了。顺着这个线索再用bt看调用栈发现中断处理函数里调用了一个辅助函数那个函数里有个条件分支在特定情况下提前返回导致缓冲区索引没有推进。6.3 临时修改变量的验证手段为了验证这个猜想我在那个分支的判断条件上做手脚直接改掉标志位再继续运行。观察一包流量之后环形缓冲区的读指针开始正常推进说明这就是根因。整个过程不到半个小时而在之前printk的方案下这类问题至少要来回编译重启四五轮。这就是KGDB的实用价值能快速在运行时验证假设而不是反复猜。7. 踩坑实录A33平台上KGDB的五个经典翻车现场7.1 串口被占、电平不对、设备名冲突最蠢也最常见的坑minicom开着就去开GDBGDB打开/dev/ttyUSB0报Device or resource busy。另一个是USB转串口模块是5V的接A33的3.3V串口表现为终端偶尔能打印但乱码严重GDB连上一堆垃圾数据协议解析直接失败。还有一个烦人的问题有些USB转串口模块芯片不稳定插拔几次后系统识别名从ttyUSB0变成ttyUSB1你脚本里写死了ttyUSB0结果永远连不上。7.2 kgdbwait卡住但GDB死活连不上现象是板子停在Waiting for connection from remote gdb但GDB target remote之后一直没反应。先检查波特率是不是两边不一致再检查设备名最后我发现是因为目标板换了串口引脚映射U-Boot用的是ttyS0但内核console却配到了ttyS2kgdbocttyS0对内核来说根本没这个设备可用。解决方法是让console和kgdboc指向同一个ttySx或者在BSP的串口平台数据里把映射理清楚。7.3 SMP四核下softlockup误报四核全开时命中一个断点GDB里随便看了一会儿dmesg里开始报BUG: soft lockup。这不是你的调试思路错了而是KGDB把所有CPU停掉之后hrtimer和watchdog线程也停了系统的时间概念失真softlockup机制误判。解决方法是启动参数加sysctl kernel.softlockup_panic0或者在bootargs里把看门狗相关的panic关掉。当然如果你只是临时调试用maxcpus1限制单核更干净。7.4 断点写不进内存GDB下断点时提示Cannot insert breakpoint或者Cannot write memory先别怀疑GDB坏了基本就是CONFIG_DEBUG_RODATAy在作怪。重新配置内核把这一项关掉重新编译烧录问题直接消失。如果你不想整个重编也可以尝试在bootargs里加rodataoff但这在老内核上不一定支持最稳妥的还是改配置。7.5 工具链版本不一致导致符号错位我一开始偷懒用系统自带的GDB 8.x加载官方旧版工具链编译的vmlinux连接是成功了但几处函数地址明显不对bt出来的调用栈里混着莫名其妙的函数名。这是因为旧内核的DWARF版本和过新的GDB解析器存在兼容性问题加上编译器版本差异导致符号表微妙偏移。后面我老老实实用BSP配套的工具链版本原样编译、原样调试一切就正常了。这里也建议你把GDB版本固定下来不要频繁升级。下表总结这五个坑的根因与解法现象根因处理方式GDB打不开串口串口被终端工具占用先退出minicom/picocom连接后乱码电平不匹配或波特率错误换TTL模块统一115200kgdbwait后连不上kgdboc指定了错误串口让console与kgdboc同设备断点写不了内存CONFIG_DEBUG_RODATA开启重配内核关闭该选项bt输出符号混乱GDB/工具链版本不一致使用BSP配套工具链8. 延伸技巧KDB、模块符号与gdb脚本化调试8.1 图形化不方便时的KDB备胎方案有时候你人在现场只有一根串线没有图形终端也懒得在笔记本上开GDBKDB可以救急。KDB是KGDB的键盘调试模式其实它就是一个跑在串口终端上的内核调试器直接命令交互。内核配置里打开CONFIG_KGDB_KDBy重编后触发方式和KGDB一样。进入后常用的命令是bp do_fork # 设断点 go # 继续运行 rd # 查看寄存器 md 0xc0008000 # 查看内存 bt # 调用栈KDB没有GDB那么方便不支持自动变量补全、脚本也弱但在纯终端环境或者网络环境很恶劣的情况下它是一个可靠的保底方案。8.2 加载内核模块的符号KGDB调试的对象不只是编译进内核的代码也可能是你正在开发的驱动模块。但模块是编译后动态加载的gdb里默认不知道模块符号怎么办先让模块在板子上加载起来然后在板子上找到加载地址cat /sys/module/my_driver/sections/.text比如输出0xc0a00000回到GDB里执行(gdb) add-symbol-file my_driver.ko 0xc0a00000之后就能对着这个模块的函数下断点了。注意不同模块的.text地址每次加载都可能变重启后要重新获取。这个功能在调试自己开发的驱动时极其好用不用把驱动编进内核也能调试。8.3 把重复操作写成gdb脚本调试内核时总有些固定套路比如每次命中某个驱动函数后你都要打印寄存器、查看缓冲区、查看描述符。手敲太多容易漏也容易错。GDB支持define命令把这些动作固化起来(gdb) define dma_check printf dma_base: %08x\n, dma_base x/16wx ring_buf info registers r0 r1 r2 end之后命中断点直接敲一句dma_check就能拿到全套信息。我再配合一个gdb启动脚本每次进入调试都自动执行这些定义把重复劳动降到最低。调试效率的提升非常明显内核调试本来就很烧脑能把机械操作自动化就尽量自动化。一点个人心得在A33上把GDBKGDB环境跑通之后我最大的感受是这套工具的价值不在于它能调试多高深的内核特性而在于它把猜测变成了验证。老BSP内核的问题往往不是你不会写驱动而是你根本看不见内核在特定时刻的状态。一个断点、一个bt、一行p经常比二十行printk管用得多。最后分享一个小习惯我在板子的系统盘里放了一份常用内核符号表导出文件就是cat /proc/kallsyms的完整记录配合模块调试时查地址非常方便。哪怕没有gdb环境这份文件也是定位问题的好帮手。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑