目录
30制作操作系统?这个听起来像天方夜谭,但是真有一本书就叫做《30天自制操作系统》,当然,自制操作系统的目的不是要去对标Windows、Linux、MacOS等流行的现代化操作系统,而是了解操作系统制作与运行的整个流程与细节,了解汇编与C语言。
下图是该书作者最终制作的操作系统的模样,至少我看到有图像查看器、调色板、文本阅读器与console终端,麻雀虽小五脏俱全,这张图就是我坚持的动力(笑)。
本系列文章的目的不是教你怎么一天天去实现上述操作系统,而是笔者在阅读并实践了《30天自制操作系统》一书后的一些笔记和整理,本文后面会提供该书以及所有光盘附带源码,感兴趣的读者可以自己动手实践,如果遇到不了解的内容,也可以结合本笔记查看。
操作系统,英文(Operating System,下文简称OS),是一个特殊的软件,往上运行着应用软件(比如我们常用的QQ,微信),往下和硬件打交道(比如CPU、内存、鼠标等),它就像是硬件和应用软件之间的桥梁与纽带。
仓库中包含该书以及所有源码文件:
https://github.com/scriptwang/30DaysMakeOS
怎么才能让电脑通电后再启动我们自制的OS?首先一定要先明白电脑是怎么启动的:
这512字节的程序就叫做bootloader,在书中叫做IPL(initial program loader),叫啥不重要,总之开机就会执行这512字节的指令,这512字节的指令再去将OS kernel加载到内存,然后启动真正的OS!
如下图所示
需要注意的是BIOS的一些设置信息和系统时钟信息存储在CMOS中,CMOS全称Complementary Metal Oxide Semiconductor(互补金属氧化物半导体),它需要通电,台式机主板上的纽扣电池就是给CMOS供电的,如果没电了,BIOS设置信息就会丢失然后恢复到默认值(假设设置了BIOS密码拔掉纽扣电池密码就没有啦),系统时钟也会停止
为啥有这样的设计?想一下为什么每次电脑开机时钟总是正确的?那么一定有一个地方一直在运行着,然后开机时告诉OS当前的系统时钟,要一直运行就得一直通电。
参考:
https://zh.wikipedia.org/wiki/BIOS
BIOS(英文:Basic Input/Output System),即基本输入输出系统,BIOS是16位汇编语言程序,只能运行在16位实模式,可访问的内存只有1MB,而UEFI是32位或64位高级语言程序(C语言程序),突破实模式限制,可以达到要求的最大寻址。实模式的内容后面会详细讲到
当电脑的电源开启,BIOS就会从主板上的ROM芯片运行,运行加电自检(POST),测试和初始化CPU、RAM、直接存储器访问控制器、芯片组、键盘、软盘、硬盘等设备。当所有的Option ROM被加载后,BIOS就试图从引导设备(如硬盘、软盘、光盘)加载引导程序,由引导程序加载操作系统。BIOS也可从网卡等设备引导。
下图是BIOS自检过程中检查出的硬盘错误,这个界面有没有很熟悉的感觉。
启动引导程序即上文所说的512字节的bootloader,在书中叫做IPL,启动引导程序后文用IPL代替
这里讲的是用汇编来写IPL程序的必要性,编程语言那么的多,为什么要选汇编?要讲明白这个问题首先明白几个概念。
参考:
https://zh.wikipedia.org/wiki/%E6%9C%BA%E5%99%A8%E8%AF%AD%E8%A8%80
机器语言(machine language)是一种指令集的体系。这种指令集称为机器码(machine code),是电脑的CPU可直接解读的资料。
所谓机器语言说白了就是一堆0101…的数字,无论任何语言最终都要翻译成机器码才能被CPU执行,下面来个示例感受一下性感的机器语言
所以要给寄存器AL赋值十进制数97用机器码写成
10110000 01100001
怎么样,性感不?任何程序最后在CPU看来都是0和1的排列组合,来看看上古时期怎么编程,就下图这玩意叫做打孔带:带孔为1,无孔为0,上古时期的程序就是一条条纸带,看得见,摸得着,怎么样,纸带是否更性感?
定义
汇编语言(Assembly Language)是任何一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言。
在汇编语言中,用助记符代替机器指令的操作码,用地址符号或标号代替指令或操作数的地址。
在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
特定的汇编语言和特定的机器语言指令集是一一对应的,不同平台之间不可直接移植。
定义很复杂,其实明白了机器码那么汇编其实很简单,汇编本质上就是机器码的助记符!比如上文提到的赋值操作1011 给它取个名儿就叫做MOV,MOV就是汇编语言,1000 代表寄存器AX,那么就给1000取个名儿叫AX,97的二进制为01100001,这个不好记,我们用十六进制表示成0x61,所以上文中性感的机器码
10110000 01100001
用汇编来表示就是
MOV AL,0x61
有没有感觉清爽了很多?至少看到汇编能猜到大概意思,看到机器码那就一脸懵逼。
但是汇编方便了人们记忆和理解,但是不利于CPU执行啊,所以产生了将汇编助记码翻译成机器码的东西,这个东西就叫做编译器。
所以IPL程序为什么要选汇编?
CPU只认识机器码,无论什么语言最后都变成机器码才能喂给CPU,编译器就是专门干这个事情的,看看主流的编程语言是怎么被CPU执行的
可以看到汇编离CPU最近(汇编本质上就是机器码的助记符而已),意味着更少的依赖以及可以操作更加底层的东西(比如直接操作CPU的寄存器)
假设用Java来编写IPL,那么它要依赖JVM虚拟机,JVM虚拟机装在哪?现在我们要制作OS,而JVM需要OS,这不就循环依赖了吗?那肯定不行!
假设用C/C++,C/C++要编译成机器码,中间也是要经过汇编的,而且很多C/C++做不到的事情还得需要汇编来做,那么为啥不直接用汇编呢?
所以经过上述分析,IPL程序用汇编编写是具有必要性的!
既然讲到汇编,那就不得不提CPU的构成,如下图
CPU构成包括计算单元(ALU)、寄存器(Register)、缓存(Cache)、程序计数器(Program Counter),寄存器和缓存都是存东西的,不过寄存器更快,大概比喻如下:
寄存器就好像你房间里床边的小抽屉,比较小,但伸手就能打开,速度贼快;
缓存(特指CPU的缓存)就像你房间里的衣柜,比较大,但是要起床才能打开,速度十分快;
物理内存(就是RAM,内库条,俗称条子)就像是你房间对面的仓库,容量很大,但是你要起床,开门,再去库房,拿完东西回来再关门,速度也挺快;
硬盘就像天安门广场,很宽很大很广,可以放下很多东西,但是你得起床,出门做公交或者地铁才能到,拿完东西再回家,那速度就比你家库房(内存)拿东西慢多了。
那我们就来讲讲你房间床边的小抽屉,因为汇编里面全是对寄存器的操作,如果不清楚寄存器,会很懵
寄存器就是右边红框内的那一坨,这里展示的是32位的寄存器,寄存器都有自己的名字,所谓寄存器,简单来讲就是临时存下数据的,就像炒菜需要几个碗分别放葱姜蒜,炒的时候一股脑锅里倒就完事,那几个临时放放葱姜蒜的碗就是寄存器。
E为Extended的缩写,表示扩展;H为High的缩写,表示高位;L为Low的缩写,表示低位,例:AH表示AX寄存器的高8位
你的床边就有这么多个小抽屉,汇编就是可以直接操作这些小抽屉的
虽然汇编让我们脱离了阅读机器码的苦海,但是很多时候书写内存地址等的时候依然会用到十六进制来表示(因为写二进制太长了),比如如下表示写入0x55,0xaa两个字节
DB 0x55,0xaa
那为什么是十六进制而不是十五进制?十四进制呢?因为4个bit刚好有十六种排列组合,8个bit就是一个字节,所以两个十六进制的数就可以表示一个字节,下面是二进制-十六进制对照表,一般十六进制数前面有个0x,比如0x45表示十六进制的45而不是十进制的
|二进制
|
十六进制
|
二进制
|
十六进制
|
二进制
|
十六进制
|
二进制
|
十六进制
|
|
0000
|
0
|
0100
|
4
|
1000
|
8
|
1100
|
C
|
|
0001
|
1
|
0101
|
5
|
1001
|
9
|
1101
|
D
|
|
0010
|
2
|
0110
|
6
|
1010
|
A
|
1110
|
E
|
|
0011
|
3
|
0111
|
7
|
1011
|
B
|
1111
|
F
|
八进制也是一样的道理,3个bit刚好八种排列组合,此处就不再赘述
首先来看书中第二天(02_day)写完的ipl.nas汇编文件源码,这个比较简单,书中使用的汇编编译器的nask,汇编源码需要被nask编译成机器码后才能被执行。
; hello-os ; TAB=4 ORG 0x7c00 ; 指明程序装载地址 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 "HELLO-OS " ; 磁盘的名称(必须为11字?,不足填空格) DB "FAT12 " ; 磁盘格式名称(必??8字?,不足填空格) RESB 18 ; 先空出18字节 ; 程序主体 entry: MOV AX,0 ; 初始化寄存器 MOV SS,AX MOV SP,0x7c00 MOV DS,AX MOV ES,AX MOV SI,msg putloop: MOV AL,[SI] ADD SI,1 ; 给SI加1 CMP AL,0 JE fin MOV AH,0x0e ; 显示一个文字 MOV BX,15 ; 指定字符颜色 INT 0x10 ; 调用显卡BIOS JMP putloop fin: HLT ; 让CPU停止,等待指令 JMP fin ; 无限循环 msg: DB 0x0a, 0x0a ; 换行两次 DB "hello, world" DB 0x0a ; 换行 DB 0 RESB 0x7dfe-$ ; 填写0x00直到0x001fe DB 0x55, 0xaa
首先解释一下出现的指令
我们来挨个分析下上述汇编源码(汇编就是从上往下顺序执行的,除非遇到JMP等跳转指令)
ORG 0x7c00 ; 指明程序装载地址
JMP entry
entry: MOV AX,0 ; 初始化寄存器 MOV SS,AX MOV SP,0x7c00 MOV DS,AX MOV ES,AX MOV SI,msg
putloop: MOV AL,[SI] ADD SI,1 ; 给SI加1 CMP AL,0 JE fin MOV AH,0x0e ; 显示一个文字 MOV BX,15 ; 指定字符颜色 INT 0x10 ; 调用显卡BIOS JMP putloop
这是让BIOS显示文字的指令的固定写法,不用纠结为啥MOV AH,0x0e和INT 0x10是写死的,人家BIOS就这样给你调用的,调就完事了
MOV AL,0x0a ;字符代码 MOV AH,0x0e ; 显示一个文字 MOV BX,15 ; 指定字符颜色 INT 0x10 ; 调用显卡BIOS
fin: HLT ; 让CPU停止,等待指令 JMP fin ; 无限循环
msg: DB 0x0a, 0x0a ; 换行两次 DB "hello, world" DB 0x0a ; 换行 DB 0 RESB 0x7dfe-$ ; 填写0x00直到0x001fe DB 0x55, 0xaa
所以上面的汇编干了一件什么事?很简单,先初始化寄存器,然后调用BIOS显示msg里面的内容:hello world,然后HLT住CPU
当然上面的汇编CPU是不认识的,需要编译成机器码,采用的是nask编译器,编译成了机器码还要生成软盘的镜像文件,作者用的是edimg工具,当然这些操作都写在Makefile里面,Makefile就是一份将源码编译成可执行文件的说明书,重点内容如下
ipl.bin : ipl.nas Makefile ../tolset/z_tools/nask.exe ipl.nas ipl.bin ipl.lst helloos.img : ipl.bin Makefile ../tolset/z_tools/edimg.exe imgin:../tolset/z_tools/fdimg0at.tek \ wbinimg src:ipl.bin len:512 from:0 to:0 imgout:helloos.img
ipl.bin将汇编编译成机器码二进制文件,helloos.img用机器码二进制文件生成软盘镜像文件
虽然作者提供了QEMU模拟器来执行,但是我更想看看在虚拟机中能跑起来不,就像在虚拟机中安装Windows那样,在Vmware中添加软盘作为启动
[图片上传失败…(image-f713ae-1621156411629)]
启动结果。打印出hello, world说明我们成功地让电脑一启动就执行我们自己的bootloader!
注意在用nask编译的过程中会输出这么一个文件:ipl.lst
nask.exe ipl.nas ipl.bin ipl.lst
这个文件显示了一些编译的详细信息,上面的汇编编译细节如下,可以看到entry在内存的0x7C50处,所以JMP entry和JMP 0x7C50是等价的,编译成的结果只有2个字节EB 4E,EB指的是JMP指令的机器码,4E是十进制的78,也就是往后跳78个字节,0x7C02 + 0x4E刚好就是0x7C50,为什么不从0x7C00开始算,呢?因为JMP entry编译成的机器码EB 4E占用两个字节,所以从0x7C02开始算
行号 内存地址 编译成了机器码(二进制) 源码 1 00000000 ; hello-os 2 00000000 ; TAB=4 3 00000000 4 ORG 0x7c00 ; 指明程序装载地址 5 00007C00 EB 4E JMP entry 6 00007C02 7 00007C02 ; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code 8 00007C02 9 00007C02 90 DB 0x90 10 00007C03 48 45 4C 4C 4F 49 50 4C DB "HELLOIPL" ; 启动扇区名称(8字节) 11 00007C0B 0200 DW 512 ; 每个扇区(sector)大小(必须512字节) 12 00007C0D 01 DB 1 ; 簇(cluster)大小(必须为1个扇区) 13 00007C0E 0001 DW 1 ; FAT起始位置(一般为第一个扇区) 14 00007C10 02 DB 2 ; FAT个数(必须为2) 15 00007C11 00E0 DW 224 ; 根目录大小(一般为224项) 16 00007C13 0B40 DW 2880 ; 该磁盘大小(必须为2880扇区1440*1024/512) 17 00007C15 F0 DB 0xf0 ; 磁盘类型(必须为0xf0) 18 00007C16 0009 DW 9 ; FAT的长度(必??9扇区) 19 00007C18 0012 DW 18 ; 一个磁道(track)有几个扇区(必须为18) 20 00007C1A 0002 DW 2 ; 磁头数(必??2) 21 00007C1C 00000000 DD 0 ; 不使用分区,必须是0 22 00007C20 00000B40 DD 2880 ; 重写一次磁盘大小 23 00007C24 00 00 29 DB 0,0,0x29 ; 意义不明(固定) 24 00007C27 FFFFFFFF DD 0xffffffff ; (可能是)卷标号码 25 00007C2B 48 45 4C 4C 4F 2D 4F 53 20 20 DB "HELLO-OS " ; 磁盘的名称(必须为11字?,不足填空格) 00007C35 20 26 00007C36 46 41 54 31 32 20 20 20 DB "FAT12 " ; 磁盘格式名称(必??8字?,不足填空格) 27 00007C3E 00 00 00 00 00 00 00 00 00 00 RESB 18 ; 先空出18字节 00007C48 00 00 00 00 00 00 00 00 28 00007C50 29 00007C50 ; 程序主体 30 00007C50 31 00007C50 entry: 32 00007C50 B8 0000 MOV AX,0 ; 初始化寄存器 33 00007C53 8E D0 MOV SS,AX 34 00007C55 BC 7C00 MOV SP,0x7c00 35 00007C58 8E D8 MOV DS,AX 36 00007C5A 8E C0 MOV ES,AX 37 00007C5C BE 7C74 MOV SI,msg 38 00007C5F putloop: 39 00007C5F 8A 04 MOV AL,[SI] 40 00007C61 83 C6 01 ADD SI,1 ; 给SI加1 41 00007C64 3C 00 CMP AL,0 42 00007C66 74 09 JE fin 43 00007C68 B4 0E MOV AH,0x0e ; 显示一个文字 44 00007C6A BB 000F MOV BX,15 ; 指定字符颜色 45 00007C6D CD 10 INT 0x10 ; 调用显卡BIOS 46 00007C6F EB EE JMP putloop 47 00007C71 fin: 48 00007C71 F4 HLT ; 让CPU停止,等待指令 49 00007C72 EB FD JMP fin ; 无限循环 50 00007C74 51 00007C74 msg: 52 00007C74 0A 0A DB 0x0a, 0x0a ; 换行两次 53 00007C76 68 65 6C 6C 6F 2C 20 77 6F 72 DB "hello, world" 00007C80 6C 64 54 00007C82 0A DB 0x0a ; 换行 55 00007C83 00 DB 0 56 00007C84 57 00007C84 00 00 00 00 00 00 00 00 00 00 RESB 0x7dfe-$ ; 填写0x00直到0x001fe ....省略若干个0 00007DF6 00 00 00 00 00 00 00 00 58 00007DFE 59 00007DFE 55 AA DB 0x55, 0xaa
从执行汇编编写的512字节的启动程序到执行C语言,这个跨度还是挺大的,作者一开始并没有去解释这个过程,先略过了这部分,而在后面讲了GDT(Global Descriptor Table)和IDT(Interrupt Descriptor Table)再去解释的。
简而言之,整个启动过程需要经过下面步骤
整个过程涉及到4个源代码文件,分别是(其中最后一个文件是汇编写的工具函数,我们以04_day里面的文件为准)
首先看一下内存分布图,当我们需要使用内存的时候,应当避免使用到已经使用过的地方,当看到内存操作一头懵的时候,建议回来看一下这个内存分布图,下图中空白处为未使用的地方
参考:http://oswiki.osask.jp/?(AT)memorymap
Makefile文件位置:04_day/Makefile
Makefile文件主要描述了怎么讲源代码文件组装成软盘镜像文件,整个过程如下所示,红色方框是源代码文件
可见C语言要编译成可执行的二进制代码要进过的步骤最多,会产生大量的中间文件,注意最后的二进制文件中haribote.sys其实包含了asmhead.nas,bootpack.c、naskfunc.nas的内容,内容顺序是从左往右。
这是启动程序ipl10.nas做的事情,涉及到的汇编代码如下,此时还是在BIOS的16位实模式(后面会详细说)下,可以通过调用BIOS提供的中断函数(INT 0x13)进行软盘读取,其中涉及到软盘的柱面,磁头,扇区等概念直接看书上的解释就好,此处不再赘述
; 读取磁盘 MOV AX,0x0820 MOV ES,AX MOV CH,0 ; 柱面0 MOV DH,0 ; 磁头0 MOV CL,2 ; 扇区2(排除了启动扇区) readloop: MOV SI,0 ; 记录失败次数寄存器 ;...省略
那么这段程序将软盘的内容装载到了什么地方?
MOV AX,0x0820 ... MOV BX,0
着重看这两句,装载软盘内容的物理起始地址为0x0820 * 0x10=0x8200处(此处计算方式可以参考后面16位实模式的讲解),装载了8704字节(排除了启动扇区的512字节),所以结束地址为0x8200+0x2200-1=0xA3FF
这段代码仍然在ipl10.nas中,因为刚才装载了软盘的内容,现在要执行装载的内容:haribote.sys,它在内存中的位置是0xc200,那么是怎么确定这个位置的呢?
; 读取完毕,跳转到haribote.sys执行! MOV [0x0ff0],CH ; IPLがどこまで読んだのかをメモ JMP 0xc200
很简单,一般向一个空软盘保存文件时,文件名会写在0x002600以后的地方,文件内容会写在0x004200以后的地方,我们来验证下,将十六进制编辑器打开最终生成的haribote.img文件
先确定haribote.sys文件第一条指令所在的位置(绝对位置),再加上装载在内存中的起始位置0x8200就行了。绝对位置为:0x004200,所以0x4200 + 0x8200 - 0x200 = 0xC200,之所以要减去0x200是因为绝对位置包括了启动扇区的512字节,所以要减掉
目前为止一直在都是使用的16位实模式在运行,该模式只支持1MB内存并且访问的是物理内存的真实地址,该模式只适用于一些引导系统的基本操作,比如上述的读取软盘到内存等,我们自制制作OS当然要转换成32位保护模式。
在转换之前,先了解下什么是实模式,什么是保护模式
准确的说是16位实模式和32位保护模式,此处的所谓模式,指的是CPU的工作模式,体现在CPU的寻址方式和寄存器的大小
实模式出现于早期x86 CPU时期,一共只有20位地址线(能使用的内存空间只有2^20/1024/1024=1MB,在这1MB内存中,前640K称为基本内存,后面384K内存留给开机必要硬件和各类BIOS本身使用),以及8个16位的通用寄存器和4个16位段寄存器,所以叫做16位实模式。
在x86系列处理器进入32位的时代,由于兼容性的原因,新的处理器(80386)保留16位的运行方式(实模式),此后多次处理器的升级换代都保留这种运行方式。甚至在包含EM64T技术的至强系列处理器中,处理器加电启动时仍然会切换到16位的实模式下运行BIOS。
16位实模式是怎么寻址的呢?因为总线有20位,能表示1M的内存空间,而段寄存器又是16位的,只有64K的寻址能力,怎么用16位的段寄存器来寻址20位的内存空间呢?答案就是分段寻址(所以用于寻址的寄存器也叫段寄存器)。所谓分段就是将1M的内存空间划分为16段,每段64KB(刚好能被16位表示完),一共就是16*64KB=1MB,然后计算物理地址的时候采用如下公式(16的十六进制为0x10)
物理地址 = 段基址 * 16 + 段内偏移地址
物理地址 = 段基址 * 0x10 + 段内偏移地址
如下图所示
例:段基址为0x1000,段内偏移地址为0x00C8,物理地址 = 0x1000 * 0x16 + 0x00C8 = 0x10000 + 0x00C8 = 0x100C8
其本质的思想也是分而治之,有没有找到一点Java中HashMap的感觉?
在这种模式下,最终计算和操作的都是内存的物理地址,是真真正正存在的,所以这也是实模式这个名字的由来,实就表示真实物理地址。其实这种模式下是不安全的,因为程序可以随意访问整个内存空间!
随着CPU的发展,CPU的地址线的个数也从原来的20根变为现在的32根,所以可以访问的内存空间也从1MB变为现在4GB,寄存器的位数也变为32位,所以叫做32位保护模式。
保护模式(英语:Protected Mode,或有时简写为 pmode)是一种80286系列和之后的x86兼容CPU的运行模式。保护模式有一些新的特性,如存储器保护,标签页系统以及硬件支持的虚拟内存,能够增强多任务处理和系统稳定度。现今大部分的x86操作系统都在保护模式下运行,包含Linux、FreeBSD、以及微软Windows 2.0和之后版本。
现在的情况是CPU的总线是32根,能支持到4GB内存,但是为了兼容性,段寄存器仍然是16位的,所以怎么用16位的段寄存器表示高达4GB的内存空间呢?
实模式下,段值(段地址的值)还是地址的一部分。在保护模式下,虽然段值仍然由原来的16位的CS、DS等寄存器表示,但是此时它们仅仅是一个索引,这些个索引指向一个数据结构的表项,表项中详细定义了一个段的起始地址、界限、属性等内容,这个数据结构,叫做GDT(Global Descriptor Table),GDT中的每一个表项,叫做描述符
每个段8字节,段里包含的内容有
16位的段寄存器低3位是不能使用的(存储了其他信息),只有高13位能使用,所以能表示0~8191的数,也就是说能表示8192个段,每个段对于应用程序而言地址都是从0开始,这也解决了多应用程序同时访问同一个物理地址的冲突问题(相当于虚拟地址)
说白了其实就是存了一张内存分段信息表,通过这张信息表做转换达到表示4GB内存的目的,具体寻址过程如下图
[图片上传失败…(image-1c74f5-1621156411627)]
转换过程主要在asmhead.nas文件中,其主要流程是
最终内存分布如下所示,可以对照内存分布图可以看到除了BIOS的1MB之外的内容主要分布在0x00100000~0x00EFFFFF之间(RAM)
0x00000000 - 0x000fffff : 虽然在启动中会多次使用,但之后就变空。(1MB) 0x00100000 - 0x00267fff : 用于保存软盘的内容。(1440KB) 0x00268000 - 0x0026f7ff : 空(30KB) 0x0026f800 - 0x0026ffff : IDT (2KB) 0x00270000 - 0x0027ffff : GDT (64KB) 0x00280000 - 0x002fffff : bootpack.hrb(512KB) 0x00300000 - 0x003fffff : 栈及其他(1MB) 0x00400000 - : 空
首先看一下最简单的C语言代码,HariMain为主函数,因为编译器是作者改过的GCC编译器,他将主函数改成了HariMain,不过这个不影响。这段代码通过编译后会拼接在asmhead.nas的bootpack:的后面,所以执行完模式转换代码后就开始执行C语言的代码!
void io_hlt(void); void write_mem8(int addr, int data); void HariMain(void) { int i; /* 変数宣言。iという変数は、32ビットの整数型 */ for (i = 0xa0000; i <= 0xaffff; i++) { write_mem8(i, 15); /* MOV BYTE [i],15 */ } for (;;) { io_hlt(); } }
0xa0000到0xaffff是表示视频的内存地址,详情可以看内存分布图,上面这段代码会在屏幕上显示白色,然后调用io_hlt无限等待
io_hlt是写在了naskfunc.nas中,这里相当于是C语言中调用了汇编,在C语言完成不了的时候可以使用汇编辅助
_io_hlt: ; void io_hlt(void); HLT RET
write_mem8函数也是用汇编来写的:先读四字节(第一个参数)到ECX寄存器,然后再读四字节(第二个参数)到AL寄存器,然后将AL的寄存器里的值赋值到ECX寄存器所代表的内存地址中,这就是C语言中指针的本质。
_write_mem8: ; void write_mem8(int addr, int data); MOV ECX,[ESP+4] ; [ESP+4]にaddrが入っているのでそれをECXに読み込む MOV AL,[ESP+8] ; [ESP+8]にdataが入っているのでそれをALに読み込む MOV [ECX],AL RET
这是第四天的效果图,完成了32位保护模式的转换以及C语言的调用,并可以在屏幕上画带颜色的矩形。
以04_day为准
; haribote-ipl ; TAB=4 CYLS EQU 10 ; 声明CYLS=10 ORG 0x7c00 ; 指明程序装载地址 ; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code JMP entry DB 0x90 DB "HARIBOTE" ; 启动扇区名称(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: MOV AX,0 ; 初始化寄存器 MOV SS,AX MOV SP,0x7c00 MOV DS,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 ; 没出错则跳转到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 next: MOV AX,ES ; 把内存地址后移0x200(512/16十六进制转换) ADD AX,0x0020 MOV ES,AX ; ADD ES,0x020因为没有ADD ES,只能通过AX进行 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 ; 读取完毕,跳转到haribote.sys执行! MOV [0x0ff0],CH ; IPLがどこまで読んだのかをメモ JMP 0xc200 error: MOV SI,msg putloop: MOV AL,[SI] ADD SI,1 ; 给SI加1 CMP AL,0 JE fin MOV AH,0x0e ; 显示一个文字 MOV BX,15 ; 指定字符颜色 INT 0x10 ; 调用显卡BIOS JMP putloop fin: HLT ; 让CPU停止,等待指令 JMP fin ; 无限循环 msg: DB 0x0a, 0x0a ; 换行两次 DB "load error" DB 0x0a ; 换行 DB 0 RESB 0x7dfe-$ ; 填写0x00直到0x001fe DB 0x55, 0xaa
; 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 SCRNY EQU 0x0ff6 ; 分辨率Y VRAM EQU 0x0ff8 ; 图像缓冲区的起始地址 ORG 0xc200 ; 这个的程序要被装载的内存地址 ; 画面モードを設定 MOV AL,0x13 ; VGA显卡,320x200x8bit MOV AH,0x00 INT 0x10 MOV BYTE [VMODE],8 ; 屏幕的模式(参考C语言的引用) MOV WORD [SCRNX],320 MOV WORD [SCRNY],200 MOV DWORD [VRAM],0x000a0000 ; 通过BIOS获取指示灯状态 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支持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:
; naskfunc ; TAB=4 [FORMAT "WCOFF"] ; 制作目标文件的模式 [INSTRSET "i486p"] ; 使用到486为止的指令 [BITS 32] ; 3制作32位模式用的机器语言 [FILE "naskfunc.nas"] ; 文件名 GLOBAL _io_hlt, _io_cli, _io_sti, _io_stihlt GLOBAL _io_in8, _io_in16, _io_in32 GLOBAL _io_out8, _io_out16, _io_out32 GLOBAL _io_load_eflags, _io_store_eflags [SECTION .text] _io_hlt: ; void io_hlt(void); HLT RET _io_cli: ; void io_cli(void); CLI RET _io_sti: ; void io_sti(void); STI RET _io_stihlt: ; void io_stihlt(void); STI HLT RET _io_in8: ; int io_in8(int port); MOV EDX,[ESP+4] ; port MOV EAX,0 IN AL,DX RET _io_in16: ; int io_in16(int port); MOV EDX,[ESP+4] ; port MOV EAX,0 IN AX,DX RET _io_in32: ; int io_in32(int port); MOV EDX,[ESP+4] ; port IN EAX,DX RET _io_out8: ; void io_out8(int port, int data); MOV EDX,[ESP+4] ; port MOV AL,[ESP+8] ; data OUT DX,AL RET _io_out16: ; void io_out16(int port, int data); MOV EDX,[ESP+4] ; port MOV EAX,[ESP+8] ; data OUT DX,AX RET _io_out32: ; void io_out32(int port, int data); MOV EDX,[ESP+4] ; port MOV EAX,[ESP+8] ; data OUT DX,EAX RET _io_load_eflags: ; int io_load_eflags(void); PUSHFD ; PUSH EFLAGS POP EAX RET _io_store_eflags: ; void io_store_eflags(int eflags); MOV EAX,[ESP+4] PUSH EAX POPFD ; POP EFLAGS RET
void io_hlt(void); void io_cli(void); void io_out8(int port, int data); int io_load_eflags(void); void io_store_eflags(int eflags); void init_palette(void); void set_palette(int start, int end, unsigned char *rgb); void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1); #define COL8_000000 0 #define COL8_FF0000 1 #define COL8_00FF00 2 #define COL8_FFFF00 3 #define COL8_0000FF 4 #define COL8_FF00FF 5 #define COL8_00FFFF 6 #define COL8_FFFFFF 7 #define COL8_C6C6C6 8 #define COL8_840000 9 #define COL8_008400 10 #define COL8_848400 11 #define COL8_000084 12 #define COL8_840084 13 #define COL8_008484 14 #define COL8_848484 15 void HariMain(void) { char *vram;/* 声明变量vram、用于BYTE [...]地址 */ int xsize, ysize; init_palette();/* 设定调色板 */ vram = (char *) 0xa0000;/* 地址变量赋值 */ xsize = 320; ysize = 200; /* 根据 0xa0000 + x + y * 320 计算坐标 8*/ boxfill8(vram, xsize, COL8_008484, 0, 0, xsize - 1, ysize - 29); boxfill8(vram, xsize, COL8_C6C6C6, 0, ysize - 28, xsize - 1, ysize - 28); boxfill8(vram, xsize, COL8_FFFFFF, 0, ysize - 27, xsize - 1, ysize - 27); boxfill8(vram, xsize, COL8_C6C6C6, 0, ysize - 26, xsize - 1, ysize - 1); boxfill8(vram, xsize, COL8_FFFFFF, 3, ysize - 24, 59, ysize - 24); boxfill8(vram, xsize, COL8_FFFFFF, 2, ysize - 24, 2, ysize - 4); boxfill8(vram, xsize, COL8_848484, 3, ysize - 4, 59, ysize - 4); boxfill8(vram, xsize, COL8_848484, 59, ysize - 23, 59, ysize - 5); boxfill8(vram, xsize, COL8_000000, 2, ysize - 3, 59, ysize - 3); boxfill8(vram, xsize, COL8_000000, 60, ysize - 24, 60, ysize - 3); boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 24, xsize - 4, ysize - 24); boxfill8(vram, xsize, COL8_848484, xsize - 47, ysize - 23, xsize - 47, ysize - 4); boxfill8(vram, xsize, COL8_FFFFFF, xsize - 47, ysize - 3, xsize - 4, ysize - 3); boxfill8(vram, xsize, COL8_FFFFFF, xsize - 3, ysize - 24, xsize - 3, ysize - 3); for (;;) { io_hlt(); } } void init_palette(void) { static unsigned char table_rgb[16 * 3] = { 0x00, 0x00, 0x00, /* 0:黑 */ 0xff, 0x00, 0x00, /* 1:梁红 */ 0x00, 0xff, 0x00, /* 2:亮绿 */ 0xff, 0xff, 0x00, /* 3:亮黄 */ 0x00, 0x00, 0xff, /* 4:亮蓝 */ 0xff, 0x00, 0xff, /* 5:亮紫 */ 0x00, 0xff, 0xff, /* 6:浅亮蓝 */ 0xff, 0xff, 0xff, /* 7:白 */ 0xc6, 0xc6, 0xc6, /* 8:亮灰 */ 0x84, 0x00, 0x00, /* 9:暗红 */ 0x00, 0x84, 0x00, /* 10:暗绿 */ 0x84, 0x84, 0x00, /* 11:暗黄 */ 0x00, 0x00, 0x84, /* 12:暗青 */ 0x84, 0x00, 0x84, /* 13:暗紫 */ 0x00, 0x84, 0x84, /* 14:浅暗蓝 */ 0x84, 0x84, 0x84 /* 15:暗灰 */ }; set_palette(0, 15, table_rgb); return; /* C语言中的static char语句只能用于数据,相当于汇编中的DB指令 */ } void set_palette(int start, int end, unsigned char *rgb) { int i, eflags; eflags = io_load_eflags(); /* 记录中断许可标志的值 */ io_cli(); /* 将中断许可标志置为0,禁止中断 */ io_out8(0x03c8, start); for (i = start; i <= end; i++) { io_out8(0x03c9, rgb[0] / 4); io_out8(0x03c9, rgb[1] / 4); io_out8(0x03c9, rgb[2] / 4); rgb += 3; } io_store_eflags(eflags); /* 复原中断许可标志 */ return; } void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1) { int x, y; for (y = y0; y <= y1; y++) { for (x = x0; x <= x1; x++) vram[y * xsize + x] = c; } return; }
TOOLPATH = ../tolset/z_tools/ INCPATH = ../tolset/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:../tolset/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 ..\tolset\z_tools\qemu\fdimage0.bin $(MAKE) -C ../tolset/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