A virtual machine is an indirection engine which redirects code and data inside of the ‘guest’ sandbox

Three ways of VM implementation

  1. Virtualization, direct execution (VMware, Virtual PC, Xen)
  2. Dynamic (just-in-time) translation (QEMU)
  3. Emulation (Bochs, Gemulator)

对一台安装了Windows NT 系列操作系统的PC来说,按下电源开关之后,CPU中首
先开始运行的是Bios,然后是MBR,接着是引导扇,然后就是NTLDR。ntoskrnl.exe和
hal.dll 都是由NTLDR来加载的。也就是说,运行NTLDR的时候,系统中还没有任何应
用程序或者驱动,当然也就没有任何基于软件的调试器可用。当然,无所不能的硬件
调试器肯定是可以的,可惜我们没有硬件调试器。

幸好有了Bochs。Bochs是一个基于LGPL的开源x86 虚拟机软件。Bochs的CPU指令
是完全自己模拟出来的,这种方式的缺点是速度比较慢;优点是具有无以伦比的可移
植性:有Gcc的地方就可以有Bochs。甚至已经有了跑在PocketPC上的Bochs。

现在的Bochs 已经实现了一定程度的调试功能,虽然在易用性和功能上还无法和
WinDbg、SoftICE相比,但优势也是很明显的:对跑在Bochs里面的代码来说,这就是
“硬件调试器”。

对Windows 版本的Bochs来说,安装目录下的bochsdbg.exe就是Bochs的调试版本。
用它来运行Bochs虚拟机就可以进行“硬件调试”。

Bochs的调试命令风格是按照GDB习惯来设计的,这对于用惯了WinDbg的人来说无
疑是痛苦的,好在这是个开源软件,看着不顺眼可以考虑自己改改。

目前版本的Bochs(Version 2.1.1)支持的调试命令如下:

[注意]

1、Bochs的文档和帮助信息中的使用说明与真实情况之间存在很大的差错和缺失,
下面的命令说明根据源码作了很多补充和修正。

2、其中涉及到的seg(段)、off(偏移)、addr(地址)、val(值)等数字,
可以使用十六进制、十进制或者八进制,但必须按照如下形式书写:

十六进制   0xCDEF0123
八进制     01234567
十进制     123456789
尤其要注意,Bochs不能自动识别16进制的数字,也不接受12345678h这种写法。

[执行控制]

c|cont                   向下执行,相当于WinDBG的“g”。

s|step|stepi [count]     单步执行,相当于WinDBG的“t”,count 默认为 1。

p|n|next                 单步执行,类似于WinDBG的“p”。

q|quit|exit             退出调试,同时关闭虚拟机。

Ctrl-C                   结束执行状态,返回调试器提示符。

Ctrl-D                   if at empty line on command line, exit
(至少在Windows版本中我没有发现Ctrl-D有什么功能)

[执行断点]

vb|vbreak [seg:off]         在虚拟地址上下断点。

lb|lbreak [addr]             在线性地址上下断点,相当于WinDBG的“bp”。

pb|pbreak|b|break [addr]     在物理地址上下断点。(为了兼容GDB的语法,地址前
可以加上一个“*”)。

blist                       显示断点状态,相当于WinDBG的“bl”。

bpd|bpe [num]               禁用/启用断点,WinDBG的“be”和“bd”。num是断
点号,可以用blist命令查询。

d|del|delete [num]           删除断点,相当于WinDBG的“bc”。mum是断点号,可
以用blist命令查询。

[读写断点]

watch read [addr]       设置读断点。
watch write [addr]       设置写断点。
unwatch read [addr]     清除读断点。
unwatch write [addr]     清除写断点。
watch                   显示当前所有读写断点。
unwatch                 清除当前所有读写断点。
watch stop|continue     开关选项,设置遇到读写断点时中断下来还是显示出来但
是继续运行。

[内存操作]

x   /nuf [addr]   显示线性地址的内容
xp /nuf [addr]   显示物理地址的内容
n           显示的单元数
u           每个显示单元的大小,u可以是下列之一:
b BYTE
h WORD
w DWORD
g DWORD64
注意: 这种命名法是按照GDB习惯的,而并不是按照inter的规范。

f           显示格式,f可以是下列之一:
x 按照十六进制显示
d 十进制显示
u 按照无符号十进制显示
o 按照八进制显示
t 按照二进制显示
c 按照字符显示
n、f、u是可选参数,如果不指定,则u默认是w,f默认是x。如果前面使用过x或
者xp命令,会按照上一次的x或者xp命令所使用的值。n默认为1。addr 也是一个
可选参数,如果不指定,addr是0,如过前面使用过x或者xp命令,指定了n=i,
则再次执行时n默认为i+1。

setpmem [addr] [size] [val]     设置物理内存某地址的内容。

需要注意的是,每次最多只能设置一个DWORD:
这样是可以的:
<bochs:1>   setpmem 0x00000000 0x4 0x11223344
<bochs:2> x /4 0x00000000
[bochs]:
0x00000000 <bogus+       0>:     0x11223344 0x00000000 0x00000000 0x00000000
这样也可以:
<bochs:1>   setpmem 0x00000000 0x2 0x11223344
<bochs:2> x /4 0x00000000
[bochs]:
0x00000000 <bogus+       0>:     0x00003344 0x00000000 0x00000000 0x00000000
或者:
<bochs:1>   setpmem 0x00000000 0x1 0x20
<bochs:2> x /4 0x00000000
[bochs]:
0x00000000 <bogus+       0>:     0x00000020 0x00000000 0x00000000 0x00000000
下面的做法都会导致出错:
<bochs:1>   setpmem 0x00000000 0x3 0x112233
Error: setpmem: bad length value = 3
<bochs:2>   setpmem 0x00000000 0x8 0x11223344
Error: setpmem: bad length value = 8

crc [start] [end]     显示物理地址start到end之间数据的CRC。

[寄存器操作]

set $reg = val               设置寄存器的值。现在版本可以设置的寄存器包括:
eax ecx edx ebx esp ebp esi edi
暂时不能设置:
eflags   cs   ss   ds   es   fs   gs

r|reg|registers reg = val   同上。

dump_cpu                     显示完整的CPU信息。

set_cpu                     设置CPU状态,这里可以设置dump_cpu所能显示出来的
所有CPU状态。

[反汇编命令]

u|disas|disassemble [/num] [start] [end]
反汇编物理地址start到end 之间的代码,如
果不指定参数则反汇编当前EIP指向的代码。
num是可选参数,指定处理的代码量。
set $disassemble_size = 0|16|32     $disassemble_size变量指定反汇编使用的段
大小。

set $auto_disassemble = 0|1         $auto_disassemble决定每次执行中断下来的
时候(例如遇到断点、Ctrl-C等)是否反汇
编当前指令。

[其他命令]
trace-on|trace-off       Tracing开关打开后,每执行一条指令都会将反汇编的结果
显示出来。

ptime                   显示Bochs自本次运行以来执行的指令条数。

sb [val]                 再执行val条指令就中断。val是64-bit整数,以L结尾,形
如“1000L”

sba [val]               执行到Bochs自本次运行以来的第val条指令就中断。val是
64-bit整数,以L结尾,形如“1000L”

modebp                   设置切换到v86模式时中断。

record ["filename"]     将输入的调试指令记录到文件中。文件名必须包含引号。

playback ["filename"]   回放record的记录文件。文件名必须包含引号。

print-stack [num]       显示堆栈,num默认为16,表示打印的条数。

?|calc                   和WinDBG的“?”命令类似,计算表达式的值。

load-symbols [global] filename [offset]
载入符号文件。如果设定了“global”关键字,则符号针
对所有上下文都有效。offset会默认加到所有的symbol地
址上。symbol文件的格式为:"%x %s"。

[info命令]

info program             显示程序执行的情况。
info registers|reg|r     显示寄存器的信息。
info pb|pbreak|b|break   相当于blist
info dirty               显示脏页的页地址。
info cpu                 显示所有CPU寄存器的值。
info fpu                 显示所有FPU寄存器的值。
info idt                 显示IDT。
info gdt [num]           显示GDT。
info ldt                 显示LDT。
info tss                 显示TSS。
info pic                 显示PIC。
info ivt [num] [num]     显示IVT。
info flags               显示状态寄存器。
info cr                 显示CR系列寄存器。
info symbols             显示symbol信息。
info ne2k|ne2000         显示虚拟的ne2k网卡信息。

弄明白了调试命令,接下来就可以着手进行NTLDR的调试工作了。下面所进行的工
作都是在Windows版Bochs 2.1.1上实现的。我们假设读者了解Bochs的基本使用方法和
术语。

首先要安装一个Windows NT 4的Bochs虚拟机。

1、创建虚拟硬盘。
运行bximage.exe,创建一个500M、flat模式的虚拟硬盘文件“C.img”。

2、创建一个Windows NT安装光盘的ISO文件“nt.iso”
如果你打算直接用光盘安装,也可以省去这一步。

3、创建bochsrc.txt
内容可参考下面:
###############################################################
megs: 32

romimage: file=$BXSHAREBIOS-bochs-latest, address=0xf0000
vgaromimage: $BXSHAREVGABIOS-lgpl-latest

ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="C.img", mode=flat, cylinders=1015, heads=16, spt=63
ata0-slave: type=cdrom, path="nt.iso", status=inserted
newharddrivesupport: enabled=1

boot: cdrom

log: nul

mouse: enabled=1

clock: sync=realtime, time0=local
###############################################################

4、创建start.bat
内容如下:
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
::假设你的Bochs安装在D:ProgramBochs
set BXSHARE=D:ProgramBochs
%BXSHARE%bochs.exe -q -f bochsrc.txt
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

把C.img、nt.iso、bochsrc.txt、start.bat放到同一个目录下,运行start.bat,
进行Windows NT的安装。

事实上,如果只是为了调试MBR、引导扇和NTLDR 的话,并没有必要安装完整的操
作系统,只要根目录下有ntldr等那几个文件就可以了。这里之所以安装Windows NT而
不是Windows 2000或者更高版本,一方面是考虑速度问题,另一方面,Windows NT 是
可以在Bochs上确保顺利完成安装的。如果要调试Windows 2000/XP/2003 的NTLDR,只
需用这些操作系统的ntldr文件替换Windows NT的即可。

安装完Windows NT之后,可以进行NTLDR的调试了。把start.bat中的“bochs.exe”
换成“bochsdbg.exe”。然后运行start.bat。

下面是操作的屏幕拷贝:

========================================================================
Bochs x86 Emulator 2.1.1<br />
February 08, 2004<br />
========================================================================
00000000000i[     ] reading configuration from bochsrc.txt
00000000000i[     ] installing win32 module as the Bochs GUI
00000000000i[     ] Warning: no rc file specified.
00000000000i[     ] using log file nul
Next at t=0       //启动bochsdbg.exe,会自动停在Bios的第一条指令上。
(0) context not implemented because BX_HAVE_HASH_MAP=0
[0x000ffff0] f000:fff0 (unk. ctxt): jmp f000:e05b             ; ea5be000f0
<bochs:1> b 0x00007c00     //MBR和引导扇都会加载在0000:7c00。
<bochs:2> c
(0) Breakpoint 1, 0x7c00 in ?? () //第一次会在MBR上中断下来。
Next at t=772567
(0) [0x00007c00] 0000:7c00 (unk. ctxt): cli                   ; fa
<bochs:3> c
(0) Breakpoint 1, 0x7c00 in ?? () //第二次会在引导扇上中断。
Next at t=773872
(0) [0x00007c00] 0000:7c00 (unk. ctxt): jmp 0x7c5d             ; eb5b
<bochs:4>b 0x00020000   //ntldr会加载在2000:0000,事实上无论是CDFS、NTFS还是FAT,
//Windows加载启动文件都是这个地址。
<bochs:5> c
(0) Breakpoint 2, 0x20000 in ?? () //在NTLDR的第一条指令上断下来了,可以开始进行调试。
Next at t=861712
(0) [0x00020000] 2000:0000 (unk. ctxt): jmp 0x1f6             ; e9f301

现在,我们可以像上帝俯看芸芸众生一样,看着操作系统一步一步启动起来,一
切尽在眼底,甚至可以看到系统启动过程中实模式切换到保护模式的情景:

(0).[28734582] [0x00020247] 2000:0247 (unk. ctxt): opsize or eax, 0x1         ; 6683c801
(0).[28734583] [0x0002024b] 2000:024b (unk. ctxt): mov cr0, eax               ; 0f22c0
(0).[28734584] [0x0002024e] 2000:0000024e (unk. ctxt): xchg bx, bx           ; 87db
(0).[28734585] [0x00020250] 2000:00000250 (unk. ctxt): jmp 0x253             ; eb01
(0).[28734586] [0x00020253] 2000:00000253 (unk. ctxt): push 0x58             ; 6a58
(0).[28734587] [0x00020255] 2000:00000255 (unk. ctxt): push 0x259             ; 685902
(0).[28734588] [0x00020258] 2000:00000258 (unk. ctxt): retf                   ; cb

References