Kernel Debugging

来自osdev
跳到导航 跳到搜索

是人都会犯错。 其中一些错误可能最终成为你的操作系统的一部分。 由于找到错误比修复错误更难,因此本页面提供了可用于定位操作系统中的错误的常用技术列表。

调试语句和日志文件

第一个解决方案可能是最简单的,并且取决于你想要从调试器中获取什么样的信息。

使用诸如DDD或GDB之类的调试器的问题是它们需要操作系统才能运行...当你要调试的是操作系统本身时,这些没法用。

调试本质上是能够在特定断点处探测变量的内容。 当你的程序达到断点时,你可以探测该变量。

这也可以在不使用调试器的情况下实现,方法是将一行代码写入屏幕或某种日志。 给你感兴趣变量的内容 - 但这意味着提前知道要检查什么变量,什么时候检查, 并且意味着每次你要检查一组不同的变量时都会重新编译内核... 但这是最简单的解决方案。

伪断点

在完全打印或日志功能不可行的地方 (例如尝试隔离单个错误的汇编语言指令时),可以通过在代码中插入 “1: jmp 1b” 指令来创建一种 “伪断点”。 这些可用于通过代码执行二进制空间分割隔离定位 (通常称为 “binary chop”)。 这个思路是将无限循环放置在怀疑有故障代码的大约一半的部分位置; 如果CPU在错误发生之前就停止了,那么你知道错误在断点之后,否则,它必然在断点之前的代码中。 重复此过程,直到错误被隔离定位出来。 不幸的是,这仅在错误的结果可以与halt指令本身区分开来的情况下起作用,并且在出现多个重复进入循环 (例如数组循环) 问题的情况下几乎没有作用。 但是你可以使用虚拟机调试器对伪断点进行单步操作 (请参阅下面的 “将调试器与vm一起使用”)。

重要提示 #1: HLT指令是一个特权指令,因此它只会在你的内核中工作。 伪断点 “1: jmp 1b” 是无特权的,并且也可以在用户模式下工作。

重要提示 #2: gcc认为它比程序员更聪明,所以如果你使用 “while(1);”,那么它会错误地假设那个循环之后的一切都不需要,并且它会从二进制中删除所有这些代码。 你必须使用内联汇编,以便gcc认为应该保持你的代码。

asm volatile ("1: jmp 1b");

使用虚拟机

虚拟机是模拟另一台计算机的程序 (Java编码人员应该熟悉该概念)。

有一些虚拟机可以模拟x86机器,我最喜欢的是Bochs (http://bochs.sourceforge.net)。 Bochs能够在任何类型的软件中设置断点 (即使它是在没有调试信息的情况下编译的!),并提供了一个额外的 “调试 out 端口 “你可以轻松地从内核代码中访问以打印调试消息。

使用有此功能的虚拟机进行调试的主要缺点是,所有代码都会显示为汇编程序 (或二进制文件,取决于你使用的机器)

- 而不是你最初写的C/C++ 源代码。
此外,模拟虚拟机比实际机器慢,并且虚拟机的行为可能甚至不像 “真正的” 硬件。


话虽如此,使用VM还有很多其他优点。 例如,你不必重新启动即可测试新操作系统,只需启动VM即可。

另一个名为Simics (https://www.simics.net) 的虚拟机不仅能够显示断点并显示寄存器信息,而且还能够打开一个端口以用于与DDD进行调试 (simics命令为 “gdb-remote”)。 使用这种组合,当你通过操作系统时,可以看到你的C源代码! 但是,Bochs虚拟机执行操作系统的速度比Simics快得多,因此可以作为 “运行” 操作系统的更好的虚拟机,而对于那些难以发现问题的人来说,Simics是更好的 “调试器”。

使用串口

用QEMU编写日志文件

QEMU允许你将发送到COM1端口的所有内容重定向到主机上的文件。 要启用此功能,你必须在启动QEMU时添加以下标志:

-serial file:serial.log

... 这里 “serial.log” 是输出文件的路径。 启用此功能后,只需 将字符写入COM1端口 即可写入日志条目 (不支持通过串行端口从文件读取)。

在真正的硬件上

当你的真实计算机由于编程错误而重置时,你可能在屏幕上放置的任何内容都会立即消失。 如果你在调试视频卡,你通常会发现自己根本没有视觉调试方法。 如果你有一对使用null-modem电缆连接的计算机,则可以改为通过串行端口发送所有调试状态,并将其记录在更稳定的开发计算机上。 使用实际的串行终端也同样有效。 它需要一些额外的布线,但是它的工作原理相当简单,并且已证明是VM日志很好的替代品。

远程调试器/GDB

由于串行工作有双向通讯,因此在出现问题时,你也可以远程控制内核。 这可能是一个简单的接口,但你也可以将GDB连接到串行端口,并有可能运行一个完整的调试器。

然而,这相当棘手,因为它需要额外的硬件和编码到内核中的特殊支持。 你可能需要阅读 内核黑客操作方法 和 (至少) 远程调试-GDB手册第20章,你的调试器可能会首先引入更多的错误。

使用mini debugger

由于集成gdb是一项艰巨的任务,因此你可以使用 mini debugger 库,该库既小又简单,用ANSI C编写 (和一点汇编)。 这是一个最小化的交互式调试器 (转储寄存器和内存,反汇编指令),它工作在串行终端 (如VT100,VT220或模拟器,如PuTTY和minicom)。 可用于 AArch64x86-64 内核。 如果需要,可以将该库用作框架来实现自制功能齐全的内核调试器。

将调试器与虚拟机一起使用

将GDB与QEMU一起使用

你可以运行QEMU来侦听 “GDB连接”,然后再开始执行任何代码来调试它。

qemu -s -S <harddrive.img>

... 将设置QEMU侦听端口1234并等待与它的GDB连接。然后,从远程或本地shell:

gdb
(gdb) target remote localhost:1234

(如有必要,将localhost替换为远程IP/URL。)然后开始执行:

但这还不是全部,你可以使用 “-g” 在GCC下使用调试符号编译源代码。 这将在内核映像本身中添加所有调试符号 (从而使其更强大)。 还有一种方法可以使用 “objcopy” 工具将所有调试信息放入单独的文件中,该工具是GNU Binutils软件包的一部分。

objcopy --only-keep-debug kernel.elf kernel.sym

这将把调试信息放入一个名为 “kernel.sym” 的文件中。 以后你可以再剥离可执行文件的调试信息

objcopy --strip-debug kernel.elf

或者,如果你使用普通二进制文件作为内核映像,则可以

objcopy -O binary kernel.elf kernel.bin

生成可以使用先前提取的调试信息进行调试的二进制文件

你可以通过将GDB指向包含调试信息的文件来导入GDB中的符号

(gdb) symbol-file kernel.elf             ;在这种情况下,kernel.elf是实际的未剥离内核映像

从那里,你可以看到实际的C源代码,因为它每行运行! (使用GDB中的stepi指令执行每行的代码行。)

示例:

$ qemu -s -S c.img
warning: could not open /dev/net/tun: no virtual network emulation
Waiting gdb connection on port 1234
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()
(gdb) symbol-file kernel.b
Reading symbols from kernel.b...done.
(gdb) break kmain                        ; This will add a break point to any function in your kernel code.
Breakpoint 1 at 0x101800: file kernel/kernel.c, line 12.
(gdb) continue
Breakpoint 1, kmain (mdb=0x341e0, magic=0) at kernel/kernel.c:12
12      {

上面开始了代码执行,并将在上面 “break kmain” 中指定的kmain处停止。 你可以使用此命令随时查看寄存器

(gdb) info registers

我不会开始解释有关GDB的所有好处,但是正如你所看到的,它是调试操作系统非常强大的工具。

或者,你可以在不知道函数名称或地址的情况下强制代码中的断点。 在代码中的某个地方放置一个无限循环伪断点

asm volatile ("1: jmp 1b");

然后在运行gdb的终端上,当你的VM挂起时,按Ctrl^C停止执行,并在调试器提示符下将你放下。那里

(gdb) set $pc += 2

将跨过无限循环,你可以开始单步,一次执行一条指令

(gdb) si

将LLDB与QEMU一起使用

LLDB支持QEMU使用的GDB服务器,因此你可以在上一节中执行相同的操作,但是需要进行一些命令修改,因为LLDB具有一些与GDB不同的命令

你可以运行QEMU来侦听 “GDB连接”,然后再开始执行任何代码来调试它。

qemu -s -S <harddrive.img>

... 将设置QEMU侦听端口1234并等待与它的GDB连接。然后,从远程或本地shell:

lldb kernel.elf
(lldb) target create "kernel.elf"
Current executable set to '/home/user/osdev/kernel.elf' (x86_64).
(lldb) gdb-remote localhost:1234
Process 1 stopped
* thread #1, stop reason = signal SIGTRAP
    frame #0: 0x000000000000fff0
->  0xfff0: addb   %al, (%rax)
    0xfff2: addb   %al, (%rax)
    0xfff4: addb   %al, (%rax)
    0xfff6: addb   %al, (%rax)

(如有必要,用远程IP/URL替换本地主机。)然后开始执行:

(lldb) c
Process 1 resuming

要设置断点,请执行以下操作:

(lldb) breakpoint set --name kmain
Breakpoint 1: where = kernel.elf`kmain, address = 0xffffffff802025d0

使用bochs调试器

在bochs中触发断点的最简单方法是将 “xchg bx,bx” 放入你的代码中。 如下

asm volatile ("xchg %bx, %bx");

然后,当你运行虚拟机时,它将停止执行并在调试器提示符下删除你。 要从那里开始步进,可以使用

bochs:1> s

使用VirtualBox调试器

不幸的是,VirtualBox开发人员删除了 “-- start-dbg” 命令行选项,因此无法在VM开始执行之前设置断点。 但是你可以做一个与GDB类似的技巧,在你的代码中某个地方放置一个无尽的循环伪断点:

asm volatile ("1: jmp 1b");

然后,当执行挂起时,访问 “调试” 菜单下的 “命令行...” (如果你在机器窗口中没有调试菜单,则必须启用调试器,请参见下文)。 在调试器命令行中,首先要做的是必须停止VM运行:

VBoxDbg> stop

这应该转储寄存器。 但如果没有,则使用以下方法获取当前RIP寄存器值:

VBoxDbg> r

一旦你得到当前的RIP(RIP-指令地址寄存器),给它添加2,并输入以下命令设置一个新的RIP (我找不到从命令行引用RIP的任何方法,所以你必须使用常量):

VBoxDbg> r rip = 0xfffffffff1000102

检查当前的RIP是否正确指向无限循环后的指令:

VBoxDbg> r

你可以使用以下命令单步执行

VBoxDbg> p

GUI前端

虽然GDB提供了基于文本的用户界面 (可通过 “-tui” 命令行选项或在GDB提示符下输入 “wh”),但你可能希望对GDB使用一个可用的GUI前端。 这些包括但不限于:

* KDbg
* Insight
* DDD
* VisualKernel

附加到QEMU会话的工作方式类似于上述命令行GDB。

在托管环境中开发

另一种可能性,这也是一个很好的架构练习,是在像Linux这样的托管环境中对每个软件模块进行编码,然后将其移植到你的操作系统。 你也可以为内核代码执行此操作,而不仅仅是usermode程序。

假设你想要开发自己的VFS接口实现。 你已经为块设备创建了接口 (如果你已经在内核中实现了它,则无关紧要)。 在这种情况下,你可以将块设备接口实现为一组包装器,适配 接口适应POSIX调用。 然后,你将在这些包装器之上实现你的VFS接口 (即,将管理内核中的文件系统驱动程序的代码)。 然后,你可以在托管环境中测试和调试你的实现,当它成熟时,你将其链接到你的真实内核中,而不是链接到你的托管实现中。 你将最终在独立环境中测试你新引入的代码,以确保它也能在那里工作。

现在,针对一些专业人士。 首先,你可以使用自己喜欢的调试器。 你也可以使用单元测试等方法,如果使用正确的方法,它比手工测试软件要好得多。

这种方法有一些缺点。 例如,当你这样编写代码时,你比较远离目标环境。 所谓的 “独立(freestanding)” 环境对未定义的行为 (尤其是未初始化的变量) 更加敏感,这进一步加剧了问题的严重性。 你可以通过要求编译器在托管测试时执行积极的优化来解决此限制,这也使软件对未定义的行为更加有数。 但是,由于最佳调试环境是最终目标环境,因此在将代码引入真实内核时,你仍然需要测试代码。

另一个可能会吓到大多数人的缺点是,这种方法要求你事先始终如一地计划你的接口。 根据你的特定要求,你可能想要避免太长的计划阶段。 比如有时你想在模块正常工作后扔掉托管实现,如果没有单元测试你就不必再费心永远维护相同的接口了。

使用IDE

如果你使用VisualKernel插件,则可以使用Visual Studio调试linux内核模块。 这里有一个教程显示正常的调试会话: http://visualkernel.com/tutorials/kgdb/

如果你有一个包含GDB的i686-elf工具链,则可以使用 VisualGDB 来编译和调试内核。 有关在Visual Studio中配置VisualGDB进行内核开发的更多信息,请参见 Visual Studio

VirtualBox

使用命令 “VBoxManage startvm -- putenv VBOX_GUI_DBG_ENABLED = true <Name>” 启动虚拟机,然后窗口上将出现 “调试” 菜单。 你可以选择 “命令行” 来打开调试提示。

有用的命令:

  • cpu x - switch CPU
  • r - views registers
  • dq <Address> - 将给定虚拟地址的48字节内存转储为Qword(8字节)
  • .pgmphystofile "File Path" - 将物理内存转储到文件
  • info help/<Name> - 查看设备信息

相关论坛主题