第一天直接写了一个“操作系统”的二进制文件,详细在:https://www.toutiao.com/a7003704649312141836
由于写二进制文件很不方便,也太繁琐,所以我们使用了编译器,编译器可以将汇编语言编译成二进制的机器码,以后就直接写汇编,然后用编译器把我们写的汇编生成二进制文件就行了。这就大大提高了人的编程效率。
所以,第二天我们用汇编语言来重写了“操作系统”。
第二天我们用汇编语言写的”操作系统“,详细在:https://www.toutiao.com/a7005537960355512839
但是,用汇编语言来写操作系统,效率还是太慢。为了提升写操作系统的效率,我们在第03天,改汇编为C语言来写操作系统。本节程序代码地址为:https://gitee.com/-/ide/project/mingminglaoshi/MakeComputerOperateSystemin30Days/edit/master/-/projects/03_day/harib00j/bootpack.c
由于C语言出现在计算机已经发展积累了一段时间一后,所以,要使用C来写操作系统,需要与汇编语言配合做一些铺垫工作。具体做哪些铺垫工作呢?外围硬件的初始化,以及CPU芯片内部寄存器的初始化。
要了解得更详细,需要了解一下前置知识:
操作系统前,计算机做的事情
主要看上面的第3条:
BIOS 里面有一段写死的代码,会帮我们把启动区的第一扇区的 512 字节的内容,原封不动复制到内存里 0x7c00 这个位置,并跳转到此处,这个是不用我们管的。
更详细可以参考:https://blog.csdn.net/sinat_34560749/article/details/104060723
也就是说,BIOS里面已经固化了一段代码,它会把磁盘上的512字节的内容复制到内存的0x7c00的这个位置。注意到,它只复制512字节。
那么准备用C来写操作系统的话,512字节不够用了,因为他只有512Bytes,太小了。所以,我们会用这块512b的内存做一些准备性的工作,操作系统的真正的功能,大部分的功能都不会在这512字节里。
所以,这512KB的空间我们就称为“启动区”了。电脑去执行操作系统的代码前,先运行这512KB区域内的程序,在这512KB内的程序中,我们做一些寄存器的设置等工作,在程序的最后,跳转到真正操作系统的代码处去执行。
启动区与操作系统综上,也就是说,首先我们要完成启动区的程序,其次才是去完成操作系统的程序。
那么启动区应该写一些什么内容呢?启动区程序能不能用C语言?
启动区程序的功能:
从软盘/磁盘/U盘/硬盘等复制操作系统的代码到内存位置0x8200处,然后就跳转到0x8200处,开始执行
启动区的程序就是复制功能、一些设置功能,所以似乎用汇编和C来写都可以的,但是最终我们还是选择了汇编来写。因为C语言设置寄存器状态还是没有汇编方便。
操作系统代码的功能:
首先:要设定一些硬件的状态,显示屏的状态等,键盘的状态,cpu周围的寄存器的状态等
其次:实现去组织调用鼠标,键盘,显示屏等硬件供用户随时使用
因为要设定硬件的状态,显示屏状态等,需要调用BIOS,而BIOS是工作在CPU16位模式的,C语言是工作在CPU32位模式的,这就决定了操作系统也不能全是C写的,操作系统有一些基础的功能还是需要用工作在16位模式的汇编来写的。
也就是说,启动区之后的操作系统,要使用汇编与C两种语言来写。
那么工作在操作系统之前的启动区程序,负责从存储介质上讲操作系统代码复制到内存上,用汇编语言写,代码量也不大,如果要设置硬件也容易,所以,启动区的程序用汇编来写。
电脑开机后,会在BIOS里将磁盘上的512字节的程序复制到内存的0x7c00处,然后跳到0x7c00处执行。
这512字节程序的作用,就是将真正操作系统的代码复制到内存的0x8200处,然后跳转到0x8200开始执行。
为何叫它启动区?因为它把真正的操作系统代码“复制”到内存里了。
那么 BIOS的代码也是复制了512B的内容到了内存里,为什么BIOS不叫启动区?
我想如果这512字节能放下一个操作系统的话,BIOS里的代码叫启动区合适。
目前来看,操作系统的代码远超过512字节。
所以,”启动区“这个名字就归这512字节了。
我们要把启动区的这512字节的程序放在启动磁盘/U盘/光盘的前512字节处,等待BIOS程序去调用。我们这里就以软盘为例,代码如下ipl10.nas:
1 ; haribote-ipl 2 ; TAB=4 3 4 CYLS EQU 10 ; 设置软磁盘的结束柱面号,操作系统放在软磁盘的0-10柱面 5 6 ORG 0x7c00 ; 将本代码放在内存的0x7c00处 7 8 ; 以下是标准FAT12格式软盘的描述 9 10 JMP entry 11 DB 0x90 12 DB "HARIBOTE" ; 设置扇区的名称 13 DW 512 ; 设置一个扇区的大小 一个扇区有多少betys,对于软磁盘来说,这个是必须是512 14 DB 1 ; 集群大小 15 DW 1 ; FAT从第1扇区开始 16 DB 2 ; FAT的个数 17 DW 224 ; 根目录最多有224个条目 18 DW 2880 ; 驱动器有2880个扇区 19 DB 0xf0 ; 媒体类型 20 DW 9 ; FAT每个区域含9个扇区 21 DW 18 ; 一个柱面一共多少个扇区 22 DW 2 ; 磁头数目 23 DD 0 ; 磁盘分区数目0 24 DD 2880 ; 驱动器含有2880个扇区 25 DB 0,0,0x29 ; 26 DD 0xffffffff ; 当前FAT格式的序列号 27 DB "HARIBOTEOS " ; 磁盘名称 28 DB "FAT12 " ; 文件格式名称 29 RESB 18 ; 对当前的18个字节不做操作 30 31 32 entry: 33 MOV AX,0 ;用AX对寄存器SS初始化 34 MOV SS,AX 35 MOV SP,0x7c00 36 MOV DS,AX 37 38 ; 读磁盘 39 40 MOV AX,0x0820 41 MOV ES,AX 42 MOV CH,0 ;柱面0 43 MOV DH,0 ;磁头0 44 MOV CL,2 ;扇区2 45 readloop: 46 MOV SI,0 ;记录失败次数 47 retry: 48 MOV AH,0x02 ; AH=0x02 :读磁盘 49 MOV AL,1 ; 1个扇区 50 MOV BX,0 51 MOV DL,0x00 ; A驱动器 52 INT 0x13 ; 调用磁盘BIOS 53 JNC next ; 读取成功,跳转到next,去读区一下个扇区 54 ADD SI,1 ; 读区失败,记录失败次数:SI+1 55 CMP SI,5 ; 如果失败次数SI>5,则显示读取失败,跳转到error 56 JAE error ; SI >= 5 57 MOV AH,0x00 ; 重置驱动器 58 MOV DL,0x00 ; A 驱动器 59 INT 0x13 ; 调用AH=0x00时对应的中断程序:重置驱动器 60 JMP retry 61 next: 62 MOV AX,ES ;获取当前内存地址 63 ADD AX,0x0020 ;在内存地址上+0x0020 64 MOV ES,AX ; ADD ES,0x020 65 ADD CL,1 ; CL+1,扇区+1 66 CMP CL,18 ; 比较CL 与18,查看有没有到达18扇区 67 JBE readloop ; CL <= 18 还没读取到18扇区,则继续去读区数据 68 MOV CL,1 ; 如果已经到了18扇区,扇区设置为1 69 ADD DH,1 ; 磁头+1 70 CMP DH,2 ; 对比磁头与2 71 JB readloop ; DH < 2,磁头小与2,继续复制 72 MOV DH,0 ; 磁头>2,将磁头设置位0 73 电脑 ADD CH,1 ; 柱面+1 74 CMP CH,CYLS ; 对比柱面与 CYLS=10 75 JB readloop ; CH < CYLS ,柱面<10,继续复制 76 77 ;以上代码完成了将操作系统代码复制到内存的0x8200处的作用 78 79 MOV [0x0ff0],CH ; 将CH的值存储到内存的[0x0ff0]位置处,CH是柱面号,柱面0-柱面10是启动区的程序 80 JMP 0xc200 ; 启动区的程序运行结束,跳转到内存的0xc200处执行 81 82 error: 83 MOV SI,msg ; 如果运行失败,就显示msg,msg的内容是"load error" 84 putloop: 85 MOV AL,[SI] ; 将msg中的字符放入AL 86 ADD SI,1 ; SI+1,等待区 msg中的下一个字符 87 CMP AL,0 ; 查看是否取到了msg最后一个字符 88 JE fin ; 如果取到了最后一个字符,跳转到fin 89 MOV AH,0x0e ; 定义中断程序号 90 MOV BX,15 ; 设置显示字符的颜色 91 INT 0x10 ; BIOS中断程序调用,显示AL中的字符 92 JMP putloop 电脑 93 fin: 94 HLT ; 让CPU在一个时钟周期内什么也不做 95 JMP fin ; 96 msg: 97 DB 0x0a, 0x0a ; 两个换行符 98 DB "load error" ; 等待被显示的字符串 99 DB 0x0a ; 换行符100 DB 0 ; msg的最后一个字符0,显示程序遇到这个字符,就跳转到fin101 102 RESB 0x7dfe-$ ; 以0x00来填充当前程序所处的内存,一直到内存地址的0x7dfe处103 104 DB 0x55, 0xaa ; 最后再写0x55,0xaa到程序的最后。用这两个字节结束表示当前程序是启动区程序。
以上程序将被BIOS程序自动复制到0x7c00处,然后运行。
程序复制了软磁盘的内容到0x8200处,若成功跳转到0xc200处,若不成功,跳转到error处。
不成功的话,显示错误提示。
成功的话,应该跳转到操作系统代码处,即内存的0x8200处呀?这里为甚么跳转到了0xc200处?
这是因为在取执行真正的操作系统代码之前,需要做一些硬件设置,cpu工作模式的改变等,而这些代码就存放在0xc200处,
我们看下0xc200处的代码asmhead.asm:
; 电脑 haribote-os boot asm; TAB=4BOTPAKEQU0x00280000; 将操作系统的代码存放到0x00280000处,等待执行DSKCACEQU0x00100000; 将软盘中的启动区代码,操作系统代码等保存到0x00100000处DSKCAC0EQU0x00008000; 0x7c00+512,BIOS将软盘的启动区代码复制到了0x7c00处,我们要把从0x8000处的内容,软盘中除了启动区的代码,从内存0x8000开始的代码复制到0x001002ff处CYLSEQU0x0ff0; 复制的柱面数存放的地址0x0ff0LEDSEQU0x0ff1 VMODEEQU0x0ff2; 显示屏显示模式放在0x0ff2处SCRNXEQU0x0ff4; 宽度放在内存0x0ff4处SCRNYEQU0x0ff6; 高度VRAMEQU0x0ff8; 像素数据缓冲区ORG0xc200; 将此代码放到内存的0xc200处,等待启动区程序跳转到此处; 显示屏显示模式的设置MOVAL,0x13; 设定显卡工作在VGA模式,分辨率为320x200x8bit位彩色MOVAH,0x00INT0x10 ;调用BIOS中的中断程序,对显卡进行设定MOVBYTE [VMODE],8; 将画面模式放入内存地址VMODE处MOVWORD [SCRNX],320; 将宽度放到内存地址SCRNX处MOVWORD [SCRNY],200; 将高度放到内存地址SCRNX处MOVDWORD [VRAM],0x000a0000; ;将0x000a0000放到内存地址 VRAM处,表明内存的0x000a0000处,是放像素质的地方,0x000a0000通过查BIOS手册可以得到; 键盘灯的设定MOVAH,0x02INT0x16 ; BIOS中关于键盘灯的中断程序 MOV[LEDS],AL; 根据AT兼容机的规格,如果要初始化PIC,必须在关闭中断前执电脑行,否则会进入终端程序无法返回,造成“挂起”现象MOVAL,0xffOUT0x21,ALNOP; 连续OUT命令中间加上空指令,提高OUT指令成功率OUT0xa1,AL ; 执行OUT指令,初始化PICCLI; 关闭中断; 为了能让CPU访问1M以上的内存,设定第20根地址线可用,即A20可用CALLwaitkbdoutMOVAL,0xd1OUT0x64,ALCALLwaitkbdoutMOVAL,0xdf; enable A20OUT0x60,ALCALLwaitkbdout; 切换到保护模式,芯片对保护操作系统有一定保护功能的模式[INSTRSET "i486p"]; 使用486的汇编指令LGDT[GDTR0]; 设定临时GDT的地址,GDT,全局内存段记录表,这里用了一个表将内存管理起来,使用这个表可以对操作系统使用的内存特殊保护MOVEAX,CR0 ; 获取CR0寄存器的置,ANDEAX,0x7fffffff; 将CR0的 31位设置为0,禁止颁OREAX,0x00000001; 将CRO寄存器的第0位设置为1,切换到保护模式MOVCR0,EAXJMPpipelineflushpipelineflush:MOVAX,1*8; 可读写的段地址 32bitMOVDS,AXMOVES,AXMOVFS,AXMOVGS,AXMOVSS,AX; bootpack程序复制MOVESI,bootpack; 源地址MOVEDI,BOTPAK; 目的地址MOVECX,512*1024/4 ; 需要复制的双字数CALLmemcpy ; 调用复制程序; 启动区程序,放在软盘上的程序也要复制一下,复制到DSKCAC处; 首先复制一个扇区MOVESI,0x7c00; 源地址MOVEDI,DSKCAC; 目的地址MOVECX,512/4 ; 传送的双字数CALLmemcpy; 剩下的扇区MOVESI,DSKCAC0+512; 源地址MOVEDI,DSKCAC+512; 电脑 目的地址MOVECX,0MOVCL,BYTE [CYLS] ; 到0x0ff0位置获取读区成功的柱面数IMULECX,512*18*2/4; 用柱面数计算双字数SUBECX,512/4; 减去IPL程序占用的双字数CALLmemcpy; 启动bootpack.c程序; 查看C代码编译好的二进制文件,发现程序的地址在内存的:2*8:0x0000001b处,所以最终跳转到了2*8:0x0000001b处; 至于跳转前的复制不知道啥意思,作者也没提MOVEBX,BOTPAK ; BOTPAK 0x00280000MOVECX,[EBX+16] ; 需要复制的双字数:0x11a8ADDECX,3; ECX += 3;SHRECX,2; ECX /= 4;JZskip; 不需要复制MOVESI,[EBX+20]; 复制的数据源0x10c8,内存这个地址是什么呢?需要复制到0x00310000处?ADDESI,EBXMOVEDI,[EBX+12]; 复制的目的地 0x00310000处CALLmemcpyskip:MOVESP,[EBX+12]; 将0x00310000设为栈地址JMPDWORD 2*8:0x0000001bwaitkbdout:IN AL,0x64AND AL,0x02JNZwaitkbdout; 如果AL与0x02 做AND操作后,不为零,就重新读区端口0x64的数据RETmemcpy:MOVEAX,[ESI] ;将ESI地址处的内容MOV到EDI地址处的内容ADDESI,4MOV[EDI],EAXADDEDI,4SUBECX,1 ;移动一次,ECX -1JNZmemcpy; 如果ECX 不等于 0 ,则复制下一个字节RETALIGNB16 ; 一直添加DB0,一直到地址能被16整除GDT0:RESB8; 保留8个字节,32位DW0xffff,0x0000,0x9200,0x00cf; 用32位的数据,设置可以读写的内存段DW0xffff,0x0000,0x9a28,0x0047; 用32为的数据,设置可以存放操作系统代码的内存段(bootpack.c文件就保存在这里)DW0GDTR0:DW8*3-1DDGDT0 ;将内存段的记录表放到GDTR0处,等待被放到LGDT寄存器里ALIGNB16bootpack:
可以看到,这里设定了显示屏显卡,设定了显示屏的像素地址,设定了键盘灯,切换到了486的32位模式,保护模式,设定了2个断记录表来管理内存,不再用段寄存器来访问内存了,
设定完成后,本程序将磁盘文件复制到了0x00100000处,为甚么复制到这里,没有特别的用处,也可以空着。
本程序将操作系统代码复制到了第二个内存段0x00280000处。
然后就跳转到了2*8:0x0000001b,第二个段的0x0000001b处,这里就是我们写的C程序编译而成的二进制文件。
我们写的c程序就是bootpack.c,它就在0x0000001b处,它也及时操作系统的代码,如下:
操作系统代码bootpack.c
// 声明函数,这个函数来自汇编,因为我们c程序中要使用,所以这里需要先声明一下void io_hlt(void);void HariMain(void){fin:io_hlt(); /* cpu空一个时钟周期 */goto fin;}
这个代码非常简单,就是cpu闲置,程序一直等待。
以后就可以在这个HariMain函数里写控制显示屏显示的程序了,用C写,而不是用汇编写。
用C写效率就很高了。
这个C代码里有个函数需要说一下,就是这个io_hlt函数,
这个函数定义在汇编代码asmfunc.nas里,如下:
; naskfunc; TAB=4[FORMAT "WCOFF"]; 指定目标文件格式[BITS 32]; 编译成32模式对应的机器码; 目标文件其他信息指定[FILE "naskfunc.nas"]; 源文件名GLOBAL_io_hlt; 声明_io_hlt为GLOBAL; 具体函数的实现[SECTION .text]; 将一下代码放在程序区_io_hlt:; 定义 void io_hlt(void);HLTRET
代码naskfunc.nas中,定义了一个函数io_hlt,供C程序去调用。
运行:
将以上ipl10.nas, asmhead.nas,bootpack.c,naskfunc.nas四个文件编译,生成操作系统映像haribote.img,编译规则如下:
ipl10.bin : ipl10.nas Makefile$(NASK) ipl10.nas ipl10.bin ipl10.lstasmhead.bin : asmhead.nas Makefile$(NASK) asmhead.nas asmhead.bin asmhead.lstbootpack.gas : bootpack.c Makefile$(CC1) -o bootpack.gas bootpack.cbootpack.nas : bootpack.gas Makefile$(GAS2NASK) bootpack.gas bootpack.nasbootpack.obj : bootpack.nas Makefile$(NASK) bootpack.nas bootpack.obj bootpack.lstnaskfunc.obj : naskfunc.nas Makefile$(NASK) naskfunc.nas naskfunc.obj naskfunc.lstbootpack.bim : bootpack.obj naskfunc.obj Makefile$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \bootpack.obj naskfunc.obj# 3MB+64KB=3136KBbootpack.hrb : bootpack.bim Makefile$(BIM2HRB) bootpack.bim bootpack.hrb 0haribote.sys : asmhead.bin bootpack.hrb Makefilecopy /B asmhead.bin+bootpack.hrb haribote.sysharibote.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
先将ipl10.nas编译成ipl10.bin待用
将asmhead.nas编译成asmhead.bin待用
将bootpack.c编译成bootpack.obj待用
将naskfunc.nas编译成naskfunc.obj待用
将naskfunc.obj与bootpack.obj链接在一起,生成bootpack.bim 然后编译生成bootpack.hrb
bootpack.hrb与asmhead.bin文件连接成haribote.sys
将ipl10.bin与haribote.sys连接在一起生成操作系统映像文件haribote.img
然后用模拟器加载映像文件haribote.img即可看到运行结果:显示屏显示一片黑。
这次主要完成了一些铺垫工作,成功用C实现了显示屏显示黑色,然后等待的状态。下一节开始,就开始控制显示屏显示我们期待的内容。
为了用C来写操作系统,我们不仅仅需要利用启动区的512字节的程序,把磁盘文件复制到内存中,还需要在把cpu的工作模式切换到32位保护模式,需要管理更大的超过1M的内存,需要用内存段记录表的形式来管理内存。
只有当以上工作完成之后,我们才能够让cpu跳转到我们写的c程序里。
另外c程序里,需要调用汇编代码,这里提供了c调用汇编代码的方式;写一个naskfunc文件,内部写上_io_hlt的具体实现。
这里还有很多细节,比如内存段记录表是什么?内存段记录表寄存器LGDR的作用是什么?这些等下次来介绍。
电脑