做A33平台的内核开发,最痛苦的事情就是出了问题只能用printk瞎猜。之前我在一个驱动里排查偶发死锁,打印点加了几十个,反复编译烧录,折腾了两三天才勉强定位到原因。后来把GDB+KGDB这套环境搭好,直接在源码级别下断点、看调用栈、查全局变量,几小时的问题几分钟就能摸清。这篇文章就把我搭建这套内核调试环境的完整过程写透,从方案选型、内核配置、板端启动到主机GDB接入,再到我踩过的一堆坑,全部记录下来。适合正在A33或其他Cortex-A7平台上做Linux驱动、内核裁剪、启动优化和崩溃分析的朋友参考。
1. 方案选型:为什么要折腾GDB+KGDB
1.1 KGDB的内核调试原理
KGDB是Linux内核自带的一套远程调试机制,可以把它理解为内核里内置了一个“调试代理”。目标机上运行的内核在编译时开启了KGDB支持,主机端用GDB通过串口或者网线与目标机的这个代理通信。平时系统正常运行,KGDB不参与任何逻辑;当GDB发出断点指令或者目标机触发异常时,内核会暂停执行,把寄存器现场、内存数据、调用栈等通过串口回传给主机GDB。
打个比方,内核就像一个不停运转的车间,KGDB是墙上的一扇观察窗。平时窗户关着,车间里的人照常干活;调试时你趴在窗口往里看,不仅能看清楚机器当前的运转状态,还能伸手翻看桌面上的图纸和参数。有了这扇窗,排查驱动错误、分析死锁、追踪内存被谁改写这些问题,就不再是盲人摸象。
这套方案的核心价值在于:不需要额外的高速仿真器硬件,也不需要把代码下载到开发板上单步跑,而是在目标机真实运行环境下直接观察和干预。对跑Linux的嵌入式平台来说,这正好解决了“问题只出现在真实运行时”的痛处。
1.2 与JTAG相比,KGDB的取舍
A33是四核Cortex-A7处理器,常见的调试方案有两类:一类是硬件JTAG调试器,另一类就是软件KGDB。JTAG方案需要专门的调试探针,价格从几千到几万不等,还要在板卡上引测试点,接线复杂。更麻烦的是,每一块需要调试的板子都得单独接好探针,产线上的问题跟进效率很低。
KGDB只需要一根串口线就把运行中的内核冻结起来,成本几乎为零,而且对驱动开发和异常分析场景来说完全够用。当然它也有硬伤:KGDB依赖中断机制工作,所以在中断上下文里下断点很容易直接把系统打死;内核启动极早期,KGDB还没初始化完成,也没法介入;如果目标是调试U-Boot或者内核第一条指令前的问题,KGDB同样无能为力,这些场景只能上JTAG。
我的选型结论很明确:日常90%的内核调试场景——驱动逻辑、死锁分析、内存破坏排查、系统调用流程追踪——KGDB都够用,而且上手快、成本低。只有在需要调试中断上下文、启动早期代码或者硬件异常时,才需要动用JTAG。这篇文章后面所有内容,都是围绕KGDB方案展开的。
提示:如果你要调试的是U-Boot阶段或者内核入口函数最开始的部分,KGDB帮不了你,不用浪费时间往下看,直接考虑硬件调试器。
2. 搭建前的准备:硬件连接与主机环境
2.1 A33平台硬件连接要点
搭建这套环境之前,先把硬件准备齐。你需要一块A33核心板或者开发板,一个USB转串口模块,一张SD卡或者网线用于加载内核镜像,以及一台x86的Linux主机。A33的调试串口一般引出在UART0的测试点或排针上,多数板卡电平是3.3V TTL,这点务必确认。
接线时只接TXD、RXD、GND三根线,不要接VCC,更不要把3.3V电平直接连到RS232电平的DB9接口上,那样大概率会烧掉串口芯片。主机TX接板子RX,主机RX接板子TX,交叉连接。市面上常见的FT232和CH340模块都能用,我习惯用FT232,稳定性更好一些。
强调一个容易被忽视的细节:串口模块的电平跳线。有些模块默认输出5V TTL,接到3.3V的A33串口上,短期内能通但长期使用有风险,而且偶尔会出现莫名奇妙的乱码。最好把跳线调到3.3V档位再上电。
2.2 主机侧工具链和串口工具
主机环境我用的Ubuntu 20.04 LTS,64位。需要安装的软件包如下:
sudo apt install build-essential libncurses-dev flex bison device-tree-compiler u-boot-tools minicom picocom sudo apt install gcc-arm-linux-gnueabihf gdb-multiarch这里有一个经验之谈:如果开发板BSP里带了交叉编译工具链,优先用BSP自带的,因为工具链版本和内核版本往往经过配套验证。用系统源里的arm-linux-gnueabihf-gcc编译多数老内核没有问题,但遇到编译器优化引发的内核问题,排查起来会多一层干扰。gdb方面,gdb-multiarch是单二进制支持多架构的版本,也可以用工具链自带的arm gdb,两者功能上等价。A33是ARMv7架构,后面连接时会用到set architecture arm指令。
串口工具方面,minicom和picocom选一个熟悉即可。我平时用minicom多一点,因为它的会话配置可以保存。实际使用中注意,minicom会独占串口设备,后面GDB要接管同一路串口时,务必先把minicom完全退出。
2.3 用minicom先验证串口通信
开始配置内核之前,先把串口链路验证通。执行:
minicom -D /dev/ttyUSB0 -b 115200 -8 -N给板子上电,如果串口能正常打印U-Boot启动日志,说明链路没问题。如果没有输出,按顺序排查:先看权限,执行sudo usermod -aG dialout $USER然后重新登录;再看设备节点,ls /dev/ttyUSB*确认识别为ttyUSB0还是ttyACM0;最后检查接线交叉是否正确,TXD/RXD是否接反。
这一步千万别跳过。串口没有打通就去做内核配置,后面出了问题根本分不清是配置问题还是硬件问题。我见过不少同事在串口没通的情况下反复改内核参数,白白浪费一整天。
3. 内核开启KGDB:配置与编译细节
3.1 需要开启的KGDB相关选项
进入内核源码目录,先加载平台默认配置:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- 平台默认配置名_defconfig然后打开图形化配置界面:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig重点确认以下几个选项是否开启,它们决定了后面能不能连得上、断点能不能生效、源码能不能对应上:
| 内核选项 | 配置路径 | 作用 |
|---|---|---|
| CONFIG_KGDB=y | Kernel hacking -> KGDB: kernel debugger | 开启KGDB内核调试框架 |
| CONFIG_KGDB_SERIAL_CONSOLE=y | 同上 | 允许通过串口作为KGDB传输通道 |
| CONFIG_DEBUG_INFO=y | Kernel hacking -> Compile-time checks and compiler options -> Compile the kernel with debug info | 生成完整的调试符号 |
| CONFIG_MAGIC_SYSRQ=y | Kernel hacking -> Magic SysRq key | 运行时通过sysrq触发调试中断 |
| CONFIG_GDB_SCRIPTS=y | Kernel hacking -> Provide GDB scripts for kernel debugging | 提供lx-symbols等内核GDB辅助脚本 |
CONFIG_DEBUG_INFO是整个调试环境的重中之重。没有调试符号,GDB连上之后只能看到汇编指令,无法关联到源码行和变量,调试体验和写汇编没啥区别。我之前在某个BSP里漏了这个选项,连接倒是正常,但list命令什么都看不到,排查了很久才发现是这里没勾。CONFIG_GDB_SCRIPTS也建议打开,后面调试内核模块时会用到lx-symbols脚本自动装载模块符号,非常方便。
3.2 menuconfig实际配置步骤
在menuconfig界面里,按下面路径逐级展开:
Kernel hacking ---> [*] KGDB: kernel debugger [*] KGDB: use kgdb format for console [*] Compile-time checks and compiler options ---> [*] Compile the kernel with debug info [*] Magic SysRq key [*] Provide GDB scripts for kernel debugging有几个事项需要单独说明。开启KGDB后,Kernel debugging菜单下会多出几个子选项,其中“KGDB: use kgdb format for console”对应的就是CONFIG_KGDB_SERIAL_CONSOLE,它让KGDB可以通过console所在的串口收发数据,这样调试串口不用额外分配,直接用现有调试串口即可。
另外,如果希望断点时有较好的源码级栈回溯,建议一并打开CONFIG_FRAME_POINTER。这个选项在部分ARM平台上会让GDB的bt(backtrace)命令输出更完整、更准确。代价是函数调用时会多几条压栈指令,内核体积和性能略有损失,但对调试用途来说完全值得。
sysrq的默认开关可能在启动参数里被关掉,配置完内核后,运行时可以用下面的命令临时开启:
echo 1 > /proc/sys/kernel/sysrq如果希望每次启动都默认开启,可以在内核配置里把CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE设为1,或者在启动参数里加上sysrq_always_enable=1。
3.3 编译产物检查与vmlinux符号确认
配置保存后开始编译:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage dtbs -j4编译完成后,除了最终的zImage和设备树dtb,还要特别关注内核源码根目录下的vmlinux文件。vmlinux是带完整符号表的未压缩内核镜像,GDB调试时加载的就是它;zImage只是用于实际启动的压缩镜像,调试时用不上。
确认符号表是否完整:
arm-linux-gnueabihf-nm vmlinux | grep ' T start_kernel'如果能看到start_kernel符号,说明CONFIG_DEBUG_INFO生效,GDB可以拿到源码级信息。如果查不到,回头检查CONFIG_DEBUG_INFO是否真的选中,或者编译过程是否因为某些错误跳过了调试信息生成。这一步一定要确认,否则后面GDB连上后连函数名都看不到,调试就无从谈起。
设备树文件也要一起编译并烧录。A33平台的dts文件在arch/arm/boot/dts/目录下,编译生成的dtb要和zImage配套使用。我踩过的一个坑是zImage和dtb版本不一致,导致串口能打印但网卡和MMC全部初始化失败,看起来像是驱动问题,折腾半天才发现是设备树不匹配。
4. 板端启动参数与KGDB等待连接
4.1 kgdboc启动参数解析
KGDB通过串口通信,这个能力由内核启动参数kgdboc描述。参数的格式是:
kgdboc=ttyS0,115200这里ttyS0对应A33的UART0,115200是波特率。这个参数告诉内核:KGDB的调试通道使用哪个串口、以什么速率通信。
一个合理的完整启动参数示例:
console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait kgdboc=ttyS0,115200console参数指定内核控制台,kgdboc参数指定调试串口,这里让它们共用ttyS0。进入KGDB调试状态后,console的输出会暂时冻结,等你continue恢复执行后自动恢复,设计上不会有冲突。
如果你的板子引出了第二路串口,更推荐的方式是让console和KGDB分开:
console=ttyS0,115200 kgdboc=ttyS1,115200 root=/dev/mmcblk0p2 rootwait这样调试内核时,目标机还能在console串口上持续输出日志,便于对照观察。不过前提是板子确实把第二路串口引出来了,很多A33核心板只引了一路调试串口,这种情况下共用ttyS0是唯一选择。
4.2 使用kgdbwait调试内核早期启动
如果希望在系统启动早期就停下来等GDB连接,在启动参数里追加kgdbwait:
console=ttyS0,115200 root=/dev/mmcblk0p2 rootwait kgdboc=ttyS0,115200 kgdbwait加上kgdbwait后,系统会在KGDB初始化完成后立刻停止,等待主机GDB连接到串口。串口输出中会出现类似这样的提示:
KGDB: Waiting for connection from remote gdb...看到这行提示,说明目标机已经完全停在等待状态。这时主机GDB就可以接管串口,接下来所有的内核执行都由你控制。
使用kgdbwait有一个风险:如果主机GDB因为某种原因连不上,系统会一直停在等待状态,看起来就像启动卡死。所以只在第一次调试早期启动问题时使用,日常调试建议去掉kgdbwait,等系统正常启动后再手动触发。
4.3 运行时通过sysrq触发调试
不带kgdbwait时,系统会正常启动到用户态。需要进入调试状态时,在目标机串口上执行:
echo g > /proc/sysrq-trigger这条命令会让内核立即停止,进入KGDB等待状态。此时主机GDB连接即可。这个方式的好处是系统先完成所有初始化,外设、文件系统、网络都就绪后再进入调试,环境更接近真实运行状态,排查驱动问题更有参考价值。
sysrq-g的触发依赖CONFIG_MAGIC_SYSRQ编译进内核,并且运行时/proc/sys/kernel/sysrq的值不为0。我习惯把启动参数写成sysrq_always_enable=1,省去每次手动echo的麻烦。注意,执行echo g之后目标机就停了,minicom会暂时没有输出,这是正常的,不是死机。
5. 主机GDB实操:连接、断点与常用技巧
5.1 启动GDB并建立串口连接
主机端启动GDB时,加载的是vmlinux而不是zImage:
gdb-multiarch vmlinux进入GDB后,按下面的顺序执行三条命令:
set architecture arm set serial baud 115200 target remote /dev/ttyUSB0第一条指定架构为ARM,第二条设置串口波特率,第三条建立与目标机的串口连接。连接成功后,GDB会提示“Remote debugging using /dev/ttyUSB0”,并停在当前内核执行位置。
这里有一个典型的报错:如果出现“Remote 'g' packet reply is too long”,说明架构设置不对,确认set architecture arm已经执行。还有一种情况是vmlinux与目标机运行的内核不一致,比如编译时间戳、配置项不同,GDB连接后看到的地址和实际运行地址对不上,断点自然无法命中。这种情况必须重新编译匹配的内核。
另外,确保此刻没有任何其他程序占用/dev/ttyUSB0,特别是minicom。GDB对串口是独占访问,一旦被占就报“can't open /dev/ttyUSB0”。
5.2 断点设置的两种方式与适用场景
KGDB支持软件断点和硬件断点两种方式。软件断点对应GDB的break命令,原理是把目标地址的指令替换为陷阱指令,触发异常后进入KGDB。硬件断点对应hbreak命令,利用处理器自带的调试寄存器实现,不修改内存指令。
在内核调试中,我更推荐优先使用硬件断点。原因有两个:一是软件断点会修改内核代码段,而代码段经常是只读映射,写保护会干扰断点设置;二是内核里自旋锁、信号量等并发场景非常多,软件断点替换指令的瞬间存在极小的时间窗口,可能引发难以复现的奇怪问题。Cortex-A7内核自带6个硬件断点寄存器,日常调试完全够用。
实际操作示例:
hbreak do_fork continue当系统执行到do_fork时自动停下,GDB显示命中断点。此时可以执行bt查看调用栈,info registers查看寄存器现场,p/variable查看变量值,x/32wx加地址查看指定内存。
5.3 驱动模块调试与符号加载
调试内核模块是KGDB最实用的场景。比如你写了一个驱动模块,加载后系统崩溃,想定位具体是哪个函数出了问题。操作流程是:
先在目标机上正常insmod加载模块,然后到主机GDB里执行lx-symbols:
lx-symbols这个脚本会自动扫描目标机的/proc/modules,解析已加载模块的符号表到GDB会话中。执行成功后,GDB会输出类似“Loading symbols from ... /module.ko”的信息,之后就可以像调试内置内核代码一样,在模块函数里下断点、查看变量了。
lx-symbols依赖CONFIG_GDB_SCRIPTS编译选项,而且GDB必须是用vmlinux启动的。如果执行后没有任何输出,先确认内核配置里CONFIG_GDB_SCRIPTS=y是否编译成功,以及vmlinux和模块是否用同一套工具链编译。
5.4 单步执行的经验与教训
GDB的next命令在KGDB场景下不太可靠。next会尝试跳过整个函数调用链,而内核线程的调用关系复杂,涉及中断切换、进程调度,next经常在尝试跳过的过程中触发意外异常,导致连接卡死。我自己用下来,最稳的方式是优先使用指令级单步stepi,或者先用bt理清调用关系,然后手动在下一层函数入口下断点。
一个典型的内核问题排查流程:
- 在问题驱动的主函数入口下硬件断点;
- continue触发一次,用info registers和bt记录现场;
- 继续continue,多触发几次,观察每次进入时的参数变化;
- 怀疑某个全局变量被异常修改时,执行watch加变量地址,设置硬件观察点;
- 变量被写入时系统立即停下,这时就能看到是哪个函数、哪条指令改写了它。
watch命令在内核调试中价值极高。之前排查一个数据被莫名改写的疑难问题,就是靠watch全局变量地址,直接抓到了元凶——一个越界写的数组。
6. 常见问题排查与避坑经验汇总
6.1 串口层的问题定位
板子上电后串口完全没有输出,这个问题排在第一位。先检查USB转串口模块被系统识别成什么设备,ls /dev/ttyUSB还是ls /dev/ttyACM,两种情况在gdb里配置的路径不一样。再确认电平跳线在3.3V档位,以及TXD/RXD交叉正确。
串口有输出但乱码,通常是波特率不匹配。确认minicom和KGDB都用115200,如果线材过长或者干扰大,可以降到57600重新测试。我在实际项目中遇到过主机侧波特率正确,但USB转串口模块晶振不准导致字符丢失的情况,换一个模块就好了,这类硬件问题不容易排查,多准备几个模块做对照。
6.2 GDB连接阶段的异常
gdb报can't open /dev/ttyUSB0,首先排查权限和占用。权限问题把用户加入dialout组;占用问题多半是minicom没有完全退出,用fuser -v /dev/ttyUSB0查看是哪个进程占着,确认后kill掉。
连接后出现“Remote 'g' packet reply is too long”,架构设置问题可能性最大。执行set architecture arm后再重新target remote。还有一个隐蔽原因:vmlinux编译时的ARM模式配置与目标机实际运行模式不一致,例如内核配置了CONFIG_THUMB2_KERNEL而GDB还在ARM模式,这时需要设置set arm fallback-mode thumb。
出现“Remote connection closed”这类问题,一般是目标机内核主动退出了调试状态。常见原因是执行了某些KGDB不支持的操作,比如在中断上下文内继续执行,或者断点打在了一个内核频繁访问的路径上,每次都触发断点导致系统无法继续。
6.3 进入断点后的死机与多核问题
断点触发后系统直接死掉,这是KGDB调试中比较头疼的坑。最常见的场景是在中断上下文、自旋锁持有状态或者禁止抢占的临界区里设置了断点。KGDB本身依赖中断机制,进入这些区域后再触发调试中断,等于自己踩自己的脚,系统直接锁死。
应对方法:不要在内核中断服务函数里下断点;如果必须观察中断路径,尝试在进入中断前的上层函数下断点,或者用tracepoint替代。另一个办法是把断点打在异常返回路径上,观察中断处理完成后的现场。
A33是四核处理器,KGDB在SMP平台上的另一个问题是:一个核停下来等待调试,其他核可能还在执行。这些还在运行的核会继续持有锁、修改数据,导致调试现场不一致,严重时直接死锁。遇到这种情况,可以在KGDB的KDB子系统中把多核限制为单核调试。进入KDB后执行cpus命令查看各核状态,然后按KDB语法把其他核停下来,再回到GDB模式继续调试。这一步在调试并发问题时要特别注意。
6.4 调试模块与符号相关的坑
lx-symbols无法加载模块符号,先检查vmlinux是否带完整符号,以及CONFIG_GDB_SCRIPTS是否编译。另一种情况是模块重新编译过,但GDB里还缓存着旧符号信息,执行lx-symbols后不加任何参数,先清空缓存再重新加载。
还有一个我踩过多次的坑:用zImage启动调试,连接正常,但符号地址和实际地址对不上。这通常是因为内核启用了地址随机化(KASLR)。遇到这种情况,在启动参数里加nokaslr,关闭内核地址随机化,保证vmlinux中的链接地址就是实际运行地址。KGDB调试强依赖地址一致性,这一步对A33平台同样有效。
6.5 实用的小技巧与习惯
最后分享几个我长期使用形成的习惯。调试时串口消息和KGDB共用一路时,进入调试状态后console会冻结,如果还需要看内核日志,可以用dmesg先把日志保存到文件,调试结束后再查看。GDB的日志也可以记录下来,启动GDB时加--args参数,配合set logging on把调试过程保存到文件,方便复盘。
遇到那种一加断点就复现、去掉断点就消失的诡异问题,不要硬刚,先怀疑KGDB本身对系统时序的影响。可以先加大断点位置与问题路径的距离,或者改用KDB模式配合静态检查,缩小范围后再回来继续用KGDB。
调试器本身也是一个会引入干扰的工具。学会在合适的场景放弃它,反而效率更高。
我个人在实际操作中的体会是,GDB+KGDB这套环境真正解决的不是“能不能调试”的问题,而是让内核调试变成一个可控、可重复、有工具支撑的过程。搭建这件事本身并不复杂,难点全在细节上:工具链一致、串口干净、调试信息完整、地址对应准确。这几点都注意到,整条链路就会很顺。希望这篇文章能帮你少走一些我走过的弯路,把宝贵的时间留在真正难啃的问题上。