使用 kgdb、kdb 和内核调试器内部原理

作者:

Jason Wessel

简介

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

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

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

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

编译内核

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

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

kgdb 的内核配置选项

要启用 CONFIG_KGDB,您应该在 内核黑客 ‣ 内核调试 下查找,并选择 KGDB:内核调试器

虽然这不是 vmlinux 文件中必须要有符号的硬性要求,但没有符号数据,gdb 往往不是很有用,因此您需要启用 CONFIG_DEBUG_INFO,它在配置菜单中称为 使用调试信息编译内核

建议但非必需,您启用 CONFIG_FRAME_POINTER 内核选项,该选项在配置菜单中称为 使用帧指针编译内核。此选项将代码插入到已编译的可执行文件中,该代码将帧信息保存在寄存器中或堆栈上的不同点,这允许诸如 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:为 kgdb 包含 kdb 前端。理论上,如果您计划在串行端口上使用 kdb,则在配置 kgdb 时,您应该已经选择了一个 I/O 驱动程序,例如 CONFIG_KGDB_SERIAL_CONSOLE 接口。

如果您想将 PS/2 样式键盘与 kdb 一起使用,则应选择 CONFIG_KDB_KEYBOARD,它在配置菜单中称为 KGDB_KDB:键盘作为输入设备CONFIG_KDB_KEYBOARD 选项在 gdb 到 kgdb 的接口中不用于任何内容。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 驱动程序最初是一个缩写,意思是“console over kgdb”。今天,它是配置如何从 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 上配置或打开了控制台,则无需指定波特率。

更多示例

您可以根据您使用的是 kdb 还是 kgdb,在以下几种场景中配置 kgdboc 以使用键盘和/或串行设备。

  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,后者会从 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

    查看内核 syslog 缓冲区

    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 来执行其工作

    • 到 gdb 特定数据包格式的转换以及从 struct pt_regs 的转换

    • 特定于体系结构的陷阱钩子的注册和注销

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

    • 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 命令。

    • 用于直接向 I/O 驱动程序发送消息(绕过内核日志)的 kdb_printf() 的实现。

    • 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 寄存器转换为 GDB 寄存器

参数

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 寄存器转换为 GDB 寄存器

参数

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 寄存器转换为 ptrace 寄存器。

参数

unsigned long *gdb_regs

一个指针,用于保存我们从 GDB 收到的寄存器。

struct pt_regs *regs

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

gdb_regs 中的 GDB 寄存器转换为 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);
    void (*enable_nmi)(bool on);
};

成员

gdb_bpt_instr

触发断点的指令。

flags

断点的标志,目前只有 KGDB_HW_BREAKPOINT

set_breakpoint

允许体系结构指定如何设置软件断点。

remove_breakpoint

允许体系结构指定如何删除软件断点。

set_hw_breakpoint

允许体系结构指定如何设置硬件断点。

remove_hw_breakpoint

允许体系结构指定如何删除硬件断点。

disable_hw_break

允许体系结构指定如何禁用单个 CPU 的硬件断点。

remove_all_hw_break

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

correct_hw_break

允许体系结构指定如何校正硬件调试寄存器。

enable_nmi

管理 NMI 触发的 KGDB 进入

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;
};

成员

name

I/O 驱动程序的名称。

read_char

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

write_char

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

flush

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

init

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

deinit

指向将取消初始化设备的函数的指针。暗示此 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 会在串行核心中调用一个回调,而串行核心又会使用 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 和键盘

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

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

kgdboc 和 kms

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

每次进入内核调试器时,它都会调用 kgdboc_pre_exp_handler(),后者又调用虚拟控制台层中的 con_debug_enter()。恢复内核执行时,内核调试器会调用 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 驱动程序如何初始化 fb_debug_enter 和 fb_debug_leave 函数以在 drivers/gpu/drm/i915/intel_fb.c 中使用通用 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。