保护模式的作用
实模式下CPU只提供了部分资源和功能供我们使用,但是在保护模式下CPU的全部资源和功能都可以供我们使用,同时CPU还提供了对于不同内存段的基于权限级别的保护方式,这样代码运行起来更安全,寻址方面,保护模式下可以寻址4G的地址空间。
保护模式下的分段管理
保护模式下也采用分段管理的方式将逻辑地址转换成线性地址,但是分段管理的基址是不同的,保护模式的分段管理更复杂,下面一张图片说面了保护模式如何从逻辑地址转换成线性地址:
这是来自于Intel官方文档的图片,在保护模式下,逻辑地址的两部分分别为,段选择子(Segment selector)和偏移量(Offset),段选择子是一种数据结构,使用它在描述表(Descriptor Table)中查找相应的段描述符(Segment Descriptor),段描述符中包含段的基址以及一些其他的保护模式需要的属性,然后通过基址与偏移量相加得到线性地址,这里不需要像实模式那样将基址左移4位。
GDT与GDTR
从上图中可以看出保护模式下的分段管理中最重要的部分就是描述符表这个结构,如果没有它就不能够实现从逻辑地址到线性地址的转换。CPU中有且只有一个全局可见的描述符表叫做全局描述符表GDT(Global Descriptor Table),所有的逻辑地址到线性地址的转换都要通过GDT,CPU使用GDTR寄存器保存GDT的基址。
上图是Intel官方文档中描述的GDTR的结构,它包含两部分,32位或者64位的GDT基地址以及16位的GDT表限,这个基地址是基于地址0的线性地址,表限是GDT的以字节为单位的大小。
使用指令lgdt以及sgdt来载入和读取GDTR中的内容。GDT的基址可以是以0为基址的任何线性地址。在切换到保护模式之前,必须载入有效的GDT。
段描述符
GDT实际上就是一个段描述符的数组,段描述符中保存关于段的基址以及其他一些属性。
上图是来自与Intel官方文档中对于段描述符的描述,可以看出段描述符中除了段基址和段限之外还有很多属性,具体请参考Intel官方手册关于段描述符的部分,但是值的注意的是GDT的第一条描述符必须是全零的,也称作NULL描述符。
段选择子与段寄存器
在保护模式中,逻辑地址是由段选择子和偏移量两部分组成的。CPU通过段选择子来选择GDT中的段描述符,从而得到段基址,然后与偏移量一起构成了现行地址。
这是Intel官方文档中对于段选择子的描述,它是一个16位的结构,包含三部分,index部分就是段描述符在段描述符表中的索引,TI表示使用GDT还是LDT,而RPL是要求权限级别。如果TI=0,RPL=0,而且我们知道段描述符是8个字节的,那么其实这个段选择子就是段描述符在GDT中的字节偏移。
什么时候使用段选择子来进行地址转换呢?段选择子保存在什么寄存器中呢?答案显而易见----段寄存器,段选择子保存在段寄存器中,Intel给出了下列情况发生地址转换:
- 直接加载段寄存器,如使用mov,pop,lds,les,lss,lgs和lfs指令。
- 隐式的加载指令,例如lcall,ljmp,ret等,sysenter,sysexit,iret,into,int n,int3指令,这些指令改变cs寄存器。
Intel官方文档给出了段寄存器的全貌,当使用上述两点提到的指令的时候就会将相应的段描述符中的内容加载到不可见部分,这样了为了减少地址转换的时间和代码的复杂度。
切换到保护保护模式
Intel官方文档中给出了一个切换到保护模式的建议,由于我们演示的情况是切换到保护模式的最简单的情况,所以官方建议中的很多都没有用到,例如分页等。我们按照官方给出的建议裁剪了一下:
- 使用cli指令抑制所有可屏蔽的中断
- 使用lgdt指令加载GDT基址和表限
- 开启CR0.PE
- 使用ljmp或者lcall指令跳转到保护模式代码
- 加载段寄存器
示例
给出一段切换到保护模式的例子,按照上面提到的操作步骤切换到保护模式:
.code16
.section .text
.equ null_selector, 0x00
.equ code_selector, 0x08
.equ vedio_selector, 0x10
.equ stack_selector, 0x18
.equ data_selector, 0x20
.globl _start
_start:
cli
xorw %ax, %ax
movw %ax, %ds
movw %ax, %es
movw %ax, %ss
# move data to 0x800
movw $32, %cx
movw $_protect_msg, %si
movw $0x800, %di
rep movsb
# move protect mode code to 0x2000
movw $_protect_length, %cx
movw $_protect, %si
movw $0x2000, %di
rep movsb
lgdt gdt_pointer
movl %cr0, %eax
orl $0x01, %eax
movl %eax, %cr0
ljmp $code_selector, $0x00
gdt_start:
null_descriptor:
.quad 0x0000000000000000
code_descriptor:
.quad 0x00cf9a002000ffff
vedio_descriptor:
.quad 0x00cf920b8000ffff
stack_descriptor:
.quad 0x00cf92007000ffff
data_descriptor:
.quad 0x00cf92000800ffff
gdt_end:
.equ gdt_len, gdt_end - gdt_start
gdt_pointer:
.short 0xffff
.int gdt_start
_protect_msg:
.ascii "In Protected-Mode."
_protect_msg_end:
.space 30-(.-_protect_msg), 0x00
.short _protect_msg_end - _protect_msg
.code32
.type _protect, @function
_protect:
movw $data_selector, %ax
movw %ax, %ds
movw $vedio_selector, %ax
movw %ax, %es
movw $stack_selector, %ax
movw %ax, %ss
movw $null_selector, %ax
movw %ax, %fs
movw %ax, %gs
movl $0x7000, %esp
movl $0x00, %esi
movl $30, %eax
movl %ds:(%eax), %ecx
call _echo
jmp .
# %edi, %ax, %ecx, %esi
.type _echo, @function
_echo:
xorl %ebx, %ebx
movl %ebx, %edi
movb $0x0c, %ah
start_echo:
movb %ds:(%esi), %al
movw %ax, %es:(%edi)
inc %ebx
movl %ebx, %edi
shll %edi
inc %esi
loop start_echo
ret
_protect_end:
.equ _protect_length, _protect_end - _protect
dummy:
.space 510-(.-_start), 0
magic_number:
.byte 0x55, 0xaa
这段代码是很容易理解的,它把数据段放在了0x800位置,把32位的保护模式代码放在了0x2000位置,GDT的基址就在gdt_start的位置,一共有5个段描述符。其中有一条video_descriptor是很特别的,它的基址是0xb8000,这是由于我们想要在屏幕上显式字符串,但是没有安装中断,只能直接把字符串写到视频缓冲区中,参考一下BIOS内存布局就知道0xb8000是字符视频缓冲区,相应的_echo就是用来处理屏幕输出的。
执行结果:
左上角的红字就是保护模式代码打印出来的结果。
出现的问题
这段代码的问题在于代码的末尾不能够使用sti打开中断屏蔽,原因上面提到了。此外还有一个问题就是不能使用qemu调试,因为qemu调试的时候也会产生#BP Trap,但是没有加载中断描述符表,会导致CPU重启。bochs调试不会产生#BP Trap,所以可以使用bochs调试。