理论准备:
实模式下的寻址相信大家已经很清楚了,它分为两个部分,一部分是段基址,另一部分是段内偏移。段基址由段寄存器值左移4位表示,段内偏移则记录了相对于某个段起始位置的偏移量,将这两个值相加就得到了所需的物理地址。
在保护模式下,相对来说其寻址方式就比较复杂了。嗯,本次先不讨论分页的问题,我们主要把注意力集中在分段上(我还是比较喜欢步步为营)。前面很多地方使用了选择子的概念,那么它究竟是一个什么东东呢?它的结构如下图所示:
若TI位为0,则在GDT中查找对应的描述符;若TI位为1,在LDT中查找对应的描述符。RPL(RequestedPrivilege Level)是通过段选择子的第0位和第1位表现出来的。处理器通过检查RPL和CPL(Current Privilege Level,当前执行程序或任务的特权等级)来确认一个访问请求是否合法。而剩下的13位用于指定相应的描述符在GDT或LDT表中的索引。
描述符的结构如下图示:
除了段基址、段界限和段属性之外,还有其它一些描述符中的属性含义如下:
段基址:由上图中的两部分(BASE 31-24 和 BSE23-0)组成
G位:段界限粒度位,该位为 0 表示单位是字节,为1表示单位是 4KB
D/B: 该位分为3中情况。
1. 在可执行代码段描述符中,这一位叫做D位。D= 1时,默认情况下指令使用32位地址及32位或8位操作数;D= 0时,默认情况下使用16位地址及16位或8位操作数。
2. 在向下扩展数据段的描述符中,这一位叫做B位。B= 1时,段的上部界限为4GB;B= 0时,段的上部界限为64KB。
3. 在描述堆栈段(由SS寄存器指向的段)的描述符中,这一位叫做B位。B= 1时,隐式的堆栈访问指令(push、pop、call等)使用32位堆栈寄存器esp;D= 0时,隐式的堆栈访问指令使用16位堆栈指针寄存器sp。
AVL: 保留位,可以被系统软件使用。
段界限:单位由 G 位决定。数值上(经过单位换算后的值)等于段的长度(字节)- 1。
P位: 段存在位,该位为 0 表示该段在内存中不存在,为 1 表示在内存中存在。
DPL:描述符特权等级,可以使0/1/2/3,数字越小特权级越大。
S位: 该位为 1 表示这是一个数据段或者代码段。为 0 表示这是一个系统段(比如调用门,中断门等)
TYPE: 根据 S 位的结果,再次对段类型进行细分。
TYPE值 | 数据段/代码段 | 系统段/门描述符 |
0 | 只读 | <未定义> |
1 | 只读,已访问 | 可以286TSS |
2 | 读/写 | LDT |
3 | 读/写,已访问 | 忙的286TSS |
4 | 只读,向下扩展 | 286调用门 |
5 | 只读,向下扩展,已访问 | 任务门 |
6 | 读/写,向下扩展 | 286中断门 |
7 | 读/写,向下扩展,已访问 | 286陷阱门 |
8 | 只执行 | <未定义> |
9 | 只执行,已访问 | 可用386TSS |
A | 执行/读 | <未定义> |
B | 执行/读,已访问 | 忙的386TSS |
C | 只执行,一致码段 | 386调用门 |
D | 只执行,一致码段,已访问 | <未定义> |
E | 执行/读,一致码段 | 386中断门 |
F | 执行/读,一致码段,已访问 | 386陷阱门 |
保护模式下不再像实模式那样提供段地址。在原来放段地址的段寄存器里含有一个选择子(selector),用于选择描述表内的一个描述符。描述符(descriptor)描述存储器段的位置、长度和访问权限。
由于段寄存器和偏移地址仍然用于访问存储器,所以保护模式指令和实模式指令是完全相同的。事实上,很多为在实模式下运行编写的程序,不用更改就可在保护模式下运行。两种模式之间的区别是微处理器访问存储段时对段寄存器的解释不同。
假设不启用分页机制,那么段选择符和段内偏移地址是如何找到最后的物理地址的呢?我们以在GDT中查找相应的段描述符为例:
首先,我们会有一个语句用于加载gdtr寄存器,该寄存器的结构如下图所示:
其中有GDT的基址及界限,这样就可以访问相应的GDT了。
接着,把相应的选择子选到合适的寄存器处。
最后,当使用(段基址:段内偏移)的形式进行寻址的时候,会将段寄存器中选择子的高13位作为描述符索引在GDT中查找相应的描述符。而描述符中记载着该段的基址,再配合相应的段内偏移即可找出该线性地址,即物理地址(不分页时)。
实例讲解:
为了能够彻底理解选择子的功效,我们给出以下示例代码,看完之后要是还不懂我选择狗带╰( ̄▽ ̄)╭
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16
LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
上面这段代码定义了GDT,按照前面的顺序,我们再看加载gdtr寄存器相关的内容:
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
经过上面这段代码gdtr的内容刚好满足了该寄存器所需的格式(低16位由语句:GdtPtr dw GdtLen-1给出GDT界限;而高32位则由上面这段代码设置为了GDT的基址,最后以mov dword [GdtPtr + 2], eax 语句填入了原本为0的地址空间中)。好了,至此gdtr寄存器相关的操作就完成了。
我们总说按照选择子的索引来查找GDT,那么此时被指向的GDT中的索引又是怎样的呢?我们看到GDT开始时首先定义了一个空描述符(后面可以看到,这个空描述符很关键),理所当然,GDT中所有的描述符所对应的在描述表中的“索引”就是它们在GDT中的定义顺序。也即有,索引0:空描述符,索引1:Normal描述符,索引2:非一致代码段,索引3:Data。
一般我们会以如下的方式来定义选择子:
; GDT 选择子
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
其实就这一小段代码来看说它是偏移也没有错,只是其中还有更玄妙的关系隐藏在里面。我们知道每个描述符的大小都是8字节,那么上面给出的选择子就都是8的倍数。有没有联想到什么?选择子的低3位是与索引描述符操作无关的,3位哦,在选择子(或者说是偏移量)中去掉低三位后发生了什么?是的,恰好就是对应的描述符索引!查到描述符后的操作相信大家就都懂了,其中有段基址,手上还有段内偏移,那还说什么,跑都跑不掉。你跑啊,你越跑我就越兴奋︿( ̄︶ ̄)︿