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 的功能与特点:

  1. 内核模式调试: KDB 直接运行在内核模式下,不需要外部调试工具或环境。它嵌入在内核中,因此在内核崩溃时,开发者仍然可以通过控制台、串口等与内核交互并进行调试。

  2. 触发式调试: KDB 可以在内核发生特定事件时自动启动,例如系统崩溃、异常或错误检测时。开发者可以设置断点,查看特定条件下的内核状态。

  3. 实时调试: KDB 不依赖外部硬件或调试器,开发者可以实时与内核交互,检查内核变量、堆栈、内存等。

  4. 支持多种接口: KDB 支持通过控制台、串口、甚至直接从调试命令行访问和调试内核,这在没有完整图形界面的环境下非常有用,尤其是嵌入式开发。

  5. 调试内核数据结构: KDB 可以直接访问和查看内核数据结构,如进程调度、内存管理、文件系统等,有助于发现底层问题。

  6. 独立于外部工具: 与 GDB 等外部工具不同,KDB 不需要额外配置,它是内核的一部分,适合用于嵌入式系统或无法连接外部调试工具的场合。

Kdb 的主要用途:

  • 内核崩溃时的调试:当内核发生崩溃时,KDB 可以自动触发并启动调试会话,开发者可以查看崩溃现场,检查内存、寄存器、调用栈等。

  • 低级调试:KDB 能直接访问内核数据结构,适合用于低级调试,特别是在无法使用外部调试器时。

  • 实时系统调试:在实时操作系统中,KDB 能即时响应系统状态并进行调试,帮助快速定位问题。

Kgdb 功能介绍

Kgdb 是一个让开发者能够通过 GDB 来调试内核代码的工具。它允许使用 GDB 对内核进行调试,特别是当内核运行在某个目标设备上时,开发者可以通过串口或网络与目标设备进行调试。Kgdb 本质上是内核对 GDB 的支持,是 GDB 和内核之间的一个桥梁。

Kgdb 的功能与特点

  1. 通过 GDB 调试内核

    • Kgdb 允许开发者在用户空间使用 GDB 调试内核代码。这意味着你可以使用 GDB 提供的强大调试功能(如断点、单步执行、堆栈跟踪等)来调试内核代码。

  2. 与 GDB 的集成

    • Kgdb 与 GDB 集成,通过串口、网络或其他通信方式将调试会话与内核调试目标连接起来。用户可以使用 GDB 的命令在内核上设置断点、单步执行、查看变量等。

  3. 远程调试

    • Kgdb 允许远程调试内核。内核通过串口、网络等通道与 GDB 主机进行通信,这对于远程调试硬件设备上的内核非常有用。

  4. 支持多种调试功能

    • 使用 GDB 时,开发者可以设置断点、单步执行、检查内存、查看寄存器内容、调试内核模块等。GDB 提供了非常强大的调试功能,可以轻松进行复杂的内核调试。

  5. 调试内核崩溃

    • Kgdb 也可以用于内核崩溃的调试。开发者可以在崩溃时通过 GDB 进行远程调试,检查崩溃现场、查看调用栈、打印内存内容等。

  6. 与 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 常见问题

  1. 无法连接调试终端: 如果 Kdb 是通过串口或其他终端设备与外部交互,设备配置不正确(例如波特率设置错误)可能导致无法连接。

  2. 调试信息不全: Kdb 提供的调试信息相对较少,可能无法提供足够的上下文信息,尤其是复杂崩溃时。此时可能需要结合其他调试工具,如 kgdb 或 crash 工具。

  3. Kdb 无法响应输入: 当系统崩溃或卡死时,Kdb 可能无法正常响应输入。可能是由于控制台或串口设备配置问题,或内核在崩溃前未能正确初始化调试端口。

Kgdb 常见问题

  1. GDB 无法连接到内核: 可能是因为内核没有正确配置以启用 Kgdb,或者外部设备(如串口或网络)连接不稳定。确保内核启用了 CONFIG_KGDB,并且通信端口和 GDB 配置正确。

  2. 内核调试信息不完整: 调试符号可能未包含在内核映像中,导致调试信息不足。需要确保内核配置了调试符号(如 CONFIG_DEBUG_INFO)。

  3. GDB 与内核版本不匹配: GDB 版本可能与内核的调试接口不兼容,导致调试时出现崩溃或连接问题。需要使用与内核版本匹配的 GDB。

  4. 调试器与内核之间的通信延迟: Kgdb 通过串口或网络进行远程调试时,可能会遇到较大的延迟,尤其是在高负载系统中。调试过程中可能会出现响应缓慢或超时问题。