《30天自制操作系统》笔记(02)——导入C语言

时间:2024-01-10 19:29:02

《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。

 ; haribote-ipl
; TAB=4 CYLS EQU ; どこまで読み込むか ORG 0x7c00 ; 指明程序的装载地址 ; 以下这段是标准FAT32格式软盘专用的代码 JMP entry
DB 0x90
DB "HARIBOTE" ; freeparam 启动区的名称可以是任意的字符串(8字节)
DW ; 每个扇区(sector)的大小(必须为512字节)
DB ; 簇(cluster)的大小(必须为1个扇区)
DW ; FAT的起始位置(一般从第一个扇区开始)
DB ; FAT的个数(必须为2)
DW ; 根目录的大小(一般设成224项)
DW ; 该磁盘的大小(必须是2880扇区)
DB 0xf0 ; 磁盘的种类(必须是0xf0)
DW ; FAT的长度(必须是9扇区)
DW ; 1个磁道(track)有几个扇区(必须是18)
DW ; 磁头数(必须是2)
DD ; 不使用分区,必须是0
DD ; 重写一次磁盘大小
DB ,,0x29 ; 意义不明,固定
DD 0xffffffff ; (可能是)卷标号码
DB "HARIBOTEOS " ; freeparam 磁盘的名称(11字节)
DB "FAT12 " ; 磁盘格式名称(8字节)
RESB ; 先空出18字节 ; 程序核心 entry:
MOV AX, ; 初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX ; 读磁盘 MOV AX,0x0820
MOV ES,AX
MOV CH, ; 柱面0
MOV DH, ; 磁头0
MOV CL, ; 扇区2
readloop:
MOV SI, ; 记录失败次数的寄存器
retry:
MOV AH,0x02 ; AH=0x02 : 读入磁盘
MOV AL, ; 1个扇区
MOV BX,
MOV DL,0x00 ; A驱动器
INT 0x13 ; 调用磁盘BIOS
JNC next ; 没出错时跳转到next
ADD SI, ; SI加1
CMP SI, ; 比较SI与5
JAE error ; SI >= 5时,跳转到 error
MOV AH,0x00
MOV DL,0x00 ; A驱动器
INT 0x13 ; 重置驱动器
JMP retry
next:
MOV AX,ES ; 把内存地址后移0x200(0x200 = 512)
ADD AX,0x0020 ; ADD AX, 512 / 16
MOV ES,AX ; 因为没有ADD ES,0x020 指令,所以这里稍微绕个弯
ADD CL, ; CL加1
CMP CL, ; 比较CL与18
JBE readloop ; 如果CL <= 18,则跳转到readloop
MOV CL,
ADD DH,
CMP DH,
JB readloop ; 如果DH < 2,则跳转到readloop
MOV DH,
ADD CH,
CMP CH,CYLS
JB readloop ; 如果CH < CYLS,则跳转到readloop ; 读完所有数据后,调到0x8200位置,即haribote.sys中的指令 MOV [0x0ff0],CH ; 将CYLS的值写到内存地址0x0ff0中。
JMP 0xc200 error:
MOV SI,msg
putloop:
MOV AL,[SI]
ADD SI, ; 给SI加1
CMP AL,
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX, ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
fin:
HLT ; 让CPU停止;等待指令
JMP fin ; 无限循环
msg:
DB 0x0a, 0x0a ; 换行2次
DB "load error" ; freeparam
DB 0x0a ; 换行
DB RESB 0x7dfe-$ ; 填写0x00,直到0x001fe DB 0x55, 0xaa

ipl10.nas

简单地说,这个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来干活了。

 ; 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], ; 记录画面模式
MOV WORD [SCRNX],
MOV WORD [SCRNY],
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,* ; 読み書き可能セグメント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,*/
CALL memcpy ; ついでにディスクデータも本来の位置へ転送 ; まずはブートセクタから MOV ESI,0x7c00 ; 転送元
MOV EDI,DSKCAC ; 転送先
MOV ECX,/
CALL memcpy ; 残り全部 MOV ESI,DSKCAC0+ ; 転送元
MOV EDI,DSKCAC+ ; 転送先
MOV ECX,
MOV CL,BYTE [CYLS]
IMUL ECX,**/ ; シリンダ数からバイト数/4に変換
SUB ECX,/ ; IPLの分だけ差し引く
CALL memcpy ; asmheadでしなければいけないことは全部し終わったので、
; あとはbootpackに任せる ; bootpackの起動 MOV EBX,BOTPAK
MOV ECX,[EBX+]
ADD ECX, ; ECX += 3;
SHR ECX, ; ECX /= 4;
JZ skip ; 転送するべきものがない
MOV ESI,[EBX+] ; 転送元
ADD ESI,EBX
MOV EDI,[EBX+] ; 転送先
CALL memcpy
skip:
MOV ESP,[EBX+] ; スタック初期値
JMP DWORD *:0x0000001b waitkbdout:
IN AL,0x64
AND AL,0x02
JNZ waitkbdout ; ANDの結果が0でなければwaitkbdoutへ
RET memcpy:
MOV EAX,[ESI]
ADD ESI,
MOV [EDI],EAX
ADD EDI,
SUB ECX,
JNZ memcpy ; 引き算した結果が0でなければmemcpyへ
RET
; memcpyはアドレスサイズプリフィクスを入れ忘れなければ、ストリング命令でも書ける ALIGNB
GDT0:
RESB ; ヌルセレクタ
DW 0xffff,0x0000,0x9200,0x00cf ; 読み書き可能セグメント32bit
DW 0xffff,0x0000,0x9a28,0x0047 ; 実行可能セグメント32bit(bootpack用) DW
GDTR0:
DW *-
DD GDT0 ALIGNB
bootpack:

asmhead.nas

目前的主函数什么都没有做。

 /* 告诉C编译器,有一个函数在别的文件里。 */

 void io_hlt(void);

 /* 是函数声明却不用{}。而用;,这表示的意思是:函数是在别的文件中,你自己找一下吧! */

 void HariMain(void)
{ fin:
io_hlt(); /* 执行naskfunc.nas里的_io_hlt */
goto fin; }

bootpack.c

这个naskfunc.nas可以说是一个封装硬件供C语言调用的函数库。

 ; naskfunc
; TAB=4 [FORMAT "WCOFF"] ; 制作目标文件的模式
[BITS ] ; 制作32位模式用的机器语言 ; 制作目标文件的信息 [FILE "naskfunc.nas"] ; 源文件名信息 GLOBAL _io_hlt ; 程序中包含的函数名 ; 以下是实际的函数 [SECTION .text] ; 目标文件中写了这些后再写程序 _io_hlt: ; void io_hlt(void);
HLT
RET

naskfunc.nas

从Makefile画出编译和部署流程

上述的ipl10.nas、asmhead.nas、bootpack.c、naskfunc.nas是如何组合成为一个OS程序的呢?下面的Makefile文件描述了编译流程。

 TOOLPATH = ../z_tools/
INCPATH = ../z_tools/haribote/ MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
CC1 = $(TOOLPATH)cc1.exe -I$(INCPATH) -Os -Wall -quiet
GAS2NASK = $(TOOLPATH)gas2nask.exe -a
OBJ2BIM = $(TOOLPATH)obj2bim.exe
BIM2HRB = $(TOOLPATH)bim2hrb.exe
RULEFILE = $(TOOLPATH)haribote/haribote.rul
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com
COPY = copy
DEL = del # デフォルト動作 default :
$(MAKE) img # ファイル生成規則 ipl10.bin : ipl10.nas Makefile
$(NASK) ipl10.nas ipl10.bin ipl10.lst asmhead.bin : asmhead.nas Makefile
$(NASK) asmhead.nas asmhead.bin asmhead.lst bootpack.gas : bootpack.c Makefile
$(CC1) -o bootpack.gas bootpack.c bootpack.nas : bootpack.gas Makefile
$(GAS2NASK) bootpack.gas bootpack.nas bootpack.obj : bootpack.nas Makefile
$(NASK) bootpack.nas bootpack.obj bootpack.lst naskfunc.obj : naskfunc.nas Makefile
$(NASK) naskfunc.nas naskfunc.obj naskfunc.lst bootpack.bim : bootpack.obj naskfunc.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.obj naskfunc.obj
# 3MB+64KB=3136KB bootpack.hrb : bootpack.bim Makefile
$(BIM2HRB) bootpack.bim bootpack.hrb 0 haribote.sys : asmhead.bin bootpack.hrb Makefile
copy /B asmhead.bin+bootpack.hrb haribote.sys haribote.img : ipl10.bin haribote.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl10.bin len:512 from:0 to:0 \
copy from:haribote.sys to:@: \
imgout:haribote.img # コマンド img :
$(MAKE) haribote.img run :
$(MAKE) img
$(COPY) haribote.img ..\z_tools\qemu\fdimage0.bin
$(MAKE) -C ../z_tools/qemu install :
$(MAKE) img
$(IMGTOL) w a: haribote.img clean :
-$(DEL) *.bin
-$(DEL) *.lst
-$(DEL) *.gas
-$(DEL) *.obj
-$(DEL) bootpack.nas
-$(DEL) bootpack.map
-$(DEL) bootpack.bim
-$(DEL) bootpack.hrb
-$(DEL) haribote.sys src_only :
$(MAKE) clean
-$(DEL) haribote.img

Makefile in haribote00j

通过Makefile,我们可以画出如下所示的编译和部署流程。

《30天自制操作系统》笔记(02)——导入C语言

总结

今后开发OS时,就可以直接在bootpack.c中写代码;当遇到C语言无法完成的情况时,就在naskfunc.nas里用汇编语言写函数,然后用bootpack.c调用这些函数。

现在的asmhead.nas程序在计算机启动时将显示器置为全黑,如下图所示。

《30天自制操作系统》笔记(02)——导入C语言

点此查看下一篇《30天自制操作系统》笔记(03)——使用Vmware