自制操作系统笔记-第三章
3.1 制作真正的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,这个中断的说明参观:https://blog.csdn.net/weixin_37656939/article/details/79684611,书上的网站访问不了。
AH=0x02(读盘)
AH=0x03(写盘)
AH=0x04(校验)
AH=0x0c (寻道)
AL=处理对象的扇区数,(只能同时处理连续的扇区)
CH=柱面号
CL=扇区号
DH=磁头号
DL=驱动器号
ES:BX=缓冲地址(校验及寻道时不使用)
返回值
FLACS.CF=0: 没有错误,AH=0 AL=传输的扇区数(这里不知道是不是书上印错了,应该是FLAGS)
FLAGS.CF=1: 有错误,错误号存入AH内(与重置功能一样)
这里是AH=0X02,所以是读盘
CF(carry flag)是一个只有一位信息的寄存器,这种只有一位的寄存器称为标志(flag),CF本是用来表示有没有进位的,但因为简单易用,所有其它地方也经常用到,这里就是表示函数调用 是否有错。
软盘结构:
80个柱面(0-79),每个柱面18个扇区(sector)(1-18)每个扇区512字节,两个磁头(0 正面和1 背面),所以一张软盘容量是80*18*512*2=1 474 560 字节 = 1440KB
含有IPL的启动区位于柱面0,磁头0,扇区1(C0-H0-S1) ,它的下一个扇区是 C0-H0-S2, 这里要加载的就是这个。
注意:软盘是按扇区读取,但是内存是按字节对地址编号的。
ES:BX=缓冲地址,这个地址就是一个内存地址,表示我们要把从软盘上读出的数据装载到内存的哪个位置,BX是16位寄存器,只能表示0-0xffff的值,也就是0-65535,最大才64K。只用一个寄存器的话就只能用64K的内存,
EBX 是32位,能处理4G内存,0-0xffffffff ( 0 - 4294967295字节)即4194304KB = 4096MB = 4GB
而早期没有EBX寄存器,所以设计了一个起辅助作用的段寄存器,使用段寄存器时,以[ES:BX]方式表示内存地址,即ES*16+BX ,如果ES取0xffff,BX也取0xffff,则为 0xffff * 16 + 0xffff = 65535*16 + 65535 = 1114095Byte = 1087KB,这样就可以访问1MB内存。
这里ES=0X0820, BX=0,所以软盘的第二个扇区的数据被装载到内存中的0x8200到0x83ff(512字节,一个扇区)的地方。0x8000 - 0x81FF这512字节是留给(copy)启动区的,要将启动区的内容读到那里。0x7c00 - 0x7DFF(512字节)用于启动区。
内存分布图:
0x8000 - 0x81FF是留给启动区的,0x7C00 - 0x7DFF也是启动区,这两个的关系是什么?我有点没看懂。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
不管要指定内存的什么地址,都必须同时指定段寄存器,如省略会把“DS”作为默认的段寄存器。
MOV CX, [1234] 其实是MOV CX, [DS: 1234]的意思。
MOV AL, [SI] 就是 MOV AL, [DS: SI] 的意思
所以DS必须预先指定为0,否则地址的值就要加上这个数的16倍,就会读写到其它地方,引起混乱。
执行make run ,如果看到下面的画面就是成功了。如果失败会显示 load error
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3.2 试错
; 读磁盘 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=0, DL=0,NIT 0x13 就是 系统复位,复位软盘状态。
3.3 读到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 ; 没出错的话跳到next ADD SI, 1 ; 往SI加1 (失败计数+1) CMP SI, 5 ; 比较SI与5 JAE error ; SI >= 5时,如果失败5次跳到error
MOV AH, 0x00 MOV DL, 0x00 ; A驱动器 INT 0x13 ; 重置驱动器(系统复位,复位软盘状态,然后再试) JMP retry
next:
MOV AX, ES ;把内存地址后移0x20
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,CL是扇区号,给ES加上0x20,ES指定读入地址。0x20是十六进制下512 除以 16的结果, 也可以写成ADD AX,512/16。
因为一个扇区是512字节,所以读入软盘上下一个扇区时,内存上的(目标)读入位置也要向后移512字节,
推导:(新的ES值*16 + BX ) - (旧的ES值 * 16 + BX) = 512 , 即 (新ES-旧ES) *16 = 512 , 即 新ES = 旧ES + 512 / 16 ,也就是 旧ES值 + 32 (0x20)。
还有,这里也可以直接给BX+512 ,即 ADD BX, 512
磁盘BIOS 读盘函数 中断处理的扇区数 1到255(0xff), 一次同时处理2个以上扇区时,不跨越多个磁道,也不能超过64KB。
这段程序,已经把软盘上C0-H0-S2到 C0-H0-S18的 512 *17 =8704字节的内容 装载到内存 0x8200 ~ 0xA3ff 了。
学习到52页。contiune
3.4 读入10个柱面
C0-H0-S18 的下一个扇区是磁盘反面的C0-H1-S1,读到C0-H1-S18,接着读下一个柱面C1-H0-S1(正面第二个柱面)。一直读到C9-H1-S18(反面第10个柱面最后一个扇区)。
next: MOV AX, ES ;把内存地址后移0x20 ADD AX, 0x0020 MOV ES, AX ;因为没有ADD ES, 0x020指令,所以这里稍微绕个弯 ADD CL, 1 ;CL加1 CMP CL, 18 ;比较CL与18 JBE readloop ;如果CL <= 18 (还没读到第18个扇区),跳转至readloop MOV CL,1 ;因为上面已经读完了18个扇区,接下来要从下一个柱面的第一个扇区开始读,所以这里要给CL=1 ADD DH,1 ; DH+1, DH=0 为正面磁头 DH=1代表就是反面磁头 CMP DH,2 ; DH与2 比较 JB readloop ; 如果 DH < 2 , 也就是0或1,则跳转到readloop MOV DH,0 ; 否则也就是DH=1, 说明一个柱面的正反两面都读完了,则恢复 正面磁头 (DH=0) ADD CH,1 ; CH+1 下一个柱面 CMP CH,CYLS JB readloop ; 如果CH < CYLS ,则跳转到readloop
JB (jump if below),小于则跳转,
CLYS,就是一个常量(意思是cylinders柱面),
程序开头使用EQU指令:
CYLS EQU 10,意思是 CYLS=10 相当于C语言的#define命令。
现在这个程序已经可以用从软盘读取的数据填满内存0x08200 ~ 0x34FFF, 0x34FFF - 0x8200 = 0x2CDFF = 183807, 算上0x8200本身,一共是183808字节(179.5KB),算上系统加载时自动装载的启动区(512字节):183808 +512=184320(字节)=180K。
3.5 着手开发操作系统
将文件保存到磁盘镜像文件里
写一个非常小的程序 haribote.nas:
fin: HLT JMP fin
将镜像文件写入磁盘(如软盘),打开这个软盘,把一个考进去,最后再把磁盘备份为一个镜像文件。这一系列操作可以通过镜像工具完成,如书中用的edimg.exe
projects/03_day/harib00e下的Makefile修改如下:
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 # 文件生成规则 ipl.bin : ipl.nas Makefile $(NASK) ipl.nas ipl.bin ipl.lst haribote.sys : haribote.nas Makefile $(NASK) haribote.nas haribote.sys haribote.lst 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 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) ipl.bin -$(DEL) ipl.lst -$(DEL) haribote.sys -$(DEL) haribote.lst src_only : $(MAKE) clean -$(DEL) haribote.img
然后执行make img, 得到haribote.img, 用二进制编辑器查看haribote.img,在0x2600的地方看到:
0x2600附近
0x4200附近,这里的F4 EB FD 其实就是上面 haribote.nas中的代码的机器码(我理解是这样)
向软盘保存文件地,文件名会写在0x2600以后的地方,
文件的内容会写在 0x4200以后的地方。
我们将操作系统本身的内容写到haribote.sys文件中,再把它保存到磁盘镜像文件里,然后从启动区执行这个haribote.sys就行了。也就是软盘上0x004200号地址的程序。
3.6 从启动区执行操作系统
程序启动区(C0-H0-S1,软盘上正面第0柱面第一扇区)的内容加载到了0x7C00 ~ 0x7DFF(512字节),软盘上第二个扇区开始一直到第10个柱面(反面)的最后一个扇区将内容加载到 0x08200 ~ 0x34FFF(179.5KB)(推导:0x34FFF - 0x8200 = 0x2CDFF = 183807, 算上0x8200本身,一共是183808字节)
所以软盘上0x4200处的内容应该位于内存0x8000 + 0x4200 = 0xc200号地址。(说明:因为软盘上第二个扇区对应内存上的是0x8200,那么软盘上的开始位置就对应的是内存上的0x8000,所以软盘上的0x4200,就相当于内存上的0x8000+0x4200)
在haribote.nas中加上ORG 0xc200(注意是haribote.nas ,不是ipl.nas),然后在ipl.nas处理的最后加上JMP 0xc200这个指令,得到 03_day/harib00f/文件夹中的内容。
haribote.nas:
; haribote-os ; TAB=4 ORG 0xc200 ;ORG 命令 表示这个程序将要被装载到内存中的什么地方
fin:
HLT JMP fin
ipl.nas:
next: MOV AX, ES ;把内存地址后移0x20 ADD AX, 0x0020 ...省略中间代码...
JB readloop ; 如果CH < CYLS ,则跳转到readloop ; 因为读完了执行haribote.sys! JMP 0xc200 error: MOV SI,msg
...省略代码...
3.7 确认操作系统的执行情况
ORG 命令 表示这个程序将要被装载到内存中的什么地方
疑问:之前用二进制编辑器查看,haribote.sys这个文件保存到haribote.img镜像中时位于0x4200,IPL会将磁盘的除启动区外的前10个柱面的数据写入内存的0x8200位置,上面说过,haribote.sys的内容在内存中对应0xC200,所以在ipl.nas中加入了JMP 0xC200,那为什么在haribote.sys中还要加ORG 0xc200这句呢? 不加会怎样?(实测,这句不加可以正常启动)
03_day/harib00g/haribote.nas
; haribote-os ; TAB=4 ORG 0xc200 ; 这个程序将要被装载到内存中的什么地方 MOV AL,0x13 ; VGA显卡,320x200x8位彩色 MOV AH,0x00 INT 0x10 fin: HLT JMP fin
设置显卡模式的BIOS中断信息:
- AH=0x00
- AL=模式:(省略一些不重要的画面模式)
- 0x03:16色字符模式,80*25
- 0x12:VGA图形模式,640*480*4位彩色模式,独特的4面存储模式(16色)
- 0x13:VGA图形模式,320*200*8位彩色模式,调色板模式 (256色)
- 0x6a:扩展VGA图形模式,800*600*4位彩色模式,独特的4面存储模式(有的显卡不支持这个模式)
- 返回值:无
如果画面模式切换正常,画面会变一片黑(见下面图片),图形模式光标会消失。
ipl.nas改名炒ipl10.nas表示读10个柱面,另外想把磁盘装载内容的结束地址告诉给haribote.sys,所以在JMP 0xc200之前加了 将 CYLS的值(也就是这里的CH写到内存0x0FF0,(为什么是0x0FF0?)
; 因为(磁盘上10个柱面的数据)读完了执行haribote.sys! MOV [0x0ff0],CH ; 将 读取柱面的数量(此时是10)写到内存的0x0FF0,这句是什么意思?记录 读了多少个柱面? JMP 0xc200
改之前的启动画面:
改完之后的启动画面:
3.8 32位模式前期准备
CPU有16位和32位两种模式,以16位模式启动,用AX、CX等16位寄存器会方便,但像EAX、ECX等32位寄存器使用起来会麻烦。16位和32位模式中机器语言命令代码不一样,同样的机器语言解释方法也不一样,所以16位和32位模式下机器语言不通用。
CPU的自我保护功能在16位下不能用。在32位下能用。
32位模式不能调用BIOS功能。BIOS是16位机器语言写的。如果有什么事情想用BIOS来做,就全部放在开头。比如画面模式的设定。
从BIOS换得键盘状态,指NumLock是开还是关。
03_day/harib00h/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是启动信息,
因为320的二进制是 1 0100 0000, 所以这里用WORD保存,200同理。
---------------------------------------------------------------------------------------
VRAM指显卡内存,它的各地址都对应着画面上的像素。VRAM分布在内存分布图上好几个不同地方,因为不同画面模式像素数不同,可以使用的内存不一样,所以把VRAM地址保存在BOOT_INFO里以备后用。
通过BIOS中断INT 0x10这个中断信息查询,可以得知这种画面模式下VRAM是0xA0000 ~ 0xAFFFF的64KB。
从内存分布图上看,0x0FF0这一块并没有使用,所以把分辨率,颜色数,键盘状态都存在这个位置附近。
3.9 开始导入C语言
03_day/harib00i/
haribote.nax改名为asmhead.nas,它的前半部分用汇编写的,后半部分用C语言写的。为了调用C语言写的程序,加了100行左右汇编代码。(暂时不讲)
C语言部分 文件名 bootpack.c
void HariMain(void) { fin: /* 这里想写上HLT,但C语言中不能用HLT */ goto fin; }
第一行是定义函数,函数名是HariMain,参数为void,返回值为void。goto相当于汇编中的JMP,实际上也是编译成JMP指令。
-------------------------------------------------------------------------------------------------------------------
.c文件编译成机器语言的步骤:
- 用cc1.exe 从 .c 生成.gas
- 用gas2nask.exe 从 .gas 生成 .nas
- 用nask.exe 从.nas 生成 .obj
- 用obi2bim.exe 从 .obj 生成 .bim
- 用 bim2hrb.exe 从 .bim 生成 .hrb
- 用 copy指令 将 asmhead.bin 与 bootpack.hrb结合起来,得到haribote.sys
cc1是C编译器,将C语言编译成汇编语言源程序,这个是用gcc改造的,输出的是gas汇编语言源程序,
gas2nask,把gas变换成nask语法。
nask 将.nas文件转成目标文件。目标文件是一种特殊的机器语言文件,必须与其他文件链接后才能变成真正可以执行的机器语言。
链接:C语言的局限性,不可能只用C语言编写所有的程序,所以其中有一部分必须用汇编来写,然后链接到C语言写的程序上。
单个的目标文件还不是独立的机器语言,为了做成完整的机器语言文件,必须将必要的目标文件全部链接上,使用obj2bim。(binary image,二进制镜像文件)。
bim 还不是完成品,只是将各部分全部链接在一起,做成一个完整的机器语言文件。为了实际使用还要针对不同操作系统的要求进行必要的加工。如加上识别用的文件头,或压缩等。为了本书要做的系统,作者开发了一个bim2hrb.exe。
-------------------------------------------------------------------------------
我们平时用的C编译器没有这么复杂的,是因为它内部也做了同样的步骤,这里的编译器是以能适应不同操作系统为前提而设计的。是特意像这样多生成一些中间文件的。好处是仅靠这个编译器就可以制作windows,linus,还有本书要开发的操作系统的可执行文件。
Makefile也做了很大修改。
----------------------------------------------------------------------------
程序是从HariMain()函数开始的,所以这个函数名不能改。
还是执行make run ,看到黑屏就说明正常启动了。
3.10 实现HLT
; naskfunc ; TAB=4 [FORMAT "WCOFF"] ; 制作目标文件的模式 [BITS 32] ; 制作32位模式用的机器语言 ; 制作目标文件的信息 [FILE "naskfunc.nas"] ; 源文件名信息 GLOBAL _io_hlt ; 程序中包含的函数名 ; 以下是实际的函数 [SECTION .text] ; 目标文件中写了这些之后再写程序 _io_hlt: ; void io_hlt(void); HLT RET
用汇编语言写了一个函数io_hlt 。
HLT 属于 I/O指令
MOV 属于传送指令
ADD 属于演算指令
用汇编写的函数之后还要与bootpack.obj链接,所以也要编译成目标文件,因此将输出格式设定为WCOFF模式,还要设定32位机器语言模式。
在nask目标文件模式下,必须设定文件名信息,然后再写明下面程序的函数名,要在函数名前加 "_",否则就不能很好地与C语言函数链接。需要链接的函数名都要用GLOBAL指令声明。
下面写一个实际的函数,先写一个与GLOBAL声明的函数名相同的标号(label),从此处开始写代码就可以,RET相当于C语言的return。
在C语言里使用这个函数
bootpack.c
/* 告诉C编译器,有一个函数在别的文件里 */ void io_hlt(void); /* 函数声明不用{}, 而用; 表示函数在别的文件中,你自己找一下吧 */ void HariMain(void) { fin: io_hlt(); /* 执行naskfunc.nas里的_io_hlt */ goto fin; }
Makefile也进行了修改,还是执行make run ,结果依然是黑屏,程序是正常的。