加载GDT表,进入保护模式
加载GDT表,实现操作系统从实模式进入保护模式
参考
操作系统学习 — 启动操作系统:进入保护模式
保护模式与实模式
GDT、GDTR、LDT、LDTR
调用门与特权级
趣谈 Linux 操作系统
在01.硬盘启动盘,加载操作系统中BIOS在0x7c00处加载MBR,并让程序跳转到该处。MBR只是一个中转站,其他功能如加载GDT表、切换保护模式,需要loader完成。
1.实模式
概念:
在程序中用到的地址都是真实的物理地址,“段基址:段内偏移地址”产生的逻辑地址就是物理地址,即程序员可见的地址完全是真实的内存地址。
物理地址(physicaladdress)=段值(segment) * 16 + 偏移(offset)
缺点:
- 没有权限划分,实模式下操作系统和用户程序属于同一特权级
- 程序中用到的地址都是真实的物理地址,逻辑地址等于物理地址
- 访问超过 64KB 的内存区域时要切换段基址,16位寄存器,2^16 = 64KB
- 共 20 条地址线,最大可用内存为 1MB
- 平坦模型,无法支持多任务,安全性无法保证
2.保护模式
概念:
保护模式下,全部32条地址线有效,可寻址高达4G字节的物理地址空间。保护模式下,地址表示方式:段(segment):偏移(offset),虽然段值仍然由原来的cs、ds等寄存器表示,但此时它仅仅变成了一个索引,这个索引指向了一个数据结构的一个表项(段描述符),表项中详细定义了段的起始地址、界限、属性等内容。这个数据结构就是全局描述符表GDT(LDT是局部的)。
在实模式下,段地址并非真实的物理地址,在计算物理地址时,还要左移 4 位(乘以 16)。和实模式不同,在 32 位保护模式下,段地址是 32 位的线性地址,如果未开启分页功能,该线性地址就是物理地址。
- 段基址:指定了这个段的起始地址,它是一个32位的值,用于计算线性地址。
- 段界限:段界限符 = 该段的大小。
- G位:G表示段界限粒度大小,假如 G = 1,粒度 = 4kb(最大段界限 = 2 20 ∗ 4 K B = 2 32 2^{20} * 4KB=2^{32} 220∗4KB=232),G = 0,粒度 = 1字节( 2 20 ∗ 1 = 2 20 2^{20} * 1=2^{20} 220∗1=220)。超过段界限,就会触发内存段保护。
- 段类型:需要看两个字段:type和S位。
- S位:S位的作用在于,指示这个段是不是系统段。只有知道了S位,type段才有意义。系统段就是说,这个段由硬件CPU运行,非系统段是由软件(OS/用户)运行。
- S = 1 代码段或数据段的描述符
- S= 0 系统段的描述符
- S = 1 代码段或数据段的描述符
3.分段机制
段描述符表
每个段都需要一个段描述符。这些描述符存储在内存的一段空间中,即全局描述符表GDT(或LDT)。
GDT与LDT的区别:
- 范围不同:GDT 是全局段描述符表,包含系统中所有进程所需的所有段描述符,而 LDT 是局部段描述符表,只有在需要更多段描述符时才使用。
- 使用限制不同:每个进程只能使用一个 GDT,但可以同时使用多个 LDT。
- 加载方式不同:GDT 的基地址和限长保存在 GDTR 寄存器中,由 CPU 加载,而 LDT 的基地址和限长保存在相应进程的 LDTR寄存器中,由操作系统加载,与进程的虚拟地址空间独立。
- 访问速度不同:由于 GDT 存储所有段描述符,因此访问 GDT 通常比访问 LDT 更快。
描述符表的长度可变,最多可以包含8K个这样的描述符(因为段选择子是16位的,其中的13bit用来作index,2^13=8K)。
段选择器
实模式下的 6 个段寄存器 CS、 DS、 ES、 FS、 GS 和 SS,在保护模式下叫做段选择器。
其结构如下图所示
- 第0-1位:请求特权级(Requestor Privilege Level, RPL),用于表示当前代码的特权级别。RPL可以取值0、1、2、3,其中0表示最高特权级,3表示最低特权级。
- 第2位:表指示器(TableIndicator, TI),用于指示该段描述符存储在全局描述符表(GDT)还是局部描述符表(LDT)中。TI的值为0表示描述符存储在GDT 中,为1表示描述符存储在 LDT 中。
- 第3-15位:索引(Index),用于标识段描述符在 GDT 或 LDT中的位置,索引的值必须是 8 的倍数。
段选择子的结构可以通过一个 16 位的寄存器来存储和传递,如 CS(代码段选择子)、DS(数据段选择子)和 SS(堆栈段选择子)等。当 CPU 访问某个段时,它会根据段选择子来确定所需的段描述符的位置,并加载该段描述符以获取相应的段基址、权限和限长等信息,从而实现对这个段的正确访问。
GDT表可以很大,存放在内存中,由 GDTR寄存器 存储它的地址。同理,LDT存储在LDTR中。其中:
- GDTR全局描述符寄存器:48位,高32位存放GDT基址,低16为存放GDT限长。
- 通过 lgdt xxxx 将xxxx存储到 GDTR 寄存器,xxxx包含GDT内存起始地址和GDT界限。
- 通过 sgdt xxxx获得当前 GDTR 寄存器的内容存储到 xxxx 当中。
- LDTR局部描述符寄存器:16位,高13为存放LDT在GET中的索引值。
以此构建GDT表:
[SECTION .gdt]
DATA_SEG_BASE equ (0x1000)
DATA_SEG_LIMIT equ 0xfffff
CODE_SEG_BASE equ (0x0)
CODE_SEG_LIMIT equ 0xfffff
; 索引<<3位 = 地址相减 。2^3 = 8字节
CODE_SELECTOR equ (gdt_code - gdt_base) ; 代码段选择子
DATA_SELECTOR equ (gdt_data - gdt_base) ; 数据段选择子
gdt_base:
dd 0, 0
gdt_code:
dw CODE_SEG_LIMIT & 0xffff ; 段界限
dw CODE_SEG_BASE & 0xffff ;
db CODE_SEG_BASE >> 16 & 0xff
; P_DPL_S_TYPE
; 其中1表示有效,00表示特权级0,1表示的代码段,1000表示代码段不可读写,可执行
db 0b1_00_1_1000
; G_DB_AVL_LIMIT
; 其中0表示段界限以字节为单,1表示此段是32位的,后四位表示段界限高4位。
db 0b0_1_00_0000 | (CODE_SEG_LIMIT >> 16 & 0xf)
db CODE_SEG_BASE >> 24 & 0xf
gdt_data:
dw DATA_SEG_LIMIT & 0xffff
dw DATA_SEG_BASE & 0xffff
db DATA_SEG_BASE >> 16 & 0xff
; P_DPL_S_TYPE
; ,其中1表示段描述符有效,00表示特权级0,1表示数据段,0010表示数据段可读写。
db 0b1_00_1_0010
; G_DB_AVL_LIMIT
; 其中1表示段界限以4KB作为单位,1表示此段是32位的,后四位表示段界限高4位。
db 0b1_1_00_0000 | (DATA_SEG_LIMIT >> 16 & 0xf)
db DATA_SEG_BASE >> 24 & 0xf
gdt_ptr:
dw $ - gdt_base - 1
dd gdt_base
寻址流程
寻址流程图下图所示:
分段机制下的虚拟地址由两部分组成,段选择子和段内偏移量。段选择子保存段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。虚拟地址中的段内偏移量应该位于 0 和段界限之间。如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。
- 1.根据段选择子在段描述表中找到对应的段描述符号。
- 2.根据段描述符获取段基址、段界限、特权等级等。
- 3.根据段基地址和段内偏移量得到物理地址。
根据GDT寻址
当 TI=0时表示段描述符在GDT中
① 先从GDTR寄存器中获得GDT基址。
② 然后再GDT中以段选择符高13位位置索引值得到段描述符。
③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址(程序给出)才得到最后的线性地址。
根据LDT寻址
当TI=1时表示段描述符在LDT中
① 还是先从GDTR寄存器中获得GDT基址。
② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。
③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
④ 用段选择符高13位位置索引值从LDT段中得到段描述符。根据位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址(程序给出)才得到最后的线性地址。
4.进入保护模式流程
进入保护模式的代码却是千奇百怪的,形式可不统一。比如进入保护模式需要三个步骤。
- 打开 A20
- 加载 gdt
- 将 cr0 的 pe 位置 1
打开 A20
打开A20,避免地址回绕
打开方式:
- 通过键盘控制器
- 调用BIOS功能
- 使用系统端口
in al, 92h
or al, 00000010b
out 92h, al
加载 gdt
GDT 有两个相关的指令:
- lgdt xxxx:xxxx有6字节,前2字节是一个gdt界限,后4字节是gdt起始地址。将 xxxx 存储到 gdtr
- sgdt xxxx:获得gdtr的值,存储到 xxxx
进入保护模式一定需要加载gdt,首先在loader建立一个GDT,然后将 GDT 的地址、界限存储到 GDTR 寄存器当中。
lgdt [gdt_ptr]
将 cr0 的 pe 位置 1
CR0 寄存器的第0位是 pe位,Protection Enable,用来启动保护模式,是保护模式的开关。所以这一步一般都是最后一步。
PE 为 0 表示在实模式下运行,PE 为 1 表示在保护模式下运行。
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
5.验证
进入保护模式流程图:
验证方法:
protected_mode:
; 设置段寄存器
mov ax, DATA_SELECTOR ; 设置数据段选择子为DATA_SELECTOR
mov ds, ax ; 将数据段寄存器设置为DATA_SELECTOR
; 转换为逻辑地址:ds:0xB7000
; ds 绑定数据段,基地址为0x1000, 对应的线性地址(物理地址)0xB7000+0x1000 = 0xB8000
; 0xB8000对应显存的物理地址,若能超过写入字符
; 说明CPU 能够正确地处理地址映射,并且成功进入了保护模式
mov byte [0xB7000], 'A'
; 测试是否成功进入保护模式.如果没有,写入内存是失败的
; 同理逻辑地址为: ds:0x100000
; 对应的线性地址(物理地址) 0x101000
xchg bx, bx
mov byte [0x100000], 0xAB
jmp $
当前未开启分页,线性地址=物理地址。