30天自制操作系统——第三天
今天是该系列的第三天,继续昨天的开发,今天的任务主要有以下几项: 1、制作真正的IPL ;2、读取软盘中的数据放入磁盘当中; 3、从启动区执行操作系统; 4、 开始导入c语言。
一、制作真正的IPL
IPL: 启动程序加载器。一般来说操作系统不会很小的,也就是说512字节完全无法转载整个操作系统,因此,我们会将加载操作系统本身的程序放入启动区当中,因此可以称之为IPL。
本次我们将进一步添加读取磁盘的代码加入到汇编程序当中。在此之前,我们先了解一些基础的知识:
BIOS 的0x13号中断:
BIOS 0x13号中有如下操作:磁盘的读、写、扇区校验、寻道等等:
AH = 0X02 ;读盘
AH = 0X03 ;写盘
AH = 0X04 ;校验
AH = 0X0C ;寻道
AL = 处理对象的上去数(只能同时处理连续的扇区)
CH = 柱面号 & 0XFF
CL =扇区号(0-5位) | (柱面号 & 0x30)>>2
DH =磁头号
DL =驱动器号
ES:BX = 缓冲地址;(校验与寻道时不使用)
返回值:
CF=0 ;没有错误,AH = 0
CF=1 ;有错无,错误号码存入AH中
除此之外用以下代码可以做到复位软盘:
AH=0X00 DL=0X00 int 0x13
至此,我们开始将软盘的数据写入内存(直接从第二个扇区开始,因为第一个扇区是启动区,之前已经加载过了)
写入到0x8000~0x81ff中。其中本次使用的软盘,柱面有80个(0 ~ 79号)用C表示;磁头有正反两个,用大写H表示;扇区有18个(1 ~ 18)用大写字母S表示。也就是说我们一开始写入的便是C0-H0-S2号。
;上面省略
;程序主主体
entry:
;将启动区的初始段地址(0000)设置给以及将启动扇区的偏移地址(7c00)进行设置到栈和数据段中
mov ax,0
mov ss,ax
mov sp,0x7c00
mov ds,ax
mov es,ax
;磁盘读取部分
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 失败次数+1
cmp si,5 ;si >= 5 时,跳转到error
JAE error
;系统复位,bios里的功能,即复位软盘
mov ah,0x00
mov dl,0x00 ;重置a驱动器
int 0x13
jmp retry
fin:
HLT ;cpu停止运行,等待指令,节约资源
jmp fin ; 无限循环
error:
moc si,msg
;程序启动后我们想要显示的内容
putloop:
mov al,[si]
add si,1 ; 给SI加1,循环打印
cmp al,0
je fin ;若数据内存该处的值为零,则该程序终止
mov ah,0x0e ; 显示一个文字,调用0x0e号子中断程序
mov bh,0 ;第0页
mov bl,11001010B ; 指定字符颜色
int 0x10 ; 设置中断程序。10h号中断代表显示字符串,调用bios的中断
jmp putloop
msg:
DB 0x0a, 0x0a ; 换行两次
DB "load error"
DB 0x0a
DB "OS made by GXU yuan"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 填写0x00直到0x001fe
DB 0x55, 0xaa ;这里标志着有启动程序,因为计算机一般先读最后两字节进行判断是否含启动程序的!!
运行后如下:
如果程序出错才会弹出错误信息!!
二、将剩余区域的全部读完
这里直接给出代码:
hello-os
;TAB=4
cyls equ 10
ORG 0x7c00 ;启动程序的装载地址一般为0x7c00~0x7dff
jmp entry
;标准的FAT12格式软盘的必备专用的代码 Stand FAT12 format floppy code,即书写在开头的文件描述系统
DB 0x90
DB "HELLOIPL" ; 启动扇区名称(8字节)
DW 512 ; 每个扇区(sector)大小(必须512字节)
DB 1 ; 簇(cluster)大小(必须为1个扇区)
DW 1 ; FAT起始位置(一般为第一个扇区)
DB 2 ; FAT个数(必须为2)
DW 224 ; 根目录大小(一般为224项)
DW 2880 ; 该磁盘大小(必须为2880扇区1440*1024/512)
DB 0xf0 ; 磁盘类型(必须为0xf0)
DW 9 ; FAT的长度(必须是9扇区)
DW 18 ; 一个磁道(track)有几个扇区(必须为18)
DW 2 ; 磁头数(必须是2)
DD 0 ; 不使用分区,必须是0
DD 2880 ; 重写一次磁盘大小
DB 0,0,0x29 ; 意义不明(固定)
DD 0xffffffff ; (可能是)卷标号码
DB "HARIBOTEOS " ; 磁盘的名称(必须为11字字节,不足填空格)
DB "FAT12 " ; 磁盘格式名称(必须8字,不足填空格)
RESB 18 ; 先空出18字节
;程序主主体
entry:
;将启动区的初始段地址(0000)设置给以及将启动扇区的偏移地址(7c00)进行设置到栈和数据段中
mov ax,0
mov ss,ax
mov sp,0x7c00
mov ds,ax
mov es,ax
;磁盘读取部分
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 失败次数+1
cmp si,5 ;si >= 5 时,跳转到error
JAE error
;系统复位,bios里的功能,即复位软盘
mov ah,0x00
mov dl,0x00 ;重置a驱动器
int 0x13
jmp retry
next:
mov ax,es ;把内存地址后移0x200,因为一个扇区512字节!!! 且段地址算法是先乘以16的,但是这里不需要逻辑上乘16 因此直接加上0x0020
add ax,0x0020
mov es,ax
add cl,1
cmp cl,18 ;cl< = 18则跳转
jbe readloop
mov cl,1
add dh,1 ;dh 是磁头
cmp dh,2
jb readloop ;如果dh<2则跳转到readloop
mov dh,0
add ch,1 ;ch 是柱面
cmp ch,CYLS
jb readloop ;如果ch<cyls,则跳转到readloop
fin:
HLT ;cpu停止运行,等待指令,节约资源
jmp fin ; 无限循环
error:
moc si,msg
;程序启动后我们想要显示的内容
putloop:
mov al,[si]
add si,1 ; 给SI加1,循环打印
cmp al,0
je fin ;若数据内存该处的值为零,则该程序终止
mov ah,0x0e ; 显示一个文字,调用0x0e号子中断程序
mov bh,0 ;第0页
mov bl,11001010B ; 指定字符颜色
int 0x10 ; 设置中断程序。10h号中断代表显示字符串,调用bios的中断
jmp putloop
msg:
DB 0x0a, 0x0a ; 换行两次
DB "load error"
DB 0x0a
DB "OS made by GXU yuan"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 填写0x00直到0x001fe
DB 0x55, 0xaa ;这里标志着有启动程序,因为计算机一般先读最后两字节进行判断是否含启动程序的!!
这里的CYLS EQU 10 就行相当于给CYLS进行定义,equ即equal的缩写。
经过使用以上代码后,就可以将软盘读取的数据将内存0x08200~0x34fff的地方,至此,当我们打开这个系统时候就会发现速度明显慢了很多。
三、从启动区执行操作系统
需要建立一个新的汇编语言文件:haribote.nas
haribote.nas:
fin:
HLT
jmp fin
然后,我们需要makefile文件,使得我们能够将haribote.nas生成文件harbibote.sys这个文件,并将sys文件存入到haribote.img的磁盘映像当中(其实就是把这个文件存入到软盘中,再把这个软盘进行打包成映像文件):
TOOLPATH = ../../z_tools/
MAKE = $(TOOLPATH)make.exe -r
NASK = $(TOOLPATH)nask.exe
EDIMG = $(TOOLPATH)edimg.exe
IMGTOL = $(TOOLPATH)imgtol.com
COPY = copy
DEL = del
#默认操作
default :
$(MAKE) img
#文件生成规则
yuan-ipl10-os.bin : yuan-ipl10-os.nas Makefile
$(NASK) yuan-ipl10-os.nas yuan-ipl10-os.bin yuan-ipl10-os.list
haribote.sys : haribote.nas Makefile
$(NASK) haribote.nas haribote.sys haribote.lst
haribote.img : yuan-ipl10-os.bin haribote.sys Makefile
$(EDIMG) imgin:../../z_tools/fdimg0at.tek wbinimg src:yuan-ipl10-os.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) yuan-ipl10-os.bin
-$(DEL) yuan-ipl10-os.list
-$(DEL) haribote.sys
-$(DEL) haribote.lst
src_only :
$(MAKE) clean
-$(DEL) haribote.img
输入make img 后会生成对应的镜像文件,然后我们打开这个镜像文件进行查看:
- 发现在0x002600附件会有文件名:
- 发现在0x004200处,存在着具体的文件内容:
至此,我们可以进行如下总结,软盘保存文件时: - 文件名写在0x002600以后的地方;
- 文件内容写在0x004200以后的地方。
知道了上述的规律后,我们就可以将操作系统本身的内容写入到sys这个文件当中,然后利用启动区进行加载,引导计算机读取到sys这个操作系统的文件当中去,就可以运行操作系统了。
因此,我们需要修改一下haribote.nas与之前的ipl10.nas(或者是IPl.nas或者之前我自己改的yuan-hello-os.nas 反正就是读取磁盘的那个汇编文件就对了)
haribote.nas:
ORG 0xc200 ;该操作系统程序最终被装载到的内存地址
;即0x8000+0x4200=0xc200; 0x8000是内存存放软盘的地址,0x4200是软盘存放文件的地方
;设置屏幕模式
mov al,0x13 ;利用bios中断程序,设置vga显卡,320x200x8位的色彩
mov ah,0x00 ;第0号子中断程序
int 0x10
ipl10.nas:(新增内容)
;阅读后,运行yuan_os.sys操作系统
MOV [0x0ff0],CH ; 将cyls的值移动到0x0ff0当中,为了告诉yuan_os.sys磁盘装载内容的结束地址,因为cyls代表了10个柱面
JMP 0xc200
以下我解释以下0x10号bios中断:
设置显卡模式:
AH = 0X00;
AL = 模式:
0x03 : 16字符模式,80x25
0x12 : VGA 图形模式,640x480x4位彩色模式,独特的4面存储模式
0x13 : VGA 图形模式,320x200x8位彩色模式,调色板模式
0x6a : 拓展VGA 图形模式,800x600x4,独特的四面存储模式
此时,我们运行后,就会发现是一片漆黑的画面:
四、开始导入c语言
由于进入32位模式后,由16位编写的BIOS难以调用,因此我们事先先将可能要用到的BIOS存入内存当中:
haribote.nas:
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
SCRNY EQU 0X0ff6 ;分辨率的y
VRAM EQU 0X0ff8 ;图像缓冲区开始地址
ORG 0xc200 ;程序最终被装载到内存的地址
;即0x8000+0x4200=0xc2000x8000是内存存放软盘的地址,0x4200是软盘存放文件的地方
;设置屏幕模式
mov al,0x13 ;利用bios中断程序,设置vga显卡,320x200x8位的色彩
mov ah,0x00 ;第0号子中断程序
int 0x10
mov byte [VMODE],8 ; 屏幕的模式(参考C语言的引用)
mov word [SCRNX],320
mov word [SCRNY],200
mov dword [VRAM],0x000a0000
;用BIOS取得键盘上各种LED指示灯的状态
mov ah,0x02
int 0x16 ;键盘BIOS
mov [LEDS],al
然后,下面写入进入32位操作系统,并能够使用c语言的汇编(具体含义之后解释,现在先导入):(这串代码就放在上一部分的后面即可!!)
; 防止PIC接受所有中断
; AT兼容机的规范、PIC初始化
; 然后之前在CLI不做任何事就挂起
; PIC在同意后初始化
MOV AL,0xff
OUT 0x21,AL
NOP ; 不断执行OUT指令
OUT 0xa1,AL
CLI ; 进一步中断CPU
; 让CPU支持1M以上内存、设置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(禁用分页)
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:
此时,我们就可以导入c语言了。但是在正式写入之前,先了解一下c文件是怎么机器语言的:
- 使用ccl.exe 将.c文件生成.gas;
- 使用gas2nask.exe 将.gas 生成 .nas ;
- 使用nask.exe 将 .nas 生成 .obj
- 使用obi2bim.exe 将.obj 生成 .bim
- 使用 bim2hrb.exe 将.bim 生成 .hrb。
- 此时已经生成了.hrb的二进制文件,最后再利用copy命令将.bin与.hrb结合起来生成.sys文件,存入到.img中。
ccl.exe是原作者自己开发的基于gcc编译器改造而来的,因为原本gcc是生成的gas的汇编文件,因此还需要使用gas2nask.exe生成.nas的汇编文件。
然后再用nask.exe将该nas汇编文件进行翻译一下,将nas先生成对应的目标文件obj,再将该目标文件与其他文件进行链接,然后生成一个二进制的映像文件,最后再转成hrb这个二进制文件即可。
再了解上述原理后,我们先做一个事情:由于c中没有hlt这个cpu停转的命令,因此本着环保主义的精神,我们自己由汇编来编写一个c的库函数:建立naskfunc.nas
//naskfunc.nas:
; naskfunc
; TAB = 4
[FORMAT "WCOFF"] ;制作的目标文件的模式,因为要与c文件对接,因此需要生成一个中间的文件
[BITS 32] ;制作32位的机器语言模式
;制作目标文件的信息
[FILE "naskfunc.nas"] ;源文件名信息
GLOBAL _io_hlt ;程序中包含的函数名,一定要以_开头,为了衔接c语言的函数库
; 以下是实际的函数内容
[SECTION .text] ;目标文件中写了这些之后再写程序
_io_hlt: ;void io_hlt(void);
HLT
RET
然后这里,写下一个主体的c语言文件:bootpack.c
//bootpack.c
void io_hlt(void);
void HariMain(void)
{
fin:
io_hlt();
//自建的hlt函数,源目标程序在naskfunc.nas中,c语言本身没有hlt这个命令
goto fin;
}
最后,改造一下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
最后,运行一下:
总结
以上就是第三天的内容,今天还是做了很多事情,而且调试+理解(建议还是读一下源代码,并且自己敲一下),感觉就算跟着别人学开发操作系统也是真的任务艰巨,在此对各位祖师爷respect!!!