3.4.2.4 段描述符
在保护模式下,段寄存器有另一个名字,叫段选择子,因为它保存的信息主要是该段在段表里索引值,用这个索引值可以从段表中“选择”出相应的段描述符。
先看看ds选择子的内容,还是用“sreg”命令:
cs:s=0x000f, dl=0x00000002, dh=0x10c0fa00, valid=1
ds:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=3
ss:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
es:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
fs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
gs:s=0x0017, dl=0x00003fff, dh=0x10c0f300, valid=1
ldtr:s=0x0068, dl=0x52d00068, dh=0x000082fd, valid=1
tr:s=0x0060, dl=0x52e80068, dh=0x00008bfd, valid=1
gdtr:base=0x00005cc8, limit=0x7ff
idtr:base=0x000054c8, limit=0x7ff
可以看到,ds的值是0x0017。段选择子是一个16位寄存器,它各位的含义如下图:
其中RPL是请求特权级,当访问一个段时,处理器要检查RPL和CPL(放在cs的位0和位1中,用来表示当前代码的特权级),即使程序有足够的特权级(CPL)来访问一个段,但如果RPL(如放在ds中,表示请求数据段)的特权级不足,则仍然不能访问,即如果RPL的数值大于CPL(数值越大,权限越小),则用RPL的值覆盖CPL的值。而段选择子中的TI是表指示标记,如果TI=0,则表示段描述符(段的详细信息)在GDT(全局描述符表)中,即去GDT中去查;而TI=1,则去LDT(局部描述符表)中去查。
看看上面的ds,0x0017=0000000000010111(二进制),所以RPL=11,可见是在最低的特权级(因为在应用程序中执行),TI=1,表示查找LDT表,索引值为10(二进制)= 2(十进制),表示找LDT表中的第3个段描述符(从0开始编号)。
LDT和GDT的结构一样,每项占8个字节。所以第3项“0x00003fff 0x10c0f300”就是搜寻好久的ds的段描述符了。用“sreg”输出中ds所在行的dl和dh值可以验证找到的描述符是否正确。
接下来看看段描述符里面放置的是什么内容:
可以看到,段描述符是一个64位二进制的数,存放了段基址和段限长等重要的数据。其中位P(Present)是段是否存在的标记;位S用来表示是系统段描述符(S=0)还是代码或数据段描述符(S=1);四位TYPE用来表示段的类型,如数据段、代码段、可读、可写等;DPL是段的权限,和CPL、RPL对应使用;位G是粒度,G=0表示段限长以位为单位,G=1表示段限长以4KB为单位;其他内容就不详细解释了。
3.4.2.4段基址和线性地址
费了很大的劲,实际上我们需要的只有段基址一项数据,即段描述符“0x00003fff 0x10c0f300”中加粗部分组合成的“0x10000000”。这就是ds段在线性地址空间中的起始地址(ds段的段基址)。用同样的方法也可以算算其它段的基址,都是这个数。
段基址+段内偏移,就是线性地址了。所以ds:0x3004的线性地址就是:
0x10000000 + 0x3004 = 0x10003004
用“calc ds:0x3004”命令可以验证这个结果。
3.4.2.5 页表
从线性地址计算物理地址,需要查找页表。线性地址变成物理地址的过程如下:
首先需要算出线性地址中的页目录号、页表号和页内偏移,它们分别对应了32位线性地址的10位+10位+12位,所以0x10003004的页目录号是64,页号3,页内偏移是4。
IA-32下,页目录表的位置由CR3寄存器指引。“creg”命令可以看到:
CR0=0x8000001b: PG cd nw ac wp ne ET TS em MP PE
CR2=page fault laddr=0x10002f68
CR3=0x00000000
PCD=page-level cache disable=0
PWT=page-level writes transparent=0
CR4=0x00000000: osxmmexcpt osfxsr pce pge mce pae pse de tsd pvi vme
说明页目录表的基址为0。看看其内容,“xp /68w 0”:
0x00000000 : 0x00001027 0x00002007 0x00003007 0x00004027
0x00000010 : 0x00000000 0x00024764 0x00000000 0x00000000
0x00000020 : 0x00000000 0x00000000 0x00000000 0x00000000
0x00000030 : 0x00000000 0x00000000 0x00000000 0x00000000
0x00000040 : 0x00ffe027 0x00000000 0x00000000 0x00000000
0x00000050 : 0x00000000 0x00000000 0x00000000 0x00000000
0x00000060 : 0x00000000 0x00000000 0x00000000 0x00000000
0x00000070 : 0x00000000 0x00000000 0x00000000 0x00000000
0x00000080 : 0x00ff3027 0x00000000 0x00000000 0x00000000
0x00000090 : 0x00000000 0x00000000 0x00000000 0x00000000
0x000000a0 : 0x00000000 0x00000000 0x00000000 0x00000000
0x000000b0 : 0x00000000 0x00000000 0x00000000 0x00ffb027
0x000000c0 : 0x00ff6027 0x00000000 0x00000000 0x00000000
0x000000d0 : 0x00000000 0x00000000 0x00000000 0x00000000
0x000000e0 : 0x00000000 0x00000000 0x00000000 0x00000000
0x000000f0 : 0x00000000 0x00000000 0x00000000 0x00ffa027
0x00000100 : 0x00faa027 0x00000000 0x00000000 0x00000000
页目录表和页表中的内容很简单,是1024个32位(正好是4K)数。这32位中前20位是物理页框号,后面是一些属性信息(其中最重要的是最后一位P)。其中第65个页目录项就是我们要找的内容,用“xp /w 0+64*4”查看:
0x00000100 : 0x00faa027
其中的027是属性,显然P=1,其他属性实验者自己分析吧。页表所在物理页框号为0x00faa,即页表在物理内存的0x00faa000位置。从该位置开始查找3号页表项,得到(xp /w 0x00faa000+3*4):
0x00faa00c : 0x00fa7067
其中067是属性,显然P=1,应该是这样。
3.4.2.6 物理地址
最终结果马上就要出现了!
线性地址0x10003004对应的物理页框号为0x00fa7,和页内偏移0x004接到一起,得到0x00fa7004,这就是变量i的物理地址。可以通过两种方法验证。
第一种方法是用命令“page 0x10003004”,可以得到信息:“linear page 0x10003000 maps to physical page 0x00fa7000”。
第二种方法是用命令“xp /w 0x00fa7004”,可以看到:
0x00fa7004 : 0x12345678
这个数值确实是test.c中i的初值。
现在,通过直接修改内存来改变i的值为0,命令是: setpmem 0x00fa7004 4 0,表示从0x00fa7004地址开始的4个字节都设为0。然后再用“c”命令继续Bochs的运行,可以看到test退出了,说明i的修改成功了,此项实验结束。
调试汇编代码
用Bochs在汇编级调试操作系统很简单,只需要运行“dbg-asm”,然后就得到了如下图所示的调试界面。
图1 Bochs调试操作系统的界面
此时是暂停在BIOS中。而我们的代码是从0x7C00位置开始的,所以先要在那里设一个断点,然后继续运行到断点:
break 0x7c00
continue
(0) Breakpoint 1, 0x00007c00 in ?? ()
Next at t=4967728
(0) [0x00007c00] 0000:7c00 (unk. ctxt): mov ax, 0x07c0 ; b8c007
接下来可以用命令help来查看调试系统的各种基本命令,这里给出了一些常用的命令
1 执行控制指令
c/cont/continue 连续执行
s/step/stepi [count] 执行count条指令,默认为1条,会跟进到函数和中断调用的内部
p/n/next [count] 执行count条指令,默认为1条,但跳过函数和中断调用
Ctrl+C 停止执行,并回到命令行提示符下
q/quit/exit 退出调试和执行
2 断点设置命令
vb/vbreak seg:offset 在虚拟地址上设置指令断点,其中seg和offset可以是以0x开始的十六进制数,或十进制,或者是以0开头的八进制数
lb/lbreak addr 在线性地址上设置断点,addr同上面的seg和offset
b/break/pb/pbreak addr 在物理地址上设置断点
info break 显示当前所有断点的信息
d/del/delete n 删除一个断点
3 内存操作指令
x /nuf addr 检查位于线性地址addr处的内存内容
xp /nuf addr 检查位于物理地址addr处的内存内容
其中参数n、u、f分别表示:
n为要显示内存单元的计数值,默认为1
u表示单元大小,默认值为w
b(bytes) 1字节
h(halfwords) 2字节
w(words) 4字节
g(giantwords) 8字节
f为显示格式,默认为x
x(hex) 显示为十六进制数
d(decimal) 显示为十进制数
u(unsigned) 显示为无符号十进制数
o(octal) 显示为八进制数
t(binary) 显示为二进制数
c(char) 显示为对应的字符
4 信息显示和CPU寄存器操作命令
r/reg/regs/registers 列表显示CPU寄存器及其内容
set $reg=val 修改某寄存器的内容。除段寄存器和标志寄存器以外的寄存器都可以
修改,如set $eax=0x01234567
creg 列出所有的CR0-CR4寄存器
sreg 列出CPU全部状态信息,包括各个段选择子(cs,ds等)以及ldtr
和gdtr等。
print-stack 打印堆栈情况。
info tab 显示页表