计算机启动时会自动加载和执行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