当前位置: 首页 > 文档资料 > FreeBSD 开发手册 >

10.4 使用 DDB 进行在线内核调试

优质
小牛编辑
131浏览
2023-12-01

尽管作为离线调试方式的 kgdb 提供了非常高级的用户界面, 但它仍然有许多无法完成的工作。 最重要的几项功能是设置断点, 以及以单步方式执行内核代码。

如果您需要对内核进行较为底层的调试, 则可以利用称为 DDB 的在线调试器。 它能够让您设置断点、 单步执行内核函数, 查看或修改内核变量等。 然而, 它不能访问内核源代码文件, 而且只能访问全局和静态符号, 而不像 gdb 那样能访问全部调试符号。

要配置您的内核使其包含 DDB, 需要在您的内核配置文件中加入

options KDB
options DDB
然后重新编译。 (参见 FreeBSD 使用手册 以了解如何配置 FreeBSD 内核的进一步详情)。

注意: 如果您使用的引导块版本较早, 则可能完全无法加载调试符号。 遇到这种情况时请更新引导块; 新版本的引导块能够自动加载 DDB 符号了。

只要使用了包含 DDB 功能的内核, 就可以通过多种途径进入 DDB 了。 第一种, 同时也是最简单的方式, 是在启动时指定引导参数 -d。 这样, 内核就会以调试方式启动, 并在开始检测设备之前进入 DDB。 这样一来, 即使是那些用于设备检测/挂接的函数, 也都能很容易地进行调试了。 使用 FreeBSD-CURRENT 的用户, 则需要在引导菜单中选择第六项来进入引导加载器提示符。

第二种情形, 是在系统启动之后进入调试器。 有两种简单的方法来达到这一目的。 如果您希望从命令行进入调试器, 只需简单地输入下面的命令:

# sysctl debug.kdb.enter=1

注意: 如果需要强制产生一次 panic, 可以使用下面的命令:

# sysctl debug.kdb.panic=1

另外, 如果您能操作系统的控制台, 也可以使用一组热键。 默认情况下, 可以通过连续按下 Ctrl+Alt+ESC 来进入调试器。 对 syscons 而言, 还可以重新映射具体的按键序列, 因此您应仔细检查来确认实际的按键。 此外, 在使用串口控制台时, 也可以使用串口线的 BREAK 来进入 DDB (在内核配置文件中, 需要加入 options BREAK_TO_DEBUGGER)。 这一选项并非默认值, 因为许多串口适配器会莫名其妙地生成 BREAK, 例如在拔下电缆时。

第三种方式是, 如果事先对内核进行了配置,在发生 panic 时, 就会自动进入 DDB。 显然, 将一台无人看管的服务器的内核进行这样的配置, 不是一项理性的做法。

如果希望启用无人值守的功能, 则可以增加:

options    KDB_UNATTENDED

到内核的编译配置文件中, 并重新联编和安装内核。

DDB 的命令和某些 gdb 命令大体类似。 您需要做的第一件事可能是设置断点:

break function-name address

默认情况下, 调试器接受十六进制的数字。 不过, 为了与符号的名字相区分, 以字母 a-f 开头的数字必须带有 0x 前缀 (对其他数字而言, 这是可选的)。 除此之外, 也可以使用简单的表达式, 例如: function-name + 0x103

要退出调试器并继续执行, 只需输入:

continue

要查看调用堆栈, 则可以使用:

trace

注意: 需要说明的是, 如果您是使用热键进入 DDB, 则内核正在执行中断服务, 因而此时的调用堆栈可能用处不大。

要去掉一处断点, 可以使用:

del
del address-expression

第一种形式可以在遇到断点之后使用, 起作用是删除当前的断点。 第二种形式可以用来删除任意的断点, 但您需要精确地指定地址; 这些地址可以通过下面的命令来获得:

show b

或者:

show break

要单步执行内核, 则可以使用:

s

这个命令会跟入函数, 但您也可以让 DDB 跟踪指令的执行, 直到对应的返回语句为止:

n

注意: 这个命令与 gdbnext 语句不同; 它更像 gdbfinish。 多按几次 n 则表示继续执行。

要检查内存中的数据, 可以用 (下面是一个例子):

x/wx 0xf0133fe0,40
x/hd db_symtab_space
x/bc termbuf,10
x/s stringbuf
来完成对 字/半字/字节 的访问, 并使用 十六进制/十进制/字符/字符串 的格式来显示。 在逗号后的数字是对象的数量。 要显示接下来的 0x10 项, 可以简单地使用:

x ,10

类似地, 使用

x/ia foofunc,10
可以对 foofunc 的前 0x10 条指令进行反汇编, 并同时显示它们相对于 foofunc 起点的偏移量。

要修改内存, 应使用 write 命令:

w/b termbuf 0xa 0xb 0
w/w 0xf0010030 0 0

这个命令的修饰参数 (b/h/w) 指定了将要写的数据的尺寸, 其后的第一个表达式表示将要写入的地址, 而余下的责备认为是写入连续内存位置的数据。

如果需要知道寄存器的当前内容, 使用:

show reg

除此之外, 也可以显示单个寄存器的值, 例如:

p $eax
并修改之:

set $eax new-value

如果希望从 DDB 调用某个内核函数, 可以简单地使用:

call func(arg1, arg2, ...)

调试器将显示其返回值。

如果想查看 ps(1) 风格的进程表, 使用:

ps

了解了内核出现了什么问题之后, 一般会希望重新启动系统。 您应牢记的一点是, 取决于内核之前出现问题的严重程度, 内核中的某些组件可能已经无法正常工作。 此时, 应执行下列操作之一来关闭和重新启动系统:

panic

这会让内核执行转存操作并重新启动, 这样, 您就可以在之后使用 gdb 进行更高级的分析了。 这个命令通常还需要配合使用一个 continue 语句才能够完成。

call boot(0)

这个命令可能是一种完好地关闭正运行的系统, 对所有磁盘执行 sync() 操作, 并正常重启系统的好办法。 假如内核中的磁盘和文件系统接口没有遭到破坏的话, 这样做能够几乎完全正常地关闭系统。

call cpu_reset()

这是在发生重大问题时的终极解决方法, 其作用与按下复位按钮是一样的。

假如需要简短的命令介绍, 可以使用:

help

我们强烈建议您打印一份 ddb(4) 的联机手册之后再开始调试, 因为在单步执行内核时, 查看联机手册是很麻烦的。