System Learning Daily 6

一个和GDT有关的Bug和相关Debug工具的使用。

Segment Descriptor中的B/D位

这是一个不小心导致的Bug。

IntelIntel Architecture Software Developer's Manual, Volume 3中写到:

D/B (default operation size/default stack pointer size and/or upper bound) flag :
Performs different functions depending on whether the segment descriptor is an executable code segment, an expand-down data segment, or a stack segment. (This flag should always be set to 1 for 32-bit code and data segments and to 0 for 16-bit code and data segments.)

进入保护模式之后,我们必然要使用32bit的代码段和数据段。但是我忘记将其置于1了。

于是就出现了一种奇怪的现象。

  1. IF flag set,程序在跳转到32bit代码段一瞬间就triple fault
  2. IF flag clear,程序可以运行,跳转指令似乎还是可以运行的,但是有关数据的指令,无法修改数据。这就造成一种怪象。例如下面这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void osloader_main(void)
{
int counter = 0;
int counter2 = 0;
int counter3 = 0;
while(1)
{
counter1 += counter2
counter2++;
if(counter2 % 2 == 0)
{
counter3++;
}
}
}

在调试过程中,里面的这些counter变量都不会改变,一直仅仅保持一个数值。在调试器中,程序还是在一直运行着的。相当诡异了。

看下图。

avator

键入siprint counter

avator

counter的数值根本没变化,这很让人迷惑。

工具

虚拟机

我是用的虚拟机有两个,一个是QEMU,一个是Bochs

具体优缺点我不太好分析,对我来说,QEMU我使用起来更加的熟悉。然而Bochs有个非常好的地方在于其自带的调试器,当然Bochs也可以使用GDB进行调试,不过自带的调试器和GDB只能选择一个。

列出来我自己的QEMU的启动参数吧。

1
2
3
4
5
6
qemu-system-i386 -m 128M -hda build/img.bin -S \
-gdb tcp::1234 \ # 设置GDB-Stub
-monitor stdio \ # 设置*QEMU*的monitor窗口
-serial file:com1.log \ # 设置串口输出的位置,可用来Debug
-d int,cpu_reset \ # 设置,当产生中断,或者cpu reset的时候,写log
-D qemu.log # 指定log的路径

Bochs的参数非常简单,就不在此列出了。

调试器

显然,我用的是GDB了。值得一提的是,我是用了一个叫做GEF的GDB插件来优化GDB的使用体验。GEF非常适合这种可能要随时检查寄存器和栈的应用场景。

不得不说,GDB还是很强大的,使用Python脚本对其进行拓展更是让其异常强大。具体参考我之前的System Learning Daily 3

工具链

就是GCC那一套了,汇编器使用nasm。

这种开发选择GCC总是没问题的。具体是根据OSDev上面的Configure构建的得到的。

gcc

需要注意的就是使用一些编译选项了,我用的是

1
CC_FLAGS = -std=gnu11 -ffreestanding -m32 -masm=intel -Wall -Wextra -Wno-address-of-packed-member -g -O0

ld

有些地方确实需要会写链接脚本(Linker Script),至少在Kernel的编译中,需要使用链接脚本。

objdump

用来生成flat binary文件和Debug用的调试符号文件。

其他

主要是一些命令行工具。

dd

构建镜像的时候使用,直接通过dd来构建一个raw的文件,里面放入MBR,OSLoader,Kernel等。

截取makefile的一个小片段

1
2
3
dd if=/dev/zero of=${IMG_BIN} bs=1024 count=1024 conv=notrunc
dd if=boot/boot.bin of=${IMG_BIN} bs=512 count=1 conv=notrunc seek=0
dd if=osloader/osloader.bin of=${IMG_BIN} bs=512 conv=notrunc seek=

xxd

用来显示一个二进制文件,这是我发现读硬盘Bug的关键。配合diff使用,最终发现了硬盘读不全的问题。

diff

1
2
3
xxd dump.bin > build/dump.bin.txt
xxd build/img.bin > build/img.bin.txt
diff build/dump.bin.txt build/img.bin.txt -y | less

当我们需要注意检查QEMU的内存中的内容是否与镜像一致时。

我们首先在QEMU的Monitor窗口中输入

1
memsave 0x7c00 0x2000 dump.bin

保存从0x7c00开始的内存,长度为0x2000,到dump.bin文件中。

再看上面那个 diff的脚本,可以直接比较镜像内容与内存中的内容的差别。