[4.2] 操作系统学习 (深入保护模式)

·

5 min read

接下来我们将讲上节没有讲完的内容, GDT

全局描述符表

我们之前说过了在保护模式下为了安全着想, 引入了 GDT, 内存段(如数据段, 代码段等)不再是简单地用段寄存器加载一下段基址就能用啦,段的信息增加了很多, 需要提前把段定义好才能使用

全局描述符表(GDT)就是为此而生, GDT 其实就是一个用于登记内存段信息的东西, GDT 因为要记录很多的东西, 所以寄存器的装不了的, GDT 通常都储存再内存中

GDT 的结构一般都是这样的:

1.png

其中 GDT 的位置不是固定的, 要使用某个寄存器指向, 后面会说

段描述符

其实简单的说段描述符就是一个段基址, 只不过里面除了段基址还多了一下信息

为什么要多这些信息呢? 这还不是为了修补实模式下安全问题, 有了段描述符我们就可以写明这个段是否可读, 是否可写, 是否可运行等等一些信息

段描述符是 64 位(8 字节)的, 我们来看一下它的结构;

63~56555453525150~474645~444342~3939~3231~1615~0
段基址(31~24)GD/BLLAVL段界限(19~16)PDPLSType段基址(23~16)段基址(15~0)段界限(15~0)

因为保护模式下地址总线位 32 位, 所以段基址也要 32 位, 这 32 位段基址会在段描述符中分 3 部分储存(适应实模式)

还有一个就是段界限, 段界限就是表示这个段最大能扩展到哪里, 扩展的方向只有上下

  • 数据段和代码段: 扩展方向向上, 即地址越来越高, 此时的段界限用来表示段内偏移的最大值
  • 栈段: 扩展方向向下, 即地址越来越低, 此时的段界限用来表示段内偏移的最小值

还有一点想要注意的是段界限的单位是有 2 个的, 一个是字节, 另一个是4KB, 我们可以通过段描述符里面的G 位(55 位)指明,如果 G 位为 0,表示段界限粒度大小为 1 字节, 为 1 则反之

介绍完一些主要属性, 我们来看看一些没有提到的属性:

  • S(43) : 指示是否是系统段, 各种称为"门(入口, 它通往一段程序)"的结构便是系统段, 也就是硬件系统需要的结构, 非软件使用的, 如调用门, 任务门, S 与 Type 配合使用
  • Type(42~32) : 用来指定本描述符的类型, 表示内存段或门的子类型, 当此段为系统段或者数据段时, 意义是不同的.

2.png

A位表示Accessed位,这是由CPU来设置的,每当该段被CPU访问过后, CPU就将此位置Io 所以,创建一个新段描述符时,应该将此位置 0。我们在调试时,根据此位便能判断该描述符是否可用啦。

C位表示一致性代码段, 一致性代码段是指如果自己是转移的目标段, 并且自己是一致性代码段,自己的特权级一定要高于当前特权级,转移后的特权级不与自己的 DPL为主, 而是与转移前的低特权级 一致,也就是昕从、依从转移前的低特权级

R位表示是否可读

X位表示该段是否可执行

E位是用来标识段的扩展方向, 也就是我们上文说的段界限的扩展方向

W 位表示段是否可写

  • DPL(45~44) : 表示此段的特权级(0(最大),1,2,3), CPU 从实模式进入保护模式时, 特权级自动置 0, 操作系统应该处于最高的 0 特权级, 用户程序通常处于 3 特权级
  • P(46) : 表示段是否存在于内存中, CPU 会自动检查, 如果为 0, CPU 会抛出异常, 让 OS 处理, 我们处理后要置 1

当初 CPU 的设计是当内存不足时,可以将段描述符中 对应的内存段换出,也就是可以把不常用的段直接换出到硬盘,待使用时再加载进来。但现在即使内存不 足时,也没有将整个段都换出去的,现在基本都是平坦模型,一般情况下,段都要 4GB 大小,换到硬盘 不也是很占空间吗?而且这些平坦的段都是公用的,换出去就麻烦啦。所以这些是未开启分页时的解决方 案,保护模式下有分页功能,可以按页( 4阻)的单位来将内存换入换出。

  • AVL(51) : 对用户程序指明此段是否可用, 对操作系统没用
  • L(53) : 设置是否是 64 位代码段, 因为我们是要在 32 位下编程, 所以置 0
  • D/B(54) : 指示有效地址(段内偏移地址)及操作数的大小, 它分为 D 或 B

对于代码段来说,此位是 D 位,若 D 为 0,表示指令中的有效地址和操作数是 16 位,指令有效地址 用 P 寄存器。若 D 为 1,表示指令中的有效地址及操作数是 32 位,指令有效地址用 EIP 寄存器。 对于栈段来说,此位是 B 位,用来指定操作数大小,此操作数涉及到战指针寄存器的选择及榜的地 址上限。若 B 为 0,使用的是 sp 寄存器,也就是拢的起始地址是 16 位寄存器的最大寻址范围., 0xFFFF. 若 B 为 1,使用的是 esp 寄存器,也就是枝的起始地址是 32 位寄存器的最大寻址范围, 0xFFFFFFFF

描述符表

我们都知道一个描述符只能用于一个段, 而我们却有很多段, 这时就需要一个表来储存这些描述符, 也就是 描述符表(DT)

一个描述符表里有多个段描述符, 描述符表可分为 全局描述符表(GDT) 局部描述符表(LDT), GDT 也就是公用的, 多个程序都可以在里面定义自己的段描述符, 而 LDT 就是私用的, 按照 CPU 的设想, 一个任务对应一个 LDT, 但在现代操作系统中很少有用 LDT 的, 所以我们不深究

GDT 是储存再内存中的, 想要一个寄存器 GDTR 指向它, GDTR 是一个 48 位的寄存器, 此寄存器是程序员不可见寄存器, 想要访问它, 是需要通过 lgdt命令, 实际上再 32 位CPU中的实模式也是有 gdt 的,不过只有 1MB 大, 我们到了保护模式要重新初始化才能使用到 4GB

GDTR 的结构是这样的

47~1615~0
GDT 内存起始位置GDT 界限(相当于 GDT 的字节大小减 1)

当我们初始化好了 gdt 那么要怎么使用呢?

这就要用到我们之前说的选择子, 选择子储存在寄存器, 相当gdt 的索引, 外加一些属性. 选择子的大小是 16 位, 选择子的结构是这样的:

15~43~21~0
描述符索引值TIRPL
  • RPL : 表示请求特权级, 关于 RPL 我们会在专门讲特权级的节中详尽说明,此处可以理解为请求者的当前特权级
  • TI : 用来指示选择子是在 GDT 还是 LDT, 0 表示在GDT, 1 在 LDT
  • 描述符的索引值 : 用此值在 GDT 中索引描述符, 前面说过 GDT 相当于一个描述符数组, 所以此描述符的索引值相当于 GDT 中的下标. 描述符索引值为13位, 最大可以索引8192个段, 这于 gdt 只能储存8192个描述符是相符合的

在保护模式下访问内存, 我们要通过这样的方式访问:选择子+段偏移, 例如 ds 里的选择子是 0x8,也就是 1000, RPL是第 0~1 位:00,TI是第 2 位:0

描述符的索引值是高 13 位:1, 所以如果访问 ds:0x9, 而 gdt 第 1 位是 0x1234, 就是 0x1234+0x9=0x123d

让 loader 进入保护模式

因为我们的 loader 后期可能会超过一扇区, 所以要改一下 mbr,

52 mov cx,4 ;待读入的扇区数
53 call rd disk m 16 J 以下读取程序的起始部分(一个扇区)

直接读4 个扇区,这样就可以避免以后频繁修改 mbr, 虽然有点浪费性能

接下来就更改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=0x7
           dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0

   GDT_SIZE   equ   $ - GDT_BASE
   GDT_LIMIT   equ   GDT_SIZE -    1 
   times 60 dq 0                     ; 此处预留60个描述符的slot
   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的指针,前2字节是gdt界限,后4字节是gdt起始地址

   gdt_ptr  dw  GDT_LIMIT 
        dd  GDT_BASE
   loadermsg db '2 loader in real.'

   loader_start:

;------------------------------------------------------------
;INT 0x10    功能号:0x13    功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址 
;AL=显示输出方式
;   0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
;   1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
;   2——字符串中含显示字符和显示属性。显示后,光标位置不变
;   3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
   mov     sp, LOADER_BASE_ADDR
   mov     bp, loadermsg           ; ES:BP = 字符串地址
   mov     cx, 17             ; CX = 字符串长度
   mov     ax, 0x1301         ; AH = 13,  AL = 01h
   mov     bx, 0x001f         ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
   mov     dx, 0x1800         ;
   int     0x10                    ; 10h 号中断

;----------------------------------------   准备进入保护模式   ------------------------------------------
                                    ;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 dword SELECTOR_CODE:p_mode_start         ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
   jmp  SELECTOR_CODE:p_mode_start         ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
                         ; 这将导致之前做的预测失效,从而起到了刷新的作用。

[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'
   mov byte [gs:161], 'M'
   mov byte [gs:162], ' '
   mov byte [gs:163], 'r'
   mov byte [gs:164], 'e'
   mov byte [gs:165], 'a'
   mov byte [gs:166], 'l'

   jmp $

include/boot.inc

   %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=0x7
           dd    DESC_VIDEO_HIGH4  ; 此时dpl已改为0

   GDT_SIZE   equ   $ - GDT_BASE
   GDT_LIMIT   equ   GDT_SIZE -    1 
   times 60 dq 0                     ; 此处预留60个描述符的slot
   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的指针,前2字节是gdt界限,后4字节是gdt起始地址

   gdt_ptr  dw  GDT_LIMIT 
        dd  GDT_BASE
   loadermsg db '2 loader in real.'

   loader_start:

;------------------------------------------------------------
;INT 0x10    功能号:0x13    功能描述:打印字符串
;------------------------------------------------------------
;输入:
;AH 子功能号=13H
;BH = 页码
;BL = 属性(若AL=00H或01H)
;CX=字符串长度
;(DH、DL)=坐标(行、列)
;ES:BP=字符串地址 
;AL=显示输出方式
;   0——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
;   1——字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
;   2——字符串中含显示字符和显示属性。显示后,光标位置不变
;   3——字符串中含显示字符和显示属性。显示后,光标位置改变
;无返回值
   mov     sp, LOADER_BASE_ADDR
   mov     bp, loadermsg           ; ES:BP = 字符串地址
   mov     cx, 17             ; CX = 字符串长度
   mov     ax, 0x1301         ; AH = 13,  AL = 01h
   mov     bx, 0x001f         ; 页号为0(BH = 0) 蓝底粉红字(BL = 1fh)
   mov     dx, 0x1800         ;
   int     0x10                    ; 10h 号中断

;----------------------------------------   准备进入保护模式   ------------------------------------------
                                    ;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 dword SELECTOR_CODE:p_mode_start         ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
   jmp  SELECTOR_CODE:p_mode_start         ; 刷新流水线,避免分支预测的影响,这种cpu优化策略,最怕jmp跳转,
                         ; 这将导致之前做的预测失效,从而起到了刷新的作用。

[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'
   mov byte [gs:161],0xA4
   mov byte [gs:162], 'M'
   mov byte [gs:163],0xA4
   mov byte [gs:164], ' '
   mov byte [gs:165],0xA4
   mov byte [gs:166], 'r'
   mov byte [gs:167],0xA4
   mov byte [gs:168], 'e'
   mov byte [gs:169],0xA4
   mov byte [gs:170], 'a'
   mov byte [gs:171],0xA4
   mov byte [gs:172], 'l'
   mov byte [gs:173],0xA4

   jmp $

然后编译

${workspace_path}=/Volumes/Other/Work/Space_OSStudy/c4/a/boot/
# 工作区绝对路径

#clean
rm c.img mbr.bin loader.bin

#c.img
bximage -func=create -hd=10M -sectsize=512 -q -imgmode=flat c.img

#mbr.bin
nasm -I include/ -o mbr.bin mbr.S
dd if=${workspace_path}mbr.bin of=${workspace_path}c.img bs=512 count=1  conv=notrunc

#loader.bin
nasm -I include/ -o loader.bin loader.S
dd if=${workspace_path}loader.bin  of=${workspace_path}c.img bs=512 count=3 seek=2 conv=notrunc

#run
# qemu-system-i386 c.img
# 这里貌似 qemu 运行不了
# bochs -f 你的配置文件

效果图:

3.png