IA-32 CPU 结合保护模式的软硬件设计,提供了 4GB 内存的寻址能力,这对仍停留在 16 位实地址模式的我们是一个极大的诱惑。 上一篇文章中,我们详细的介绍了 32 位保护模式与内存分段机制的寻址机制、以及相关的寄存器、内存结构: 详解 32 位保护模式与内存分段机制
光说不练假把式,本文我们就来看看如何在代码中从 16 位实地址模式跳转到 32 位保护模式中,然后通过直接写显存完成之前文章中“Hello World my OS!”字符串的显示。
上一篇文章中,我们看到了 GDT 描述符的结构:
根据该结构,我们可以定义一个宏:
%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
通过下面方法调用实现一个 GDT 描述符的创建:
Descriptor 段基址(4字节) 段界限(4字节) 段属性(2字节)
在 32 位保护模式中,为了操作系统的安全,系统不再接收硬件中断,而是设计了软件中断与信号机制,分别用来实现硬件对操作系统内核的中断与操作系统内核对应用程序的中断。 那么,要进入 32 位操作系统,首先必须要做的就是屏蔽硬件中断,命令非常简单,汇编中,CLI 命令就是用来实现硬件中断的屏蔽的。 关于软件中断,我们后面的文章再来详细介绍。
通过段基址 16 + 段偏移的计算方法,计算出的最大地址为 ffffh fh + ffffh = 10ffefh,超过 1MB 约 64KB。 那么,如果我们传递 ffff:ffff 地址给 CPU,CPU 会如何处理呢?在 8086 CPU 中,CPU 会自动将大于 fffffh 的物理地址减去 fffffh 作为实际的物理地址。 随着 80286 的诞生,从 A0 到 A19 的 20 位地址总线提高到了 32 位,这意味着此前传入的超过 fffffh 的无效地址此时已经变得可以访问了,但历史上很多程序使用了这样的技巧,所以在新的软硬件设计中需要一个标志来标识 A20 地址总线师傅可用,从而决定超过 1MB 寻址范围的地址是否需要回卷,这个标志就是 92h 端口的 bit 1,通过下面的汇编代码,将 92h 端口的 bit 1 置位就可以实现 A20 总线的开启:
in al, 92h ; 读取 92h 端口数据
or al, 00000010b ; 将 92h 端口 bit 1 置位
out 92h, al ; 将修改后数据输出到 92h 端口
CPU 中有 4 个控制寄存器:
CR0 的结构如下图所示:
上图中位于 CR0 寄存器 bit 0 的 PE 标志,就是用来表示是否已开启保护模式的标志,当 PE 位为 1 则表示当前系统运行在保护模式下,CPU 就会通过上一篇日志中通过 GDTR、段选择子以及描述符表来进行寻址,所以在进入保护模式前的最后一步,就是要置位 CR0 的 PE 位,来开启保护模式。
在 8086 的时代,BIOS ROM 占用 384KB 内存空间,可用于 RAM 的空间最大为 640KB,在这 640KB 的空间中,B0000h 到 B0FFFh 的 4KB 空间提供给显存使用,来实现单色模式下的显示,BIOS 将这段内存映射为显存,只要在这个范围内写入数据,显卡就会读取到相应的数据完成现实。 到了 1983 年,显卡与显示技术提升,彩色显示器问世,原有的 4KB 显存映射空间已经无法满足彩色显示器的显示需要了,于是 IBM 划分了 B8000h 到 BFFFFh 的 32KB 内存空间用来映射到显存,而 C0000h 到 C7FFFh 的 32KB 内存空间用来映射显卡 ROM,从而实现彩色字符模式的展示。 VGA 模式显卡诞生后,显存增长到 256KB,被映射到了 A0000h 到 AFFFFh 的地址空间。 本文,我们的目标是在 32位保护模式下显示一行文字,因此需要使用彩色字符模式下的 B8000h 到 BFFFFh 的 32KB 地址空间了。
如上所述,32 KB 的显存 RAM 映射到了 B8000h 到 BFFFFh 内存空间中,总计可以显示 25 行,每行 80 个字符,每个字符占用 2 个字节,分别是字符的 ASCII 码与属性。 字符属性如下图所示:
要进入保护模式,需要按照上述描述,按下列步骤进行一系列操作:
有了上述的基础知识和代码,我们就可以编写进军保护模式的代码了:
; ---------------- 内存段描述符宏 -------------
; 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_CODE32: Descriptor 0, SegCode32Len - 1, 4098h ; 非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, 92h ; 显存首地址
; ------------------ END OF GDT ----------------
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; ------------------ GDT 选择子 -----------------
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; --------------- END OF 段选择子 ----------------
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
; 初始化段基址寄存器
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 初始化非一致代码段描述符
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
; 准备加载 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 处
[SECTION .s32] ; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 赋值视频段选择子
xor ecx, ecx
mov cl, [MessageLength]
mov edi, 80 * 2 * 2 ; 屏幕第 2 行, 第 0 列
mov ah, 8Ch ; 1000: 黑底闪烁, 1100: 红字
xor bx, bx
loop_label:
mov al, [BootMessage + bx]
add bx, 1
mov [gs:edi], ax
add edi, 2
loop loop_label
jmp $
BootMessage: db "Hello World my OS!"
MessageLength: db ($-BootMessage)
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
可以看到,在程序起始处,我们定义了 org 0100h,这是 DOS 操作系统内存加载的地址,我们没有如同开始时那样,让程序被加载到 0c700h,来让虚拟机直接加载。 因为虚拟机要求第 511、512 字节必须为 0xAA55,但随着我们启动代码的增加,510 字节的限制显然无法容纳我们的全部代码,那么,有什么办法来解决呢?很简单,也是实际上操作系统常常会去做的一件事 — 在 510 字节的启动扇区内直接跳转到下一扇区的代码处进行执行,本文的代码我们通过 DOS 系统完成这个操作,具体的启动方式可以参考前的文章: 如何调试操作系统
代码中在进行上述一系列操作后,进行了一个跳转:
jmp dword SelectorCode32:0
通过 jmp 命令,将段选择子 SelectorCode32 地址处的内存值载入 cs 寄存器。 dword 关键字指定了 jmp 参数指定的偏移量 0 位 32 位值,并从此处开始,每当进行 jmp,都将偏移量视为 32 位值,而无需再加上 dword 关键字,于是从此,程序从 16 位进入了 32 位模式中。
执行上述代码,显示出了:
https://en.wikipedia.org/wiki/IBM\_Personal\_Computer。 https://stackoverflow.com/questions/1797765/assembly-invalid-effective-address。 https://www.nasm.us/doc/nasmdo10.html。