操作系统实验四:保护模式之局部任务(LDT)初探+Bochs调试进阶
在上一个实验,我们基本认识了Bochs调试。在这一个实验,让我们更全面的学习Bochs调试。以下关于Bochs调试的内容摘自http://bbs.,是我在网上能找到的关于Bochs调试最全的信息。使用的版本是Bochs(Version 2.1.1),但在如今版本Bochs-2.4.2依然通用。
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
八进制 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
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 [segff] 在虚拟地址上下断点。
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命令查询。
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 开关选项,设置遇到读写断点时中断下来还是显示出来但是继续运行。
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
xp /nuf [addr] 显示物理地址的内容
n 显示的单元数
u 每个显示单元的大小,u可以是下列之一:
b BYTE
h WORD
w DWORD
g DWORD64
注意: 这种命名法是按照GDB习惯的,而并不是按照inter的规范。
f 显示格式,f可以是下列之一:
x 按照十六进制显示
d 十进制显示
u 按照无符号十进制显示
o 按照八进制显示
t 按照二进制显示
c 按照字符显示
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: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: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: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
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状态。
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等)是否反汇编当前指令。
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"。
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网卡信息。
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网卡信息。
通过前几次实验,我们对于实模式与保护模式间的跳转已经有了充分认知。此次实验以及接下来的几次实验主要集中在认识保护模式下的一些特性上,如多任务的基础(局部任务LDT)、分页机制、中断、异常、I/O等。为了尽可能将实验设计的简单,我尽量将以上实验压缩在引导扇区的512字节内。为了节省空间,我将pm16.c精简了许多,它只负责跳转到保护模式(程序限制在80字节以内),在pm32.c里会重新加载新的GDT(程序加载到内存0x7c50处)。
此次实验内容为:
(pm16.c部分)
1.清屏,并跳转到保护模式(pm32.c,内存地址0x7c50)
(pm32.c部分)
1.加载新的GDT
2.为GDT中局部任务描述符设置LDT的基地址
3.显示字符串This is protect model.
4.跳转到局部任务
5.显示字符串This is local model.
6.进入死循环
实验代码如下:
code:run.c(因为重新设置了pm32.c的内存分布,所以需要修改run.c中的几个数据,下面是修改好了的run.c代码)
程序代码:
//文件:run.c //功能:编译操作系统的实验代码并创建img,生成bochs配置文件,运行bochs。 //说明:实验代码由16位部分引导程序与32位部分引导程序组成。 // 16位部分引导程序放在引导扇区的前半部分,0~79字节 // 32位部分引导程序放在引导扇区的后半部分,80~509字节 // 510、511字节放引导程序结束标记:0x55、0xaa //运行:请使用yc09编译器编译运行。 //作者:miao //时间:2010-1-13 #define FDISK_SIZE 1474560 //镜像大小:1.4MB //虚拟机设置 char *pmSrc = "megs: 32 \n" "romimage: file=BIOS-bochs-latest, address=0xf0000 \n" "vgaromimage: VGABIOS-elpin-2.40 \n" "floppya: 1_44=pm.img, status=inserted \n" "boot: a \n" "log: pm.out \n" "mouse: enabled=0 \n" "keyboard_mapping: enabled=1, map=x11-pc-us.map \n"; //编译指定代码文件并放入镜像指定位置 //filename:要编译的文件名 imgBuffer:保存到的镜像缓冲区 //startIndex:指定起始位置 limitSize:编译后程序限定大小 int CompileFile(char *fileName, byte *imgBuffer, int startIndex, int limitSize) { char *tempBuffer; //保存部分引导程序的临时缓冲区 //编译此部分引导程序,结果放到tempBuffer中 int length = YC_CompileCpp(&tempBuffer, fileName, 0, 0); if(length <= 0 || length >= limitSize) { printf("文件:%s中存在一些错误或文件过大(超过%d字节):%d字节\n",fileName,limitSize,length); return 1; } printf("文件:%s 编译成功,大小为:%d字节。\n", fileName, length); //将1此部分引导程序放到镜像引导扇区缓冲区指定起始位置 memcpy(imgBuffer + startIndex, tempBuffer, length); free(tempBuffer); return 0; } int main(int argc, char **argv) { char * filePath = argv[0]; //当前文件夹路径 char fileName[MAX_PATH]; //用于缓存各个文件名 //将可执行文件的完整路径去掉文件名,保留文件夹路径 for( int i = strlen(filePath);filePath[i] != '\\';i--) filePath[i] = '\0'; byte *imgBuffer = new byte[FDISK_SIZE];//镜像缓冲区 _start: //编译16位部分引导程序并放在引导扇区的前半部分,0~79字节 if(CompileFile("pm16.c", imgBuffer, 0, 80)) goto _restart; //编译32位部分引导程序并放在引导扇区的后半部分,80~509字节 if(CompileFile("pm32.c", imgBuffer, 80, 512-80-2)) goto _restart; //0000H-01FFH 为FAT引导扇区[第0扇区] 以55 AA标志结束 长度为200H(512)字节 imgBuffer[510] = 0x55; imgBuffer[511] = 0xaa;//标记软盘引导结尾 //创建操作系统镜像pm.img if(YC_writefile("pm.img", imgBuffer, FDISK_SIZE) != FDISK_SIZE) { printf("写: %s 文件过程中出现错误。\r\n", fileName); goto _restart; } printf("\n%s 创建成功。\n", fileName); //生成操作系统虚拟机配置文件pm.src YC_writefile("pm.src", pmSrc, strlen(pmSrc)); //运行虚拟机 YC_WinExec(strcat(strcpy(fileName, filePath), "bochs.exe"), "-q -f pm.src"); _restart: printf("\n点击回车重新编译运行!\n\n\n"); while(getchar() != '\n'); goto _start; return 0; }
code:pm.h
程序代码:
//文件:pm.h //功能:pm16.c与pm32.c的公共头文件 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc09编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行 //作者:miao //时间:2010-1-30 //定义GDT属性 #define DA_32 0x4000 //32位段 #define DA_DRW 0x92 //存在的可读写数据段属性值 #define DA_CR 0x9A //存在的可执行可读代码段属性值 //定义LDT属性 #define DA_LDT 0x82 //局部描述符表类型值 #define SA_TIL 0x4 //将TI位置1,表示是LDT选择子 typedef unsigned int t_32; //4字节 typedef unsigned short t_16; //2字节 typedef unsigned char t_8; //1字节 typedef int t_bool;//4字节 typedef unsigned int t_port;//4字节 //存储段描述符/系统段描述符 struct DESCRIPTOR //共 8 个字节 { t_16 limit_low; //Limit 2字节 t_16 base_low; //Base 2字节 t_8 base_mid; //Base 1字节 t_8 attr1; //P(1) DPL(2) DT(1) TYPE(4) 1字节 t_8 limit_high_attr2; //G(1) D(1) 0(1) AVL(1) LimitHigh(4) 1字节 t_8 base_high; //Base 1字节 }; #define Descriptor(bas,len,attr) { (len) & 0xffff, \ (bas) & 0xffff, \ ((bas)>>16)&0xff, \ (attr) & 0xff, \ (((attr)>>8) &0xf0) + (((len)>>16) & 0x0f), \ ((bas) >> 24) & 0xff } \
code:pm16.c
程序代码:
//文件:pm16.c //功能:切换到保护模式,跳转到32位代码段 //说明:我试图仅在引导扇区编写保护模式的相关实验,因此将这个程序精简了很多。 // 它只负责跳转到保护模式,其他的工作都在pm32.c下完成。 // pm16.c只占引导扇区的前半部分0~79字节。 // pm32.c部分会加载到内存0x7c50处。 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc09编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行 //作者:miao //时间:2010-1-30 #define YCBIT 16 //告诉编译器,以16位格式编译程序 #define YCORG 0x7c00 //告诉编译器,在7c00处加载程序 #include "pm.h" //GDT界限,只负责跳转到保护模式,到时会加载新的GDT DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(0x7c50, 0xfffff, DA_CR | DA_32), //32位代码段(pm32.c),可执行可读 }; //GDT 选择子,根据GDT界限设置偏移量值 #define SelectorCode32 8*1 //指向32位段处 #pragma pack(1) struct GDT_PTR { unsigned short size; void *addr; }GDT_PTR GdtPtr = {sizeof(label_gdt), (char*)label_gdt}; //段界限,基地址 #pragma pack() asm void main() { mov ax, cs mov ds, ax mov es, ax //清屏 mov ah, 06h //屏幕初始化或上卷 mov aL, 00h //AH = 6, AL = 0h mov bx, 1110h //蓝色底色 mov cx, 0 //左上角: (0, 0) mov dl, 4fh //第0列 mov dh, 1fh //第0行 int 10h //显示中断 lgdt GdtPtr //加载 GDTR cli //关中断 //打开地址线A20 in al, 92h or al, 00000010b out 92h, al //准备切换到保护模式,置cr0的PE位为1 mov eax, cr0 or eax, 1 mov cr0, eax //真正进入保护模式 jmp dword SelectorCode32:0x0 }
code:pm32.c
程序代码:
//文件:pm32.c //功能:保护模式下32位代码段,功能为加载新的GDT,初始化LDT,跳入LDT局部任务 //说明:32位部分引导程序放在镜像引导扇区的后半部分,80~509字节中,程序大小不能超过这个限制 //运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序 //提示:请先用yc09编译run.c文件,生成run.exe程序 // 之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果 ,点击回车再次编译运行 //作者:miao //时间:2010-1-30 #define YCBIT 32 //告诉编译器,以32位格式编译程序 #define YCORG 0x0 //此指会对在编译时对变量函数等产生地址基址偏移量,简单起便,设置为0 #include "pm.h" #define ProtecAddr 0x7c50 //进入保护模式后的程序基址 //LDT界限 DESCRIPTOR label_ldt[] = { // 段基址 段界限 属性 Descriptor(ProtecAddr, 0xfffff, DA_CR | DA_32), //32位代码段(pm32.c),可执行可读 }; //LDT 选择子,根据pm32.c中的LDT界限设置偏移量值 #define SelectorLDTCodeA 8*0+SA_TIL //指向32位段局部任务处 //GDT界限,注意,这个与pm16.c中的GDT不同,从pm16.c跳转过来后会立即载入这个新的GDT DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(ProtecAddr, 0xfffff, DA_CR | DA_32), //32位代码段(pm32.c),可执行可读 Descriptor(0xb8000, 0xffff, DA_DRW), //显存地址段,可读可写 Descriptor(ProtecAddr, 0xfffff, DA_DRW | DA_32), //令32位代码段(pm32.c)的变量可以读写 Descriptor(0, 0xfffff, DA_LDT),//局部描述符,段基址和32位代码段相同,调用时需要加上偏移量 }; //GDT 选择子,根据pm32.c中的GDT界限设置偏移量值 #define SelectorCode32 8*1 //指向32位段处代码段,可执行可读 #define SelectorVideo 8*2 //指向显存首地址 #define SelectorData32 8*3 //指向32位段处,这样,在程序中的变量就可以读写了 #define SelectorLDT 8*4 //指向LDT,通过这个跳转到局部任务 #pragma pack(1) struct GDT_PTR { t_16 size; void *addr; }GDT_PTR GdtPtr = {sizeof(label_gdt), (char*)&label_gdt + ProtecAddr}; //段界限,基地址 #pragma pack() char Msg1[] = "This is protect model."; char Msg2[] = "This is local model."; asm void LDTCode();//局部代码段, 由32 位代码段跳入 asm void DispStr();//显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置 //32 位代码段. 由实模式跳入 asm void main() { lgdt cs:GdtPtr //加载新的GDTR mov eax, SelectorVideo mov gs, ax //视频段选择子(目的) mov eax, SelectorData32 //令32位代码段的变量(printPlace)可以读写 mov ds, ax //在GDT的上设置好LDT的基地址,然后加载局部描述符(LDT) xor eax, eax mov eax, &label_ldt + ProtecAddr mov word label_gdt+8*4+2, ax shr eax, 16 mov byte label_gdt+8*4+4, al mov byte label_gdt+8*4+7, ah mov ax, SelectorLDT lldt ax //下面显示一个字符串(显示已经到达保护模式信息) mov esi, &Msg1 //源数据偏移 mov edi, ((80 * 0 + 0) * 2) //目的数据偏移。屏幕第0行, 第0列。 call DispStr jmp SelectorLDTCodeA:&LDTCode //跳转到局部任务 } //局部代码段, 由32 位代码段跳入 asm void LDTCode() { //下面显示一个字符串(显示已经到达保护模式信息) mov esi, &Msg2 //源数据偏移 mov edi, ((80 * 1 + 0) * 2) //目的数据偏移。屏幕第1行, 第0列。 call DispStr _end: jmp _end } //显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置 asm void DispStr() { mov ah, 14h //蓝底红字(ah = 14h) //循环逐个将字符串输出 _DispStr: mov al, ds:[esi]//因为可读,才能用cs指向当前段的Msg1字符串 inc esi cmp al, '\0' //判断是否字符串结束 jz _stop mov gs:[edi], ax add edi, 2 jmp _DispStr _stop: //显示完毕 ret }