保护模式进阶 -- 再回实模式

2020-01-30 00:54:13   最后更新: 2020-01-31 10:03:19   访问数量:69




上一篇文章中,我们看到了如何从实地址模式进入到保护模式:

进军保护模式

 

但是那一段简短的程序中,存在着很多不足,例如,数据直接在内存中读写,数据实际上没有被保护模式保护起来,同时,由于没有堆栈段,无法实现函数调用,到最后,我们的程序在死循环中结束,更优雅的方式实际上是能够返回到实地址模式并正常的退出程序,而不是一直死循环下去

本文,我们就来修改上一篇文章中的程序,实现保护模式的进阶功能

 

内存空间的创建

首先我们需要在内存上开辟数据段与堆栈段的空间:

[SECTION .data1] ; 数据段 ALIGN 32 [BITS 32] LABEL_DATA: SPValueInRealMode dw 0 BootMessage: db "Hello World my OS!", 0 OffsetBootMessage equ BootMessage - $$ DataLen equ $ - LABEL_DATA ; 全局堆栈段 [SECTION .gs] ALIGN 32 [BITS 32] LABEL_STACK: times 512 db 0 TopOfStack equ $ - LABEL_STACK - 1

 

 

数据段我们按需使用,存储了预留的用于存储实地址模式堆栈寄存器 sp 值的内存变量 SPValueInRealMode 和我们要显示的字符串 BootMessage 及他的偏移,以及数据段大小 DataLen

而全局堆栈段由于需要在程序中进行压栈与出栈操作,所以必须提前预留,我们预期了 512 字节的栈空间

我们知道,汇编中,堆栈寄存器值降低为压栈操作,增加则为出战操作,因此栈顶在高地址处,也就是这里我们定义的 TopOfStack 变量

 

创建 GDT 描述符

在 GDT 中,我们依次写入数据段与堆栈段的描述符:

LABEL_DESC_DATA: Descriptor 0, DataLen-1, 92h ; 可读写数据段,界限 64KB LABEL_DESC_STACK: Descriptor 0, TopOfStack, 4093h ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器

 

 

段基址我们填写了 0,因为我们要通过代码读取上述在内存空间中开辟的实际数据段与堆栈段的基地址

段界限与段属性是已知的,直接填写即可

段属性的具体取值可以参看:

详解 32 位保护模式与内存分段机制

 

 

 

对于数据段,92h 表示:

  • G 位为 0 -- 以字节为粒度
  • B 位为 0 -- 界限位 64KB
  • S 位为 1 -- 表示是数据段
  • TYPE 位为 2 -- 可读写

 

对于堆栈段,4093h 则表示:

  • G 位为 0 -- 以字节为粒度
  • B 位为 1 -- 默认使用 esp 作为栈指针寄存器
  • S 位为 1 -- 表示是数据段
  • TYPE 位为 3 -- 可访问、可读写

 

初始化段基址

和代码段的段基址初始化一样,我们先计算出物理地址,再赋值到描述符的对应位置:

; 初始化数据段描述符 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_DATA mov word [LABEL_DESC_DATA + 2], ax shr eax, 16 mov byte [LABEL_DESC_DATA + 4], al mov byte [LABEL_DESC_DATA + 7], ah ; 初始化堆栈段描述符 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_STACK mov word [LABEL_DESC_STACK + 2], ax shr eax, 16 mov byte [LABEL_DESC_STACK + 4], al mov byte [LABEL_DESC_STACK + 7], ah

 

 

这样,我们就完成了数据段与堆栈段的创建和初始化

 

创建段选择子

SelectorData equ LABEL_DESC_DATA - LABEL_GDT SelectorStack equ LABEL_DESC_STACK - LABEL_GDT

 

 

为段寄存器赋值段选择子

mov ax, SelectorData mov ds, ax ; 数据段选择子 mov ax, SelectorStack mov ss, ax ; 堆栈段选择子 mov esp, TopOfStack

 

 

函数编写与调用

xor edi, edi mov edi, 80 * 2 * 2 ; 屏幕第 2 行, 第 0 列 xor esi, esi mov esi, OffsetBootMessage call DisplayString DisplayString: push eax mov ah, 8Ch ; 0000: 黑底 1100: 红字 cld .loop_label: lodsb test al, al jz .over_print mov [gs:edi], ax add edi, 2 jmp .loop_label .over_print: pop eax ret

 

 

我们已经完成了对上一篇文章中代码的改造,引入了数据段,对数据实现了保护,并且通过堆栈段的引入,实现了函数调用

接下来,完成所有工作的程序我们就要让他回到实地址模式中

 

原理描述

还记得我们是怎么从实地址模式进入保护模式的吗:

  1. 准备 GDT
  2. 通过 lgdt 指令加载 gdtr
  3. 通过 cli 指令关闭硬件中断
  4. 打开 A20 地址线
  5. 置 cr0 的 PE 位,打开保护模式
  6. 跳转到保护模式代码段起始地址处

 

所以我们从后往前进行反向操作是不是就可以了呢?

从上一篇文章的讲述中,我们就知道,只要 cr0 的 PE 位被复位为 0,那么 CPU 就会通过 16 位实地址模式的段基址 * 16 + 段偏移来计算物理地址

最容易想到的,只要在进入保护模式之前,将各个段基址寄存器的值保存下来,在复位 PE 位之前将他们更新为原值,然后就可以实现跳转回实地址模式了

但实际上,80X86 CPU 内部还有一系列 64 位段描述符缓冲寄存器

 

段描述符高速缓冲寄存器

保护模式寻址需要通过 GDTR 寄存器 + 段选择子定位到 GDT 中的描述符,再通过描述符中的段基址定位

而程序大部分时间实在同一个段内运行的,如果每次地址切换都进行上述一系列操作,对于 CPU 的性能来说显然是非常浪费的,于是,CPU 内部针对六个段基址寄存器:CS、DS、ES、FS、GS、SS 分别有一个对应的段描述符高速缓冲寄存器

作为缓冲寄存器,用户是不能直接操作的,但每一次对任何一个段基址寄存器的赋值操作都会更新对应的缓冲寄存器

在实地址模式下,除 CS 外所有的缓冲寄存器都必须拥有相同的段界限:0xffff,以及段属性:0x92,CS 对应的高速缓冲寄存器的段属性则必须为 0x98

因此,我们需要分别创建符合上述要求的代码段描述符与非代码段描述符

 

跳回实地址模式切换步骤

剩下的工作就比较简单了,只要按照跳转到保护模式时操作的反操作即可:

  1. 段描述符高速缓冲寄存器赋值
  2. 复位 PE 位,切换到实地址模式
  3. 跳转到实地址模式代码起始地址
  4. 关闭 A20 地址线
  5. 打开硬件中断
  6. 通过触发 BIOS 中断退出程序

 

BIOS 21H 中断

BIOS 21H 中断用于,进行各种 IO 操作,具体可以参见:

http://spike.scu.edu.au/~barry/interrupts.html#ah4c

 

如果在触发 INT 21H 时,AH 值为 4CH,则退出当前程序

 

创建用于编写返回代码的代码段和

在 GDT 中插入描述符

LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, 92h ; Normal 描述符 LABEL_DESC_CODE16: Descriptor 0, 0ffffh, 98h ; 非一致代码段, 用于跳回 16 BITS 模式

 

 

如上所述,这个段描述符用于在切换到实地址模式后按要求填充段描述符高速缓冲寄存器,因此段界限与段属性是固定的

 

初始化写入段基址

; 初始化 16 位代码段描述符 mov ax, cs movzx eax, ax shl eax, 4 add eax, LABEL_SEG_CODE16 mov word [LABEL_DESC_CODE16 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE16 + 4], al mov byte [LABEL_DESC_CODE16 + 7], ah

 

 

对于 LABEL_DESC_NORMAL 描述符,我们只是希望用它填充高速缓冲寄存器,并不需要通过他定位到内存中具体的段,所以无需初始化他的段基址

 

创建段选择子

SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT

 

 

编写代码

; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式 [SECTION .s16code] ALIGN 32 [BITS 16] LABEL_SEG_CODE16: ; 初始化段描述符高速缓冲寄存器 mov ax, SelectorNormal mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ; 复位 PE 位,切换到实地址模式 mov eax, cr0 and al, 0feh mov cr0, eax LABEL_GO_BACK_TO_REAL: jmp word 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值 Code16Len equ $ - LABEL_SEG_CODE16 LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, [SPValueInRealMode] ; 关闭 A20 地址线 in al, 92h and al, 0fdh out 92h, al ; 打开硬件中断 sti ; 触发 BIOS int 21h 中断,回到实地址模式 mov ax, 4c00h int 21h

 

 

启动虚拟机,我们可以看到:

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

 

; ---------------- 内存段描述符宏 ------------- ; usage: Descriptor Base, Limit, Attr ; Base: dd ; Limit: dd (low 20 bits available) ; Attr: dw (lower 4 bits of higher byte are always 0) %macro Descriptor 3 dw %2 & 0FFFFh ; 段界限1 dw %1 & 0FFFFh ; 段基址1 db (%1 >> 16) & 0FFh ; 段基址2 dw ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh) ; 属性1 + 段界限2 + 属性2 db (%1 >> 24) & 0FFh ; 段基址3 %endmacro ; ------------ DOS 加载初始内存地址 ----------- org 0100h jmp LABEL_BEGIN ; ------------------- GDT --------------------- [SECTION .gdt] ; GDT ; 段基址, 段界限, 属性 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符 LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, 92h ; Normal 描述符 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, 4098h ; 非一致代码段 LABEL_DESC_CODE16: Descriptor 0, 0ffffh, 98h ; 非一致代码段, 用于跳回 16 BITS 模式 LABEL_DESC_DATA: Descriptor 0, DataLen-1, 92h ; 可读写数据段,界限 64KB LABEL_DESC_STACK: Descriptor 0, TopOfStack, 4093h ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, 92h ; 显存首地址 ; ------------------ END OF GDT ---------------- GdtLen equ $ - LABEL_GDT ; GDT长度 GdtPtr dw GdtLen - 1 ; GDT界限 dd 0 ; GDT基地址 ; ------------------ GDT 选择子 ----------------- SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT SelectorData equ LABEL_DESC_DATA - LABEL_GDT SelectorStack equ LABEL_DESC_STACK - LABEL_GDT SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT ; --------------- END OF 段选择子 ---------------- [SECTION .data1] ; 数据段 ALIGN 32 [BITS 32] LABEL_DATA: SPValueInRealMode dw 0 BootMessage: db "Hello World my OS!", 0 OffsetBootMessage equ BootMessage - $$ DataLen equ $ - LABEL_DATA ; 全局堆栈段 [SECTION .gs] ALIGN 32 [BITS 32] LABEL_STACK: times 512 db 0 TopOfStack equ $ - LABEL_STACK - 1 [SECTION .s16] [BITS 16] LABEL_BEGIN: ; 初始化段基址寄存器 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, 0100h mov [LABEL_GO_BACK_TO_REAL+3], ax mov [SPValueInRealMode], sp ; 初始化 16 位代码段描述符 mov ax, cs movzx eax, ax shl eax, 4 add eax, LABEL_SEG_CODE16 mov word [LABEL_DESC_CODE16 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE16 + 4], al mov byte [LABEL_DESC_CODE16 + 7], ah ; 初始化非一致代码段描述符 xor eax, eax mov ax, cs shl eax, 4 add eax, LABEL_SEG_CODE32 ; 计算非一致代码段基地址物理地址 mov word [LABEL_DESC_CODE32 + 2], ax shr eax, 16 mov byte [LABEL_DESC_CODE32 + 4], al mov byte [LABEL_DESC_CODE32 + 7], ah ; 初始化数据段描述符 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_DATA mov word [LABEL_DESC_DATA + 2], ax shr eax, 16 mov byte [LABEL_DESC_DATA + 4], al mov byte [LABEL_DESC_DATA + 7], ah ; 初始化堆栈段描述符 xor eax, eax mov ax, ds shl eax, 4 add eax, LABEL_STACK mov word [LABEL_DESC_STACK + 2], ax shr eax, 16 mov byte [LABEL_DESC_STACK + 4], al mov byte [LABEL_DESC_STACK + 7], ah ; 准备加载 GDTR xor eax, eax ; 清空 eax 寄存器 mov ax, ds shl eax, 4 add eax, LABEL_GDT ; 计算出 GDT 基地址的物理地址 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址 ; 加载 GDTR lgdt [GdtPtr] ; 关闭硬件中断 cli ; 打开 A20 地址总线 in al, 92h or al, 00000010b out 92h, al ; 置位 PE 标志位,打开保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 跳转进入保护模式 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, ; 并跳转到 Code32Selector:0 处 LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov sp, [SPValueInRealMode] ; 关闭 A20 地址线 in al, 92h and al, 0fdh out 92h, al ; 打开硬件中断 sti ; 触发 BIOS int 21h 中断,回到实地址模式 mov ax, 4c00h int 21h [SECTION .s32] ; 32 位代码段. 由实模式跳入. [BITS 32] LABEL_SEG_CODE32: mov ax, SelectorData mov ds, ax ; 数据段选择子 mov ax, SelectorVideo mov gs, ax ; 赋值视频段选择子 mov ax, SelectorStack mov ss, ax ; 堆栈段选择子 mov esp, TopOfStack xor edi, edi mov edi, 80 * 2 * 2 ; 屏幕第 2 行, 第 0 列 xor esi, esi mov esi, OffsetBootMessage call DisplayString jmp SelectorCode16:0 DisplayString: push eax mov ah, 8Ch ; 0000: 黑底 1100: 红字 cld .loop_label: lodsb test al, al jz .over_print mov [gs:edi], ax add edi, 2 jmp .loop_label .over_print: pop eax ret SegCode32Len equ $ - LABEL_SEG_CODE32 ; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式 [SECTION .s16code] ALIGN 32 [BITS 16] LABEL_SEG_CODE16: ; 跳回实模式: mov ax, SelectorNormal mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax mov eax, cr0 and al, 0feh mov cr0, eax LABEL_GO_BACK_TO_REAL: jmp word 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值 Code16Len equ $ - LABEL_SEG_CODE16 ; END of [SECTION .s16code]

 

 

http://spike.scu.edu.au/~barry/interrupts.html#ah4c

https://nasm.us/doc/nasmdoc6.html

《Orange's》

 






操作系统      os      system      保护模式      数据段      堆栈段      实地址模式     


京ICP备15018585号