操作系统真象还原 学习笔记04--保护模式入门

selph
selph
发布于 2021-01-24 / 1323 阅读
1
3

操作系统真象还原 学习笔记04--保护模式入门

本篇简单介绍了保护模式和实模式的异同、全局描述符表 GDT ,以及如何进入保护模式。

进入保护模式的三个步骤:

  • 打开A20地址线
  • 初始化全局描述符表 GDT(lgdt指令)
  • 将控制寄存器CR0的PE位置1开启CPU的保护模式

本篇难点:

  • 全局描述符表的使用原理

初见保护模式

保护模式的概念

保护模式首次出现在80286 CPU上;在保护模式下,物理内存地址不能被直接访问,需要经过地址转换之后才能进行访问,地址转换是处理器和操作系统共同完成的,处理器硬件上提供转换部件,操作系统提供转化过程中所需要的页表。

实模式和保护模式是CPU的概念,实模式的CPU运行环境是16位,保护模式是32位,实模式指的是32位CPU在16位模式下运行的状态。


段寄存器的变化

在保护模式下,除了段寄存器,其他寄存器都被扩展成了32位,保护模式提供的安全性很大一部分体现在了内存段的描述方面;

偏移地址和实模式一样,但段基址会有很大不同,为了给不同的内存段添加不同的属性,专门找了个数据结构--全局描述符表来描述,每一个表项称为段描述符,64字节,用来描述各个内存段的起始地址、大小、权限等信息;

全局描述符表很大,所以放在内存中,用专门的寄存器---GDTR寄存器指向这个数据结构;

段寄存器里保存的就不再是段基址了,里面保存的是段选择子,selector,用来在全局描述符表中索引段描述符;

由于访问内存对CPU来说很慢,所以这里应用了缓存技术,将段信息用一个寄存器来缓存(段描述符缓冲寄存器),CPU每次获取到整理好的段描述符信息,都会存在这里,直到下一次获取新的段描述符信息

因为CPU是兼容实模式,所以在实模式下,段描述符缓冲寄存器里存放的是段基址左移4位的值


地址访问

IA-32体系的CPU访问内存的方式还是分段策略,段基址:段内相对偏移地址,从80386CPU开始,段基址和段内偏移地址都是32位的了,仅用段内偏移即可访问完整4GB内存了

在实模式与保护模式运行的中间,有一个过度模式--虚拟8086模式;


寄存器的变化

在实模式下,寄存器都有固有使命,比如基址寄存器bx,bp,变址寄存器si,di

到保护模式了之后,寄存器的功能就不是那么固定了,所有通用寄存器都可以当基址寄存器,除了esp以外都可以做变址寄存器。

全局描述符表 GDT

到了保护模式下,不再是简单的用段寄存器加载一下段基址就能用了,段的信息增加了很多,需要提前定义好段才能用,全局描述符表 GDT 就是存储段信息的数据结构。

全局描述符表 GDT 的表项,段描述符,就是用来描述段的属性的,该结构是8字节大小。段描述符格式如下图所示:

在这里插入图片描述

段基址 与 段界限 和 G位

保护模式总线宽度32位,所以段基址也是要用32位表示;

段界限表示段边界的扩展最值,分为向上扩展(段界限表示最大值,例如数据段,代码段)和向下扩展(段界限表示最小值,例如栈段)两种,段界限是个单位量,要么是1字节(G=0)要么是4KB(G=1),取决于描述符中的G位;

段界限边界值 = (描述符中的段界限值 + 1) * (段界限颗粒度大小) - 1

段界限是用来限制段偏移地址的(如果不限制,则仅用偏移地址即可访问全部内存)

如果访问的地址超过了段界限的最值,则CPU会抛出异常,程序员负责写相应异常的处理程序。

至于段描述符为啥把段基址,段界限分散成这么多段来存放,是历史原因,当时为了兼容80286这个半成品而导致的。

字段 S

段描述符在CPU眼里分为两大类,系统段和数据段

  • 凡是硬件运行需要的东西都是系统,代码段属于系统段
  • 凡是软件运行需要的东西都是数据,代码段属于非系统段(数据段)

S位决定段是否属于系统段,S=0则表示系统段,S=1则是数据段

S位要和Type字段配合在一起才能知道确定的段描述符的类型,只有S位确定了,Type字段才有意义

各种称为门的结构都是系统段,任务门,调用门等,门便是入口。

字段 Type

在这里插入图片描述

Type字段共4位,目前主要关注非系统段,表中的4位值分别含义是:

  • A位,Accessed位,由CPU设置,每当该段被CPU访问后,CPU就将此置1;

    在创建新的段描述符的时候,应该将此置0,在调试是能判断该描述符是否可用。

  • C位,Conforming位,一致性代码段,也叫依从代码段;

    如果自己是转移的目标段,并且自己是一致性代码段,自己的特权级一定要高于当前特权级,转移后的特权级与转移前的特权级一致,也就是依从转移前的特权级

    C=1表示是一致性代码段,C=0表示不是

  • R位,表示可读,R=1可读,R=0不可读

    不可读代码段是用来限制代码指令的,如果读了不可读的内存,CPU会抛出异常

  • X位,表示可执行,X=1可执行(代码段),X=0不可执行(内存段)

  • E位,Extend,表示扩展方向,0为向上扩展,地址越来越高(代码段,数据段),1为向下扩展,地址越来越低(栈段)

  • W位,Writeable,表示写入,1可写入,0不可写入,写入不可写入的数据段CPU会抛出异常

字段 DPL

Descriptor Privilege Level,描述符特权级

因为DPL字段占2位,所以分为0,1,2,3一共四种特权级,CPU从实模式进入保护模式后,特权级自动变为0,保护模式下的代码是操作系统的一部分,操作系统应该处于最高特权级,而用户程序处于3特权级

特权级具体指的是描述符所指的内存段的特权级别

字段 P

Present,表示段是否存在,如果段存在于内存,P=1,否则P=0

P字段是CPU来检查的,如果为0,CPU会抛出异常,CPU只负责检查,不负责处理

设计之初是用于当内存不够用的时候,将内存不常用的段放到硬盘里,有了分页功能后,就用分页功能来完成这件事了

字段 AVL

Available,可用的,操作系统可以随意使用,对硬件来说没有专门的用途

字段 L

用来设置是不是64位代码段,1表示是,0表示否

字段 D/B

用来指示有效地址及操作数的大小,这是兼容80286CPU的产物,与指令有关的段是代码段和栈段:

  • 对代码段来说,此位是D位,D为0表示16位,D为1表示32位
  • 对栈段来说,此位是B位,B为0表示16位,B为1表示32位

全局描述符表 GDT、局部描述符表 LDT 及选择子

全局描述符表 GDT

全局描述符表就像是一个数组,而其中的段描述符就是数组中的元素,通过段选择子进行索引,可以获取指定的段描述符

全局描述符表的全局,指的是多个程序都可以在里面定义自己的段描述符,是公用的

全局描述符表位于内存中,通过专门的寄存器 GDTR 指向他的位置

在这里插入图片描述

使用之前需要对该寄存器进行初始化,指令为:lgdt 48位内存数据

这个指令在进入保护模式前后都可以执行,进入保护模式需要有GDT,在保护模式中还可以换个GDT加载

GDTR寄存器分为两部分,前16位是GDT的界限,后32位是GDT的起始地址,由于GDT的大小是16位二进制,界限最大值是2^16=65536,而每个描述符是8字节大小,则最多可存下65536/8=8192个段或门。


选择子 Selector

原本在实模式下的段寄存器里存储的是段基址,在保护模式下,段寄存器里存入的内容变成了选择子:

在这里插入图片描述

段寄存器是16位,选择子也是16位:

第0-1位存入的是RPL,Request Privilege Level,请求特权级,请求者的特权级,可以表示为 0,1,2,3,4 四种特权级

第2位TI位,Table Indicator,用来指示选择子在GDT(TI=0)中还是在LDT(TI=1)中索引描述符。

剩下的13位是索引值,最大位2^13=8192个

选择子的作用,是确定段描述符,确定段的基地址,特权级,界限等

在保护模式下,IA-32 CPU依然是通过段基址:段内偏移地址的方式进行寻址,不过计算方式有所变化,因为段基址和段内偏移地址都是32位了,所以实际地址直接将选择子确定段描述符后,CPU自动从段描述符中取出段基址,直接加上偏移地址

GDT中第0个段描述符不可用,主要是为了避免选择子未初始化就使用的情况,如果用了则CPU会抛出异常


局部描述符表 LDT

LDT是一个系统段,也是一个局部描述符表,是全局描述符表的一个表项,是个嵌套的描述符表,用于程序内部进行段属性的设置


描述符种类很多,但描述符高32位中的第8~12位始终是不变的,12位必须是S,8-11位必须是type,其他位没有强制要求,所以很多系统段如中断门,陷阱门都是在中断描述符表中的。

打开 A20 地址线

实模式下存在一个A20地址回绕,超过1M的内容会被忽略,只有打开A20地址线,才能访问1M以外的内存空间:

向端口 0x92 的第1个位置写入1即可

in al, 0x92
or al, 0000_0010B
out 0x92, al

保护模式的开关,CR0 寄存器的 PE 位

这是进入保护模式的最后一步(第三步)

程序员不可见寄存器--控制寄存器CR0-CR3,可用来展示或者控制CPU的运行状态,其中的CR0寄存器中的第0位,便是保护模式的开关,将此位置一即可开启CPU的保护模式:

mov eax, cr0
or eax, 0x00000001
mov cr0, eax

进入保护模式

接下来通过实践来真正进入保护模式~

由于保护模式是在loader.bin中进入的,而loader.bin将会超过512字节,所以需要先对之前的程序进行一个小的修改

mbr.S

首先是 mbr.S(完整代码在上一篇笔记中有):

        mov eax ,LOADER_START_SECTOR    ;起始扇区lba地址
        mov bx  ,LOADER_BASE_ADDR       ;写入的地址
        mov cx  ,4                      ;待读入的扇区数
        call rd_disk_m_16               ;以下读取程序的起始部分

这里将cx,也就是待读入的扇区数由1改到4

boot.inc

下一个要更新的文件是 boot.inc :

;---------------loader and kernel---------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;---------------GDT 描述符属性------------------
DESC_G_4K       equ     1_00000000000000000000000b      ;颗粒度:4K
DESC_D_32       equ     1_0000000000000000000000b       ;操作数和地址大小:32位
DESC_L          equ     0_000000000000000000000b        ;是否是64位代码段:否
DESC_AVL        equ     0_00000000000000000000b         ;不用此位,暂设置为:0
DESC_LIMIT_CODE2 equ    1111_0000000000000000b          ;段界限19-16位
DESC_LIMIT_DATA2 equ    DESC_LIMIT_CODE2                ;段界限19-16位
DESC_LIMIT_VIDEO2 equ   0000_000000000000000b           ;?????
DESC_P          equ     1_000000000000000b              ;表示段存在
DESC_DPL_0      equ     00_0000000000000b               ;特权级:0
DESC_DPL_1      equ     01_0000000000000b               ;特权级:1
DESC_DPL_2      equ     10_0000000000000b               ;特权级:2
DESC_DPL_3      equ     11_0000000000000b               ;特权级:3
DESC_S_CODE     equ     1_000000000000b                 ;表示非系统段
DESC_S_DATA     equ     DESC_S_CODE                     ;同上
DESC_S_SYS      equ     0_000000000000b                 ;表示系统段
DESC_TYPE_CODE  equ     1000_00000000b                  ;Type字段-代码段:x=1,c=0,r=0,a=0
DESC_TYPE_DATA  equ     0010_00000000b                  ;Type字段-数据段:x=0,e=0,w=1,a=0

DESC_CODE_HIGH4 equ     (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00
DESC_DATA_HIGH4 equ     (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00
DESC_VIDEO2_HIGH4 equ   (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B

;--------------选择子 属性-------------------
RPL0    equ     00b
RPL1    equ     01b
RPL2    equ     10b
RPL3    equ     11b
TI_GDT  equ     000b
TI_LDT  equ     100b

注意!!!

这里书上有个问题,会导致后续程序运行出错,也算是个坑吧

在boot.inc的代码中,其中的显卡段的高4字节部分:

DESC_VIDEO2_HIGH4 equ   (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00

显存的起始地址是0xb8000,在段描述符低4字节中段基址0-15位存储的是0x8000,所以段描述符高4字节最初8位是段基址的23-16位的值应该是0xB,而不是0x00,所以这一行应该改成:

DESC_VIDEO2_HIGH4 equ   (0x00<<24) + DESC_G_4G + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B

loader.S

%include "boot.inc"
SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP        equ     LOADER_BASE_ADDR

jmp     loader_start

;构建 GDT 及其内部的描述符
GDT_BASE:
        dd      0x00000000
        dd      0x00000000

CODE_DESC:
        dd      0x0000FFFF
        dd      DESC_CODE_HIGH4

DATA_STACK_DESC:        ;直接用普通的数据段作为栈段
        dd      0x0000FFFF
        dd      DESC_DATA_HIGH4

VIDEO_DESC:
        dd      0x80000007      ;limit=(0xbffff - 0xb8000)/4k = 7
        dd      DESC_VIDEO_HIGH4;此时dpl为0

GDT_SIZE        equ     $ - GDT_BASE    ;获取 GDT 大小
GDT_LIMIT       equ     GDT_SIZE - 1    ;获取 段界限

times   60      dq      0       ;预留60个空位,为以后填入中断描述符表和任务状态段TSS描述符留空间
                                ;times 60 表示后面的内容循环60次,是nasm提供的伪指令

SELECTOR_CODE   equ     (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA   equ     (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO  equ     (0x0003 << 3) + TI_GDT + RPL0

;以下是 gdt 指针,前2字节是gdt界限,后4字节是gdt起始地址
gdt_ptr dw      GDT_LIMIT
        dd      GDT_BASE

loadermsg db '2 loader in real.'

;---------------------------------------------------------
;INT 0x10       功能号:0x13     功能描述符:打印字符串
;---------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX = 字符串长度
;(DH,DL)=坐标(行,列)
;ES:BP=字符串地址
;AL=显示输出方式
;0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
;1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
;2——字符串中只含显示字符和显示属性。显示后,光标位置不变。
;3——字符串中只含显示字符和显示属性。显示后,光标位置改变。
;无返回值


loader_start:
        ;显示字符串,表示当前在实模式
        mov sp, LOADER_BASE_ADDR
        mov bp, loadermsg       ;ES:BP 字符串地址
        mov cx, 17              ;字符串长度
        mov ax, 0x1301          ;AH=13h,AL=01h
        mov bx, 0x001f          ;页号为0(BH=0h),蓝底粉红字(BL=1fh)
        mov dx, 0x1800          ;
        int 0x10                ;int 10 BIOS中断

        ;准备进入保护模式
        ;1.打开A20地址线
        in al, 0x92
        or al, 00000010B
        out 0x92, al

        ;2.加载GDT
        lgdt [gdt_ptr]

        ;3.将CR0的PE位置1
        mov eax, cr0
        or eax, 0x00000001
        mov cr0, eax

        jmp dword SELECTOR_CODE:p_mode_start    ;刷新流水线
                                               ;流水线是CPU 的工作方式,会把当前指令和后面的几个指令同时放在流水线中重叠执行,由于之前的代码是16位,接下来的代码变成32位了,指令按照16位进行译码会出错,通过刷新流
水线可以解决这个问题

[bits 32]       ;编译成32位程序
p_mode_start:
        mov ax, SELECTOR_DATA
        mov ds, ax
        mov es, ax
        mov ss, ax
        mov esp, LOADER_STACK_TOP
        mov ax, SELECTOR_VIDEO
        mov gs, ax
        mov byte [gs:160],'P'

        jmp $

编译,写入到硬盘:

nasm -I include/ -o mbr.bin mbr.S 
nasm -I include/ -o loader.bin  loader.S

dd if=boot/mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc
dd if=boot/loader.bin of=hd60M.img bs=512 count=4 seek=2 conv=notrunc

运行Bochs

image-20210124230100337

远跳转指令jmp

从实模式进入保护模式有两个问题需要解决:

  • 一是段描述符缓冲寄存器的值需要改成32位的值,因为在保护模式下,段描述符缓冲寄存器的值还是16位的话,如果使用了这个寄存器就会出现问题,可以通过call,jmp,ret等指令来间接实现

  • 二是需要将CPU流水线清空,这就只能使用jmp来实现了

所以用jmp一举两得。

保护模式下内存段的保护

加载选择子时候的保护:判断选择子的索引是否在范围内:

描述符表基地址 + 选择子中的索引值 * 8 + 7 <= 描述符表基地址 + 描述符表界限值

代码段和数据段的保护:检查地址边界

EIP 中的偏移地址 + 指令长度 - 1 <= 实际段界限大小

偏移地址 + 数据长度 - 1 <= 实际段界限大小

机器码不能跨段,否则CPU会抛出异常


栈段的保护:栈的地址范围

实际段界限+1<=esp-操作数大小<=0xFFFFFFFF

在这里插入图片描述

参考资料


评论