生成更多调试信息

这洞府里可有称手的兵器?

相信你已经发现了, 到目前为止 OS lab 实验环境一直都没有支持调试, 可谓万事俱备, 只欠东风.

不用多说你也知道, 调试是一件非常重要的事情, 尤其是在 OS 这类大型软件中. 这也是为什么 Linux 内核在初始化的时候硬是要先提供一个几乎在什么地方都能用的 printk 函数. 还好目前我们并不是要从零开始写个 EOS, 或者是给 EOS 写驱动. 我们的目的只是为了借助调试器, 通过在 EOS 内下断点分析其行为.

在此之前, 一些额外的手段也是必要的.

额外的手段

这种需求并不奇怪: 有的时候我们需要根据函数名来确定其虚拟地址, 或者是根据虚拟机中获取到的虚拟地址来判断当前正在执行哪个函数. 然而, 此时你也许会一脸懵逼, 不知如何是好.

你应该还记得, 我们在构建 EOS 时, 针对每一个 .asm 文件, 不仅输出了 .o 或者 .bin, 还输出了一个 .lst 文件. 这个文件的内容正是汇编源文件中每条语句对应的二进制机器指令, 以及其所在的内存地址. 对于内核 kernel.dll 来说, 我们也能生成一个类似的东西:

i686-w64-mingw32-objdump -M intel -d build/kernel.dll

objdump 可以用来输出生成的二进制中的许多信息, 我们通常用它来对二进制进行反汇编. 上述命令会以 Intel 语法 (-M intel) 输出 build/kernel.dll 的反汇编信息 (-d). 执行后你应该能看到控制台中输出了一大堆内容, 这就是我们需要的信息.

你会说: 刚刚的输出甚至都一眼望不到头, 我们要怎么看清呢? 没关系, 输出重定向到文件即可:

i686-w64-mingw32-objdump -M intel -d build/kernel.dll > build/lst/kernel.lst

此时 build/lst/kernel.lst 就已经存储了刚刚输出的反汇编信息.

支持调试

使用 QEMU 作为虚拟机运行 EOS 可以让我们通过 GDB 远程接入进行调试:

qemu-system-i386 -S -s -drive file=build/floppy.img,if=floppy,format=raw &

这个命令和之前启动 QEMU 的命令有两点不同:

  1. -S -s: 前者会让 CPU 启动后立刻暂停, 方便我们调试; 后者会在本机 1234 端口开启一个 GDB stub;

  2. 结尾的 &: 我不说你可能都不会注意到这个符号. 它的目的是让 QEMU 在后台运行, 不要占用终端, 否则我们没办法在终端中操作 GDB.

运行之后, 你会看到弹出窗口中的 QEMU 停住了, 等待我们用 GDB 去远程连接它. 这个时候我们需要在终端中启动 GDB:

gdb

关于 GDB 的详细用法, 你可以参考第三章. 此时我们需要先指定要调试的文件, 因为我们要调试内核, 所以执行:

(gdb) file build/kernel.dll

你会看到 GDB 已经载入了其中的调试符号. 接着我们开始远程调试:

(gdb) target remote :1234

此时 QEMU 依然不为所动, GDB 输出了以下信息:

Remote debugging using :1234
0x0000fff0 in ?? ()

并且等待我们的输入, 刚才我们说到 QEMU 目前处于暂停状态, CPU 还在 FFF0h 处等着执行 BIOS 的指令, 于是我们在 GDB 的终端中输入:

(gdb) c

此时 QEMU 就会继续执行了.

如何下断点

比方说我们想调试 EOS 中进程的创建, 此时需要在 PsCreateProcess 函数下断点. 我们先回到终端, 按下 Ctrl+C 后, GDB 会开始接受我们的命令:

(gdb) b PsCreateProcess

然后输入 c 继续执行. 此时如果我们在 EOS 中运行之前的 hello.exe, GDB 会立即停住:

Program received signal SIGTRAP, Trace/breakpoint trap.
0x800110da in PsCreateProcess ()
(gdb)

这说明我们的断点起了作用. 之后愿意单步执行, 还是想输出内存地址的值, 一切都随你.

调试用户应用程序

那如果我们想调试 hello.exe 怎么办呢? 这个时候你想直接在 hello.exemain 函数下断点, GDB 会报错:

(gdb) b main
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n])

我们必须首先让 GDB 载入 hello.exe:

(gdb) add-symbol-file user/build/hello.exe

这个时候就可以在 main 函数下断点了:

(gdb) b main
Breakpoint 1 at 0x401b74: file /home/oslab/eos/user/src/hello.c, line 3.
(gdb)

还可以更方便

我们每次打开 GDB 都需要手动载入内核 DLL, 然后再连接 1234 端口的 QEMU. 其实可以在 ~/eos 目录新建一个 .gdbinit 文件:

file build/kernel.dll
target remote | qemu-system-i386 -S -gdb stdio -drive file=build/floppy.img,if=floppy,format=raw

GDB 会自动执行这两个命令. 需要解释一下: 第二条命令会在 target remote 的同时启动 QEMU, 此时 GDB 会通过管道直接和 QEMU 建立连接; 而 QEMU 这边由于没有接收到参数 -s, 所以不会在端口 1234 监听 GDB, 取而代之的是参数 -gdb stdio, 即通过标准输入输出 (管道) 接收 GDB 的命令.

此时 eos 的目录结构为:

eos
├── build
├── src
├── user
├── util
├── vm
├── .gdbinit      # GDB 初始化脚本
├── Makefile
└── License.txt

为了防止 GDB 由于安全原因不自动加载初始化脚本, 我们需要新建 ~/.gdbinit 并写入:

add-auto-load-safe-path ~/eos/.gdbinit

这样我们就可以更方便的开启调试了, 只需要:

cd ~/eos
gdb

GDB 就会和 QEMU 同时启动, 立即进入调试模式.

更新 Makefile

目前我们有两个新需求:

  1. 每次构建内核时, 生成新的 kernel.lst;

  2. 可以一键开启调试.

更新后的 Makefile 如下:

--- Makefile
+++ Makefile
@@ -1,5 +1,16 @@
+# external parameters
+export DEBUG = 1
+export OPT_LEVEL = 1
 export CROSS_PREFIX = i686-w64-mingw32-
-DEBUG_ARG = -D_DEBUG -g
+
+# judge if is debug mode
+ifeq ($(DEBUG), 0)
+	DEBUG_ARG =
+	OPT_ARG = -O$(OPT_LEVEL)
+else
+	DEBUG_ARG = -D_DEBUG -g
+	OPT_ARG =
+endif

 # directories
 export TOP_DIR = $(shell if [ "$$PWD" != "" ]; then echo $$PWD; else pwd; fi)
@@ -30,7 +41,7 @@
 USER_IMPORT := $(wildcard $(USER_DIR)/import/*.*)

 # C compiler
-CFLAGS := $(DEBUG_ARG)
+CFLAGS := $(DEBUG_ARG) $(OPT_ARG)
 CFLAGS += -m32 -c -nostdlib -nostdinc -fsigned-char -pipe
 CFLAGS += -fno-builtin -fno-omit-frame-pointer -ffreestanding
 PRIVATE_CFLAGS := -D_KERNEL_ -D_I386
@@ -47,6 +58,10 @@
 PRIVATE_LDFLAGS += --out-implib $(TARGET_DIR)/libkernel.a
 export LD = $(CROSS_PREFIX)ld $(LDFLAGS)

+# objdump
+OBJDFLAGS := -M intel -d
+export OBJD = $(CROSS_PREFIX)objdump $(OBJDFLAGS)
+
 # virtual machine
 VMFLAGS := -q
 VMFILE = vm/bochsrc
@@ -59,7 +74,7 @@
 IMGEDIT_DIR = $(UTIL_DIR)/imgedit
 IMGEDIT = $(IMGEDIT_DIR)/build/imgedit

-.PHONY: all kernel user run runvm clean
+.PHONY: all kernel user run runvm debug clean

 all: kernel user $(TARGET_DIR)/floppy.img

@@ -76,6 +91,9 @@
 runvm: all
 	-$(QEMU) file=$(TARGET_DIR)/floppy.img,if=floppy,format=raw

+debug: all
+	-gdb
+
 clean:
 	$(MAKE) -C $(USER_DIR) clean
 	-rm -rf $(USER_DIR)/sdk/*
@@ -100,6 +118,7 @@
 # EOS kernel
 $(TARGET_DIR)/kernel.dll: $(KERNEL_TARGETS)
 	$(LD) $(PRIVATE_LDFLAGS) -o $@ $(KERNEL_TARGETS)
+	$(OBJD) $@ > $(LST_DIR)/kernel.lst

 # EOS object
 $(OBJ_DIR)/%.o: $(KERNEL_SRC_DIR)/%.c

一些问题

在目前的调试环境中还存在一个比较严重的问题: 如果尝试使用 GDB 调试 EOS 内核, 并且在调试断点时输入 layout src, 你会发现 GDB 提示你 No Source Avaliable. 这会对你的调试体验造成一定的影响, 因为本来你可以使用 p <var_id> 来输出一个变量的值, 但是由于 GDB 找不到源代码, 你目前并不能这么做, 只能通过调用约定EBP 来推断你想要输出的变量的地址.

但是你会发现, 在调试用户应用程序时并不会出现这种情况. GDB 可以完美的读取到用户应用程序的调试符号信息, layout src 一切正常, 并且可以输出所有变量的值.

这个问题教程作者目前依然没有解决, 推测可能由以下原因造成:

  • MinGW 在编译 DLL 时输出了不能被 GDB 正确读取的调试信息;

  • NASM 输出了不正确的调试信息, 导致最终连接而成的 DLL 无法正常被 GDB 读取;

  • GDB 不能正确处理调试 DLL 的情况;

  • 其他未知原因.

上述推测尚未经过任何证实, 如果你有兴趣, 可以尝试自行解决这个问题.

作者并不是全能的

如你所见, 作者也遇到了很棘手的问题. 任何人类都有弱点 (究极生物/究极反则生命体除外), 作者并非全知全能, 并且也会犯错.

如果你发现作者编写的本教程中存在疏漏甚至错误, 或者你在某些问题上与作者持不同意见, 欢迎和作者 Max Xing (x#MaxXSoft.net) 讨论. 说不定下一版实验教程, 你也会成为作者.

Last updated

Was this helpful?