《80X86汇编语言程序设计教程》九 分段管理机制及纯DOS环境搭建
1、 保护模式简介
1) 存储器管理机制
a)背景
80386及其以上CPU把地址在1M以下的内存称为常规内存,把地址在1M以上的内存称为扩展内存。虚拟存储器是一种软硬件结合的技术,用于提供比实际物理主存更大的存储空间。80386要实现支持任务隔离、代码和数据共享,还要支持特权保护。
b)地址空间和地址转换
保护模式下,段是虚拟存储器中大小可变的存储块。段的位置、大小和使用情况等信息使用段描述符来描述。虚拟存储器的地址(逻辑地址)由指示段描述符的选择子和段内偏移构成,这样的地址合集称虚拟地址空间,80386支持64TB虚拟地址空间。
只有物理存储器中的数据才能被访问,所以虚拟地址空间必须映射到物理地址空间。而每一个任务有一个虚拟空间,为了避免并行任务多个虚拟地址空间同时映射到一个物理地址,采用线性地址空间来隔离他们。线性地址空间由一位线性地址构成,与物理地址空间对等,80386容量为4GB。
80386分两步实现虚拟地址到物理地址的转换:
i)通过描述符表和描述符,分段管理机制实现虚拟地址空间到线性地址空间的映射,实现将2维虚拟地址转换为1维线性地址。
ii)分页管理机制把线性地址空间与物理地址空间分成大小相同的页(4KB),并在线性地址空间的页与物理地址空间的页之间建立映射表,从而实现线性地址到物理地址的转换。
须注意的是,第2步是可选的,当禁用分页管理机制(CR0的PG位为0)时,线性地址就是物理地址。
2) 保护机制
在多任务系统,保护机制实现不同任务之间以及同一任务内的保护。
a)不同任务之间的保护
每个任务有自己独立的虚拟地址空间,有各自的虚拟地址到物理地址的映射函数,这些函数随着任务的切换而切换。因此,不同任务的相同虚拟地址可以映射到不同的物理地址。各个任务之间通过独有的虚拟地址来实现隔离。
b)同一任务内的保护
同一任务内,有4种执行特权等级(按数据重要性和代码可信度指定),用于限制对任务中的段进行访问。特权级别用0~3表示,0特权等级最高,比较时用“里层”,“外层”表示。每个段有特定的特权等级,特权级别限制是指,只有足够级别的程序,才可对相应的段进行访问。任务中是在4个特权等级中的某个特权级中运行,某时刻特权级用当前特权级(Current Privilege Level)表示,标记为CPL。当前代码对某个段进行访问时,要比较CPL与目标段的特权等级,只有CPL在目标段特权等级的同层或者内层才能访问,否则触发异常。
不同任务间,通过各有的虚拟地址空间实现隔离,于此同时,它们共享操作系统,即操作系统位于它们虚拟空间的同一片地址空间。须注意的是,现在操作系统通常只用了2个特权等级:R0级和R3级。操作系统和驱动程序运行在R0级,而普通运用程序运行在R3级。
2、 分段管理机制
1) 段定义和虚拟地址到线性地址转换
保护模式下段由3个参数进行定义:段基地址、段界限和段属性
a)段基地址(Base Address):规定线性地址中段起始地址,长32位,因此段可以从32位线性地址空间中的任何一个字节开始。
b)段界限(Limit):规定段的大小,长20位,单位为字节或者4K(页面大小),在段属性G位来标识段粒度(G = 0表示以字节为单位,最大范围为1MB;而G = 1时以4K为单位,最大范围为4GB)。当G = 0时,段范围为Base~Base+Limit;当G = 1时,段范围为Base~Base + Limit * 4K + 0FFFH = Base + (Limit << 12)+ 0FFFH,此时,这种转换为字节的表示形式,相当于具有了32位的段界限。
c)段属性(Attributes):规定段的主要特性,包括访问特性,后续。这里先介绍两个与段界限相关的位:一个是上面提到的G位段粒度,还有一个是ED位段扩展方向。
段扩展通过增加段界限Limit实现,ED位表示扩展方向,ED = 0表示向高扩展,如数据段;而ED = 1表示向低扩展,如堆栈段。注意,只有数据段的段属性(堆栈段看作特殊数据段)才有向高扩展和向低扩展之分,其他段都是自然的向高扩展。
当段最大为1M字节,在向高扩展时,从0~Limit偏移合法,而Limit+1~1M-1偏移非法;而在向低扩展时,从0~Limit偏移非法,而Limit+1~1M-1偏移合法。在每次从虚拟地址转换为线性地址时,都要对偏移进行检查,如果偏移不在有效的范围内,则引发异常。
2) 存储段描述符
用于表示段定义时3个参数的数据称描述符。每个描述符长8个字节。保护模式下,每个段都有一个相应的描述符来描述。描述符可分为:存储段描述符、系统段描述符、门描述符(控制描述符)。
a)存储段描述符
存放的是可由程序直接进行访问的代码和数据的段,也被称为代码和数据段描述符。假设某个段描述符起始地址为m,那么8个字节内容安排如下:
m + 7 | m + 6 | m + 5 | m + 4 | m + 3 | m + 2 | m + 1 | m |
Base 31…24 | Attributes | Segment Base 23…0 | Segment Limit 15…0 |
其中段属性Attributes具体内容如下:
m + 6 | m + 5 | ||||||||||||||
7 | 6 | 5 | 4 | 3 | 2 | 1 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | ||
G | D | AVL | Limit(19…16) | P | DPL | DT | TYPE | ||||||||
可以看到,32位段基址与20位段界限都被拆分成了2个域,其原因是为了兼容80286,80286存储段描述符只使用低6个字节,而高2个字节被置0,80386为了在兼容它又实现扩展,利用了高2个字节,因此才有了这幅怪模样。段属性Attributes各位含义:
i)P位(Present位,存在位):P = 1表示描述符对地址转换有效或者该描述符描述的段存在;P = 0则刚好相反,对它的使用将引发异常。
ii)DPL域(Descriptor Privilege Level域,描述符特权级域):规定所描述段特权级,用于特权检查,以决定对该段能否进行访问。
iii)DT位(Descriptor Type位,描述符类型位):对于存储段描述符DT = 1,用来区别于系统描述符与门描述符。
iv)TYPE域:说明存储段描述符所描述的存储段的具体属性
位0(A位---Accessed):表示描述符是否已经被访问。当把描述符相应段选择子装入段寄存器时,80386将其置1,以便给操作系统测试该访问位。
位1(W位或者R位---Write或Read):E = 0时,为W位,用于标志数据段是否可写;E = 1时,为R位,标志代码段是否可读(R = 1表示可读可执行,否则只能执行不能读)。
位2(ED位或C位--- Extend Direction或Consistent):E = 0时为ED位,表示数据段(包括堆栈段)的扩展方向(ED = 0表示向高扩展);E = 1时为C位,描述代码段是否为一致代码段(C = 0表示不是一致代码段)
位3(E位--- Executable):所描述的段是代码段还是数据段,E = 0为数据段(包括堆栈段),不可执行;E = 1为代码段,可执行。
v)G位(Granularity位,段界限粒度位):G = 0段界限粒度位字节,G = 1段界限粒度为4K。
vi)D位
在描述可执行段的描述符中(E = 1):D位决定指令使用地址及操作数默认大小,D = 1表示32位段,D = 0表示16位段。可使用地址超越前缀和寄存器超越前缀来改变默认地址或操作数位数大小。
在向低扩展数据段描述符中(E = 0,ED = 1):D位决定段的上部界限,D = 1表示段的上部界限为4G,D = 0表示上部界限为64KB,为了兼容80286。
在描述由SS寄存器寻址的段描述符中:D位决定隐式堆栈访问指令(如PUSH和POP)使用何种堆栈指针寄存器。D = 1时表示使用ESP,D = 0表示使用SP。
vii)AVL位:软件可利用位,80386不对该位(包括后续系列)做任何定义。
b)关于一致代码段的一些说明
这个教材上没有具体介绍,而在后面的特权级变换时执行的保护检测要牵涉到它,这里特别说明一下。原书有段话大意是:一致代码段是一种特殊的存储段,为在多个特权等级执行的程序提供对子例程的共享支持,而不要求改变特权级,那么任何特权级的程序可以使用段间调用指令调用例程,并在调用者所具有的特权级执行该例程。可做如下理解:
i)内核需要安全,不允许用户程序干涉,但是可提供给用户程序共享
ii)内核不知道用户数据,内核不依赖用户程序数据,不用转移到用户程序中来
iii)用户程序只能访问内核共享出来的某些共享段,这些段叫一致代码段,而用户程序中的普通代码段叫非一致代码段
iv)用户程序不能访问内核不共享的段
c)存储段描述符的结构类型表示
描述符结构类型可定义如下:
1 DESCRIPTOR struct 2 LimitL dw 0 ;段界限低16位 3 BaseL dw 0 ;基地址低16位 4 BaseM db 0 ;基地址中间8位 5 Attributes dw 0 ;段属性(含段界限高4位) 6 BaseH db 0 ;基地址高8位 7 DESCRIPTOR ends
如描述一个CodeA代码段,其属性为:只可执行的有效32位代码段,基地址12345678H,以4K字节为单位的界限值为10H,描述符特权级DPL = 0。可声明如下:
CodeA DESCRIPTOR <10H,5678H,34H,0C098H,12H>
3) 全局和局部描述符表
80386把描述符组织成线性表,称为描述符表,有3种类型:全局描述符表GDT(Global Descriptor Table)、局部描述符表LDT(Local Descriptor Table)和中断描述符表IDT(Interrupt Descriptor Table)。GDT与IDT整个系统中只有一张,LDT每个任务可以有一张。每个描述符表可以有8K(8096)个描述符,每个描述符为8B,那么表最大为8096*8B = 64KB。
每个任务的LDT含有自己的代码段、数据段、堆栈段以及调用门、任务门等描述符,LDT随任务的切换而切换。LDT可以使各任务私有的各个段与其他任务隔离,而GDT则使各任务都需要的段能够被共享。一个任务可使用虚拟空间分为两半,一半的描述符在GDT中,一半在LDT中,而每张描述符表最多可达8096个描述符,故最大虚拟地址空间为:4GB*8096*2 = 64TB。
4) 段选择子
在保护模式下,段寄存器存放的不是段值,而是段选择子。长16位,结构如下:
15~3 | 2 | 1~0 |
描述符索引Index | TI | RPL |
段选择子高13位为描述符索引(即描述符在描述符表中的下标);TI位(位2)指示是从GDT读取描述符(TI = 0)还是从LDT中读取描述符(TI = 1);RPL为请求特权级(Requested Privilege Level),用于特权检查。由于一张描述符表最大为64KB,每个描述符为8B,最多可包含2^13个(8096个)描述符,刚好用13位可以作为索引。Index = 0,TI = 0,RPL任意的选择子为空选择子,对应于GDT中的0号描述符,它总是不会被处理器访问,否则产生异常(一般RPL也置为全0);而Index = 0,TI = 1的选择子不是空选择子,它对应于LDT中的0号描述符。
5) 段描述符高速缓冲寄存器
由于保护模式下段寄存器保存的不是段基址,而是段选择子,为了避免在需要时总是要查表来获得段基址,80386给每个段寄存器配有程序员不可见的高速缓冲寄存器,称之为段描述符高速缓冲寄存器或者描述符投影寄存器。每次装载段选择子时(初始化或者任务切换时),处理器自动从描述符表取出相应描述符,把其中信息保存到高速缓冲寄存器中,它将一直保持,直到新的选择子装入段寄存器。各段高速缓冲寄存器中的内容如下:
段 寄 存 器 | 段基地址 | 段界限 | 其它段属性 | |||||||||
存 在 性 | 特 权 级 | 已 存 取 | 粒 度 G | 扩 展 方 向 | 可 读 性 R | 可 写 性 W | 可 执 行 E | 堆 栈 大 小 | 一 致 特 权 | |||
CS | 32位基地址 | 32位段界限 | p | d | d | d | d | d | N | Y | - | d |
SS | 32位基地址 | 32位段界限 | p | d | d | d | d | r | w | N | d | - |
DS | 32位基地址 | 32位段界限 | p | d | d | d | d | d | d | N | - | - |
ES | 32位基地址 | 32位段界限 | p | d | d | d | d | d | d | N | - | - |
FS | 32位基地址 | 32位段界限 | p | d | d | d | d | d | d | N | - | - |
GS | 32位基地址 | 32位段界限 | p | d | d | d | d | d | d | N | - | - |
其中:32位段基地址直接取自描述符,32位段界限取自描述符中的段界限并转换为字节单位的值。“Y”表示“是”,“N”表示“否”,“r”表示必须可读,“w”表示必须可写,“p”表示必须存在,“d”表示根据描述符中属性而定。
从上表可以看出,要从虚拟地址得到线性地址异常简单,不再需要查表,只需要做两件事:通过段界限检查段内偏移是否合法,其次根据段基地址+段内偏移,得到的就是线性地址。
3、 80386控制寄存器和系统地址寄存器
用于控制工作方式,控制分段管理机制以及分页管理机制的实施。
1) 控制寄存器
参考“80X86汇编语言程序设计教程》八 80386程序设计基础”。
2) 系统地址寄存器
参考“80X86汇编语言程序设计教程》八 80386程序设计基础”。补充一点,GDTR利用结构类型定义伪描述符如下:
1 PDESC struct 2 Limit dw 0 ;16位GDT所在段界限值 3 Base dd 0 ;32位GDT所在段段基地址 4 PDESC ends
4、 虚拟纯DOS环境搭建
在继续之前,先搭建一下纯DOS的调试模式,因为接下来的测试必须在纯DOS系统下。
1) 安装虚拟机vmware
2) 下载DOS7.1版(最后一个DOS版本),在虚拟机中安装vmware,说下步骤,这里有几个要注意的地方。
a)选择客户机操作系统的时候选“其他”,版本为“DOS”
b)选择好镜像以后启动,按F2进入BIOS,设置从软盘启动
c)一路点击“OK”或者“continue”,中途将重启一次,安装时界面如下:
d)接下来,不要安装任何扩展或者辅助的软件,在“Add-on”选项时选择“no”。
e)我们需要的是一个纯DOS系统,所以也不要高端内存管理软件
f)也不需要任何驱动
g)开启所有支持的功能
h)然后将再度重启,这个时候要按F2进入BIOS,调整为从硬盘启动
i)以下就是进入纯DOS系统了
3) 虚拟DOS系统与宿主系统共享文件听说很麻烦,我这里采用一个强硬一点的方法。
a)下载软件DiskGenius,这是一个磁盘分区与数据恢复软件
b)关闭虚拟DOS系统,在DiskGenius 软件的“硬盘”菜单下的“打开虚拟硬盘文件”,选择DOS7.1的虚拟硬盘
c)打开以后,将看到DOS7.1的系统盘
d)这个时候就可以复制文件进来
e)须注意,操作完成以后必须通过“硬盘”菜单下的“光标虚拟硬盘文件”来关闭它,否则vmware虚拟机将无法启动DOS系统。
4) 关于调试的话,使用RedASM附带的CV,CV版本是4.10,在安装目录“\masm32\bin”目录下,需要复制5个文件到DOS7.1虚拟机根目录:“cv.exe”、“EED1CXX.DLL”、“EMD1D1.DLL”、“SHD1.DLL”以及“TLD1LOC.DLL”。缺一不可,否则无法调试。复制进入以后,使用“CV XXX.exe”来启动调试,在“options”选项中的“preferences..”的选项卡内,将“32-bit registers”点上可以查看32位寄存器内容,按F2(或ALT + 7)打开寄存器窗口,按F10单步步过,F8单步步入,F9下断点,ALT + 5查看内存。CV工具调试在模式切换将导致死机(其实目前为止,我发现所有的DEBUG工具都不能调试模式切换)。