在测试本书源码的时候,我选择的环境是Vmware+IDA,当然也可以使用单纯的gdb调试器,但是IDA集成了源码分析,汇编等功能,使用起来较为方便。至于选择这个环境的原因在于Vmware使用的人较多,而且联合IDA调试比较方便。
这里就记录一下Vmware的相关设置
debugStub.listen.guest32 = "TRUE"//联合调试
debugStub.hideBreakpoints= "TRUE"//用硬件断点代替软件断点
debugStub.listen.guest32.remote = "TRUE"//远程调试
#monitor.debugOnStartGuest32 = "TRUE"//在运行BIOS启动代码时下断点
在阅读本书之前,我一直认为CPU内部的寄存器只有寥寥几个,如通用寄存器、EFLAGS、段、IP寄存器这几个分类,但是等到读完本书并且读到了《软件调试》后才发现,实际上CPU的寄存器还有许许多多,并且关于这些寄存器的用法都有着特别的规定,比如在段式管理中出现的GDTR寄存器。
在IA-32架构下,CPU有三种工作模式:
实模式是对8086CPU的兼容,而保护模式则是IA-32处理器工作的最主要模式,也是多道程序操作系统所工作的环境,而虚拟86模式则是实现了多个实模式的并发运行。
保护模式是一个庞大的内容,包括了GDT、LDT、分页、中断、特权级、TSS、I/O保护等等内容。
需要指出的,实现这些机制是需要硬件设施的,这也意味着对于这些机制,我们是不可能在软件调试器中看到每一步的实现的。
在保护模式下,由于ip寄存器的大小达到了32bit,所以寻址能力达到了4GB之多,在全新的保护模式下,我们不再需要将内存地址划分为 段寄存器:偏移 这样模式,但是段寄存器并没有被取代,但是寄存器内部的内容不再是一个段基址了,而是一个包含了线性表的相对偏移的索引。这个线性表就是GDT。需要注意的是,这个索引是按字节为粒度的。
GDT是一个数组,每一个表项分别描述了线性内存中的一块区域,描述的内容包括段基址(线性地址)、段界限、属性这三个内容,我们把GDT中的表项称为描述符(Descriptor),在保护模式中只有一个GDT发挥作用。需要注意的是GDT代表的是经典的段式管理,所以每一个描述符描述的内存空间不一样相等,而且两个描述符中间可能存在空洞。
那么描述符中的属性有些什么呢?比较重要的有
在CPU进行内存操作的时候,会根据描述符进行相应的检查,如果检查不通过会出现相应的异常。
当我刚刚了解到上面这个知识的时候,我一直在想一个问题,CPU是如何知道GDT的基地址的呢?是约定俗称的吗?实际上,这个问题的答案很简单,在CPU内部有一个专门的寄存器用于存放GDT的基地址,名称叫做GDTR。实际上,这种专门开辟一个寄存器来存放一个特定内容的做法在IA-32架构里面还很多。使用如下指令就可以将GDT表放入这个寄存器中,注意到,这里使用了一个lgdt操作码,对应的还有sgdt指令。
lgdt [GdtPtr]
需要注意的是,放入寄存器的并不是单纯的GDT的基地址,而是下面这样一个结构体
struct
{
word GdtLen;//以字节为单位
dword GdtBase;
}
所以一个GDT最多可以放 2 13 2^{13} 213个描述符
LDT大体上和GDT差不多,加载其基地址的指令也差不多lldt something
仅仅有一点点区别:
在上面我们提到了段寄存器中的内容,这里我们正式把他赋予一个名字,选择子。选择子的结构如下
选择子的低2位和特权级有关,第3位为TI位,这个位指定了我们的选择子选择的是GDT里面的描述符还是LDT里面的描述符。
现在我们已经了解了保护模式下必须的段式模式中绝大部分东西(除了特权级),那么CPU是如何进行模式切换的呢?有以下几个步骤:
现在我们已经了解了保护模式下必须的段式模式中绝大部分东西(除了特权级),那么CPU是如何进行模式切换的呢?有以下几个步骤:
由于要兼容A20的程序,在实模式下A20这个位的地址线总是为0的,所以为了开启保护模式,达到4GB的寻址能力,我们必须打开A20地址线。我们通过以下指令来开启A20地址线。
in al,92h
or al,00000010b
out 92h,a1
通过这个例子我们可以看到微机系统中CPU控制系统其他部分的通用方法:通过向约定的端口读/写操作字来控制外设和芯片。
就如同前面所说的那样,CPU的通用寄存器、段寄存器、PSW等寄存器都只是冰山上的一角,除了这些之外CPU还有其他其他用途的寄存器,在这里我们第一次遇见了专用寄存器Cr0,这个寄存器的最低位置1标志着CPU工作在保护模式下。