1.概述
本次博客对应orange's书中3.1节的内容,针对实模式跳转到保护模式的汇编代码。本章实验内容较少且步骤简单,因此我们把重心放在汇编代码的分析与理解上,同时对GDT和LDT进行简单介绍。水平有限,欢迎指正。
2.实模式和保护模式简介
2.1 实模式
这里说的实模式和保护模式是指在IA32下CPU的两种工作模式,他们最大的区别就在于寻址的方式上。为何会有实模式呢?因为早期的Intel8086CPU是16位的,所以只有16位的寄存器、数据总线,以及20位的地址总线。这时候内存是裸露给处理器的,只要获得段地址和偏移地址,就可以经过cs:eip的方式进行寻址。大家熟悉的BIOS程序就是运行在CPU的实模式之下的,其中cs:ip=0000:7c00的引导程序段跳转正是用的实模式寻址方式。显然这种模式已经不适用于当今32位以及64位的CPU了,但是为了向前兼容,PC开机时一段时间内依旧会处于实模式下,而后才跳转到保护模式。
2.2 保护模式
由于一系列的启动程序并不需要很高的寻址要求,所以开机时进入的实模式也就被保存下来了。然而对于32位的CPU来说,拥有了32位地址线,寻址空间达到了4GB,因此实模式的寻址显得十分不够用了。这时候我们需要重新考虑寻址规则,这也是保护模式和实模式最大的不同。在保护模式下,依旧采用段:偏移的形式来进行寻址。只不过这时候的段值并非真实的地址了,而是一个索引,这个索引指向一个表,表里定义了起始地址、属性等信息。换句话说,对内存进行操作的时候,需要找到操作对象所对应的一个索引,通过索引找到操作的起始地址,再加上偏移量找到具体地址。这种寻址方式安全的地方就在于,对内存的操作必须通过一段规范的索引表单,从而避免了对内存的随意访问修改。再者,由于索引的存在,在每次使用索引的时候会有相应的使用记录,这样的记录确保了访问的安全性,这也是保护模式中“保护”二字的重要体现。后续在解析汇编代码的时候会深入了解这种索引的机制,也就是GDT(LDT)。
3.汇编代码解析
3.1 简述
这里代码解析主要针对原书中chapter3/a/pmtest1.sam的汇编代码解析,笔者会从头到尾地分析这段代码如何从实模式跳转到保护模式。
3.2 描述符
所谓的描述符就是以下代码宏定义的Descriptor,这段代码在pm.inc中:
; 描述符
; 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 ; 共 8 字节
;
看上去似乎比较难以理解,先不用管这段代码具体指的是什么,我们大致可以感觉出来这是对描述符结构的定义。那么这个定义里有什么呢,根据注释我们知道有段界限、段基址、属性这三个基本结构,也就是描述符指定的对象在哪里、有多大、有什么特征三个问题。所以对此我们先了解这么多,现在正式进入pmtest文件里。
3.3GDT段
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len - 1, DA_C + DA_32; 非一致代码段
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; 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 .gdt]
3.3.1 GDT
我们很容易就在代码中发现了Descriptor这个词,也就是描述符,说明描述符是重点。先看前三行的Descriptor描述符,看上去组成了一个数组,这其实就是我们说的GDT。所谓GDT,就是Gobal Descriptor Table,用于提供段式存储机制。前面我们提到,保护模式的寻址方式依旧是段:偏移量这种结构,只不过这时候的段并非具体的地址了,而是一个个对应的描述符,这样说其实不严谨,但可以简单先这样认为。还记得上文我们分析的描述符结构吗,基址、界限、属性,你一定猜到了这就是为了寻址而设定的。后面的GdtLen顾名思义就是GDT的长度,GdtPtr用于存储GDT的界限和基地址。所以GDT就是一个由一堆描述符组成的表,而每个描述符中又记录了地址信息,因此这个表也就是一张查找索引的表,这些代码的作用就是描绘了这样的一个有索引信息的表。
3.3.2 GDT选择子
接下来是两个GDT选择子,从代码直观上来看,是某个描述符相对于GDT基址的偏移。而事实上,选择子只是稍微比地址偏移复杂一些,多了一位的TI值和RPL特权级,这与访问的对象和权限有关。在这里我们可以简单地认为这个选择子通过地址偏移找到描述符表基址中的具体描述符,通过描述符的段基址和相应的偏移找到具体的线性地址。
3.3.3 为何要有选择子(个人推测)
这时候一定有人要问了,我之前不是定义了一堆描述符的基地址了吗,为什么我不可以直接调用描述符从而进入里面的地址?为何还要算出某个描述符和GDT基地址的偏移从而多出选择子这一步?这个问题困扰了我很久,原书中也并没有详细说明,我只能谈谈个人的看法,可能不够严谨。的确,我们在GDT段定义了一堆的描述符以及他们的各种信息,从而得到了一个表。但是问题是,我们后续的程序没办法通过描述符的名字本身去访问这个描述符的信息。比如我们需要找到显存,我们当然希望直接通过DESC_VIDEO找到对应的地址。但是别忘了,能够被我们访问的只有GDT这张表,而DECS_VEDIO这个名字在这张表里是没有存储的。所以只能通过一个偏移来找到它,就像数组的下标一样,这个偏移或者说下标就是选择子。
3.4 16位代码段
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 初始化 32 位代码段描述符
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
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
; END of [SECTION .s16]
16位代码段看上去十分复杂,其实按照功能区划分就十分明朗。首先是将32位代码段描述符初始化,这里具体汇编代码我也没搞清楚,总之就是这个作用了。接着的代码的作用就是把之前定义的GDT存储下来,这里存储GDT的结构叫做GDTR,R就是寄存器的意思。然后把中端关闭避免产生错误。接着打开A20地址线,这个作用是什么呢?要知道在实模式下只有20位地址线,编号从0到19。而切换到保护模式后有32位地址线,这时候如果不把后面的地址线打开的话就会产生错误,所以我们这时候要打开第21个地址线也就是A20。下面的代码就是把cr0设为1,表示系统进入了保护模式。紧接着跟上jmp指令,这里的jmp看上去有些不同,多了dword,具体原因可以参考原书。就这样,我们就实现了从16位实模式跳转到32位保护模式了。
3.5 32位代码段
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子(目的)
mov edi, (80 * 11 + 79) * 2 ; 屏幕第 11 行, 第 79 列。
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'P'
mov [gs:edi], ax
; 到此停止
jmp $
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
当系统从实模式跳转进入保护模式的时候,开始执行32位代码段。一开始以ax为中介,把SelectVedio也就是显存写到gs中。然后往edi中写入地址偏移量,接着分别在ah和al里写入显存需要写入的东西。最后将ax(低8位为al,高8位为ah)写入显存,就可以实现对显存的操作了。注意这里的[gs:edi]寻址方式就是先找到显存的基址,然后通过偏移量寻找目标。
4.实验总结
操作上本次实验没有任何难度,主要挑战在于描述符的理解和GDT结构的设计,以及汇编代码究竟是如何一步步从实模式跳转到保护模式的。总而言之就是寻址方式中加进了一种类似于索引的方式,使处理器对内存的访问需要通过GDT来获取描述符对应的实际地址,从而间接完成寻址。本节实验成功后bochs会显示出以下画面,可以在最右侧看见我们写入显存的P字符:
这里要注意一下,只把跳转的pmtest代码写进软盘是无效的,需要把第一章的简单引导程序也一起写入软盘,或者自己写一份引导程序也是可以的。
5.附录
本节内容中GDT,GDTR以及LDT的内容易分辨不清,这里提供一下对这些名词的解释GDT,LDT,GDTR,LDTR 详解,有需要的读者可以自行查阅加深印象(其实是我自己),同时感谢作者的分享。