计算机启动时会自动加载和执行IPL程序,但IPL程序只能占用512字节。若直接用IPL写OS,空间不够用。所以IPL程序一般用于将真正的OS程序加载到内存某处(记作A),然后跳转到A。这样计算机就可以执行OS的程序了。
本篇通过修改上一篇的IPL,让它真正实现加载OS程序的功能。同时,将IPL程序代码和OS代码放到不同的源代码文件中;用C语言来编写以后的OS代码;用Makefile来编译源代码。
有了本篇的基础,就算是正式开始编写OS源代码了。
1、制作真正的IPL
磁盘的最初512字节是启动区,所以要装载下一个512字节的内容,这次修改ipl.nas内容,添加的内容如下:
MOV AX,0x0820 MOV ES,AX MOV CH,0 ; 柱面0 MOV DH,0 ; 磁头0 MOV CL,2 ; 扇区2 MOV AH,0x02 ; AH=0x02 : 读盘 MOV AL,1 ; 1个扇区 MOV BX,0 MOV DL,0x00 ; A驱动器 INT 0x13 ; 调用磁盘BIOS JC error
新指令JC(jump if carry):如果进位标志(carry flag)是1的话,就跳转
INT 0x13的意思是BIOS第0x13号函数
相关内容:
AH=0x02(读盘)
AH=0x03(写盘)
AH=0x04(检验)
AH=0x0c(寻道)
AL=处理对象的扇区数;(只能同时处理连续的扇区)
CH=柱面号 &oxff
CL=扇区号(0-5位)|(柱面号&ox300)>>2;
DH=磁头号;
DL=驱动器号;
ES:BX=缓冲地址;(检验及寻道时不使用)
返回值:
FLACS.CF==0:没有错误,AH==0
FLAGS.CF==0:有错误,错误号码存入AH内(与重置(reset)功能一样)
关于软盘的基础知识

一张软盘有80个柱面、2个磁头、18个扇区(Cylinder:0~79;Header:0~2;Sector:1~18),1个扇区有512个字节,所以软盘的容量是80*2*18*512=1440KB。
启动IPL的启动区,位于C0-H0-S1(柱面0,磁头0,扇区1的缩写),下一个扇区就是C0-H0-S2
向一个软盘保存文件时,文件名会从0x2600开始往后存,文件的内容会从0x4200开始往后存。
接下来就是缓冲区地址:一个内存地址,将软盘上读取的数据装载到内存的哪个位置。如果使用一个寄存器来表示内存地址的话,BX只能表示0~0xffff的值,最大才64k内存,显然不够
解决办法是增加一个EBX的寄存器,这样就可以处理4G内存。但是在BIOS的年代还没有出现32位的寄存器。
这时候只能采用一个起辅助作用的段寄存器了。于是使用ES:BX这样的方式表示地址
例如写成“MOV AL, [ES:BX]”表示ES*16+BX的内存地址
从上面的代码可知:ES=0x8200, BX=0,也即是软盘的数据被加载到内存的”0x8200~0x83ff”之间,因为”0x8000~0x81ff”这512字节是留给启动区的,启动区的内容读到那里,”0x7c00~0x7dff”用于启动区,ox7e00以后直到ox9fbff为止的区域都没有特别的用途,OS可以随便使用
2、出错时的处理
接着添加下面的代码
; 读磁盘 MOV AX,0x0820 MOV ES,AX MOV CH,0 ; 柱面0 MOV DH,0 ; 磁头0 MOV CL,2 ; 扇区2 MOV SI,0 ; 记录失败次数的寄存器 retry: MOV AH,0x02 ; AH=0x02 : 读入磁盘 MOV AL,1 ; 1个扇区 MOV BX,0 MOV DL,0x00 ; A驱动器 INT 0x13 ; 调用磁盘BIOS JNC fin ; 没有出错的话就跳转到fin ADD SI,1 ; SI+1 CMP SI,5 ; 比较SI与5 JAE error ; SI >= 5 时,跳转到error MOV AH,0x00 MOV DL,0x00 ; A驱动器 INT 0x13 ; 重置驱动器 JMP retry
JNC指令:进位标志是0的话就跳转
JAE指令:也是跳转条件,是大于或者等于就跳转
接下来就是错误处理,AH=0x00,DL=0x00,INT=0x13(系统复位),即是复位软盘状态再读一次
3、读到18扇区
继续添加代码:
; 读磁盘 MOV AX,0x0820 MOV ES,AX MOV CH,0 ; 柱面0 MOV DH,0 ; 磁头0 MOV CL,2 ; 扇区2 readloop: MOV SI,0 ; 记录失败次数的寄存器 retry: MOV AH,0x02 ; AH=0x02 : 读入磁盘 MOV AL,1 ; 1个扇区 MOV BX,0 MOV DL,0x00 ; A驱动器 INT 0x13 ; 调用磁盘BIOS JNC next ; 没出错时跳转到next ADD SI,1 ; SI+1 CMP SI,5 ; SI与5 JAE error ; SI >= 5 时,跳转到error MOV AH,0x00 MOV DL,0x00 ; A驱动器 INT 0x13 ; 重置驱动器 JMP retry next: MOV AX,ES ; 把内存地址后移0x200 ADD AX,0x0020 MOV ES,AX ; 因为没有ADD ES,0x020指令,所以稍微绕开 ADD CL,1 ; CL+1 CMP CL,18 ; 比较CL与18 JBE readloop ; 如果CL <= 18 跳转至readloop
JBE指令(jump if below or equal):意思是小于等于则跳转
要读取下一个扇区,只需要CL+=1,ES+=0x20(即512/16)
CL为扇区号,ES指定读入地址
通过上面的循环,我们将磁盘上的C0-H0-S2~C0-H0-S18共计512*17=8704字节的内容装载到了内存的0x8200~0xa3ff处
4、读入10个柱面
C0-H0-S18扇区的下一个扇区,是磁盘反面的C0-H1-S1,这次从0xa400读入
接着上面的代码,我们继续添加如下代码:
MOV CL,1 ADD DH,1 CMP DH,2 JB readloop ; 如果DH < 2,则跳转到readloop MOV DH,0 ADD CH,1 CMP CH,CYLS JB readloop ; 如果CH < CYLS,则跳转到readloop
JB指令:如果小于的话就跳转
到目前为止,我们已经把软盘最初的10*2*18*512=180KB内容完整无误地装载到内存
5、着手开发操作系统
接下来编写一个简单的程序,只是让它HLT,代码如下:
fin: HLT JMP fin
32位模式前期准备
修改haribote.nas的代码如下:
; haribote-os ; TAB=4 ; 有关BOOT_INFO CYLS EQU 0x0ff0 ; 设定启动区 LEDS EQU 0x0ff1 VMODE EQU 0x0ff2 ; 关于颜色数目的信息。颜色的位数 SCRNX EQU 0x0ff4 ; 分辨率的X(screen X) SCRNY EQU 0x0ff6 ; 分辨率的Y(screen Y) VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址 ORG 0xc200 ; 这个程序将要被装载到内存的什么地方? MOV AL,0x13 ; VGA显卡,320x200x8bit位彩色 MOV AH,0x00 INT 0x10 MOV BYTE [VMODE],8 ; 记录画面模式 MOV WORD [SCRNX],320 MOV WORD [SCRNY],200 MOV DWORD [VRAM],0x000a0000 ; 用BIOS取得键盘上各种LED指示灯的状态 MOV AH,0x02 INT 0x16 ; keyboard BIOS MOV [LEDS],AL fin: HLT JMP fin
6、开始导入C语言
haribote.nas修改名字为asmhead.nas,因为文件的前半部分为汇编,后面为C语言
; haribote-os boot asm
; TAB=4
BOTPAK EQU 0x00280000 ; bootpackのロード先
DSKCAC EQU 0x00100000 ; ディスクキャッシュの場所
DSKCAC0 EQU 0x00008000 ; ディスクキャッシュの場所(リアルモード)
; 有关BOOT_INFO
CYLS EQU 0x0ff0 ; 设定启动区
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色数目的信息。颜色的位数。
SCRNX EQU 0x0ff4 ; 分辨率的X(screen x)
SCRNY EQU 0x0ff6 ; 分辨率的Y(screen y)
VRAM EQU 0x0ff8 ; 图像缓冲区的开始地址
ORG 0xc200 ; 这个程序将要被装载到内存的什么地方呢?
; 画面モードを設定
MOV AL,0x13 ; VGA显卡,320x200x8bit彩色
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; 记录画面模式
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000
; 用BIOS取得键盘上各种LED指示灯的状态
MOV AH,0x02
INT 0x16 ; keyboard BIOS
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
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
; memcpyはアドレスサイズプリフィクスを入れ忘れなければ、ストリング命令でも書ける
ALIGNB 16
GDT0:
RESB 8 ; ヌルセレクタ
DW 0xffff,0x0000,0x9200,0x00cf ; 読み書き可能セグメント32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 実行可能セグメント32bit(bootpack用)
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
asmhead.nas
1. 把IPL程序作为一个独立的源文件(ipl10.nas)开发,编译后生成二进制文件(ipl10.bin)。
2. 把OS程序作为若干独立的源文件开发,编译后生成二进制文件(haribote.sys)。haribote.sys就是我们的OS程序。
3. 用二进制的方式把ipl10.bin写入haribote.img(磁盘映像文件,看作一个软盘即可)的第一个扇区(这样,计算机启动时就会自动加载ipl10.bin程序)。
4. 把haribote.sys作为一个文件复制到haribote.img。根据上文的预备知识可知,这个文件的内容会从软盘的0x4200位置开始往后存。

Comments