使用 kgdb、kdb 和内核调试器内部机制

作者:

Jason Wessel

简介

内核有两个不同的调试器前端 (kdb 和 kgdb),它们与调试核心接口。如果正确配置内核,可以在编译时和运行时动态地在这两个调试器前端之间切换。

Kdb 是一个简单的 shell 风格界面,可以在带有键盘的系统控制台或串行控制台上使用。 可以使用它来检查内存、寄存器、进程列表、dmesg,甚至设置断点以在特定位置停止。 Kdb 不是源码级调试器,但可以设置断点并执行一些基本的内核运行控制。 Kdb 主要用于进行一些分析,以帮助开发或诊断内核问题。如果代码是用 CONFIG_KALLSYMS 构建的,则可以在内核内置程序或内核模块中按名称访问某些符号。

Kgdb 旨在用作 Linux 内核的源码级调试器。 它与 gdb 一起用于调试 Linux 内核。 预期可以使用 gdb “中断”到内核中,以检查内存、变量并查看调用堆栈信息,类似于应用程序开发人员使用 gdb 调试应用程序的方式。 可以在内核代码中放置断点并执行一些有限的执行步骤。

使用 kgdb 需要两台机器。 其中一台机器是开发机器,另一台是目标机器。 要调试的内核在目标机器上运行。 开发机器针对 vmlinux 文件运行 gdb 实例,该文件包含符号(不是引导映像,例如 bzImage、zImage、uImage...)。 在 gdb 中,开发人员指定连接参数并连接到 kgdb。 开发人员与 gdb 建立的连接类型取决于在测试机器的内核中编译为内置程序或可加载内核模块的 kgdb I/O 模块的可用性。

编译内核

  • 为了启用 kdb 的编译,必须首先启用 kgdb。

  • kgdb 测试编译选项在 kgdb 测试套件一章中描述。

kgdb 的内核配置选项

要启用 CONFIG_KGDB,应在 Kernel hacking ‣ Kernel debugging 下查找,然后选择 KGDB: kernel debugger

虽然这不是对 vmlinux 文件中包含符号的硬性要求,但没有符号数据的 gdb 往往不是很有用,因此需要启用 CONFIG_DEBUG_INFO,这在配置菜单中称为 Compile the kernel with debug info

建议(但不是必需)启用 CONFIG_FRAME_POINTER 内核选项,这在配置菜单中称为 Compile the kernel with frame pointers。 此选项会将代码插入到已编译的可执行文件中,该代码会将帧信息保存在寄存器中或堆栈中的不同位置,从而允许 gdb 等调试器在调试内核时更准确地构建堆栈回溯。

如果使用的架构支持内核选项 CONFIG_STRICT_KERNEL_RWX,则应考虑将其关闭。 此选项将阻止使用软件断点,因为它将内核内存空间的某些区域标记为只读。 如果 kgdb 支持你使用的架构,则如果希望在启用 CONFIG_STRICT_KERNEL_RWX 选项的情况下运行,则可以使用硬件断点,否则需要关闭此选项。

接下来,应选择一个或多个 I/O 驱动程序来互连调试主机和调试目标。 早期启动调试需要支持早期调试的 KGDB I/O 驱动程序,并且驱动程序必须直接构建到内核中。 Kgdb I/O 驱动程序配置通过内核或模块参数进行,可以在描述参数 kgdboc 的部分中了解更多信息。

以下是启用或禁用 kgdb 的 .config 符号的示例集

# CONFIG_STRICT_KERNEL_RWX is not set
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y

kdb 的内核配置选项

Kdb 比坐在内核调试核心之上的简单 gdbstub 复杂得多。 Kdb 必须实现一个 shell,并且还在内核的其他部分中添加一些辅助函数,这些函数负责打印出有趣的数据,例如运行 lsmodps 时看到的内容。 为了将 kdb 构建到内核中,请按照与 kgdb 相同的步骤操作。

kdb 的主要配置选项是 CONFIG_KGDB_KDB,这在配置菜单中称为 KGDB_KDB: include kdb frontend for kgdb。 从理论上讲,如果计划在串行端口上使用 kdb,则在配置 kgdb 时,你可能已经选择了 I/O 驱动程序,例如 CONFIG_KGDB_SERIAL_CONSOLE 接口。

如果想将 PS/2 样式的键盘与 kdb 一起使用,则应选择 CONFIG_KDB_KEYBOARD,这在配置菜单中称为 KGDB_KDB: keyboard as input deviceCONFIG_KDB_KEYBOARD 选项未用于 kgdb 的 gdb 接口中的任何内容。 CONFIG_KDB_KEYBOARD 选项仅适用于 kdb。

以下是启用/禁用 kdb 的 .config 符号的示例集

# CONFIG_STRICT_KERNEL_RWX is not set
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
CONFIG_KGDB_KDB=y
CONFIG_KDB_KEYBOARD=y

内核调试器引导参数

本节介绍影响内核调试器配置的各种运行时内核参数。 以下章节介绍了如何使用 kdb 和 kgdb,并提供了一些配置参数的示例。

内核参数:kgdboc

kgdboc 驱动程序最初是 meant to stand for “kgdb over console” 的缩写。 如今,它是配置如何从 gdb 与 kgdb 通信以及想要用于与 kdb shell 交互的设备的主要机制。

对于 kgdb/gdb,kgdboc 旨在与单个串行端口一起使用。 它旨在涵盖这样一种情况,即希望使用串行控制台作为主控制台,并使用它来执行内核调试。 也可以在未指定为系统控制台的串行端口上使用 kgdb。 kgdboc 可以配置为内核内置程序或内核可加载模块。 如果将 kgdboc 构建到内核中作为内置程序,则只能使用 kgdbwait 和早期调试。

可以选择激活 KMS(内核模式设置)集成。 将 KMS 与 kgdboc 一起使用,并且视频驱动程序具有原子模式设置挂钩时,可以在图形控制台上进入调试器。 恢复内核执行时,将恢复先前的图形模式。 此集成可以用作一种有用的工具,可帮助诊断崩溃或使用 kdb 分析内存,同时允许运行完整的图形控制台应用程序。

kgdboc 参数

用法

kgdboc=[kms][[,]kbd][[,]serial_device][,baud]

如果一起使用任何可选配置,则必须遵守上面列出的顺序。

缩写

  • kms = 内核模式设置

  • kbd = 键盘

可以配置 kgdboc 以使用键盘和/或串行设备,具体取决于是否在以下情况之一中使用 kdb 和/或 kgdb。 如果一起使用任何可选配置,则必须遵守上面列出的顺序。 将 KMS 与仅 gdb 结合使用通常不是很有用的组合。

使用可加载模块或内置程序
  1. 作为内核内置程序

    使用内核引导参数

    kgdboc=<tty-device>,[baud]
    
  2. 作为内核可加载模块

    使用命令

    modprobe kgdboc kgdboc=<tty-device>,[baud]
    

    以下是如何格式化 kgdboc 字符串的两个示例。 第一个用于使用第一个串行端口的 x86 目标。 第二个示例用于使用第二个串行端口的 ARM Versatile AB。

    1. kgdboc=ttyS0,115200

    2. kgdboc=ttyAMA1,115200

使用 sysfs 在运行时配置 kgdboc

在运行时,可以通过将参数写入 sysfs 来启用或禁用 kgdboc。 以下是两个示例

  1. 在 ttyS0 上启用 kgdboc

    echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
    
  2. 禁用 kgdboc

    echo "" > /sys/module/kgdboc/parameters/kgdboc
    

注意

如果要在 tty 上配置控制台,则无需指定波特率,该控制台已配置或打开。

更多示例

可以配置 kgdboc 以使用键盘和/或串行设备,具体取决于是否在以下情况之一中使用 kdb 和/或 kgdb。

  1. 仅通过串行端口的 kdb 和 kgdb

    kgdboc=<serial_device>[,baud]
    

    示例

    kgdboc=ttyS0,115200
    
  2. 带有键盘和串行端口的 kdb 和 kgdb

    kgdboc=kbd,<serial_device>[,baud]
    

    示例

    kgdboc=kbd,ttyS0,115200
    
  3. 带有键盘的 kdb

    kgdboc=kbd
    
  4. 带有内核模式设置的 kdb

    kgdboc=kms,kbd
    
  5. 带有内核模式设置和通过串行端口的 kgdb 的 kdb

    kgdboc=kms,kbd,ttyS0,115200
    

注意

Kgdboc 不支持通过 gdb 远程协议中断目标。 除非有代理将控制台输出拆分到终端程序,否则必须手动发送 SysRq-G。 控制台代理为调试器提供单独的 TCP 端口,为“人”控制台提供单独的 TCP 端口。 代理可以负责为你发送 SysRq-G

当使用没有调试器代理的 kgdboc 时,可能会在两个入口点之一连接调试器。 如果在加载 kgdboc 后发生异常,则应在控制台上打印一条消息,说明它正在等待调试器。 在这种情况下,断开终端程序的连接,然后在它的位置连接调试器。 如果要中断目标系统并强制进入调试会话,则必须发出 Sysrq 序列,然后键入字母 g。 然后断开终端会话并连接 gdb。 如果不喜欢这样做,可以选择对 gdb 进行黑客攻击,以便在初始连接时发送 SysRq-G,或者使用允许未修改的 gdb 执行调试的调试器代理。

内核参数:kgdboc_earlycon

如果指定内核参数 kgdboc_earlycon 并且串行驱动程序注册了支持轮询的引导控制台(不需要中断并实现非阻塞 read() 函数),则 kgdb 将尝试使用引导控制台工作,直到它可以转换为 kgdboc 参数指定的常规 tty 驱动程序。

通常只有一个引导控制台(尤其是实现了 read() 函数的控制台),因此仅添加 kgdboc_earlycon 本身就足以使其工作。 如果有多个引导控制台,则可以添加引导控制台的名称以进行区分。 请注意,通过引导控制台层和 tty 层注册的名称对于同一端口而言并不相同。

例如,在一个板上要明确地说明,可以执行以下操作

kgdboc_earlycon=qcom_geni kgdboc=ttyMSM0

如果设备上唯一的引导控制台是“qcom_geni”,则可以简化为

kgdboc_earlycon kgdboc=ttyMSM0

内核参数:kgdbwait

内核命令行选项 kgdbwait 使 kgdb 在内核引导期间等待调试器连接。 只有在将 kgdb I/O 驱动程序编译到内核中,并将 I/O 驱动程序配置指定为内核命令行选项时,才能使用此选项。 kgdbwait 参数应始终跟在内核命令行中 kgdb I/O 驱动程序的配置参数之后,否则 I/O 驱动程序将不会在要求内核使用它进行等待之前配置。

当使用此选项时,内核将在 I/O 驱动程序和架构允许的情况下尽早停止并等待。 如果将 kgdb I/O 驱动程序构建为可加载内核模块,则 kgdbwait 将不会执行任何操作。

内核参数:kgdbcon

kgdbcon 功能允许在 gdb 连接到内核时在 gdb 中看到 printk() 消息。 Kdb 不使用 kgdbcon 功能。

当调试器连接并运行时,Kgdb 支持使用 gdb 串行协议将控制台消息发送到调试器。 有两种方法可以激活此功能。

  1. 使用内核命令行选项激活

    kgdbcon
    
  2. 在配置 I/O 驱动程序之前使用 sysfs

    echo 1 > /sys/module/debug_core/parameters/kgdb_use_con
    

注意

如果在配置 kgdb I/O 驱动程序之后执行此操作,则该设置将不会生效,直到下次重新配置 I/O 时。

重要说明

不能在作为活动系统控制台的 tty 上使用 kgdboc + kgdbcon。 不正确用法的示例是

console=ttyS0,115200 kgdboc=ttyS0 kgdbcon

可以将此选项与不在系统控制台上的 tty 上的 kgdboc 一起使用。

运行时参数:kgdbreboot

kgdbreboot 功能允许更改调试器处理重新启动通知的方式。 对于该行为,有 3 个选项。 默认行为始终设置为 0。

1

echo -1 > /sys/module/debug_core/parameters/kgdbreboot

完全忽略重新启动通知。

2

echo 0 > /sys/module/debug_core/parameters/kgdbreboot

将分离消息发送到任何附加的调试器客户端。

3

echo 1 > /sys/module/debug_core/parameters/kgdbreboot

在重新启动通知时进入调试器。

内核参数:nokaslr

如果使用的架构默认启用 KASLR,则应考虑将其关闭。 KASLR 随机化内核映像映射到的虚拟地址,并混淆 gdb,gdb 从 vmlinux 的符号表中解析内核符号的地址。

使用 kdb

在串行端口上快速启动 kdb

这是一个如何使用 kdb 的快速示例。

  1. 使用内核参数在引导时配置 kgdboc

    console=ttyS0,115200 kgdboc=ttyS0,115200 nokaslr
    

    在内核引导后配置 kgdboc;假设使用的是串行端口控制台

    echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
    
  2. 手动或通过等待 oops 或故障进入内核调试器。 可以通过多种方式手动进入内核调试器;所有方式都涉及使用 SysRq-G,这意味着必须在内核配置中启用 CONFIG_MAGIC_SYSRQ=y

    • 以 root 身份登录或使用超级用户会话时,可以运行

      echo g > /proc/sysrq-trigger
      
    • 使用 minicom 2.2 的示例

      按:CTRL-A f g

    • 当 telnet 到支持发送远程中断的终端服务器时

      按:CTRL-]

      键入:send break

      按:Enter g

  3. 在 kdb 提示符下,可以运行 help 命令以查看可用命令的完整列表。

    kdb 中一些有用的命令包括

    lsmod

    显示内核模块的加载位置

    ps

    仅显示活动进程

    ps A

    显示所有进程

    summary

    显示内核版本信息和内存使用情况

    bt

    使用 dump_stack() 获取当前进程的回溯

    dmesg

    查看内核系统日志缓冲区

    go

    继续系统

  4. 完成使用 kdb 后,需要考虑重新启动系统或使用 go 命令恢复正常的内核执行。 如果已将内核暂停了很长时间,则依赖于及时联网或与实际挂钟时间有关的应用程序可能会受到不利影响,因此在使用内核调试器时应考虑到这一点。

使用键盘连接的控制台快速启动 kdb

这是一个如何将 kdb 与键盘一起使用的快速示例。

  1. 使用内核参数在引导时配置 kgdboc

    kgdboc=kbd
    

    在内核引导后配置 kgdboc

    echo kbd > /sys/module/kgdboc/parameters/kgdboc
    
  2. 手动或通过等待 oops 或故障进入内核调试器。 可以通过多种方式手动进入内核调试器;所有方式都涉及使用 SysRq-G,这意味着必须在内核配置中启用 CONFIG_MAGIC_SYSRQ=y

    • 以 root 身份登录或使用超级用户会话时,可以运行

      echo g > /proc/sysrq-trigger
      
    • 使用笔记本电脑键盘的示例

      按住:Alt

      按住:Fn

      按并释放带有标签的键:SysRq

      释放:Fn

      按并释放:g

      释放:Alt

    • 使用 PS/2 101 键键盘的示例

      按住:Alt

      按并释放带有标签的键:SysRq

      按并释放:g

      释放:Alt

  3. 现在键入 kdb 命令,例如 helpdmesgbtgo 以继续内核执行。

使用 kgdb / gdb

为了使用 kgdb,必须通过将配置信息传递给 kgdb I/O 驱动程序之一来激活它。 如果未传递任何配置信息,kgdb 将不会执行任何操作。 仅当加载和配置 kgdb I/O 驱动程序时,Kgdb 才会主动连接到内核陷阱挂钩。 如果取消配置 kgdb I/O 驱动程序,kgdb 将取消注册所有内核挂钩点。

如果启用了 CONFIG_SYSFSCONFIG_MODULES,则可以通过将新的配置字符串回显到 /sys/module/<driver>/parameter/<option> 来在运行时重新配置所有 kgdb I/O 驱动程序。 可以通过传递空字符串来取消配置驱动程序。 连接调试器时,无法更改配置。 在尝试取消配置 kgdb I/O 驱动程序之前,请确保使用 detach 命令分离调试器。

使用 gdb 连接到串行端口

  1. 配置 kgdboc

    使用内核参数在引导时配置 kgdboc

    kgdboc=ttyS0,115200
    

    在内核引导后配置 kgdboc

    echo ttyS0 > /sys/module/kgdboc/parameters/kgdboc
    
  2. 停止内核执行(中断到调试器)

    为了通过 kgdboc 连接到 gdb,必须首先停止内核。 有多种方法可以停止内核,包括使用 kgdbwait 作为引导参数,通过 SysRq-G,或运行内核直到它出现异常并等待调试器连接。

    • 以 root 身份登录或使用超级用户会话时,可以运行

      echo g > /proc/sysrq-trigger
      
    • 使用 minicom 2.2 的示例

      按:CTRL-A f g

    • 当 telnet 到支持发送远程中断的终端服务器时

      按:CTRL-]

      键入:send break

      按:Enter g

  3. 从 gdb 连接

    示例(使用直接连接的端口)

    % gdb ./vmlinux
    (gdb) set serial baud 115200
    (gdb) target remote /dev/ttyS0
    

    示例(kgdb 到 TCP 端口 2012 上的终端服务器)

    % gdb ./vmlinux
    (gdb) target remote 192.168.2.2:2012
    

    连接后,可以像调试应用程序一样调试内核。

    如果在连接时遇到问题或在调试时出现严重错误,则最常见的情况是希望 gdb 详细说明其目标通信。 在键入 target remote 命令之前,通过键入以下内容来执行此操作

    set debug remote 1
    

请记住,如果在 gdb 中继续,并且需要再次“中断”,则需要发出另一个 SysRq-G。 通过在 sys_sync 处放置断点可以轻松创建一个简单的入口点,然后可以从 shell 或脚本运行 sync 以中断到调试器中。

kgdb 和 kdb 互操作性

可以在 kdb 和 kgdb 之间动态转换。 调试核心会记住上次使用的模式,并自动以相同的模式启动。

在 kdb 和 kgdb 之间切换

从 kgdb 切换到 kdb

有两种方法可以从 kgdb 切换到 kdb:可以使用 gdb 发出维护数据包,也可以盲目地键入命令 $3#33。 每当内核调试器以 kgdb 模式停止时,它将打印消息 KGDB or $3#33 for KDB。 重要的是要注意必须一次性正确键入序列。 不能键入退格键或删除键,因为 kgdb 会将其解释为调试流的一部分。

  1. 通过盲目键入从 kgdb 更改为 kdb

    $3#33
    
  2. 使用 gdb 从 kgdb 更改为 kdb

    maintenance packet 3
    

    注意

    现在必须终止 gdb。 通常,按 CTRL-Z 并发出命令

    kill -9 %
    

从 kdb 更改为 kgdb

可以通过两种方式从 kdb 更改为 kgdb。 可以通过从 kdb shell 提示符发出 kgdb 命令来手动进入 kgdb 模式,也可以在 kdb shell 提示符处于活动状态时连接 gdb。 kdb shell 查找 gdb 通过 gdb 远程协议发出的典型第一个命令,如果看到其中一个命令,它会自动更改为 kgdb 模式。

  1. 从 kdb 发出命令

    kgdb
    
  2. 在 kdb 提示符下,断开终端程序的连接,并在其位置连接 gdb。

从 gdb 运行 kdb 命令

可以使用 gdb monitor 命令从 gdb 运行一组有限的 kdb 命令。 不希望执行任何运行控制或断点操作,因为它会中断内核调试器的状态。 如果已连接 gdb,则应使用 gdb 执行断点和运行控制操作。 更实用的命令是诸如 lsmod、dmesg、ps 或可能一些内存信息命令之类的命令。 要查看可以运行的所有 kdb 命令,可以运行 monitor help

示例

(gdb) monitor ps
1 idle process (state I) and
27 sleeping system daemon (state M) processes suppressed,
use 'ps A' to see all.
Task Addr       Pid   Parent [*] cpu State Thread     Command

0xc78291d0        1        0  0    0   S  0xc7829404  init
0xc7954150      942        1  0    0   S  0xc7954384  dropbear
0xc78789c0      944        1  0    0   S  0xc7878bf4  sh
(gdb)

kgdb 测试套件

在内核配置中启用 kgdb 时,还可以选择启用配置参数 KGDB_TESTS。 启用此选项将启用一个特殊的 kgdb I/O 模块,该模块旨在测试 kgdb 内部函数。

kgdb 测试主要供开发人员测试 kgdb 内部机制以及开发新的 kgdb 架构特定实现的工具。 这些测试实际上不适用于 Linux 内核的最终用户。 主要的文档来源是查看 drivers/misc/kgdbts.c 文件。

还可以通过设置内核配置参数 KGDB_TESTS_ON_BOOT 在编译时配置 kgdb 测试套件以运行核心测试集。 此特定选项旨在用于自动化回归测试,不需要修改内核引导配置参数。 如果启用了此选项,则可以通过指定 kgdbts= 作为内核引导参数来禁用 kgdb 测试套件。

内核调试器内部机制

架构特定信息

内核调试器被组织成许多组件

  1. 调试核心

    调试核心位于 kernel/debugger/debug_core.c 中。 它包含

    • 一个通用的 OS 异常处理程序,其中包括在多 CPU 系统上将处理器同步到停止状态。

    • 与 kgdb I/O 驱动程序通信的 API

    • 调用架构特定的 kgdb 实现的 API

    • 在使用调试器时执行安全内存读取和写入内存的逻辑

    • 除非被架构覆盖,否则软件断点的完整实现

    • 调用 kdb 或 kgdb 前端到调试核心的 API。

    • 原子内核模式设置的结构和回调 API。

      注意

      kgdboc 是调用 KMS 回调的位置。

  2. kgdb 架构特定的实现

    此实现通常位于 arch/*/kernel/kgdb.c 中。 例如,arch/x86/kernel/kgdb.c 包含实现 HW 断点以及动态注册和取消注册此架构上的陷阱处理程序的初始化的详细信息。 架构特定的部分实现

    • 包含一个架构特定的陷阱捕捉器,该捕捉器调用 kgdb_handle_exception() 以启动 kgdb 关于执行其工作

    • 与 struct pt_regs 之间转换 gdb 特定数据包格式

    • 注册和取消注册架构特定的陷阱挂钩

    • 任何特殊的异常处理和清理

    • NMI 异常处理和清理

    • (可选)HW 断点

  3. gdbstub 前端(又名 kgdb)

    gdbstub 位于 kernel/debug/gdbstub.c 中。 它包含

    • 实现 gdb 串行协议的所有逻辑

  4. kdb 前端

    kdb 调试器 shell 分解为许多组件。 kdb 核心位于 kernel/debug/kdb 中。 其他一些内核组件中有许多辅助函数,使 kdb 能够检查和报告有关内核的信息,而无需获取可能导致内核死锁的锁。 kdb 核心实现以下功能。

    • 一个简单的 shell

    • kdb 核心命令集

    • 用于注册其他 kdb shell 命令的注册 API。

      • 一个自包含的 kdb 模块的一个很好的例子是用于转储 ftrace 缓冲区的 ftdump 命令。 请参阅:kernel/trace/trace_kdb.c

      • 有关如何动态注册新 kdb 命令的示例,可以从 samples/kdb/kdb_hello.c 构建 kdb_hello.ko 内核模块。 要构建此示例,可以在内核配置中设置 CONFIG_SAMPLES=yCONFIG_SAMPLE_KDB=m。 稍后运行 modprobe kdb_hello,下次进入 kdb shell 时,可以运行 hello 命令。

    • kdb_printf() 的实现,它将消息直接发送到 I/O 驱动程序,绕过内核日志。

    • kdb shell 的 SW / HW 断点管理

  5. kgdb I/O 驱动程序

    每个 kgdb I/O 驱动程序都必须为以下内容提供实现

    • 通过内置程序或模块进行配置

    • 动态配置和 kgdb 挂钩注册调用

    • 读取和写入字符接口

    • 用于从 kgdb 核心取消配置的清理处理程序

    • (可选)早期调试方法

    任何给定的 kgdb I/O 驱动程序都必须与硬件非常紧密地配合,并且必须以一种既不启用中断又不更改系统上下文的其他部分的方式进行操作,而不会完全恢复它们。 当 kgdb 核心需要输入时,它将重复“轮询”kgdb I/O 驱动程序以获取字符。 如果没有可用数据,则 I/O 驱动程序应立即返回。 这样做可以为将来以某种方式接触看门狗硬件的可能性,从而使目标系统在启用它们时不会重置。

如果要为新架构添加 kgdb 架构特定的支持,则架构应在其架构特定的 Kconfig 文件中定义 HAVE_ARCH_KGDB。 这将为该架构启用 kgdb,并且此时必须创建架构特定的 kgdb 实现。

在其 asm/kgdb.h 文件中必须在每个架构上设置几个标志。 这些是

  • NUMREGBYTES:

    所有寄存器的大小(以字节为单位),以便可以确保它们都适合一个数据包。

  • BUFMAX:

    GDB 将读入的缓冲区的大小(以字节为单位)。 这必须大于 NUMREGBYTES。

  • CACHE_FLUSH_IS_SAFE:

    如果调用 flush_cache_range 或 flush_icache_range 始终安全,则设置为 1。 在某些架构上,由于我们将其他 CPU 保留在保持模式中,因此调用这些函数对于 SMP 可能不安全。

对于常见的后端,在 kernel/kgdb.c 中还可以找到以下函数,除非标记为(可选),否则必须由特定于架构的后端提供。如果架构不需要提供特定的实现,则可以使用默认函数。

int kgdb_skipexception(int exception, struct pt_regs *regs)

(可选)提前退出 kgdb_handle_exception

参数

int exception

异常向量号

struct pt_regs *regs

当前的 struct pt_regs

在某些架构上,需要在删除断点后发生的断点异常时跳过该异常。这可以在 kgdb 的特定于架构的部分中实现。

void kgdb_breakpoint(void)

编译到断点中

参数

void

无参数

描述

这将实现为每个架构的静态内联。此函数由 kgdb 核心调用,以执行特定于架构的陷阱,以使 kgdb 进入异常处理。

int kgdb_arch_init(void)

执行任何特定于架构的初始化。

参数

void

无参数

描述

此函数将处理任何特定于架构的回调的初始化。

void kgdb_arch_exit(void)

执行任何特定于架构的反初始化。

参数

void

无参数

描述

此函数将处理任何特定于架构的回调的反初始化,用于动态注册和取消注册。

void pt_regs_to_gdb_regs(unsigned long *gdb_regs, struct pt_regs *regs)

将 ptrace regs 转换为 GDB regs

参数

unsigned long *gdb_regs

指向以 GDB 想要的顺序保存寄存器的指针。

struct pt_regs *regs

当前进程的 struct pt_regs

regs 中的 pt_regs 转换为 GDB 期望的寄存器格式,存储在 gdb_regs 中。

void sleeping_thread_to_gdb_regs(unsigned long *gdb_regs, struct task_struct *p)

将 ptrace regs 转换为 GDB regs

参数

unsigned long *gdb_regs

指向以 GDB 想要的顺序保存寄存器的指针。

struct task_struct *p

所需进程的 struct task_struct

p 中睡眠进程的寄存器值转换为 GDB 期望的格式。当 kgdb 无法访问 struct pt_regs 时,将调用此函数,因此它应该用在 switch_to 期间保存在 struct thread_struct 线程字段中的内容填充 gdb 寄存器 gdb_regs

void gdb_regs_to_pt_regs(unsigned long *gdb_regs, struct pt_regs *regs)

将 GDB regs 转换为 ptrace regs。

参数

unsigned long *gdb_regs

指向保存从 GDB 收到的寄存器的指针。

struct pt_regs *regs

指向 struct pt_regs 的指针,用于保存这些值。

gdb_regs 中的 GDB regs 转换为 pt_regs,并将它们存储在 regs 中。

int kgdb_arch_handle_exception(int vector, int signo, int err_code, char *remcom_in_buffer, char *remcom_out_buffer, struct pt_regs *regs)

处理特定于架构的 GDB 数据包。

参数

int vector

发生的异常的错误向量。

int signo

发生的异常的信号编号。

int err_code

发生的异常的错误代码。

char *remcom_in_buffer

已读取的数据包的缓冲区。

char *remcom_out_buffer

用于写入数据包的 BUFMAX 字节缓冲区。

struct pt_regs *regs

当前进程的 struct pt_regs

此函数必须处理“c”和“s”命令数据包,以及用于设置/删除硬件断点的数据包(如果使用)。如果硬件需要处理其他数据包,则在此处处理。如果代码想要处理更多数据包,则应返回 -1,如果想要退出 kgdb 回调,则返回 01

void kgdb_arch_handle_qxfer_pkt(char *remcom_in_buffer, char *remcom_out_buffer)

处理特定于架构的 GDB XML 数据包。

参数

char *remcom_in_buffer

已读取的数据包的缓冲区。

char *remcom_out_buffer

用于写入数据包的 BUFMAX 字节缓冲区。

void kgdb_call_nmi_hook(void *ignored)

在当前 CPU 上调用 kgdb_nmicallback()

参数

void *ignored

此参数仅在此处用于匹配原型。

如果您使用的是 kgdb_roundup_cpus() 的默认实现,则将为每个 CPU 调用此函数。如果您未实现 kgdb_call_nmi_hook(),则将使用默认值。

void kgdb_roundup_cpus(void)

使其他 CPU 进入保持模式

参数

void

无参数

描述

在 SMP 系统上,我们需要引起其他 CPU 的注意,并使它们进入已知状态。这应该执行使其他 CPU 调用 kgdb_wait() 所需的操作。请注意,在某些架构上,NMI 方法不用于汇总所有 CPU。通常,这些架构可以不实现此功能并获得默认值。

在非 SMP 系统上,不会调用此函数。

void kgdb_arch_set_pc(struct pt_regs *regs, unsigned long pc)

程序计数器的通用回调

参数

struct pt_regs *regs

当前的 struct pt_regs

unsigned long pc

程序计数器的新值

此函数处理更新程序计数器,并且需要特定于架构的实现。

void kgdb_arch_late(void)

执行任何特定于架构的初始化。

参数

void

无参数

描述

此函数将处理任何特定于架构的回调的后期初始化。这是一个可选函数,用于处理诸如硬件断点的后期初始化之类的事情。默认实现不执行任何操作。

struct kgdb_arch

描述特定于架构的值。

定义:

struct kgdb_arch {
    unsigned char           gdb_bpt_instr[BREAK_INSTR_SIZE];
    unsigned long           flags;
    int (*set_breakpoint)(unsigned long, char *);
    int (*remove_breakpoint)(unsigned long, char *);
    int (*set_hw_breakpoint)(unsigned long, int, enum kgdb_bptype);
    int (*remove_hw_breakpoint)(unsigned long, int, enum kgdb_bptype);
    void (*disable_hw_break)(struct pt_regs *regs);
    void (*remove_all_hw_break)(void);
    void (*correct_hw_break)(void);
};

成员

gdb_bpt_instr

触发断点的指令。

标志

断点的标志,目前只有 KGDB_HW_BREAKPOINT

set_breakpoint

允许架构指定如何设置软件断点。

remove_breakpoint

允许架构指定如何删除软件断点。

set_hw_breakpoint

允许架构指定如何设置硬件断点。

remove_hw_breakpoint

允许架构指定如何删除硬件断点。

disable_hw_break

允许架构指定如何为单个 cpu 禁用硬件断点。

remove_all_hw_break

允许架构指定如何删除所有硬件断点。

correct_hw_break

允许架构指定如何更正硬件调试寄存器。

struct kgdb_io

描述 I/O 驱动程序与 KGDB 通信的接口。

定义:

struct kgdb_io {
    const char              *name;
    int (*read_char) (void);
    void (*write_char) (u8);
    void (*flush) (void);
    int (*init) (void);
    void (*deinit) (void);
    void (*pre_exception) (void);
    void (*post_exception) (void);
    struct console          *cons;
};

成员

名称

I/O 驱动程序的名称。

read_char

指向将返回一个字符的函数的指针。

write_char

指向将写入一个字符的函数的指针。

刷新

指向将刷新任何挂起写入的函数的指针。

初始化

指向将初始化设备的函数的指针。

反初始化

指向将反初始化设备的函数的指针。这意味着此 I/O 驱动程序是临时的,并且希望被替换。当 I/O 驱动程序被替换或显式取消注册时调用。

pre_exception

指向将为 I/O 驱动程序执行任何准备工作的函数的指针。

post_exception

指向将为 I/O 驱动程序执行任何清理工作的函数的指针。

cons

如果 I/O 设备是控制台则有效;否则为 NULL。

kgdboc 内部原理

kgdboc 和 uarts

kgdboc 驱动程序实际上是一个非常薄的驱动程序,它依赖于底层硬件驱动程序具有“轮询钩子”,tty 驱动程序连接到这些钩子。在 kgdboc 的初始实现中,更改了 serial_core 以公开一个低级 UART 钩子,用于在原子上下文中进行单个字符的轮询模式读取和写入。当 kgdb 向调试器发出 I/O 请求时,kgdboc 会调用 serial core 中的回调,而 serial core 又使用 UART 驱动程序中的回调。

将 kgdboc 与 UART 结合使用时,UART 驱动程序必须在 struct uart_ops 中实现两个回调。来自 drivers/8250.c 的示例

#ifdef CONFIG_CONSOLE_POLL
    .poll_get_char = serial8250_get_poll_char,
    .poll_put_char = serial8250_put_poll_char,
#endif

围绕创建轮询驱动程序的任何实现细节都使用 #ifdef CONFIG_CONSOLE_POLL,如上所示。请记住,必须以一种可以从原子上下文中调用的方式实现轮询钩子,并且必须在返回时恢复 UART 芯片的状态,以便系统在调试器分离时可以恢复正常。您需要非常小心您考虑的任何类型的锁,因为在这里失败很可能意味着按下重置按钮。

kgdboc 和键盘

kgdboc 驱动程序包含用于配置与连接的键盘的通信的逻辑。仅当在内核配置中设置 CONFIG_KDB_KEYBOARD=y 时,键盘基础架构才会编译到内核中。

PS/2 类型键盘的核心轮询键盘驱动程序位于 drivers/char/kdb_keyboard.c 中。当 kgdboc 在名为 kdb_poll_funcs[] 的数组中填充回调时,此驱动程序将挂接到调试核心。kdb_get_kbd_char() 是轮询硬件以获取单个字符输入的顶层函数。

kgdboc 和 kms

kgdboc 驱动程序包含逻辑,用于在您使用 kgdboc=kms,kbd 时请求图形显示切换到文本上下文,前提是您具有具有帧缓冲控制台和原子内核模式设置支持的视频驱动程序。

每次进入内核调试器时,它都会调用 kgdboc_pre_exp_handler(),而 kgdboc_pre_exp_handler() 又会在虚拟控制台层中调用 con_debug_enter()。恢复内核执行时,内核调试器会调用 kgdboc_post_exp_handler(),而 kgdboc_post_exp_handler() 又会调用 con_debug_leave()

任何想要与内核调试器和原子 kms 回调兼容的视频驱动程序都必须实现 mode_set_base_atomicfb_debug_enterfb_debug_leave operations。对于 fb_debug_enterfb_debug_leave,可以选择使用通用的 drm fb 辅助函数或为硬件实现自定义函数。以下示例显示了 drivers/gpu/drm/i915/intel_display.c 中 .mode_set_base_atomic 操作的初始化

static const struct drm_crtc_helper_funcs intel_helper_funcs = {
[...]
        .mode_set_base_atomic = intel_pipe_set_base_atomic,
[...]
};

以下示例显示了 i915 驱动程序如何在 drivers/gpu/drm/i915/intel_fb.c 中初始化 fb_debug_enter 和 fb_debug_leave 函数以使用通用的 drm 辅助函数

static struct fb_ops intelfb_ops = {
[...]
       .fb_debug_enter = drm_fb_helper_debug_enter,
       .fb_debug_leave = drm_fb_helper_debug_leave,
[...]
};

贡献者

以下人员为本文档做出了贡献

  1. Amit Kale <amitkale@linsyssoft.com>

  2. Tom Rini <trini@kernel.crashing.org>

在 2008 年 3 月,本文档由以下人员完全重写

在 2010 年 1 月,本文档已更新为包含 kdb。