制作真正的IPL:相关知识
关键代码:
- 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号函数,根据BIOS网页:
- 磁盘读、写,扇区校验(verify),以及寻道(seek)
- AH=0x02;(读盘) ---这次用的是这个功能
- AH=0x03;(写盘)
- AH=0x04;(校验)
- AH=0x0c;(寻道)
- AL=处理对象的扇区数;(只能同时处理连续的扇区)
- CH=柱面号 & 0xff;
- CL=扇区号(0-5位)|(柱面号&0x300)>>2;
- DH=磁头号;
- DL=驱动器号;
- ES:BX=缓冲地址;(校验及寻道时不使用)
返回值:
FLACS:CF==0: 没有错误,AH==0
FLAGS.GF==1: 有错误,错误号码存入AH内(与重置(reset)功能一样)
进位标志:FLAGS.CF,调用此函数之后,如果没错,进位标志是0,如果有错,进位标志就是1。(本次是用来报告BIOS函数调用是否有错)
进位标志是一个只能存储1位信息的寄存器,除此之外CPU还有其他几个只有1位的寄存器,这种1位寄存器我们称为标志。
软盘磁盘:
在有多个软盘驱动器时,用磁盘驱动器号来指定从哪个驱动器的软盘上读取数据。现在的电脑基本都是只有1个软盘驱动器,以前一般是2个,只有一个就指定0号。软盘一共有80个柱面,有正反两个磁头分别是0号、1号(都可以记录数据)。在磁盘这个圆环上均匀等分了18个扇区,分别称扇区1,扇区2.......综上,一张软盘有80个柱面,2个磁头,18个扇区,且一个扇区有512个字节,所以一张软盘的容量是80×2×18×512=1474560Byte=1440KB
含有IPL的启动区位于C0-H0-SI(柱面0,磁头0,扇区1的缩写),下一个扇区是C0-H0-S2(本次要装载的扇区)。
缓冲区地址:这是一个内存地址,表明我们要把从软盘上读出的数据装载到内存的哪个位置。在设计BIOS时,CPU没有32位的寄存器,只设计了一个起辅助作用的段寄存器,在指定内存地址的时候可用这个段寄存器。
以ES:BX这种方式表示内存地址,写成“MOV AL,[ES:BX]”,表示ES×16+BX的内存地址。可理解为先用ES指定一大块区域,再用BX指定一个具体地址。这种方法可以使用1M以内的内存。我们指定的是ES=0x0820,BX=0,因为0x8000-0x81ff这512个字节是留给启动区的。我们在使用内存地址时,都要同时指定段寄存器,如果省略就是默认”DS:”作为默认的段寄存器。“MOV AL,[SI]”=“MOV AL,[DS:SI]”。(注意:段寄存器要预先初始化为0,否则地址的值要加上这个数的16倍),下面运行查看结果:
试错:重点总结、关键代码及注释
因为有时候软盘不能读数据,需要多度几次,所以加入部分代码(读5次)
; 读磁盘
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是一个条件跳转指令:“Jump if not carry” 意思是进位标志是0的话就跳转;
JAE也是条件跳转:“Jump if above or equal” 意思是大于或等于时跳转。
重新读盘之前,需要做一次“系统复位”AH=0x00,DL=0x00,INT 0x13复位软盘状态,再读一次。
读到18扇区(harib00c)
往后多读几个扇区:
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的十六进制),或者给BX+512;最后是一个循环,表示循环读取2-18个扇区,这里可以直接设置AL的值为17,但是作者提到BIOS的“补充说明部分”:指定处理的扇区数,范围在0x01-0xff(指定0x02以上的数值时,要特别注意能够连续处理多个扇区的条件。如果是FD的话,似乎不能跨越多个磁道,也不能超过64KB的界限。)作者的解释是循序渐进,用循环较好,因为后面的程序如果这样写会出问题。
到了这一步我们已经把磁盘上C0-H--S2到C0-H0-S18的512x17=8704个字节的内容装载到内存的0x8200-0xa3ff处。
读入10个柱面(harib00d)
新增内容:
; 读磁盘
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
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条件跳转指令:“jump if below”如果小于就跳转
EQU开头的新指令(CYLS EQU 10 ):“equal”相当于C语言中的#define命令,常用来声明常数。“CYLS EQU 10”的意思就是 CYLS=10。
到了这一步我们已经把软盘最初的10×2×18×512=184320byte=180KB内容完整得装到内存中,运行“make install”运行需要一定的时间,画面没有什么变化,但是这个程序已经用从软盘读出的数据填满了内存“0x08200-0x34fff”的地方。
在这里卡了很久:
弹出一个窗口提醒(因为我没有装到磁盘):说明我的电脑跟这个操作系统不兼容,但是会生成相关的文件.img,ipl.bin,ipllst。
着手开发操作系统
将下面这段程序保存为haribote.nas,用nask编译,输出成hanbote.sys,再保存到磁盘映像haribote.img里,程序里面就让它HLT。
Fin:
HLT
JMP fin
因为这一步用到软盘磁盘和兼容的Windows而我电脑不匹配也没有软盘,所以我没有具体操作,还是用之前的BZ工具来查看作者已经映像好的haribote.img文件。先复制harib00e文件到tolset文件夹中,打开命令输入make img指令做个映像文件,生成haribote.img,用二进制编辑器查看:
打开haribote.sys文件,恰好是三个字节
作者总结为:一般向一个空软盘保存文件时,①文件名会写在0x002600以后的地方;②文件的内容会写在0x004200的地方。
下面我们将操作系统本身的内容写到名为haribote.sys文件中,再保存到磁盘映像里,从启动区执行这个haribote.sys就行了。
- 从启动区执行操作系统
从启动区开始,把磁盘上的内容装载到内存0x8000号地址,所以磁盘0x4200处的内容位于内存0x8000+0x4200=0xc200号地址。往haribote.nas里加上ORG 0xc200,然后在ipl.nas处理的最后加上JMP 0xc200这个指令。最后make run一下
确认操作系统的执行情况
本次的haribote.nas:
; haribote-os
; TAB=4
ORG 0xc200 ; 这个程序将要被装载到内存的什么地方
MOV AL,0x13 ; VGA显卡、320x200x8位彩色
MOV AH,0x00
INT 0x10
fin:
HLT
JMP fin
为什么ORG设为0xc200: 执行磁盘映像上位于0x004200号地址的程序,从启动区开始把磁盘内容装载到0x8000号地址所以0x4200处内容应该位于内存0x8000+0x4200=0xc200号地址。
设定AH=0x00,调用显卡BIOS的函数,可以切换显示模式:
- AH=0x00;
- AL=模式:(省略了一些不重要的画面模式)
- 0x13: 16色字符模式,80x25
- 0x12: VGA图形模式,640x480x4位彩色模式,独特的4面存储模式
- 0x13: VGA图形模式,320x200x8位彩色模式,调色板模式
- 0x6a: 扩展VGA图形模式,800x600x4位彩色模式,独特的4面存储模式(有的显 卡不支持这个模式)
- 返回值:无
这次选择的是0x13的画面模式,8位彩色模式可以用256种颜色,如果画面模式切换正常,画面应该是一片漆黑。图形模式光标会消失:
其他地方的修改:
- ipl.nas->ipl10.nas(说明这个程序只能读入10个柱面)
- 在JMP 0xc200之前加入一行命令,把cyls的值写入内存地址0x0ff0中,把磁盘装载内容的结束地址告诉给haribote.sys
- 现在这一步为止,我们把启动区里与haribote.sys没有关系的前后部分也读进来了。
- 32位模式前期准备
32位CPU模式优势:使用32位寄存器较16位CPU模式方便;可使用内存容量远远大于1MB;CPU的自我保护功能在32位下能用。
用32位模式就不能调用BIOS功能,因为BIOS是用16位机器语言写的,如果要用,就全部都放在开头先做。修改之后的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显卡、320x200x8位彩色
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]里保存的是0xa0000,VRam指的是显卡内存,用来显示画面的内存,里面的各个地址对应着画面上的像素,参考(AT)BIOS支持网页在INT 0x10的说明最后写“VRam是0xa0000~0xaffff的64KB”,INT 0x16是记录LED灯的状态,我们把画面的像素数、颜色数、以及从BIOS取得的键盘信息都保存起来,位置在内存的0x0ff0附近。
开始导入C语言
修改内容:haribote.sys:前半部分是汇编语言,后半部分是C语言。Haribote.nas->asmhead.nas,为了调用C语言的程序,添加了100行左右的汇编代码。
C语言部分(bootpack.c):
void HariMain(void)
{
fin:
/*C语言中不能用HLT */
goto fin;
}
Goto指令相当于JMP指令。
Bootpack.c变成机器语言的过程:
首先,使用ccl.exe从bootpack.c生成bootpack.gas.
第二步,使用Gas2nask.exe从bootpack.gas生成 bootpack.nas.
第三步,使用Nask.exe从bootpack.nas生成bootpack.obj.
第四步,使用Obi2bim.exe从bootpack.obj生成bootpack.bim.
最后,使用Bim2hrb.exe从bootpack.bim生成bootpack.hrb.
这样就做成了机器语言,再使用copy指令将asmhead.bin与bootpack.hrb单纯结合到起来,就成了haribote.sys。
具体Makefile:
实现HLT(harib00j)
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
新指令:RET:相当于C中的return
用汇编写的函数,之后还要与bootpack.obj链接,所以也要编译成目标文件。在nask目标文件的模式下,必须设定文件名信息,然后再写明下面程序的函数名,注意要在函数名的前面加上“_”,否则就不能很好地与C语言函数链接,需要链接的函数名都要用GLOBAL指令声明,如上代码所示,先写一个与用GLOBAL声明的函数名相同的标号(label)再从此往下写代码。
Bootpack.c:
/* 告诉C编译器,有一个函数在别的文件里 */
void io_hlt(void);
/* 是函数声明却不用{ },而用;,这表示的意思是:函数是在别的文件中,你自己找一下 */
void HariMain(void)
{
fin:
io_hlt(); /* 执行naskfunc.nas里的_io_hlt */
goto fin;
}