Switching to Protected-Mode

保护模式的作用

实模式下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给出了下列情况发生地址转换:

  1. 直接加载段寄存器,如使用mov,pop,lds,les,lss,lgs和lfs指令。
  2. 隐式的加载指令,例如lcall,ljmp,ret等,sysenter,sysexit,iret,into,int n,int3指令,这些指令改变cs寄存器。
段寄存器不仅仅是16位,只是对于程序可见的部分是16位,此外还有不可见的部分:

Intel官方文档给出了段寄存器的全貌,当使用上述两点提到的指令的时候就会将相应的段描述符中的内容加载到不可见部分,这样了为了减少地址转换的时间和代码的复杂度。


切换到保护保护模式

Intel官方文档中给出了一个切换到保护模式的建议,由于我们演示的情况是切换到保护模式的最简单的情况,所以官方建议中的很多都没有用到,例如分页等。我们按照官方给出的建议裁剪了一下:

  1. 使用cli指令抑制所有可屏蔽的中断
  2. 使用lgdt指令加载GDT基址和表限
  3. 开启CR0.PE
  4. 使用ljmp或者lcall指令跳转到保护模式代码
  5. 加载段寄存器
其实还应该使用sti指令打开中断,但是现在的情况是没有设置中断描述符表,所以打开中断后由于没有中断处理程序,CPU会陷入错误中,导致重启。

示例

给出一段切换到保护模式的例子,按照上面提到的操作步骤切换到保护模式:

.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调试。


参考

《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide》
《自己动手写操作系统》
《x86/x64体系探索与编程》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值