上次讲解到了OS内核的开始以及在进入保护模式之前需要了解一些概念。首先给出这部分内容的完整代码,然后分别来介绍。
BOTPAK EQU 0x00280000
DSKCAC EQU 0x00100000
DSKCAC0 EQU 0x00008000
CYLS EQU 0x0ff0
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2
SCRNX EQU 0x0ff4
SCRNY EQU 0x0ff6
VRAM EQU 0x0ff8
org 0xc200 ; 这里的org并不是真的可以决定在内存中的加载位置(由ipl决定),而是为了让编译器可以算出正确的标签所代表的内存位置
mov al, 0x13 ; 320×200×8位色
mov ah, 0x00
int 0x10 ; 0x10号中断,ah=0时为VGA显卡图形模式
mov byte[VMODE], 8
mov word[SCRNX], 320
mov word[SCRNY], 200
mov dword[VRAM], 0x000a0000
mov ah, 0x02
int 0x16 ; ah=0x02时获取键盘led灯状态,保存在al中
mov [LEDS], al
; PIC关闭一切中断
; 根据AT兼容机的规格,如果要初始化PIC
; 必须在CLI之前进行,否则有时会挂起
; 随后进行PIC的初始化
MOV AL,0xff
OUT 0x21,AL
NOP ; 如果连续执行OUT指令,有些机种会无法正常运行
OUT 0xa1,AL
CLI ; 禁止CPU级别的中断
; 为了让CPU能够访问1MB以上的内存空间,设定A20GATE
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
; 切换到保护模式
[INSTRSET "i486p"] ; 要想使用486指令
LGDT [GDTR0] ; 设定临时GDT
MOV EAX,CR0
AND EAX,0x7fffffff ; 设bit31为0(为了禁止颁)
OR EAX,0x00000001 ; 设bit0为1(为了切换到保护模式)
MOV CR0,EAX
JMP pipelineflush
pipelineflush:
MOV AX,1*8 ; 可读写的段32bit
MOV DS,AX
MOV ES,AX
MOV FS,AX
MOV GS,AX
MOV SS,AX
; bootpack的传送
MOV ESI,bootpack ; 传送源
MOV EDI,BOTPAK ; 传送目的地
MOV ECX,512*1024/4
CALL memcpy
; 磁盘数据最终转送到它本来的位置去
; 首先从启动扇区开始
MOV ESI,0x7c00 ; 传送源
MOV EDI,DSKCAC ; 传送目的地
MOV ECX,512/4
CALL memcpy
; 所有剩下的
MOV ESI,DSKCAC0+512 ; 传送源
MOV EDI,DSKCAC+512 ; 传送目的地
MOV ECX,0
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; 从柱面数变换为字节数除以4
SUB ECX,512/4 ; 减去IPL
CALL memcpy
; 必须由asmhead来完成的工作,至此全部完毕
; 以后交由bootpack完成
; bootpack的启动
MOV EBX,BOTPAK
MOV ECX,[EBX+16]
ADD ECX,3 ; ECX += 3;
SHR ECX,2 ; ECX /= 4;
JZ skip ; 没有要转送的东西时
MOV ESI,[EBX+20] ; 转送源
ADD ESI,EBX
MOV EDI,[EBX+12] ; 转送目的地
CALL memcpy
skip:
MOV ESP,[EBX+12] ; 栈初始值
JMP DWORD 2*8:0x0000001b
waitkbdout:
IN AL,0x64
AND AL,0x02
IN AL,0x60 ; 空读(为了清空数据接收缓冲区中的垃圾数据)
JNZ waitkbdout ; AND的结果如果不是0,就跳到waitkbdout
RET
memcpy:
MOV EAX,[ESI]
ADD ESI,4
MOV [EDI],EAX
ADD EDI,4
SUB ECX,1
JNZ memcpy ; 减法运算的结果如果不是0,就转到memcpy
RET
ALIGNB 16
GDT0:
RESB 8 ; NULL selector
DW 0xffff,0x0000,0x9200,0x00cf ; 可以读写的段(segment)32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 可以执行的段(segment)32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
31-36行做的事情在于,如果在切换模式的(指从实模式切换至保护模式)过程中发生了中断,CPU是不能停下来去处理中断的(因为处理的方式都没有切换过来),所以,在这之前,需要关闭中断。两句out指令用于屏蔽主PIC和从PIC的中断发送,CLI用于停止CPU级别的中断。NOP指令使得CPU空转一个时钟周期,这是为了防止两句OUT连用存在的隐患(例如没有优化到位的竞争冒险)。
接下来会调用waitkbdout函数,所以先来介绍110-115行的内容,这是在清除缓冲器数据,把键盘缓冲区中的数据转移出来,并且清空,循环读取直至控制器数据为0,跳出函数体。
40-46行,初始化键盘数据,启动键盘控制电路。向0x60输出0xdf这条指令是为了开启1M以上的内存空间,在x86(以及x86以后)的架构中,计算机刚启动时都必须进入实模式,而为了使用这个模式,就不得不先把1MB以外的内存屏蔽掉,使得此时的状态能够模拟出8086时的工作状态,换句话说就是为了向下兼容,在软件级别上使得其与8086工作模式相同。而这条out指令将会使得A20GATE信号箱转为1状态(也就是禁止其抑制状态),也就开启了大内存的支持。
50行准备进入保护模式,之后的汇编指令将会被翻译成32位机器码。而由于保护模式和实模式寻址方式不同,要想使得段寄存器有效,就必须使用GDT,52行这里暂时制定一个GDT,实际的GDT将会在以后去完成。设置了CR0之后,便正式进入保护模式。57行这个突兀的jmp指令的目的在于,由于保护模式的机器语言会采用管道机制,也就是会预先解释下面的指令,但是,由于刚刚转换成保护模式,下面的一条语句还是按照以前的方式来解释的,直接去执行会出错,所以专门加一条jmp指令是为了容错,让计算机重新解释一遍后面的语句,防止奇怪的bug。而58到64行就是用来初始化这些32位寄存器的。
由于接下来会用到内存传送函数,因此先来解释117-124行。这个函数的目的是以4字节为单位进行内存数据的复制,将以ESI数据为首地址的内存数据传送到以EDI数据为首地址的内存中,传送数据的大小是ECX中的值。
66-71行就是借助了内存复制函数,把将来将要写的操作系统内核的部分加载到内存(在合川先生的教程中把它明明为bootpack,也就是用于启动的程序包,但是由于我们将要写的操作系统比较小,还并不至于用一个专门的bootloader来启动,所以把bootpack和OS写在一起,所以这里的bootpack就可以理解成操作系统内核程序本身,在后面的程序中将不再使用bootpack这个名字,而是使用OSMain这个名字,特此说明)。到90行为止,都是在加载数据。之前给启动区空出来的内存位置,我们把启动区也加载进去,而之前ipl只加载了10个扇区,这里内存够用了,就把剩下的加载完毕。
97-105行来解析系统文件头,这里要具体说明为什么如此编写有点复杂。简单来说,我们在完成这些设定以后,就想用C语言来作为主要语言进行操作系统的开发,但C语言编译后不像汇编语言,你可以直接指定每一段函数的加载位置,C语言编译后函数的加载位置是由编译器临时决定的,再加之不同编译器以及编译参数对编译内容进行的优化也不尽相同,所以,我们无法指定C语言文件编译后各函数之间的执行顺序。而这里就是在对文件头进行分析,找出合适的执行切入点,我们才可以把我们用C语言的入口函数加载到需要的地方。
106-108行就跳转到刚才已经加载合适的程序部分,也就是说,接下来就该执行我们将有C语言编写的入口函数了(我个人将其命名为OSMain)。关于当前只是进行大概解释的部分,会在后续的文章中详细介绍。
当前的程序已经成功的把裸机通过软驱启动运行了ipl加载了程序并且切换至了32位保护模式,取得了4G内存的寻址空间,最后还提供了接口使得我们可以将程序的主逻辑转交至C语言,增加开发效率。下次将会开始使用C语言进行主要开发语言,将会介绍OSMain函数的写法。