三. 保护模式

实模式与保护模式下的分段机制

程序想要在计算机上运行,就必须将源代码编译链接成二进制的可执行文件之后才可能被操作系统加载执行。如果在加载的过程中,程序的地址都是绝对的物理地址,那么程序就必须放在一个固定的地方,那么拥有两个相同地址的程序就只能运行一个了。

于是,分段机制就产生了。让CPU通过 段基址:段内偏移 来访问任意内存,这样程序就可以实现重定位。也就是说,段内偏移相对于段基址是不变的。无论段基址是多少,只要给出段内偏移,CPU就能访问到正确的指令。于是加载用户程序时,只要将整个段的内容复制到新的位置,再将段基址寄存器中的地址改为该地址,程序便可准确无误的运行,因为程序中用的是偏移地址,相对于新的段基址,该偏移地址处的内容还是一样的,如图所示:

mark

到了保护模式下,虽然访问内存同样是通过段基址:段内偏移的方式进行访问。而且段值同样是存放在原来16位的段寄存器中,但是这些段寄存器存放的不在是段基址,而是相当于一个数组索引的东西,通过这个索引,可以找到一个表项。在这个表项中,存放了段基址等很多的属性,这个表项称为段描述符表。一个段描述符只用来定义一个内存段。代码段要占用一个段描述符,数据段和栈段同样分别要占一个段描述符,这些描述符就存放在全局描述附符表中(GDT)。

一个段描述符表中,会存放很多的段描述符数据,每一个段描述符为8字节,它的格式如下图所示

段描述符格式

G位表示段界限粒度,为0时表示粒度为1字节,为1时表示粒度为4KB

实际段界限=(描述符中的段界限+1)*粒度-1,假设段界限为0xfffff,G位为1时,实际段界限=0x100000*4KB-1 = 0xFFFFFFFF。如果偏移地址超过了段界限,CPU会抛出异常

S为表示是否是系统段。当S为1时,表示非系统段,为0表示系统段。

type字段的属性和S用关系,用一张图来表示

mark

P位表示段是否位于内存中。

L位用来设置是否为64位代码段

D/B位表示有效地址及操作数的大小。对于代码段来说,此位是D位。为0时表示有效地址和操作数是16位。对于栈段来说,此为是B位,为0时表示使用的16位的栈指针寄存器

我们想要从实模式进入保护模式,就必须构建段描述符表,并将构建好的段描述符加载到全局描述符表中,这是进入保护模式的第一步

实模式下的寻址方式

在计算机的上古时代,还只有16位的CPU,此时我们只能访问1MB的内存空间,这个阶段也还没有保护模式的概念

在上古时代的CPU设计中,访问内存需要通过 段基址:段内偏移 来访问内存,因为当时还是16位的CPU,所以当时的基址寄存器同样也是16位的,16位所能表示的最大地址空间为 2^10 * 2^6 = 64KB 也就是说,访问超过64KB的内存空间,就需要切换段基址。

访问内存的方式是通过 物理地址 = 段基址*16 + 段内偏移,当时的地址总线是20位的,也就是刚好能表示1MB的内存空间,而CPU只有16位,想要访问到20位的地址空间,就只能通过特殊的方式处理一下,当时CPU的设计者就在地址处理单元中动了手脚,自动将段基址*16,也就是左移4位,在和16位的段内偏移相加,组成20位的物理地址

通过上面这种内存访问的方式,能够表示的最大内存是 0xffff:0xffff=0x10ffef = 1M + 64K - 16B超过1M的内存部分被称为高端内存区HMA,由于实模式下的地址线是20位,最大寻址空间是1MB,即0x0~0xfffff。超过1MB内存的部分在逻辑上也是正常的,但物理内存中并没有与之对应的部分。为了让段基址:段内偏移的策略任然可用,CPU采用的做法是将超过1MB的部分自动回绕为0,继续从0地址开始映射

地址回绕如图

地址回绕

了解决上述兼容性问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根)的有效性,被称为A20Gate:

  1. 如果A20Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;

  2. 如果A20Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式即取模方式

如果想要从实模式进入到保护模式下,A20Gate就必须打开,否则只能访问到0x10FFEF的内存空间,这也是我们进入保护模式做的第二步

进入保护模式

前面介绍了进入保护模式的前两步,现在还剩下最后一步,就可以进入保护模式。

打开cr0控制寄存器的PE位,也就是将PE位置1,这是保护模式的开关

通过前面的介绍,进入保护模式要分三步走
1. 打开A20
2. 加载gdt
3. 将cr0的PE位置1

下面就来看看具体的代码实现

%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start


; 构建全局描述符表,并填充段描述符,段描述符的大小为8字节,在这里将其分为低4字节与高4字节来定义
; dd=define double-word,为4字节
;--------------------------------------------------------

; gdt的起始地址为GDT_BASE的地址,且gdt的第0个描述符不可用,所以将其直接定义为0
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
            dd DESC_VIDEO_HIGH4

GDT_SIZE equ   $-GDT_BASE
GDT_LIMIT equ GDT_SIZE - 1
times 60 dq 0

SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0     ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0     ; 同上
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0    ; 同上 

gdt_ptr dw GDT_LIMIT    ;gdt的前2字节是段界限,后4字节是段基址
        dd GDT_BASE
loadermsg db 'loader in real.'

loader_start:
    mov sp, LOADER_BASE_ADDR
    mov bp, loadermsg
    mov cx, 15
    mov ax, 0x1301
    mov bx, 0x001f
    mov dx, 0x1800
    int 0x10

;---------------------------
;准备进入保护模式
;1. 打开A20
;2. 加载gdt
;3. 将cr0的PE位置1
;---------------------------


;-------打开A20--------
    in al, 0x92
    or al, 0000_0010b
    out 0x92, al

;-------加载gdt-------
    lgdt [gdt_ptr]

;------cr0第0位置1-----
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax

    jmp SELECTOR_CODE:p_mode_start


[bits 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 $

这这段代码中,前面主要是为段描述符表填充数据,因为一个段描述符占8字节,所以分了两个dword来填充,主要是为了方便。然后在实模式下打印了一句话。接着进入到保护模式中,并在进入到保护模式之后打印了一个字符P

构建段描述符的数据定义如下

LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2

;----------------------------
;gdt描述符属性

DESC_G_4K   equ   1_00000000000000000000000b
DESC_D_32   equ    1_0000000000000000000000b
DESC_L      equ     0_000000000000000000000b    ;  64位代码标记,此处标记为0便可。
DESC_AVL    equ      0_00000000000000000000b    ;  cpu不用此位,暂置为0
DESC_LIMIT_CODE2  equ 1111_0000000000000000b
DESC_LIMIT_DATA2  equ DESC_LIMIT_CODE2
DESC_LIMIT_VIDEO2  equ 0000_000000000000000b
DESC_P      equ           1_000000000000000b
DESC_DPL_0  equ            00_0000000000000b
DESC_DPL_1  equ            01_0000000000000b
DESC_DPL_2  equ            10_0000000000000b
DESC_DPL_3  equ            11_0000000000000b
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    ;x=1,c=0,r=0,a=0 代码段是可执行的,非依从>的,不可读的,已访问位a清0.
DESC_TYPE_DATA  equ           0010_00000000b    ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上>扩展的,可写的,已访问位a清0.

;DESC_CODE_HIGH4 = 1100 1111 1001 1000 0000 0000 代码段的高4字节
DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + 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 = 1100 1111 1001 0010 0000 0000
DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00

;DESC_VIDEO_HIGH4= 1100 0000 1001 0010 0000 1011
DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + 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

运行效果如下

mark

接下来看看GDT中都有哪些数据

mark

GDT中的第0位是不可用的,第1位是代码段,第2位是数据段,第三位是显存的数据段

cr0控制寄存器中的数据

mark

PE位大写就表示PE位为1。

猜你喜欢

转载自blog.csdn.net/cw312644167/article/details/80051309