《30天自制操作系統》筆記(02)——導入C語言
進度回顧
在上一篇,記錄了計算機開機時加載IPL程序(initial program loader,一個nas匯編程序)的情況,包括IPL代碼(helloos.nas)、編譯生成helloos.img文件、用虛擬機QEMU加載helloos.img、制作U盤啟動盤和用物理機加載helloos.img。
計算機啟動時會自動加載和執行IPL程序,但IPL程序只能占用512字節。若直接用IPL寫OS,空間不夠用。所以IPL程序一般用于將真正的OS程序加載到內存某處(記作A),然后跳轉到A。這樣計算機就可以執行OS的程序了。
在上一篇中的IPL程序只是個hello world式的試驗品,本篇通過修改上一篇的IPL,讓它真正實現加載OS程序的功能。同時,將IPL程序代碼和OS代碼放到不同的源代碼文件中;用C語言來編寫以后的OS代碼;用Makefile來編譯源代碼。
有了本篇的基礎,就算是正式開始編寫OS源代碼了。
OS開發設計方案
關于軟盤的預備知識
一張軟盤有80個柱面、2個磁頭、18個扇區(Cylinder:0~79;Header:0~2;Sector:1~18),1個扇區有512個字節,所以軟盤的容量是80*2*18*512=1440KB。
向一個軟盤保存文件時,文件名會從0x2600開始往后存,文件的內容會從0x4200開始往后存。
我們的OS開發設計方案如下
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位置開始往后存。
實現一個開發結構完整的OS
完備的IPL程序
下面的代碼是完備的IPL程序,它讀了10個柱面上的代碼到內存,所以文件名從helloos.nas改成了ipl10.nas。

1 ; haribote-ipl 2 ; TAB=4 3 4 CYLS EQU 10 ; どこまで読み込むか 5 6 ORG 0x7c00 ; 指明程序的裝載地址 7 8 ; 以下這段是標準FAT32格式軟盤專用的代碼 9 10 JMP entry 11 DB 0x90 12 DB "HARIBOTE" ; freeparam 啟動區的名稱可以是任意的字符串(8字節) 13 DW 512 ; 每個扇區(sector)的大小(必須為512字節) 14 DB 1 ; 簇(cluster)的大小(必須為1個扇區) 15 DW 1 ; FAT的起始位置(一般從第一個扇區開始) 16 DB 2 ; FAT的個數(必須為2) 17 DW 224 ; 根目錄的大小(一般設成224項) 18 DW 2880 ; 該磁盤的大小(必須是2880扇區) 19 DB 0xf0 ; 磁盤的種類(必須是0xf0) 20 DW 9 ; FAT的長度(必須是9扇區) 21 DW 18 ; 1個磁道(track)有幾個扇區(必須是18) 22 DW 2 ; 磁頭數(必須是2) 23 DD 0 ; 不使用分區,必須是0 24 DD 2880 ; 重寫一次磁盤大小 25 DB 0,0,0x29 ; 意義不明,固定 26 DD 0xffffffff ; (可能是)卷標號碼 27 DB "HARIBOTEOS " ; freeparam 磁盤的名稱(11字節) 28 DB "FAT12 " ; 磁盤格式名稱(8字節) 29 RESB 18 ; 先空出18字節 30 31 ; 程序核心 32 33 entry: 34 MOV AX,0 ; 初始化寄存器 35 MOV SS,AX 36 MOV SP,0x7c00 37 MOV DS,AX 38 39 ; 讀磁盤 40 41 MOV AX,0x0820 42 MOV ES,AX 43 MOV CH,0 ; 柱面0 44 MOV DH,0 ; 磁頭0 45 MOV CL,2 ; 扇區2 46 readloop: 47 MOV SI,0 ; 記錄失敗次數的寄存器 48 retry: 49 MOV AH,0x02 ; AH=0x02 : 讀入磁盤 50 MOV AL,1 ; 1個扇區 51 MOV BX,0 52 MOV DL,0x00 ; A驅動器 53 INT 0x13 ; 調用磁盤BIOS 54 JNC next ; 沒出錯時跳轉到next 55 ADD SI,1 ; SI加1 56 CMP SI,5 ; 比較SI與5 57 JAE error ; SI >= 5時,跳轉到 error 58 MOV AH,0x00 59 MOV DL,0x00 ; A驅動器 60 INT 0x13 ; 重置驅動器 61 JMP retry 62 next: 63 MOV AX,ES ; 把內存地址后移0x200(0x200 = 512) 64 ADD AX,0x0020 ; ADD AX, 512 / 16 65 MOV ES,AX ; 因為沒有ADD ES,0x020 指令,所以這里稍微繞個彎 66 ADD CL,1 ; CL加1 67 CMP CL,18 ; 比較CL與18 68 JBE readloop ; 如果CL <= 18,則跳轉到readloop 69 MOV CL,1 70 ADD DH,1 71 CMP DH,2 72 JB readloop ; 如果DH < 2,則跳轉到readloop 73 MOV DH,0 74 ADD CH,1 75 CMP CH,CYLS 76 JB readloop ; 如果CH < CYLS,則跳轉到readloop 77 78 ; 讀完所有數據后,調到0x8200位置,即haribote.sys中的指令 79 80 MOV [0x0ff0],CH ; 將CYLS的值寫到內存地址0x0ff0中。 81 JMP 0xc200 82 83 error: 84 MOV SI,msg 85 putloop: 86 MOV AL,[SI] 87 ADD SI,1 ; 給SI加1 88 CMP AL,0 89 JE fin 90 MOV AH,0x0e ; 顯示一個文字 91 MOV BX,15 ; 指定字符顏色 92 INT 0x10 ; 調用顯卡BIOS 93 JMP putloop 94 fin: 95 HLT ; 讓CPU停止;等待指令 96 JMP fin ; 無限循環 97 msg: 98 DB 0x0a, 0x0a ; 換行2次 99 DB "load error" ; freeparam 100 DB 0x0a ; 換行 101 DB 0 102 103 RESB 0x7dfe-$ ; 填寫0x00,直到0x001fe 104 105 DB 0x55, 0xaa
簡單地說,這個ipl10.nas讀了軟盤(U盤)最開始的10個柱面,即C0-H0-S1到C9-H1-S18。那么從軟盤(U盤)讀到的這些內容放到哪里了呢?答:放到了內存的0x8000到0x34FFF這一段空間,如下表所示。
序號 |
軟盤(U盤)位置 |
內存位置 |
備注 |
1 |
C0-H0-S1 |
0x8000~0x81FF |
實際上沒有讀這一扇區,這一扇區存的是IPL程序 |
2 |
C0-H0-S2 |
0x8200~0x83FF |
從軟盤(U盤)的512字節到內存的512字節的一一對應。 |
3 |
C0-H0-S3 |
0x8400~0x85FF |
同上 |
…… |
…… |
…… |
…… |
360(10*2*18) |
C9-H1-S18 |
0x34E00~0x34FFF |
同上 |
從剛剛的軟盤預備知識中可知,haribote.sys程序會被加載到內存的(0x8000+0x4200=0xc200)處。所以IPL程序中會有"JMP 0xc200"這一行代碼。這行代碼的意思是:把10個柱面讀到內存后,haribote.sys就準備好了,IPL可以功成身退。下一步就從haribote.sys的第一句指令開始運行我們的OS。
拆分出OS源代碼文件
我們的目的是用C語言寫OS,所以當前給出如下幾個OS源代碼文件。
源代碼文件 |
功能 |
asmhead.nas |
承接IPL程序,調用bootpack.c中的主函數 |
bootpack.c |
OS程序主函數 |
naskfunc.nas |
用匯編語言寫一些供C語言調用的函數 |
下面分別列出這三個源代碼文件的內容。
源代碼asmhead.nas中用日語注釋的地方是原作者在后續章節中解釋的,現在我也不知道是什么意思。我只知道asmhead.nas起了一個承上啟下的作用,以后就可以越來越多得用C來干活了。

1 ; haribote-os boot asm 2 ; TAB=4 3 4 BOTPAK EQU 0x00280000 ; bootpackのロード先 5 DSKCAC EQU 0x00100000 ; ディスクキャッシュの場所 6 DSKCAC0 EQU 0x00008000 ; ディスクキャッシュの場所(リアルモード) 7 8 ; 有關BOOT_INFO 9 CYLS EQU 0x0ff0 ; 設定啟動區 10 LEDS EQU 0x0ff1 11 VMODE EQU 0x0ff2 ; 關于顏色數目的信息。顏色的位數。 12 SCRNX EQU 0x0ff4 ; 分辨率的X(screen x) 13 SCRNY EQU 0x0ff6 ; 分辨率的Y(screen y) 14 VRAM EQU 0x0ff8 ; 圖像緩沖區的開始地址 15 16 ORG 0xc200 ; 這個程序將要被裝載到內存的什么地方呢? 17 18 ; 畫面モードを設定 19 20 MOV AL,0x13 ; VGA顯卡,320x200x8bit彩色 21 MOV AH,0x00 22 INT 0x10 23 MOV BYTE [VMODE],8 ; 記錄畫面模式 24 MOV WORD [SCRNX],320 25 MOV WORD [SCRNY],200 26 MOV DWORD [VRAM],0x000a0000 27 28 ; 用BIOS取得鍵盤上各種LED指示燈的狀態 29 30 MOV AH,0x02 31 INT 0x16 ; keyboard BIOS 32 MOV [LEDS],AL 33 34 ; PICが一切の割り込みを受け付けないようにする 35 ; AT互換機の仕様では、PICの初期化をするなら、 36 ; こいつをCLI前にやっておかないと、たまにハングアップする 37 ; PICの初期化はあとでやる 38 39 MOV AL,0xff 40 OUT 0x21,AL 41 NOP ; OUT命令を連続させるとうまくいかない機種があるらしいので 42 OUT 0xa1,AL 43 44 CLI ; さらにCPUレベルでも割り込み禁止 45 46 ; CPUから1MB以上のメモリにアクセスできるように、A20GATEを設定 47 48 CALL waitkbdout 49 MOV AL,0xd1 50 OUT 0x64,AL 51 CALL waitkbdout 52 MOV AL,0xdf ; enable A20 53 OUT 0x60,AL 54 CALL waitkbdout 55 56 ; プロテクトモード移行 57 58 [INSTRSET "i486p"] ; 486の命令まで使いたいという記述 59 60 LGDT [GDTR0] ; 暫定GDTを設定 61 MOV EAX,CR0 62 AND EAX,0x7fffffff ; bit31を0にする(ページング禁止のため) 63 OR EAX,0x00000001 ; bit0を1にする(プロテクトモード移行のため) 64 MOV CR0,EAX 65 JMP pipelineflush 66 pipelineflush: 67 MOV AX,1*8 ; 読み書き可能セグメント32bit 68 MOV DS,AX 69 MOV ES,AX 70 MOV FS,AX 71 MOV GS,AX 72 MOV SS,AX 73 74 ; bootpackの転送 75 76 MOV ESI,bootpack ; 転送元 77 MOV EDI,BOTPAK ; 転送先 78 MOV ECX,512*1024/4 79 CALL memcpy 80 81 ; ついでにディスクデータも本來の位置へ転送 82 83 ; まずはブートセクタから 84 85 MOV ESI,0x7c00 ; 転送元 86 MOV EDI,DSKCAC ; 転送先 87 MOV ECX,512/4 88 CALL memcpy 89 90 ; 殘り全部 91 92 MOV ESI,DSKCAC0+512 ; 転送元 93 MOV EDI,DSKCAC+512 ; 転送先 94 MOV ECX,0 95 MOV CL,BYTE [CYLS] 96 IMUL ECX,512*18*2/4 ; シリンダ數からバイト數/4に変換 97 SUB ECX,512/4 ; IPLの分だけ差し引く 98 CALL memcpy 99 100 ; asmheadでしなければいけないことは全部し終わったので、 101 ; あとはbootpackに任せる 102 103 ; bootpackの起動 104 105 MOV EBX,BOTPAK 106 MOV ECX,[EBX+16] 107 ADD ECX,3 ; ECX += 3; 108 SHR ECX,2 ; ECX /= 4; 109 JZ skip ; 転送するべきものがない 110 MOV ESI,[EBX+20] ; 転送元 111 ADD ESI,EBX 112 MOV EDI,[EBX+12] ; 転送先 113 CALL memcpy 114 skip: 115 MOV ESP,[EBX+12] ; スタック初期値 116 JMP DWORD 2*8:0x0000001b 117 118 waitkbdout: 119 IN AL,0x64 120 AND AL,0x02 121 JNZ waitkbdout ; ANDの結果が0でなければwaitkbdoutへ 122 RET 123 124 memcpy: 125 MOV EAX,[ESI] 126 ADD ESI,4 127 MOV [EDI],EAX 128 ADD EDI,4 129 SUB ECX,1 130 JNZ memcpy ; 引き算した結果が0でなければmemcpyへ 131 RET 132 ; memcpyはアドレスサイズプリフィクスを入れ忘れなければ、ストリング命令でも書ける 133 134 ALIGNB 16 135 GDT0: 136 RESB 8 ; ヌルセレクタ 137 DW 0xffff,0x0000,0x9200,0x00cf ; 読み書き可能セグメント32bit 138 DW 0xffff,0x0000,0x9a28,0x0047 ; 実行可能セグメント32bit(bootpack用) 139 140 DW 0 141 GDTR0: 142 DW 8*3-1 143 DD GDT0 144 145 ALIGNB 16 146 bootpack:
目前的主函數什么都沒有做。

1 /* 告訴C編譯器,有一個函數在別的文件里。 */ 2 3 void io_hlt(void); 4 5 /* 是函數聲明卻不用{}。而用;,這表示的意思是:函數是在別的文件中,你自己找一下吧! */ 6 7 void HariMain(void) 8 { 9 10 fin: 11 io_hlt(); /* 執行naskfunc.nas里的_io_hlt */ 12 goto fin; 13 14 }
這個naskfunc.nas可以說是一個封裝硬件供C語言調用的函數庫。

1 ; naskfunc 2 ; TAB=4 3 4 [FORMAT "WCOFF"] ; 制作目標文件的模式 5 [BITS 32] ; 制作32位模式用的機器語言 6 7 8 ; 制作目標文件的信息 9 10 [FILE "naskfunc.nas"] ; 源文件名信息 11 12 GLOBAL _io_hlt ; 程序中包含的函數名 13 14 15 ; 以下是實際的函數 16 17 [SECTION .text] ; 目標文件中寫了這些后再寫程序 18 19 _io_hlt: ; void io_hlt(void); 20 HLT 21 RET
從Makefile畫出編譯和部署流程
上述的ipl10.nas、asmhead.nas、bootpack.c、naskfunc.nas是如何組合成為一個OS程序的呢?下面的Makefile文件描述了編譯流程。

1 TOOLPATH = ../z_tools/ 2 INCPATH = ../z_tools/haribote/ 3 4 MAKE = $(TOOLPATH)make.exe -r 5 NASK = $(TOOLPATH)nask.exe 6 CC1 = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet 7 GAS2NASK = $(TOOLPATH)gas2nask.exe -a 8 OBJ2BIM = $(TOOLPATH)obj2bim.exe 9 BIM2HRB = $(TOOLPATH)bim2hrb.exe 10 RULEFILE = $(TOOLPATH)haribote/haribote.rul 11 EDIMG = $(TOOLPATH)edimg.exe 12 IMGTOL = $(TOOLPATH)imgtol.com 13 COPY = copy 14 DEL = del 15 16 # デフォルト動作 17 18 default : 19 $(MAKE) img 20 21 # ファイル生成規則 22 23 ipl10.bin : ipl10.nas Makefile 24 $(NASK) ipl10.nas ipl10.bin ipl10.lst 25 26 asmhead.bin : asmhead.nas Makefile 27 $(NASK) asmhead.nas asmhead.bin asmhead.lst 28 29 bootpack.gas : bootpack.c Makefile 30 $(CC1) -o bootpack.gas bootpack.c 31 32 bootpack.nas : bootpack.gas Makefile 33 $(GAS2NASK) bootpack.gas bootpack.nas 34 35 bootpack.obj : bootpack.nas Makefile 36 $(NASK) bootpack.nas bootpack.obj bootpack.lst 37 38 naskfunc.obj : naskfunc.nas Makefile 39 $(NASK) naskfunc.nas naskfunc.obj naskfunc.lst 40 41 bootpack.bim : bootpack.obj naskfunc.obj Makefile 42 $(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \ 43 bootpack.obj naskfunc.obj 44 # 3MB+64KB=3136KB 45 46 bootpack.hrb : bootpack.bim Makefile 47 $(BIM2HRB) bootpack.bim bootpack.hrb 0 48 49 haribote.sys : asmhead.bin bootpack.hrb Makefile 50 copy /B asmhead.bin+bootpack.hrb haribote.sys 51 52 haribote.img : ipl10.bin haribote.sys Makefile 53 $(EDIMG) imgin:../z_tools/fdimg0at.tek \ 54 wbinimg src:ipl10.bin len:512 from:0 to:0 \ 55 copy from:haribote.sys to:@: \ 56 imgout:haribote.img 57 58 # コマンド 59 60 img : 61 $(MAKE) haribote.img 62 63 run : 64 $(MAKE) img 65 $(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin 66 $(MAKE) -C ../z_tools/qemu 67 68 install : 69 $(MAKE) img 70 $(IMGTOL) w a: haribote.img 71 72 clean : 73 -$(DEL) *.bin 74 -$(DEL) *.lst 75 -$(DEL) *.gas 76 -$(DEL) *.obj 77 -$(DEL) bootpack.nas 78 -$(DEL) bootpack.map 79 -$(DEL) bootpack.bim 80 -$(DEL) bootpack.hrb 81 -$(DEL) haribote.sys 82 83 src_only : 84 $(MAKE) clean 85 -$(DEL) haribote.img
通過Makefile,我們可以畫出如下所示的編譯和部署流程。
總結
今后開發OS時,就可以直接在bootpack.c中寫代碼;當遇到C語言無法完成的情況時,就在naskfunc.nas里用匯編語言寫函數,然后用bootpack.c調用這些函數。
現在的asmhead.nas程序在計算機啟動時將顯示器置為全黑,如下圖所示。
文章列表