以下图2, 图4和图5截自Intel手册
全局描述符表
- 全局描述符表中存放着段描述符, 每个段描述符8个字节.
- 为了跟踪全局描述符表, 处理器内部有一个48位寄存器, 叫做全局描述符表寄存器(GDTR), GDTR分为两部分,分别为32位的线性地址和16的边界, 32位线性基地址部分保存的是全局描述符表在内存中的起始线性地址, 16位边界部分是全局描述符表的边界(界限), 其值等于表的大小(总字节数) - 1, 因为从偏移0开始. 如果界限值是0, 表示表的大小是1字节.
- 因为GDT的界限是16位的, 故该表最大是2 ^ 16字节, 也就是65536字节(64KB), 又因为一个描述符8字节, 所以该表最多定义8192个描述符.在进入保护模式之后, 处理器立即要按新的内存访问模式工作, 所以, 必须在进入保护模式之前定义GDT, 但是, 由于实模式下只能访问1MB的内存, 故GDT通常多定义在1MB以下的内存范围中, 当然, 允许在进入保护模式后换个位置重新定义GDT
存储器段描述符
每个描述符占8字节, 下图中, 上面位高32位, 下面为低32位
- 段描述符中, 指定了32位的段起始地址和20位的段边界, 在实模式下, 段地址并非真实的物理地址, 计算物理地址时需要左移4位. 和实模式不同, 32位保护模式下, 段地址是32位的,如果未开启分页功能, 该线性地址就是物理地址. 段基地址可以是0~4GB范围内的任意地址, 段界限用来限制段的扩展范围, 因为访问内存的方法是用段基地址加上偏移量, 所以, 对于向上扩展的段, 如代码段和数据段来说, 偏移量从0开始递增, 段界限决定了偏移量的最大值; 对于向下扩展的段,如栈段来说, 段界限决定了偏移量的最小值.
- G位是粒度位, 用于解释段界限的含义, G = 0时, 段界限以字节为单位, 此时段的扩展范围是从1字节到1MB, 因为段界限是20位的, 当G = 1时, 段界限以4KB位单位, 这样, 段的扩展范围是从4KB到4GB.
- S位用于指定描述符的类型, S = 0时, 表示是一个系统段, S = 1时, 表示是一个代码段或数据段(栈也属于特殊数据段)
- DPL表示描述符的特权级, 特权级分别是0, 1, 2, 3, 其中0是最高特权级, 3是最低特权级, 刚进保护模式时执行的代码具有0特权级(可以看成是从处理器那里继承来的), 在这里, 描述符的特权级用于指定要访问该段做必须具有的特权级, 如果这里的数值是2, 那么, 只有特权级0, 1和2的程序能够访问该段, 特权级3访问时, 处理器会予以阻止.
- P是段存在位, P位用于指示描述符所对应的段是否存在, 一般来说, 描述符所指示的段都位于内存中. 但是, 当内存紧张时, 有可能只是建立了描述符, 对应的内存空间并不存在, 这时, 就应当把描述符P位清0, 表示段并不存在. 另外, 同样是在内存紧张的情况下, 会把很少用到的段换出到硬盘, 腾出空间给急需内存的程序使用(当前正在执行的), 这时, 同样要把P位清0, 当再次轮到它执行时, 再装入内存, 然后将P位置1. P位是由处理器负责检查的, 每当通过描述符访问内存中的段时, 如果P位是0, 处理器就产生一个中断, 通常该中断处理程序是由操作系统提供, 该处理过程的任务是负责将该段从硬盘换回内存, 并将P位置1, 在多用户, 多任务的系统中, 这时一种常用的虚拟内存调度策略.
- D/B位是默认的操作数大小或默认的栈指针大小, 该标志对不同的段有不同的效果, 对于代码段, 此位称作D位, 用于指示指令中默认的偏移地址和操作数大小, D = 0表示是16位的, D = 1, 指示是32位的. 如果D = 0, 处理器在这个段执行时, 将使用16位指令指针寄存器IP, 否者使用32位的EIP. 对于栈段来说, 该位就做B位, 用于在进行隐式的栈操作时, 是使用SP还是使用ESP, 隐式的栈指令包括 push, pop和call等, 如果该位位0, 则使用SP, 栈段的上部边界位0xffff, 否者使用ESP, 上部边界为0xffffffff.
- L位是64位代码位, 保留此位给64位处理器使用
- TYPE共4位, 指示描述符的子类型, 或者说是类别, 对数据段来说, 分别是X, E, W, A位, 对代码段来说是X, C, R, A位 上表中, A位是已访问位, 每当该段被访问时, 处理器自动将该位置1, 对该位清0是由软件(操作系统)负责的, 通过定期监视该位的状态, 可以统计出该段的使用频率. X代表执行, E位指示扩展方向, W位指示可写, C位指示段是否为特权级依从的, C = 0表示非依从的代码段, 这样的代码段只能供同特权级的程序使用, 或者通过门调用, C = 1则表示这样的代码段为依从代码段, 可以从特权级比它低的程序调用并进入. 代码段总是可以执行的, 但是为了防止程序被破坏, 它是不能写入的. 它的R位不是用来限制处理器是否可读的, 而是用来限制程序的, 一个典型的例子是使用段超越前缀CS:来访问代码段中的内存.
- AVL位是软件可以使用的位, 通常由操作系统来使用他, 处理器不使用它.
段选择子
- 3~15是段描述符在描述符中的索引, TI = 0时表示描述符在GDT中, TI = 1时, 描述符在LDT中, RPL是请求特权级, 表示给出当前选择子的那个程序的特权级, 正是该程序要求访问这个内存段.
段描述符高速缓存器
- 如上图所示, 在32位处理器上运行时, 每个段寄存器还包括一个不可见的部分, 称为描述符高速缓存器, 用来存放段的线性基地址, 段界限和段属性, 既然不可见, 那就是处理器不希望我们访问它. 事实上, 我们也没有任何办法来访问这个不可见部分, 它是由处理器内部使用的.
- 当处理器在执行任何改变段选择器(段寄存去CS, DS等)的指令时(比如pop, jmp, call, ret等), 就将指令中提供的索引号(就是选择子中的索引)乘以8(一个描述符8字节)作为偏移地址, 同GDTR中提供的线性基地址相加, 以访问GDT, 如果没有发现什么问题(比如超出了GDT的界限), 就自动将找到的描述符加载到不可见的描述符高速缓存部分. 加载的部分包括段的线性基地址, 段界限和段的访问属性. 此后, 每当有访问内存时, 就不再访问GDT中的描述符, 直接用当前段寄存器描述符高速缓存器提供的线性基地址.
进入保护模式
- 从0x92端口读取的数据, 第2位为开A20, 第一位 = 1为重启
- 在保护模式下, 不允许使用mov指令改变段寄存器CS的内容, 比如: mov cs, ax, 企图这样做将导致处理器产生一个无效操作码的异常中断.
下面是一段主引导区代码, 实验环境书中也有说, 我的资源中也有配书资源下载, 里面有说如何配置VirtualBox和Bochs, 这里说一下配置Bochs, 配书资源中关于配置Bochs中的
Disk&Boot --> ATA channel 0 --> First HD/CD on channel 0 --> Type of disk image这个选项是vpc, 我用2.6.2配置时选vpc无法启动, 得选flat才可以, 2.6.0貌似选vpc可以
- mov ax, cs
- mov ss, ax
- mov sp, 0x7c00
- ; 计算GDT所在地址的逻辑段地址
- mov ax, [cs:gdt_base + 0x7c00]
- mov dx, [cs:gdt_base + 2 +0x7c00]
- mov bx, 0x10
- div bx
- mov ds, ax ; 商为段地址
- mov bx, dx ; 余数为偏移地址
- ; 全局描述符#0, 第一项必须为0
- mov dword [bx + 0x00], 0x00000000
- mov dword [bx + 0x04], 0x00000000
- ; 全局描述符#1, 代码段描述符
- ; 线性基地址: 0x00007c00, 段界限: 0x01ff, 段界限在数值上等于段的长度 - 1, 因此该段的长度是0x200, 即512字节
- ; 粒度位字节(G = 0), 属于存储器段(S = 1)
- ; 32位的段(D/B = 1), 位于内存当中(P = 1)
- ; 段的特权级位0(DPL = 00)
- ; 只执行(TYPE = 1000)
- mov dword [bx + 0x08], 0x7c0001ff
- mov dword [bx + 0x0c], 0x00409800
- ; 全局描述符#2, 数据段描述符
- ; 线性基地址: 0x000b8000, 段界限: 0xffff
- ; 粒度位字节(G = 0), 属于存储器段(S = 1)
- ; 32位的段(D/B = 1), 位于内存当中(P = 1)
- ; 段的特权级位0(DPL = 00)
- ; 可读可写, 向上扩展的数据段(TYPE = 0010)
- mov dword [bx + 0x10], 0x8000ffff
- mov dword [bx + 0x14], 0x0040920b
- ; 全局描述符#3, 栈段描述符
- ; 线性基地址: 0x00000000, 段界限: 0x07a00
- ; 粒度位字节(G = 0), 属于存储器段(S = 1)
- ; 32位的段(D/B = 1), 位于内存当中(P = 1)
- ; 段的特权级位0(DPL = 00)
- ; 可读可写, 向下扩展的数据段, 在这里是栈段(TYPE = 0110)
- mov dword [bx + 0x18], 0x00007a00
- mov dword [bx + 0x1c], 0x00409600
- ; 初始化全局描述符表
- mov word [cs:gdt_size + 0x7c00], 31 ; 全局描述符表界限(总字节数 - 1)
- lgdt [cs:gdt_size + 0x7c00] ; 加载全局描述符表
- ; 开A20
- in al, 0x92
- or al, 0x02
- out 0x92, al
- cli ; 保护模式下中断机制尚未建立, 关中断
- ; 打开保护模式, cr0寄存器0位置1
- mov eax, cr0
- or eax, 1
- mov cr0, eax
- ; 进入保护模式, 详情参见本书P199 11.7 清空流水线并串行化处理器
- ; 注意, 不管你用的是16位远转移, 还是32位远转移
- ; 因为现在已经处于保护模式下, 处理器都将把第一个参数0x0008视为选择子
- ; 而不是实模式下的逻辑段地址
- jmp dword 0x0008:_ProtectMode
- [bits 32]
- _ProtectMode:
- ; 选择子00000000000_10_000B, 索引为2的描述符
- mov ax, 0x10
- mov ds, ax
- mov byte [0x00], 'H'
- mov byte [0x02], 'e'
- mov byte [0x04], 'l'
- mov byte [0x06], 'l'
- mov byte [0x08], '0'
- mov byte [0x0a], ' '
- mov byte [0x0c], 'O'
- mov byte [0x0e], 'n'
- mov byte [0x10], 'z'
- mov byte [0x12], '.'
- mov byte [0x14], '.'
- mov byte [0x16], '.'
- hlt ; 因为关中断, 所以处理器不会被唤醒
- gdt_size dw 0
- gdt_base dd 0x7e00
- times 510 - ($ - $$) db 0
- dw 0xaa55