Qemu是一款计算机模拟器,与Vmware和VirtualBox类似,但是Qemu能够模拟多种硬件,同时支持调试。本文主要介绍Qemu与GDB结合调试操作系统的引导。
引导用nasm编写,代码如下
mov ah, 0x88
int 0x15 ; 调用BIOS中断,获取虚拟内存大小,结果保存在ax,单位为kb
mov bx, ax
mov ah, 0x0e
mov al, 'H'
int 0x10 ; 调用BIOS中断,ah=0e代表打印al的内容到控制台
mov al, 'i'
int 0x10
jmp $ ; $ 指当前指令的位置,跳回自己,表示永久循环
times 510-($-$$) db 0
dw 0xaa55
nasm boot.asm -o boot.bin # 编译到二进制
nasm boot.asm -l boot.lst # 编译可查看的字节码对照表
生成的lst文件显示了编译后的二进制代码以及对应的地址,汇编源码
1 00000000 B488 mov ah, 0x88
2 00000002 CD15 int 0x15
4 00000004 89C3 mov bx, ax
6 00000006 B40E mov ah, 0x0e
7 00000008 B048 mov al, 'H'
8 0000000A CD10 int 0x10
9 0000000C B069 mov al, 'i'
10 0000000E CD10 int 0x10
12 00000010 EBFE jmp $
14 00000013 00<rept> times 510-($-$$) db 0
15 000001FE 55AA dw 0xaa55
这里的第二列是汇编后二进制指令的地址,在调试的时候会用到。
qemu-system-i386 -s -S -m 512 -hda boot.bin -nographic
qemu-system-i386
是目标硬件架构
-S
表示“freeze CPU at start up”,即启动后CPU停止等待,此时需要使用gdb来恢复运行。
-m
指定运行RAM大小,单位为MB
使用 -hda
、-hdb
、-hdc
和 -hdd
来引用 Parallel ATA (PATA) 硬盘,-cdrom
指定 CD 或 DVD 镜像文件或设备, 这些选项都采用文件名作为参数。光驱占用了 -hdc
的位置,因此不能同时使用这两个选项。
-boot
参数用于指定启动设备,c
代表从第一块硬盘启动,d
代表从CD-ROM启动
-nographic
用于不显示图形界面,在后台运行
qemu将boot.bin作为内核启动,开机时CPU处于实模式,CS=0xFFFF,IP=0x0000,所以PC=CS:IP=0xFFFF0,CPU执行0xFFFF0(ROM BIOS),这里后面的代码是固化在芯片中的,包括检查RAM、键盘、显示器、硬软磁盘,最后一步最重要,将磁盘0磁道0扇区(引导扇区,即boot.bin中的前512字节)读入到内存的0x7c00处,然后设置CS=0x07c0, IP=0x0000, 此时CPU就开始执行0x7c00的指令了,也就是引导扇区的指令。本文要调试的是内核引导,也就是从0x7c00处开始调试。
由于使用了-S
参数,此时PC还处在0xFFFF0处。
打开gdb,连接远程调试,qemu打开的gdbserver默认端口为1234
chiyiw@IdeaY400:~$ gdb
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()
(gdb)
连接成功后即可进行调试,可以看出目前pc=0xfff0,使用si
可以单步执行,我们要调试的是0x7c00处的代码,所以直接设置断点到0x7c00,然后使用c
继续运行,CPU运行到PC=0x7c00时会停止等待,不会执行0x7c00的指令。
(gdb) b *0x7c00
Breakpoint 1 at 0x7c00
(gdb) c
Continuing.
Breakpoint 1, 0x00007c00 in ?? ()
(gdb) i r
eax 0xaa55 43605
ecx 0x0 0
edx 0x80 128
ebx 0x0 0
esp 0x6ef0 0x6ef0
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x7c00 0x7c00
eflags 0x202 [ IF ]
cs 0x0 0
ss 0x0 0
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
使用i r
命令可以查看当前寄存器的值,我们在0x7c00处的代码是mov ah, 0x88
,此时可以看到ah还是0xaa,使用si
进行单步调试,再查看寄存器的值
(gdb) si
0x00007c02 in ?? ()
(gdb) i r
eax 0x8855 34901
ecx 0x0 0
edx 0x80 128
ebx 0x0 0
esp 0x6ef0 0x6ef0
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x7c02 0x7c02
eflags 0x202 [ IF ]
cs 0x0 0
ss 0x0 0
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
此时可以看到在执行了mov ah, 0x88
指令后,ah被置为了0x88,同时下一条指令的地址为0x00007c02。我们的目标是获取到虚拟内存的大小,即要执行int 0x15
指令,该指令位于0x7c02,这个指令的地址可以从boot.lst中看到,即0x7c00+0x0002,中断执行的结果会保存在ax中,单位是kb。使用si
单步执行。
(gdb) si
0x0000f85c in ?? ()
(gdb) i r
eax 0x8855 34901
ecx 0x0 0
edx 0x80 128
ebx 0x0 0
esp 0x6eea 0x6eea
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0xf85c 0xf85c
eflags 0x97 [ CF PF AF SF ]
cs 0xf000 61440
ss 0x0 0
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
此时可以看到0x7c02被执行,但是下一条指令不是0x7c04,同时ax中的值也没有变化,这是由于BIOS中断处理是一系列的操作,但是最后还是会回到当前指令处,即int 0x13
的一系列指令执行完后会回到0x7c04处,因此,我们只要在0x7c04处设一个断点即可查看int 0x13
的结果。
(gdb) b *0x7c04
Breakpoint 2 at 0x7c04
(gdb) c
Continuing.
Breakpoint 2, 0x00007c04 in ?? ()
(gdb) i r
eax 0xfc00 64512
ecx 0x0 0
edx 0x80 128
ebx 0x0 0
esp 0x6ef0 0x6ef0
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x7c04 0x7c04
eflags 0x202 [ IF ]
cs 0x0 0
ss 0x0 0
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
(gdb)
在执行0x7c04前,打印出寄存器的值,此时可以看到,虚拟内存的值已经被读入到了ax中,它的值是0xfc00,十进制是64512。也就是说,当前机器的虚拟内存为64512kb=63M。(使用0x88最多能够探测到64M)
至此,gdb调试内核的过程结束。以下是一些记录:
layout asm
显示当前运行的汇编指令,但是它是AT&T风格的, 可以使用set disassembly-flavor intel
换成intel风格set disassemble-next-line
在停止等待时显示即将执行的指令.gdbinit
文件配置gdb,在gdb启动时自动加载winheight name +lines
可以增加某个窗口的高度,使用-
降低高度,其中的name
可以使用命令补全x/x 0x01
查看内存地址 0x01
处的值,以十六进制显示,第二个x代表十六进制,同理可以用 x/c 0x01
以字符形式查看,d
为十进制,o
为八机制