上一篇文章中,我们看到了如何从实地址模式进入到保护模式: 进军保护模式 但是那一段简短的程序中,存在着很多不足,例如,数据直接在内存中读写,数据实际上没有被保护模式保护起来,同时,由于没有堆栈段,无法实现函数调用,到最后,我们的程序在死循环中结束,更优雅的方式实际上是能够返回到实地址模式并正常的退出程序,而不是一直死循环下去。 本文,我们就来修改上一篇文章中的程序,实现保护模式的进阶功能。
首先我们需要在内存上开辟数据段与堆栈段的空间:
[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 中,我们依次写入数据段与堆栈段的描述符:
LABEL_DESC_DATA: Descriptor 0, DataLen-1, 92h ; 可读写数据段,界限 64KB
LABEL_DESC_STACK: Descriptor 0, TopOfStack, 4093h ; 32 位全局堆栈段,可读写数据段,且栈指针默认使用 esp 寄存器
段基址我们填写了 0,因为我们要通过代码读取上述在内存空间中开辟的实际数据段与堆栈段的基地址。 段界限与段属性是已知的,直接填写即可。 段属性的具体取值可以参看: 详解 32 位保护模式与内存分段机制
对于数据段,92h 表示:
对于堆栈段,4093h 则表示:
和代码段的段基址初始化一样,我们先计算出物理地址,再赋值到描述符的对应位置:
; 初始化数据段描述符
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
我们已经完成了对上一篇文章中代码的改造,引入了数据段,对数据实现了保护,并且通过堆栈段的引入,实现了函数调用。 接下来,完成所有工作的程序我们就要让他回到实地址模式中。
还记得我们是怎么从实地址模式进入保护模式的吗:
所以我们从后往前进行反向操作是不是就可以了呢? 从上一篇文章的讲述中,我们就知道,只要 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。 因此,我们需要分别创建符合上述要求的代码段描述符与非代码段描述符。
剩下的工作就比较简单了,只要按照跳转到保护模式时操作的反操作即可:
BIOS 21H 中断用于,进行各种 IO 操作,具体可以参见: http://spike.scu.edu.au/~barry/interrupts.html#ah4c。
如果在触发 INT 21H 时,AH 值为 4CH,则退出当前程序。
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》。