这次任务是将CPU从实模式切换到保护模式,然后显示满屏的白色。
当我按下机箱开关CPU一条一条的执行指令,它将软盘内容复制到我们指定的内存地址处,然后跳转到那里执行。这个时的CPU工作在16位实模式下,能访问的内存空间范围理论上说是:0000H:0000H~FFFFH:FFFFH=4GB(其中0000H~FFFFH表示64KB),但由于刚开机时CPU第20位地址总线是关闭的(就是总为0),所以实际能访问的内存空间范围是:0000H:0000H~F000H:FFFFH=1MB(这里是按照偏移量取最大,也可以按段寄存器取最大来算不过那样要复杂一些了)。如果给出的内存地址超过这个范围也不会出现错误,而是会自动回滚到内存中第一个单元从新开始数。其实就连这1MB的内存空间也不是我们可以随便拿来用的,况且我们想要做的是一个32位的操作系统,要使用4G的内存空间,将CPU从默认的实模式切换到32位保护模式是我们迫切的愿望。
上面已经说了刚开机时电脑第20位地址总线是关闭的,这导致无法访问到超过20位的地址空间。要访问32位地址空间,必须要将这一位打开。实际上打开这一位的操作叫做A20 enable。这里不去过分的谈A20的历史,感兴趣可以看这里:A20历史
打开A20的方法看这里:打开A20的几种方法
最简单的打开A20的方法是使用BIOS中断。
代码在这里:
MOV AX,0X2401;如写0X2400则关闭A20
INT 0X15
打开A20后,我采用段和偏移的方式已经可以访问4G的内存空间,但现在并没有内存的保护机制。你可能要问了,什么是保护机制?
其实这正是重点所在,在保护模式下段寄存器中存储的已经不再是某个具体的段,而是某个段的一个索引。索引指向的是一个最大4G的内存段空间。这样每个段都可以对应最大4G的空间。我们可以定义不同索引指向的段,有的段是系统存放区,我们要规定它的属性是只读和可执行。这样当有应用程序试图破坏系统时,就会因为没有权限而失败。这仅仅是对保护模式最浅显的讲解。现在你已经知道它可以保护操作系统免于被恶意破坏了。
那么话说回来,之前段寄存器里存放的是段值,现在变成段索引了,如何让CPU知道这些从而按计划办事?
CPU中有用于控制和确定处理器运行模式和当前运行模式特性的寄存器CR0,CR1,CR2,CR3它们都是32位寄存器。其中CR0正是一个与保护模式相关的控制寄存器。CR0的最低位PE位是实模式和保护模式的开关。默认是0表示工作在实模式下,我们只要将它设置为1CPU就可以工作在保护模式下了。
代码在这里:
MOV EAX,0X00000001
MOV CR0,EAX
既然现在CPU已经工作在32位保护模式下了,我要尝尝鲜我要在显示整个屏幕的白色。因为在进入保护模式之前已经设定好了显示模式是320*200*8bit调色板冒失(通过BIOS中断设定的),所以现在我可以通过向0xa0000~0xaffff这段内存空间写入颜色来显示。现在问题就来了,我已经不可以用16位模式下的 0xa000:0来指向物理地址0xa0000了,因为现在段寄存器的意义变了。所以我需要另外想办法了。
解决问题的方法总比问题多。其实CPU中存在一个叫GDTR的64位寄存器。它里面用于存放GDT(全局描述符表)的内存开始地址和界限(GDT的信息)。每次给段寄存器赋值某个索引(段选择子)后,CPU就会到GDTR指定的GDT表中查找对应的段,然后再通过一个32位偏移寄存器来指定这个段中4G空间的任意一个地址进行访问。
上面说的GDT其实就是一张每行64位的一张表,每行定义一个具体的段,指定了段在内存中的基地址,界限和属性。特别的第一个记录的段基址,界限和属性全是0。我想向0xa0000~0xaffff处写数据,所以我定义了一个GDT表。
代码在这里:
GDT:
NULL <span style="white-space:pre"> </span>RESB 8
DIS_gdt <span style="white-space:pre"> </span>DW 0xffff,0x0000,0x920a,0x00ca;0xa0000~0xaffff 可读可写不可执行
GDT_END
定义好啦GDT还要定义GDT的信息,因为GDTR寄存器需要这些信息。
代码在这里:
GDT_INF:
<span style="white-space:pre"> </span>DW<span style="white-space:pre"> </span>2*8-1<span style="white-space:pre"> </span>;GDT界限
<span style="white-space:pre"> </span>DD<span style="white-space:pre"> </span>GDT<span style="white-space:pre"> </span>;GDT基地址
定义好了GDT信息还要定义段选择子(也就是段选择索引)
代码在这里:
Dis_sel<span style="white-space:pre"> </span>EQU<span style="white-space:pre"> </span>DIS_gdt<span style="white-space:pre"> </span>- GDT<span style="white-space:pre"> </span>;表示相对于GDT基地址的偏移用于在GDT中索引
毫无疑问现在最重要的工作都已经做好了,只剩下一点问题了,比如如何写入,比如如何测试,相信这些都不算什么问题。所以下面直接贴上整个代码,然后测试一下。马上就做。
代码在这里:
<span style="white-space:pre"> </span>;---------------------------------------------
;文件名:head.nas
;功能:从16位进入32位,显示满屏白色
;---------------------------------------------
ORG 0xc200 ;因为下面有标号
JMP MODESET
;----------------------------------------------
;全局描述符表GDT定义 ;界限 ;基址 ;属性
;----------------------------------------------
ALIGNB 16
GDT:
Null RESB 8
Dis_gdt DW 0xffff,0x0000,0x920a,0x00ca ;可读写不可执行
Code_gdt DW 0xffff,0x0000,0x9a00,0x0047 ;可执行可读不可写
GDT_END
;GDT基址和界限信息
GDT_INF:
DW 3*8-1 ;GDT界限
DD GDT ;GDT基地址
;段选择子
Dis_sel EQU Dis_gdt-GDT;
Code_sel EQU Code_gdt-GDT;
MODESET:
;设置显示模式320x200x8bit调色板模式
MOV AL,0x13 ;
MOV AH,0x00
INT 0x10
;开启A20,BIOS中断方法
CLI
MOV AX, 0x2401 ;如果是0x2400则关闭A20地址线
INT 0x15
[INSTRSET "i486p"] ;要使用486的指令
LGDT [GDT_INF] ;装载GDT基址和界限到GDTR寄存器
;设置控制及模式寄存器CR0切换到保护模式
MOV EAX,0x00000001
MOV CR0,EAX
;设定数据段寄存器
MOV AX,Dis_sel
MOV DS,AX
;显示满屏白色
CALL SHOW
SHOW:
MOV EAX,0 ;指定显存地址
DIS:
MOV BYTE[EAX],0x0f ;白色0x0f=15
ADD EAX,1
CMP EAX,0xffff
JBE DIS
;跳转到32位代码处开始执行
JMP DWORD Code_sel:C32CODE+0x0000001b;C代码的第1b字节开始处才是可执行的指令
ALIGNB 16
C32CODE:
这段代码被加载到内存中0xc200:0的位置开始执行,如何加载在前一篇文章已经有讲过了,还有上面那段代码中多了一些文中没有提到的代码,她们到底是做什么的?在这里就不具体的说了,但在下篇文章里一定会详细的说清楚的。通过这篇文章如果加深了你对实模式到保护模式切换的理解我也算是心满意足了。
这是演示效果:
看似简单的过程也可能会让你费尽心思。这段代码我前前后后研究了好久,经历过无数次失败,总算今天可以写出来和大家分享,当然里面的一些叙述一定会有错误,但也不必较真,算是当作理解操作系统的辅助资料吧。
说说调试中的失败案例吧:
1.在打开A20之前一定要关闭CPU中断,也就是添加一条CLI指令,否者会失败。我就是因为忘了这一条指令导致后来无论如何都不显示屏幕的白色,苦恼了好久。切记!
2.因为我用的是NASK编译器,所以有时候标号的使用和其他的编译器会有区别。所以当编译器报错时一定要多注意是否是标号的问题,是不是多了一个冒号,或者是少了什么。
3.有时可能写代码没用多长时间,反而是调试话费了大量的时间。所以说当我们每写一行代码时都要想到以后要调试这一行代码,这样也许会节省调试的时间。