4.6.4. 使用 Kdb/Kgdb 调试内核
4.6.4.1. Kdb/Kgdb 概述
Kdb 是 Linux 内核的调试工具,旨在帮助开发者在没有完整操作系统的情况下,直接在内核模式下进行调试。它通常用于处理内核崩溃后的情况,或是内核运行时出现问题时进行调试。下面是 Kdb 的发展历史:
2001年:Kdb 的初步版本由 Kurt Garloff 开发。Kdb 是为 Linux 内核提供的一种简单的内核调试工具。它的设计目标是提供一种不依赖于外部系统的调试方法,能够直接在控制台上与内核交互。
2000年代初期:Kdb 成为 Linux 内核开发者调试的一个重要工具,它可以在系统崩溃或内核问题发生时帮助开发者诊断和修复问题。Kdb 运行在内核模式下,因此即使操作系统崩溃或停止响应,开发者仍能通过串口或控制台进行调试。
2000年代中期:随着内核功能的不断增加,Kdb 开始集成到更多的内核版本中。它可以在内核启动时启用,提供更强大的调试支持。
目前:Kdb 依然是内核开发中常用的工具,特别是在低级故障排查和崩溃后分析中。它通常用于更简单、即时的内核调试,但由于功能上比 GDB 更为简单,开发者往往还需要配合其他工具来进行更复杂的调试。
Kgdb 旨在用作 Linux 内核的源代码级调试器。它与 gdb 一起用于调试 Linux 内核。期望 gdb 可用于“侵入”内核以检查内存,变量并查看调用堆栈信息,类似于应用程序开发人员使用 gdb 调试应用程序的方式。可以在内核代码中放置断点并执行一些有限的执行步骤。下面是 Kgdb 的发展历史:
2001年:KGDB 的初步工作由 Jason Wessel 提出和实现。KGDB 是基于 GDB 实现的,它通过串行端口、以太网或其他通信接口与目标内核进行通信,使得开发者可以像调试用户空间程序一样调试内核。
2002年:KGDB 在 Linux 2.6 内核中开始引起广泛关注。它使得开发者能够在用户空间使用 GDB 调试内核空间代码,极大提高了调试的灵活性。与 KDB 相比,KGDB 支持更复杂的调试操作,如设置断点、单步执行等,类似于调试用户空间程序的方式。
2000年代后期:KGDB 支持的功能逐渐增多,包括支持多种硬件平台(如 x86、ARM 等)和更高效的远程调试功能。
2010年代:KGDB 与其他调试工具(如 QEMU 等)结合使用,支持通过虚拟化平台进行内核调试。它的调试方式更加灵活,可以使用远程调试和硬件仿真工具进行更复杂的调试。
目前:KGDB 继续得到内核开发者和调试人员的使用,尤其是在内核开发过程中,它与 GDB 和 KDB 配合使用,提供了强大的调试能力。虽然 KGDB 配置较为复杂,但它仍然是 Linux 内核开发者进行内核级别调试的首选工具之一。
4.6.4.2. Kdb/kgdb 功能介绍
Kdb 功能介绍
Kdb 是 Linux 内核内建的调试器,允许开发人员在内核运行时进行调试,通常用于 低级调试,例如崩溃分析、内存检查、查看内核状态等。
Kdb 的功能与特点:
内核模式调试: KDB 直接运行在内核模式下,不需要外部调试工具或环境。它嵌入在内核中,因此在内核崩溃时,开发者仍然可以通过控制台、串口等与内核交互并进行调试。
触发式调试: KDB 可以在内核发生特定事件时自动启动,例如系统崩溃、异常或错误检测时。开发者可以设置断点,查看特定条件下的内核状态。
实时调试: KDB 不依赖外部硬件或调试器,开发者可以实时与内核交互,检查内核变量、堆栈、内存等。
支持多种接口: KDB 支持通过控制台、串口、甚至直接从调试命令行访问和调试内核,这在没有完整图形界面的环境下非常有用,尤其是嵌入式开发。
调试内核数据结构: KDB 可以直接访问和查看内核数据结构,如进程调度、内存管理、文件系统等,有助于发现底层问题。
独立于外部工具: 与 GDB 等外部工具不同,KDB 不需要额外配置,它是内核的一部分,适合用于嵌入式系统或无法连接外部调试工具的场合。
Kdb 的主要用途:
内核崩溃时的调试:当内核发生崩溃时,KDB 可以自动触发并启动调试会话,开发者可以查看崩溃现场,检查内存、寄存器、调用栈等。
低级调试:KDB 能直接访问内核数据结构,适合用于低级调试,特别是在无法使用外部调试器时。
实时系统调试:在实时操作系统中,KDB 能即时响应系统状态并进行调试,帮助快速定位问题。
Kgdb 功能介绍
Kgdb 是一个让开发者能够通过 GDB 来调试内核代码的工具。它允许使用 GDB 对内核进行调试,特别是当内核运行在某个目标设备上时,开发者可以通过串口或网络与目标设备进行调试。Kgdb 本质上是内核对 GDB 的支持,是 GDB 和内核之间的一个桥梁。
Kgdb 的功能与特点:
通过 GDB 调试内核:
Kgdb 允许开发者在用户空间使用 GDB 调试内核代码。这意味着你可以使用 GDB 提供的强大调试功能(如断点、单步执行、堆栈跟踪等)来调试内核代码。
与 GDB 的集成:
Kgdb 与 GDB 集成,通过串口、网络或其他通信方式将调试会话与内核调试目标连接起来。用户可以使用 GDB 的命令在内核上设置断点、单步执行、查看变量等。
远程调试:
Kgdb 允许远程调试内核。内核通过串口、网络等通道与 GDB 主机进行通信,这对于远程调试硬件设备上的内核非常有用。
支持多种调试功能:
使用 GDB 时,开发者可以设置断点、单步执行、检查内存、查看寄存器内容、调试内核模块等。GDB 提供了非常强大的调试功能,可以轻松进行复杂的内核调试。
调试内核崩溃:
Kgdb 也可以用于内核崩溃的调试。开发者可以在崩溃时通过 GDB 进行远程调试,检查崩溃现场、查看调用栈、打印内存内容等。
与 KDB 配合使用:
Kgdb 和 KDB 并不是互相排斥的,实际上,它们可以一起使用。KDB 允许开发者在不依赖外部工具的情况下进行内核调试,而 Kgdb 允许通过 GDB 进行更详细和复杂的调试。
Kgdb 的主要用途:
远程调试:通过串口、以太网等通信方式,Kgdb 可以用于远程调试嵌入式设备或其他目标机器上的内核。这对于开发和维护嵌入式系统或其他特殊硬件平台非常有帮助。
复杂内核调试:当内核代码需要进行复杂的调试时,开发者可以利用 GDB 的强大功能,如多线程调试、内存分析等,来定位和解决问题。
内核模块调试:开发者可以使用 GDB 调试内核模块,分析模块加载、执行过程中的问题。
Kdb 与 Kgdb 对比
| 特性/功能 | KDB | KGDB |
|---|---|---|
| 调试方式 | 内核内建的调试工具,直接在内核运行时交互 | 通过 GDB 外部调试器与内核进行调试,通常使用串口或网络连接 |
| 使用场景 | 适用于简单、快速的内核调试,尤其在系统崩溃时启动调试 | 适用于复杂的内核调试,特别是远程调试或需要 GDB 功能时 |
| 调试功能 | 提供基本的调试命令,如查看堆栈、寄存器、内存等 | 提供 GDB 的全部调试功能,如断点、单步执行、内存分析等 |
| 调试复杂度 | 功能较简单,适合低级调试,定位基本问题 | 功能强大,适合进行复杂调试任务 |
| 调试设备 | 无需外部设备,仅通过控制台、串口等与内核交互 | 依赖 GDB 和外部通信设备(串口、网络等) |
| 调试实时性 | 提供即时调试,尤其在系统崩溃时快速响应 | 由于依赖外部调试器,可能会稍有延迟 |
| 适用性 | 适用于没有外部调试工具或硬件的环境,如嵌入式设备 | 适用于需要强大调试功能和远程调试的环境 |
| 远程调试支持 | 不支持远程调试 | 支持远程调试,可以通过串口、网络进行连接 |
| 与 KDB 配合使用 | 可与 KGDB 配合使用,进行简单调试 | 可与 KDB 配合使用,进行更复杂调试 |
4.6.4.3. Kdb/Kgdb 具体使用方法
开启 kdb/kgdb
X5 内核默认并不支持 Kdb/kgdb,需要对内核做一些修改。
执行 ./bd.sh boot menuconfig,将 kgdb 相关配置项修改为如下状态:
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y
CONFIG_KDB_DEFAULT_ENABLE=0x1
修改保存后,可以打开 kernel/arch/arm64/configs/hobot_x5_soc_defconfig 确保 kgdb 相关的配置项已经正确被配置。
板端启动 kdb 调试
板端启动 kdb 调试有两种方式,可以选择在 Uboot 阶段配置启动,也可以在进入内核后再启动。
Uboot 阶段启动 kdb
在 UBoot 内通过修改并储存 bootargs 修改内核的 command line 选项,加入(如使用 ttyS0)后启动:
Hobot# setenv bootargs kgdboc=ttyS0,115200 kgdbwait
Hobot# run bootcmd
等待内核启动后,会打印下面日志提示进入 kgdb:
Starting kernel ...
[ 0.000000] Linux version 6.1.83-DR-PL5.1_V1.0.14 (sxq@DESKTOP-6VORLA0) (aarch64-none-linux-gnu-gcc (Arm GNU Toolchain 11.3.Rel1) 11.3.1 20220712, GNU ld (Arm GNU Toolchain 11.3.Rel1) 2.38.20220708) #1 SMP PREEMPT Wed Dec 4 20:10:42 CST 2024
[ 0.000000] Kernel command line: console=ttyS0,115200n8 root=/dev/mmcblk0p9 ro rootwait hobotboot.slot_suffix=_a hobotboot.reason=COLD_BOOT hobotboot.medium=MMC hobotboot.mode=normal hobotboot.ab_switch_reason=normal hobotboot.pmic_type=single-pmic kgdboc=ttyS0,115200 kgdbwait
[ 0.111978] audit: type=2000 audit(0.090:1): state=initialized audit_enabled=0 res=1
[ 0.144479] (NULL device *): no horizon,gpio-banks in node /soc/disp_apb/disp_iomuxc@3e0a0054
[ 0.182704] SCSI subsystem initialized
[ 0.262379] Initialise system trusted keyrings
[ 0.294398] Key type asymmetric registered
[ 0.294409] Asymmetric key parser 'x509' registered
[ 0.393114] KGDB: Waiting for connection from remote gdb...
Entering kdb (current=0xffff0000058a0000, pid 1) on processor 7 due to Keyboard Entry
[7]kdb>
内核启动后启动 kdb
在进入内核后再启动 kgdb 的方式如下:
# 先卸载 watchdog 驱动(如果存在)
rmmod hobot_watchdog
echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
echo g > /proc/sysrq-trigger
日志如下:
root@buildroot:~# echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
root@buildroot:~# echo g > /proc/sysrq-trigger
Entering kdb (current=0xffff0000cca42b80, pid 1003) on processor 4 due to Keyboard Entry
[4]kdb>
注意: 在 X5 BSP 的 debug 版本中,Watchdog 设备默认被注册,但是看门狗计时器并没有使能,所以不需要执行卸载 Watchdog 操作。
Kdb 调试命令介绍
在 Kdb 命令终端中输入 help 命令即可显示出 Kdb 的命令列表:
[7]kdb> help
Command Usage Description
----------------------------------------------------------
md <vaddr> Display Memory Contents, also mdWcN, e.g. md8c1
mdr <vaddr> <bytes> Display Raw Memory
mdp <paddr> <bytes> Display Physical Memory
mds <vaddr> Display Memory Symbolically
mm <vaddr> <contents> Modify Memory Contents
go [<vaddr>] Continue Execution
rd Display Registers
rm <reg> <contents> Modify Registers
ef <vaddr> Display exception frame
bt [<vaddr>] Stack traceback
btp <pid> Display stack for process <pid>
bta [<state_chars>|A] Backtrace all processes whose state matches
btc Backtrace current process on each cpu
btt <vaddr> Backtrace process given its struct task address
env Show environment variables
set Set environment variables
help Display Help Message
? Display Help Message
cpu <cpunum> Switch to new cpu
kgdb Enter kgdb mode
ps [<state_chars>|A] Display active task list
pid <pidnum> Switch to another task
reboot Reboot the machine immediately
lsmod List loaded kernel modules
sr <key> Magic SysRq key
dmesg [lines] Display syslog buffer
defcmd name "usage" "help" Define a set of commands, down to endefcmd
kill <-signal> <pid> Send a signal to a process
summary Summarize the system
per_cpu <sym> [<bytes>] [<cpu>]
Display per_cpu variables
grephelp Display help on | grep
bp [<vaddr>] Set/Display breakpoints
bl [<vaddr>] Display breakpoints
bc <bpnum> Clear Breakpoint
be <bpnum> Enable Breakpoint
bd <bpnum> Disable Breakpoint
ss Single Step
dumpcommon Common kdb debugging
dumpall First line debugging
dumpcpu Same as dumpall but only tasks on cpus
ftdump [skip#entries] [cpu]
Dump ftrace log; -skip dumps last #entries
下面简单介绍这些命令:
内存相关命令
| 命令 | 参数 | 描述 |
|---|---|---|
md |
<vaddr> |
显示内存内容,<vaddr> 为虚拟地址,支持 WcN 参数来控制显示格式和块大小(例如 md8c1)。 |
mdr |
<vaddr> <bytes> |
以原始格式显示从 <vaddr> 地址开始的 <bytes> 字节内存。 |
mdp |
<paddr> <bytes> |
显示物理内存内容,从 <paddr> 地址开始显示指定字节数。 |
mds |
<vaddr> |
符号化显示从虚拟地址 <vaddr> 开始的内存内容。 |
mm |
<vaddr> <contents> |
修改内存内容,将虚拟地址 <vaddr> 处的内存修改为指定的内容 <contents>。 |
执行控制命令
| 命令 | 参数 | 描述 |
|---|---|---|
go |
[<vaddr>] |
继续程序执行。如果指定了虚拟地址 <vaddr>,则从该地址开始执行。也可以用来退出 kdb 界面回到内核命令行状态。 |
bt |
[<vaddr>] |
显示栈回溯。可以指定 <vaddr> 作为起始地址。 |
bta |
[<state_chars>\|A] |
回溯所有进程的栈,按进程状态 <state_chars> 过滤,A 表示所有进程。 |
btc |
回溯当前进程在所有CPU上的栈。 | |
btt |
<vaddr> |
从指定的结构体地址 <vaddr> 开始回溯进程的栈。 |
ss |
进行单步调试。 | |
kill |
<-signal> <pid> |
向指定进程发送信号,<signal> 是信号类型,<pid> 是进程ID。 |
pid |
<pidnum> |
切换到指定的进程ID(pidnum)进行调试。 |
sr |
<key> |
触发魔术SysRq键,通常用于紧急操作,如强制重启等内核级操作。 |
寄存器相关命令
| 命令 | 参数 | 描述 |
|---|---|---|
rd |
显示当前CPU的寄存器内容。 | |
rm |
<reg> <contents> |
修改寄存器的内容,<reg> 为寄存器名称,<contents> 为新的值。 |
环境变量命令
| 命令 | 参数 | 描述 |
|---|---|---|
env |
显示当前的环境变量。 | |
set |
设置环境变量的值。 |
进程相关命令
| 命令 | 参数 | 描述 |
|---|---|---|
ps |
[<state_chars>\|A] |
显示当前活动任务列表,可以按进程状态 <state_chars> 过滤,A 表示所有任务。 |
lsmod |
列出当前加载的内核模块。 | |
pid |
<pidnum> |
切换到指定的进程ID(pidnum)进行调试。 |
btp |
<pid> |
显示指定进程ID (pid) 的栈信息。 |
内核调试命令
| 命令 | 参数 | 描述 |
|---|---|---|
dmesg |
[lines] |
显示内核日志缓冲区的内容,可以指定显示的行数 lines。 |
dumpcommon |
执行通用的内核调试转储。 | |
dumpall |
执行完整的内存转储调试。 | |
dumpcpu |
仅转储CPU上的任务相关信息。 |
自定义命令与帮助命令
| 命令 | 参数 | 描述 |
|---|---|---|
defcmd |
name "usage" "help" |
定义一组自定义命令,包含用法说明和帮助文档。 |
grephelp |
显示帮助信息,可以通过管道(\|)进行筛选。 |
其他命令
| 命令 | 参数 | 描述 |
|---|---|---|
kgdb |
进入KGDB模式,进行低层次的调试。 | |
summary |
显示系统的简要总结信息。 | |
per_cpu |
<sym> [<bytes>] [<cpu>] |
显示某个符号(<sym>)的每个CPU相关的变量,可以指定字节数 <bytes> 和CPU <cpu>。 |
bl |
显示当前的断点信息。 | |
bp |
<vaddr> |
设置断点,在指定虚拟地址处停止执行。 |
bc |
<bpnum> |
清除指定的断点。 |
be |
<bpnum> |
启用指定的断点。 |
bd |
<bpnum> |
禁用指定的断点。 |
下面是一些 Kdb 命令的执行日志:
# 显示栈回溯
[7]kdb> bt
Stack traceback for pid 999
0xffff0000c60f0000 999 1 1 7 R 0xffff0000c60f09f0 *bash
CPU: 7 PID: 999 Comm: bash Tainted: P C O 6.1.83-DR-PL5.1_V1.0.14 #11
Hardware name: D-Robotics X5 EVB LP4 1_B board (DT)
Call trace:
dump_backtrace+0xd8/0x130
show_stack+0x18/0x30
dump_stack_lvl+0x68/0x84
dump_stack+0x18/0x34
kdb_dump_stack_on_cpu+0x88/0x90
kdb_show_stack+0x90/0xa0
kdb_bt1+0xc4/0x140
kdb_bt+0x328/0x37c
kdb_parse+0x2c4/0x63c
kdb_main_loop+0x434/0x7b4
kdb_stub+0x270/0x444
kgdb_cpu_enter+0x168/0x66c
kgdb_handle_exception+0xcc/0x120
kgdb_compiled_brk_fn+0x28/0x40
call_break_hook+0x68/0x7c
brk_handler+0x1c/0x60
# 显示指定虚拟内存内容,如下是显示当前的内核完整版本字符串
[0]kdb> md linux_banner
0xffff800008baae48 65762078756e694c 2e36206e6f697372 Linux version 6.
0xffff800008baae58 2d52442d33382e31 31565f312e354c50 1.83-DR-PL5.1_V1
0xffff800008baae68 78732820302e312e 4f544b5345444071 .1.0 (sxq@DESKTO
0xffff800008baae78 414c524f56362d50 6372616128202930 P-6VORLA0) (aarc
0xffff800008baae88 656e6f6e2d343668 672d78756e696c2d h64-none-linux-g
0xffff800008baae98 28206363672d756e 20554e47206d7241 nu-gcc (Arm GNU
0xffff800008baaea8 696168636c6f6f54 522e332e3131206e Toolchain 11.3.R
0xffff800008baaeb8 2e31312029316c65 3232303220312e33 el1) 11.3.1 2022
# 显示当前CPU的寄存器内容
[7]kdb> rd
x0: ffff800009268000 x1: 0000000000000001 x2: ffff800009268558
x3: 0000000000000000 x4: ffff0000ff746b60 x5: ffff0000ff746b60
x6: 0000000000000000 x7: ffff800009184748 x8: 00000000ffffefff
x9: ffff80000912c748 x10: ffff800009184748 x11: 00000000000002fa
x12: 00000000000008ee x13: ffff80000912c748 x14: 0000000000000000
x15: fffffffffffed7d8 x16: 0000000000000000 x17: 0000000000000000
x18: 0000000000000018 x19: 0000000000000067 x20: ffff80000912c000
x21: ffff80000911a000 x22: 0000000000000006 x23: 0000000000000000
x24: 0000000000000000 x25: ffff800008af8f40 x26: 0000000000000000
x27: 0000000000000000 x28: ffff0000c60f0000 x29: ffff800019563c80
x30: ffff800008116184 sp: ffff800019563c80 pc: ffff8000081160ec
pstate: 60400009 v0: ?? v1: ?? v2: ?? v3: ?? v4: ?? v5: ?? v6: ?? v7: ??
v8: ?? v9: ?? v10: ?? v11: ?? v12: ?? v13: ?? v14: ?? v15: ?? v16: ??
v17: ?? v18: ?? v19: ?? v20: ?? v21: ?? v22: ?? v23: ?? v24: ?? v25: ??
v26: ?? v27: ?? v28: ?? v29: ?? v30: ?? v31: ?? fpsr: 00000000
fpcr: 00000000
Kgdb 远程连接单板调试
板端进入 Kdb 操作界面后,可执行 kgdb 命令,等待主机 gdb 远程连接:
kdb> kgdb
Entering please attach debugger or use $D#44+ or $3#33
在提示等待连接时,关闭串口终端(避免占用串口)。之后在主机上使用 X5 BSP 所使用的编译工具链 opt/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gdb,加载内核 vmlinux(X5 BSP 源码中的路径是 out/build/kernel/vmlinux)。为了方便调用,建议在 .bashrc 中创建别名:
alias arm_gdb='/opt/arm-gnu-toolchain-11.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gdb'
然后,通过主机串口(如 /dev/ttyUSB0)连接目标机(若设备有权限要求,加 sudo):
arm_gdb out/build/kernel/vmlinux
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
连接上后,即可使用 gdb 命令进行调试。
关于 Kdb/Kgdb 的更多使用方法可以参考 Using kgdb, kdb and the kernel debugger internals。
4.6.4.4. 常见问题
Kdb 常见问题
无法连接调试终端: 如果 Kdb 是通过串口或其他终端设备与外部交互,设备配置不正确(例如波特率设置错误)可能导致无法连接。
调试信息不全: Kdb 提供的调试信息相对较少,可能无法提供足够的上下文信息,尤其是复杂崩溃时。此时可能需要结合其他调试工具,如 kgdb 或 crash 工具。
Kdb 无法响应输入: 当系统崩溃或卡死时,Kdb 可能无法正常响应输入。可能是由于控制台或串口设备配置问题,或内核在崩溃前未能正确初始化调试端口。
Kgdb 常见问题
GDB 无法连接到内核: 可能是因为内核没有正确配置以启用 Kgdb,或者外部设备(如串口或网络)连接不稳定。确保内核启用了 CONFIG_KGDB,并且通信端口和 GDB 配置正确。
内核调试信息不完整: 调试符号可能未包含在内核映像中,导致调试信息不足。需要确保内核配置了调试符号(如 CONFIG_DEBUG_INFO)。
GDB 与内核版本不匹配: GDB 版本可能与内核的调试接口不兼容,导致调试时出现崩溃或连接问题。需要使用与内核版本匹配的 GDB。
调试器与内核之间的通信延迟: Kgdb 通过串口或网络进行远程调试时,可能会遇到较大的延迟,尤其是在高负载系统中。调试过程中可能会出现响应缓慢或超时问题。