文章目录
-
-
- [1. 制作真正的IPL](#1. 制作真正的IPL)
- [2. harib00b例程](#2. harib00b例程)
- [3. harib00c例程](#3. harib00c例程)
- [4. harib00d例程](#4. harib00d例程)
- [5. harib00g例程](#5. harib00g例程)
- [5. harib00h例程](#5. harib00h例程)
- [5. harib00i例程](#5. harib00i例程)
- [5. harib00j例程](#5. harib00j例程)
-
本章的操作系统文件叫做haribote.img。
1. 制作真正的IPL
对helloos5中的ipl.nas添加内容:
shell
; 读磁盘
MOV AX,0x0820
MOV ES,AX
MOV CH,0 ; 磁道号的低8位(柱面号)
MOV DH,0 ; 磁头号
MOV CL,2 ; 低5位放入所读的起始扇区,位7-6表示磁道号的高2位
MOV AH,0x02 ; AH=0x02 : 读盘
MOV AL,1 ; 处理1个扇区
MOV BX,0
MOV DL,0x00 ; 需要进行写操作的驱动器号,A驱动器
INT 0x13 ; 磁盘服务程序
JC error ; 如果进位(jump if carry)
本次新增了一个JC
(jump if carry)指令,表示如果进位为1的话,就跳转。本代码中它与上一行的INT 0x13
组合使用,如果INT 0x13
返回值使进位标志位(carry flag)为置位为1,则执行JC。
另外还新增了INT 0x13
指令,目的就是调用BIOS的0x13函数,磁盘读、写、扇区校验(verify)或者寻道(seek):
AH=0x02;
:读盘。AH=0x03;
:写盘。AH=0x04;
:校验。AH=0x0c;
:寻道。AL=处理对象的扇区数;
:只能处理连续的扇区。CH=柱面号 & 0xff;
。CL=扇区号(0~5bits) | (柱面号 & 0x300) >> 2;
。DH=磁头号;
。ES:BX=缓冲区地址;
:校验及寻道时不使用。INT 0x13 返回值:
FLAGS.CF==0;
:没有error,AH==0。FLAGS.CF==1;
:有error,错误码存入AH(类似于重置reset功能)。

柱面(cylinder)就是同心圆环,扇区(sector)是将圆环划分成的圆弧长条。一张软盘由80个柱面(C0, C1, C2...),2个磁头(H0和H1),每个柱面被划分为18个扇区(S1, S2, S3...),每个扇区是512字节容量,因此一张软盘的容量为80*2*18*512 = 1474569Byte = 1440KB
。
启动区IPL位于C0-H0-S1位置。
一个16bits寄存器只能寻址64KB(0xffff),于是设计了一个起辅助作用的段寄存器(segment register),用于指定缓冲区的内存地址。当使用段寄存器时,以ES:BX
的方式来表示地址,写作MOV AL, [ES:BX]
,代表ES*16+BX,即最大可寻址0x10FFEF(1114095,大约1MB的内存)。设置ES=0x0820,BX=0,即将C0-H0-S2扇区内容加载到0x8200 ~ 0x83ff地址区域。并没有特殊的含义,只是由于启动区加载到0x7c00 ~ 0x7dff区域,所以0x07e00 ~ 0x9fbf之间没有特别用途,操作系统可以随意使用,因此就选择了0x8200之后的区域。
实际上只要期望指定地址,都会用到段寄存器,如果没有显式指定,那就是默认使用DS(数据段)段寄存器。例如:
MOV CX, [1234]
实际上表示MOV CX, [DS:1234]
;MOV AL, [SI]
实际上表示MOV AL, [DS:SI]
。
因为存在这样的规则,所以DS必须首先初始化为0。
2. harib00b例程
bash
# 添加多次读取软盘的操作
; 读磁盘
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 ; jump if not carry,如果不进位跳转到fin
ADD SI,1 ; SI = SI + 1
CMP SI,5 ; SI与5做比较
JAE error ; SI >= 5 跳转到error (jump if above or equal)
MOV AH,0x00
MOV DL,0x00 ; 磁盘驱动器A
INT 0x13 ; 重置驱动器
JMP retry
存在一次读取软盘失败的情况,因此这段代码尝试读取5次。
JNC
表示jump if not carry,如果进位标志位为0的话,就跳转。JAE
表示jump if above or equal,如果大于或等于时跳转。
再跳转到retry,即重新尝试读取软盘之前,复位了AH和DL寄存器,这两个操作可以认为是"系统复位",它复位了软盘的状态。
3. harib00c例程
bash
; 读磁盘2~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 ; jump if not carry, 如果不进位(没有错误)则跳转next
ADD SI,1 ; SI = SI + 1
CMP SI,5 ; SI和5作比较
JAE error ; SI >= 5 ,则跳转到error(jump if above or equal)
MOV AH,0x00
MOV DL,0x00 ; A驱动器
INT 0x13 ; 重置BIOS驱动器
JMP retry
next:
MOV AX,ES ; 把内存地址后移0x200,即512个字节
ADD AX,0x0020 ; 对ES+20H或者对BX+512D,都是读取下一个扇区(20H = 512D/16)
MOV ES,AX ; 因为没有ADD ES,0x020指令,所以这里稍微绕个弯
ADD CL,1 ; CL = CL + 1,扇区寄存器+1
CMP CL,18 ; 比较CL和18
JBE readloop ; CL <= 18 跳转readloop(jump if below or equal)
JBE
表示jump if below or equal,小于或等于则跳转。
本次是使用循环,每次读取一个扇区,其实如果将AL设置为17,似乎也能将第2~18共17个扇区读取到内存中,但是存在安全隐患。
4. harib00d例程
bash
CYLS EQU 10
(省略中间的一部分...)
; 读取10个柱面
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的第0x13号程序
JNC next ; 0x13运行正常(进位标志位为0)跳转到next
ADD SI,1 ; SI = SI+1
CMP SI,5 ; 比较SI与5
JAE error ; if 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 = CL + 1
CMP CL,18 ; 比较CL与18
JBE readloop ; if CL <= 18;跳转到readloop
MOV CL,1 ; if CL > 18; CL = 1
ADD DH,1 ; DH = DH + 1
CMP DH,2 ; 比较DH与2
JB readloop ; if DH < 2; 跳转到readloop
MOV DH,0 ; if DH >= 2; DH = 0
ADD CH,1 ; CH = CH + 1
CMP CH,CYLS ;
JB readloop ; if CH < CYLS; 跳转到readloop
JB
表示如果小于。EQU
用于定义一个变量,类似于C语言的#define
,源文件首行的CYLS EQU 10
就是表示声明CYLS(cylinder)为10。
截至目前,已经可以把软盘最初的102 18*512=184 320Byte = 180KB内容读到内存了,将被加载在内存的0x08200 ~ 0x34fff地址区域。如果执行make stall
就可以将程序安装到磁盘上。
(184320 - 512) = 183808Byte = 0x2ce00
(磁盘的首个512字节被加载到0x7c00了,所以以后的数据总共183808字节)
0x08200 + 0x2ce00 = 0x35000
(从0x8200地址开始加载183808字节内容其实也可以直接使用0x08000 + 0x2d000 = 0x35000
计算)
5. harib00g例程
再次之前,一直做的是启动区ipl的开发。接下载才正式开始操作系统。
先以一个非抢占cpu的死循环作为一个基础文件:
bash
# 文件命名为 haribote.nas
fin:
HLT
JMP fin
使用nask编译haribote.nas,输出haribote.sys。
将haribote.sys保存到(或集成到)haribote.img中,具体操作步骤可以理解为:
- 将haribote.img挂载到某目录(或某文件夹)下;
- 将haribote.sys放置在该目录(或该文件夹)中;
- 卸载haribote.img。
这样就成功将haribote.sys保存到了haribote.img中。
在Makefile中:
shell
# 节选
TOOLPATH = ../z_tools/
MAKE = $(TOOLPATH)make.exe -r
EDIMG = $(TOOLPATH)edimg.exe
default :
$(MAKE) img
haribote.img : ipl.bin haribote.sys Makefile
$(EDIMG) imgin:../z_tools/fdimg0at.tek \
wbinimg src:ipl.bin len:512 from:0 to:0 \
copy from:haribote.sys to:@: \
imgout:haribote.img
img :
$(MAKE) haribote.img
制作一个完整的haribote.img,需要依赖ibl.bin和haribote.sys两个二进制文件。通过make img
指令生成haribote.img。新做成的haribote.img与无haribote.sys的img比较可以看到:
在0x2600和0x4200地址处的差别。可见0x2600存放了haribote.sys的文件名,0x4200存放了haribote.sys的文件内容。
内存应该如何处理磁盘上haribote.sys的文件内容呢,haribote.sys才是操作系统的本体。0x4200 + 0x8000 = 0xc200
,即haribote.sys应该位于内存的0xc200处。因此haribote.nas源文件的开头应该指定内存地址ORG 0xc200
。
但是当前的操作系统只有一个不抢占cpu的循环代码,因此为了验证操作系已经加载到指定位置,就在操作系统中添加一些基础的显示配置:
bash
; haribote-os
; TAB=4
ORG 0xc200 ; 从内存0xc200开始,0x8000+0x4200 = 0xc200
MOV AL,0x13 ; VGA图形模式320x200x8bit彩色
MOV AH,0x00
INT 0x10 ; 调用显卡BIOS
fin:
HLT
JMP fin
设置显卡模式:
AH = 0x00;
AL = 模式
0x03
:16色字符模式,80*250x12
:VGA图形模式,6404804位彩色模式,独特的4面存储模式。0x13
:VGA图形模式,3202008位彩色模式,调色板模式。0x6a
:扩展VGA图形模式,8006004位彩色模式,,独特的4面存储模式。(有的显卡不支持)
- 返回值:无
当前设置了AL为0x13,因此执行make img
和make run
之后,虚拟机的画面应该会变为一片漆黑,光标也会消失。
5. harib00h例程
CPU的16位模式和32位模式中,机器语言的命令代码不同,同样的机器语言,解释方法也不同,所以16位模式的机器语言在32位模式下无法运行,反之亦然。
BIOS是16位机器语言写的,如果使用32位模式,就无法调用BIOS功能了。所以将期望调用BIOS的操作放在开头来做。
除了harib00g中配置的画面模式,还需要获取键盘状态,例如,NumLock等。
直接在haribote.nas中修改:
bash
; haribote-os
; TAB=4
; BOOT_INFO
CYLS EQU 0x0ff0 ; 设定启动区
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2 ; 关于颜色的信息,颜色的位数
SCRNX EQU 0x0ff4 ; 分辨率X
SCRNY EQU 0x0ff6 ; 分辨率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
配置画面模式之后,把所配置的信息保存在内存中,以备后续切换画面模式时试用。
暂且将启动时的信息命名为BOOT_INFO。
[VRAM]表示的是显卡内存(video RAM),是一个用来显示画面的内存。它的各个地址对应画面的像素,可以利用这一机制在画面上绘制有颜色的图案。
VRAM实际上分布在内存的很多地方,这是由于不同的画面模式有不同的像素数,所以当画面模式不同时可能使用不同的VRAM地址。当前在INT 0x10画面模式下,VRAM是0xa0000 ~ 0xaffff这64KB。
5. harib00i例程
开始使用c语言。
具体的逻辑后续会在c语言源文件中实现,因此将haribote.nas重命名为asmhead.nas,并添加用于调用c语言函数的代码。
bash
# asmhead.nas
; haribote-os boot asm
; TAB=4
BOTPAK EQU 0x00280000
DSKCAC EQU 0x00100000
DSKCAC0 EQU 0x00008000
; BOOT_INFO
CYLS EQU 0x0ff0
LEDS EQU 0x0ff1
VMODE EQU 0x0ff2
SCRNX EQU 0x0ff4
SCRNY EQU 0x0ff6
VRAM EQU 0x0ff8
ORG 0xc200
; �����[�h����
MOV AL,0x13 ; VGA�O���t�B�b�N�X�A320x200x8bit�J���[
MOV AH,0x00
INT 0x10
MOV BYTE [VMODE],8 ; �����[�h��������iC�����Q����j
MOV WORD [SCRNX],320
MOV WORD [SCRNY],200
MOV DWORD [VRAM],0x000a0000
; �L�[�{�[�h��LED����BIOS��������
MOV AH,0x02
INT 0x16 ; keyboard BIOS
MOV [LEDS],AL
; PIC����������������t����������
; AT���@��d�l���APIC������������A
; �����CLI�O����������A����n���O�A�b�v���
; PIC��������������
MOV AL,0xff
OUT 0x21,AL
NOP ; OUT����A��������������@������������
OUT 0xa1,AL
CLI ; ����CPU���x������������~
; CPU���1MB�����������A�N�Z�X��������AA20GATE����
CALL waitkbdout
MOV AL,0xd1
OUT 0x64,AL
CALL waitkbdout
MOV AL,0xdf ; enable A20
OUT 0x60,AL
CALL waitkbdout
; �v���e�N�g���[�h��s
[INSTRSET "i486p"] ; 486������g�������L�q
LGDT [GDTR0] ; �b��GDT����
MOV EAX,CR0
AND EAX,0x7fffffff ; bit31��0����i�y�[�W���O��~����j
OR EAX,0x00000001 ; bit0��1����i�v���e�N�g���[�h��s����j
MOV CR0,EAX
JMP pipelineflush
pipelineflush:
MOV AX,1*8 ; ��������\�Z�O�����g32bit
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
; �����f�B�X�N�f�[�^��{�����u��]��
; ����u�[�g�Z�N�^���
MOV ESI,0x7c00 ; �]����
MOV EDI,DSKCAC ; �]����
MOV ECX,512/4
CALL memcpy
; �c��S��
MOV ESI,DSKCAC0+512 ; �]����
MOV EDI,DSKCAC+512 ; �]����
MOV ECX,0
MOV CL,BYTE [CYLS]
IMUL ECX,512*18*2/4 ; �V�����_�����o�C�g��/4����
SUB ECX,512/4 ; IPL������������
CALL memcpy
; asmhead����������������S����I������A
; �����bootpack��C���
; bootpack��N��
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] ; �X�^�b�N�����l
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 ; �����Z�������0�������memcpy��
RET
; memcpy��A�h���X�T�C�Y�v���t�B�N�X�����Y�������A�X�g�����O����������
ALIGNB 16
GDT0:
RESB 8 ; �k���Z���N�^
DW 0xffff,0x0000,0x9200,0x00cf ; ��������\�Z�O�����g32bit
DW 0xffff,0x0000,0x9a28,0x0047
DW 0
GDTR0:
DW 8*3-1
DD GDT0
ALIGNB 16
bootpack:
一个基础的c语言文件bootpack.c,可惜c语言中没有类似HLT指令的函数操作:
c
void HariMain(void)
{
fin:
goto fin;
}
后续的玩法是这样的:
- 使用ccl.exe编译器从c源文件(bootpack.c或其他)生成bootpack.gas(汇编语言);
- 使用gas2nask.exe从bootpack.gas生成bootpack.nas(翻译为nas文件);
- 使用nask.exe从bootpack.nas生成bootpack.obj(目标文件);
- 使用obj2bim.exe从bootpack.obj生成bootpack.bim(这一步做链接,生成binary image);
- 使用bim2hrb.exe从bootpack.bim生成bootpack.hrb(对.bim文件制作纸娃娃格式);
- 将bootpack.hrb和asmhead.bin合并在一起就成了haribote.sys。
单个的目标文件(.o或.obj)并不是完整的机器语言,需要把所有的目标文件链接起来。最终生成一个二进制文件映像(binary image)。所谓映像,可以这样理解:映像文件并不是文件本来的状态,而是一种代替形式。
所以实际上.bim文件也不是本来的形态,而是一个替代形式,并且不是完整品。它只是将所有的目标文件链接在一起组成了一个完整的机器语言文件,为了能实际使用,还需要针对不同操作系统的要求做必要的加工,例如用于识别的文件头,压缩等。
本次为了做成适合"纸娃娃"操作系统,专门使用bim2hrb.exe工具:输入.bim文件输出.hrb文件。
针对以上步骤Makefile文件做了很多修改:
shell
# 节选
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
# �f�t�H���g����
default :
$(MAKE) img
# �t�@�C������K��
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
bootpack.bim : bootpack.obj Makefile
$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
bootpack.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
纸娃娃操作系统的主函数是HariMain,如果本例程执行后是黑屏的,表示是正常的。
5. harib00j例程
还是使用nask汇编语言写一个HLT的函数,文件命名为naskfunc.nas。
shell
# naskfunc.nas
; naskfunc
; TAB=4
[FORMAT "WCOFF"] ; 制作目标文件的模式(不明白这个)
[BITS 32] ; 制作32位模式用的机器语言
; 制作目标文件的信息
[FILE "naskfunc.nas"] ; 原文件名信息
GLOBAL _io_hlt ; 程序中包含的函数名
; 实际的函数
[SECTION .text] ; 目标文件中写了这些之后再写程序
_io_hlt: ; void io_hlt(void);
HLT
RET
在CPU指令中,HLT属于I/O指令,MOV属于传送指令,ADD属于演算指令。所以这个函数起名了io_hlt,可以使用c语言void io_hlt(void);
声明这个函数。
将输出格式定为WCOFF(有点不理解),并设定为32位机器语言模式。
在nask语言模式下,需要设定文件名信息,再写明函数的函数名,前面加下划线"_",目的是能够与C语言函数链接。需要链接的函数名使用"GLOBAL"指令声明。
在bootpack.c中调用io_hlt函数:
shell
# bootpack.c
void io_hlt(void); // 声明函数
void HariMain(void)
{
fin:
io_hlt(); /* 调用函数 */
goto fin;
}
当然运行起来仍然是一个黑屏。