进入32位保护模式
有一说一 x86是有点恶心。
进入保护模式很简单,只需要将cr0
寄存器的PE位置1就行了。但是进入之前以及进入后需要一些额外的工作。
16位实模式和32位保护模式有几点不同:
- 16位实模式只能寻址最多1MB内存(段+偏移),32位则可以使用4GB内存,不开启分页的话可以直接线性寻址
- 16位里面的段寄存器在32位里面变成了段选择器,32位扩展了段寄存器,低16为仍然可以直接操作,高位由CPU自己使用
- 32位下访问内存需要通过段描述符和段选择器实现
- 32位下的中断不再是从0x0000开始的中断向量表(high 16 bits:段地址 low 16 bits:偏移)
由于这些不同,在进入保护模式前我们需要一些准备工作。
设置GDTR寄存器
GDTR
gdtr
是一个48位的寄存器,高32位用来存放段描述符地址,低16位用来存放段描述符的界限,由于段描述符占8个字节,所以低16位最多可以存放\(2^{16} \div 8 = 8192\)个段描述符,界限为(0~8191)。
GDT (Global Descriptor Table)
前面提到的段描述符就是对要使用段的描述,比如数据段ds
可以给它一些属性,属性描述如下:
high 32 bits
31 24 23 22 21 20 19 16 15 14 13 12 7 0
+----------------+--+--+--+--+--------+--+----+--+--------+----------------+
| base 24~31 |G |DB|L |A |seg16~19|P |DPL |S | TYPE | base 16~23 |
+----------------+--+--+--+--+--------+--+----+--+--------+----------------+
low 32 bits
31 16 15 0
+------------------------------------+-------------------------------------+
| base 0~15 | seg 0~15 |
+------------------------------------+-------------------------------------+
如你所见,基地址(base)和 段大小(seg)被分割成了好几块,实在是恶心(兼容性的锅)。下面解释下:
seg: 段大小,即段可用内存的范围(栈段向下),共20位
base: 基地址,共32位,被分割成了3份
G:粒度,如果为0,表示段以为1字节扩展,否则以4KB为单位扩展,由于段大小为20位,所以当G置0时最多能使用1MB内存,置1时则为4GB(\(2^{20} \cdot 4096\))
D/B: Default Operation Size 或 Upper Bound,表示是否用32位偏移地址、操作数、栈指针,总之32位保护模式总是置1就对了
L:留给64位用的,这里总是置0
A: Available, 多出来的位,CPU不用,留给OS用的,我们不用,总是置0
P:Segment Present,给CPU判断通过描述符访问的对象是否在内存中,我们还没用分页,总是在内存中,所以置1
DPL: Descriptor Privilege Level,表示描述符的特权等级,总共有4级特权(0~3)所以占两个位,某些指令只能由0特权的程序跑(比如OS),我们就是在写OS,所以总是用0特权级
S:表示描述符的类型,置1时表示代码段或数据段,置0时表示系统段和门描述符(同样根据TYPE细分)
TYPE:这个就有点复杂了,对数据段来说这4位分别表示
- X 可执行,显然,数据段不能执行
- E 扩展方向,0表示向上扩展,即普通的数据段,1表示向下扩展,即栈
- W 0表示只读,1表示可读可写
- A Accessed,表示段是否最近被访问过,操作系统记账用,比如内存紧张时可以看看哪些没用过的(置0的)把它们交换到磁盘上
对代码段来说这4位分别表示
- X 同数据段,不过代码段总是可执行的
- C 表示是否允许低特权级的程序执行当前特权的代码,置1时表示允许,置0时则只允许高于或等于当前特权的执行
- R 0表示只能执行,1表示可执行且可读
- A 同数据段
准备GDT
我们的bootloader会加载到0x7c00这里,并且这个bootloader会用掉512字节,我们会把GDT给放在这个0x7c00 + 512后面,即物理地址0x00007e00(当然GDT可以随便放,只要不影响使用)。这里我们会用到3个段,数据段、代码段、栈段,所以GDT应该有3个条目,但是,CPU规定第零个条目必须是空的,即填0,所以,实际上会有4个条目,一共占用 \(4 \cdot 8 = 32\) 字节,也就是说gdtr
的低16位的界限就是31 (0~31共32字节)。
根据上面对段描述符的解释,我们可以得到,数据段的高位是 0x004920b
即:
段基地址的17~31位为0,16位为b
(显示地址的最高位),
粒度是字节(G=1),
不是系统段(S=1),
是32位的段(D=1),
在内存中(P=1),
特权为0(DPL=00),
可读写的数据段(TYPE=0010),
这里我们直接把数据段其实放在了0xb8000(文本模式的显示地址)大小是64KB,所以低位是 0x8000ffff
画个图就是这样:
注意:第一个条目应该是空的,这里只是演示
+-----------+ -> 0x00007e08
| 0x004920b |
+-----------+ -> 0x00007e04
| 0x8000fff |
+-----------+ -> 0x00007e00
|bootloader |
+-----------+ -> 0x00007c00
~ ~
~ ~
| |
+-----------+ -> 0x00000000
同样的,我们可以得到代码段为:
high: 0x00409800
low : 0x7c0001ff
栈段为:
high: 0x00409600
low : 0x00007a00
所以准备GDT的代码就是:
; ds is set to logic base of 0x00007e00
; bx is set to offset of 0x00007e00
; empty descriptor
mov dword [bx+0x00], 0x0
mov dword [bx+0x04], 0x0
; data seg descriptor
mov dword [bx+0x08], 0x8000ffff
mov dword [bx+0x0c], 0x0040920b
; code seg descriptor
mov dword [bx+0x10], 0x7c0001ff
mov dword [bx+0x14], 0x00409800
; stack seg descriptor
mov dword [bx+0x18], 0x00007a00
mov dword [bx+0x1c], 0x00409600
设置GDTR
设置gdtr
需要设置高32位的基地址和低16位的界限,这里我们把这两个放在一起
gdt_bound dw 0 ; 2 bytes
gdt_base dd 0x00007e00 ; 4 bytes
这连续的6个字节刚好可以放进gdtr
,由于已经设置了基地址为0x00007e00
这里只需要设置界限即可
mov word [cs:gdt_bound+0x7c00], 31
注意,这里必须加上0x7c00
(因为bootloader代码地址在这里开始)。
接下来就是设置gdtr
寄存器了:
lgdt [cs:gdt_boud+0x7c00]
这样lgdt
就会从[cs:gdt_boud+0x7c00]
出读取连续的6个字节,完成设置。
进入保护模式
只需要
mov eax, cr0
or eax, 1
mov cr0, eax
好了,现在就是保护模式了。接下来的代码必须按32位编译了,因此需要加上 bits 32
,原因是32下指令的机器码都不一样了。
进入之前
前面提到,保护模式下访问内存的方式变了, 拿0x0000:0x7c00
举例,以前是逻辑地址(段地址+偏移),现在是段选择器+偏移,同样的段寄存器,但使用方式变了。下面是段选择器(即扩展后段寄存器的低16位)的描述:
15 3 2 0
+----------------------+--+----+
| descriptor index |TI| RPL|
+----------------------+--+----+
其中descriptor index
即前文GDT中条目的索引,TI
即 table indicator,为1时表示描述符在LDT(后面再说)中,为0时表示在GDT中,RPL
表示特权,现在填0即可(即00)。
还有点需要注意的是,在进入32位保护模式前,我们的代码是按照16位模式编译的,在为进入时很多指令可能进到了流水线,一旦进到32位保护模式,这些已经缓存的指令还是16位的,这样会对后续的执行造成影响,甚至出错。所以,我们需要清空这些东西,最简单的办法就是做一个远跳转。
有了这些前置准备,下面可以写代码了
mov eax, cr0
or eax, 1
mov cr0, eax
jmp dword 0x0010:start
bits 32
start:
; do something
代码中段选择器是0x0010
,按照上面的描述,换成2进制表示为0000_0000_0010_000
表示代码段(见准备GDT处)的索引。
演示
完整代码protect_mode.asm