导入系统

回顾一下我们昨天写好的代码(建议复制后更新一下,确保我们的出发点一致):

; TAB=4
CLYS    EQU     10
		ORG		0x7c00			; 指明程序装载地址
; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code
		JMP		entry
		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		AX, 0x0820
		MOV		ES, AX
		MOV		CH, 0  ; 0号柱面
		MOV		DH, 0  ; 0号磁头(正面)
		MOV		CL, 2  ; 2号扇区
readloop:
		MOV		SI, 0  ; 记录失败次数的寄存器(本来应该用CX,但是CL和CH已经被占用了)
retry:
		MOV		AH, 0x02
		MOV		AL, 1
		MOV		BX, 0
		MOV 	DL, 0x00
		INT  	0x13
		JNC  	next
		ADD  	SI, 1  ; jnc没有执行,则说明有错误发生了!
		CMP  	SI, 5
		JAE  	error  ;  如果si中的错误次数大于等于5,则跳转到error代码段
		MOV  	AH, 0x00  ; 重置
		MOV  	DL, 0x00  ; 重置
		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
		CMP		CL, 18
		JBE		readloop
        MOV     CL, 1
        ADD     DH, 1
        CMP     DH, 2
        JB      readloop
	    MOV     DH, 0
        ADD     CH, 1
        CMP     CH, 10
        JBE     readloop
        JMP     ok
fin:
		HLT						; 让CPU停止,等待指令
		JMP		fin				; 无限循环
ok:
	MOV SI, ok_msg	
	JMP putloop
error:
	MOV SI, err_msg
	JMP putloop
putloop:
		MOV AL, [SI]
		ADD SI, 1
		CMP AL, 0
		JE fin
		MOV AH, 0x0e
		MOV BX, 15
		INT 0x10
		JMP putloop
ok_msg:
		DB "Reading 10 cylinders finished! Congratulations!", 0xd, 0xa, 0
err_msg:
		DB "An error happened! Please check your code.", 0xd, 0xa, 0

这一程序会读出软盘上10个柱面、每个柱面18个扇区的信息。在正式开始今天的内容前,我们先来解决一个疑问。

为什么一开始用于指定扇区编号的CL寄存器被设置成了2?

首先我们先回忆,每个柱面一共有18个扇区,都从1开始编号。每个扇区大小均为512个字节,其二进制表示为0x200,恰好是每次ES的增量的16倍(0x020乘以十进制的16等于0x200)。那么为什么一开始不从第一个扇区开始读呢?因为第一个扇区是留给启动区的,从第0号柱面的第2个开始才是真正的数据。至于为什么ES每次只增加0x20而不是0x200,这是因为我们访问内存时满足的格式是[ES:BX],即真正访问的地址是ES × 16 + BX。注意到基址寄存器BX作为16位寄存器,访问的空间极为有限,而当时我们又不能升级BX为EBX(32位寄存器),因此只能人为添加一个寄存器作为辅助,从而大大提升了可访问的内存空间。

完成了引导区的编写,我们需要写一个被引导的操作系统。

; 操作系统本体
    MOV SI, msg
putloop:
    MOV AL, [SI]
    ADD SI, 1
    CMP AL, 0
    JE fin
    MOV AH, 0x0e
    MOV BX, 15
    INT 0x10
    JMP putloop
fin:
    HLT
    JMP fin
msg:
    DB "Operating system is loaded.", 0

将该文件保存为os.nas,随后在命令行中执行以下指令:

..\tolset\z_tools\nask.exe ipl.nas ipl.bin
..\tolset\z_tools\nask.exe os.nas os.sys
..\tolset\z_tools\edimg.exe imgin:..\tolset\z_tools\fdimg0at.tek wbinimg src:ipl.bin len:512 from:0 to:0 copy from:os.sys to:@: imgout:os.img

注意,我们此时直接编译得到的不再是.img文件,而是ipl.binos.sys文件,只有在得到二者后再合并后,才能得到最终的镜像文件os.img。下面用Visual Studio Code打开os.img,选择十六进制编辑器

我们可以看到一些系统的信息,正是我们之前在FAT32软盘引导代码中填写的部分。下拉到0x2600位置,可以发现这里赫然储存着我们的文件名。

继续下拉到0x4200位置,可以发现这里储存着我们的文件内容。

我们现在的思路是执行ipl.nas,通过ipl.nas将软盘中的数据读取到内存中,之后跳转到内存的相应位置开始执行操作系统文件。
但先别急着执行os.img,因为我们的ipl.nas目前还没有跳转到内存指定位置!我们按以下方式修改ipl.nas中的fin代码段:

fin:
		HLT						; 让CPU停止,等待指令
        MOV     [0x0ff0], CH    ; 保存读取柱面的数量
		JNC		0xc200			; 若无异常则跳转到系统起始位置
		JMP		fin				; 无限循环

等等,为什么是0xc200而不是0x4200?!这是因为我们本来是从0x8000开始储存读取到的软盘数据的,如果系统文件原先在软盘的0x4200,那么此时在内存中对应为0x8000+0x4200=0xc200。重新编译后,得到新的os.img。注意,我们此时的编译方法相较于以前更复杂了。

..\tolset\z_tools\nask.exe ipl.nas ipl.bin
..\tolset\z_tools\nask.exe os.nas os.sys
..\tolset\z_tools\edimg.exe imgin:..\tolset\z_tools\fdimg0at.tek wbinimg src:ipl.bin len:512 from:0 to:0 copy from:os.sys to:@: imgout:os.img

运行新的镜像文件,我们会得到奇怪的输出。这是怎么回事?!查阅原书我们发现在操作系统文件os.nas的开头缺少了一条重要的语句:ORG 0xc200。如果我们没有指定程序起始地点的话,就会默认从0开始装载程序,此时代码和信息对应的内存就会以0为起点翻译成机器语言。具体举例来说,此时msg可能被编译器翻译成了在0x100的位置,但是实际上它被读取到了0c300的位置,因此操作系统无法读取正确的程序。

修改后的os.nas文件如下:

; 操作系统本体
    ORG 0xc200
    MOV SI, msg
putloop:
    MOV AL, [SI]
    ADD SI, 1
    CMP AL, 0
    JE fin
    MOV AH, 0x0e
    MOV BX, 15
    INT 0x10
    JMP putloop
fin:
    HLT
    JMP fin
msg:
    DB "Operating system is loaded.", 0xd, 0xa 0

编译、合并、运行!大功告成!

接下来我们要准备从16位模式进入32位模式,此时可使用的内存大大增加,但是汇编语言翻译的结果也发生了变化,而且也不能再使用BIOS。为此,我们对os.nas稍作修改并更名为asmhead.nas

; 操作系统本体
CYLS    EQU     10      ; 读取范围
LEDS    EQU     0x0ff1  ; 键盘灯信息
VMODE   EQU     0x0ff2  ; 关于颜色数目的信息、颜色的位数
SCRNX   EQU     0x0ff4  ; 分辨率的X
SCRNY   EQU     0x0ff6  ; 分辨率的Y
VRAM    EQU     0x0ff8  ; 图像缓冲期的开始地址

    ORG 0xc200
    MOV AL, 0x13    ; VGA 显卡, 320×200×8位颜色
    MOV AH, 0x00
    INT 0x10
    MOV BYTE [VMODE], 8 ; 记录画面模式
    MOV WORD [SCRNX], 320
    MOV WORD [SCRNY], 200
    MOV DWORD [VRAM], 0x000a0000

    MOV SI, msg
putloop:
    MOV AL, [SI]
    ADD SI, 1
    CMP AL, 0
    JE fin
    MOV AH, 0x0e
    MOV BX, 15
    INT 0x10
    JMP putloop
fin:
    HLT
    JMP fin
msg:
    DB "VRAM loaded successfully.", 0xd, 0xa, 0

其中VRAM是视频内存(video RAM)的简称,在INT 0x10模式下,VRAM被分配在0xa00000xaffff之间64KB大小的空间。我们人为规定此时电脑屏幕支持的分辨率为320×200×8
按下面命令编译合并。

..\tolset\z_tools\nask.exe ipl.nas ipl.bin
..\tolset\z_tools\nask.exe asmhead.nas asmhead.sys
..\tolset\z_tools\edimg.exe imgin:..\tolset\z_tools\fdimg0at.tek wbinimg src:ipl.bin len:512 from:0 to:0 copy from:asmhead.sys to:@: imgout:os.img

运行后发现输出的字符样式发生了改变!我们刷新了显存,并重新输出了信息!

引入C语言

新建文件bootpack.c,之所以取名为此是因为这只是C语言编写的引导性文件,还没涉及到系统本身。和先前一样,我们还是可以用引导文件完成一些看似是“操作系统”的任务。

void HariMain(void)
{
fin:
    goto fin;
}

注意⚠️:一定要在最后留一个空行,否则编译不会通过!这是GCC编译器的硬性要求。这段代码和我们先前编写的汇编操作系统完全一样,只是在不停地循环!但最大的区别在于,我们无法直接使用HLT!此外,还需要注意,我们定义主函数就是HariMain,这个名字不能修改!

32位模式,启动!

下面的内容和原书有些出入,因为原书作者将增加的汇编代码留到了第八天才讲,相当于买了个关子,而多出来的100多行代码是我们进入32位模式的关键,接下来我们跳转到原书第八天的最后一节,彻底攻克32位模式汇编代码。

点击查看完整代码
; 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显卡,320x200x8位
		MOV		AH,0x00
		INT		0x10
		MOV		BYTE [VMODE],8	; 显示模式
		MOV		WORD [SCRNX],320
		MOV		WORD [SCRNY],200
		MOV		DWORD [VRAM],0x000a0000
; 通过BIOS获取指示灯状态
		MOV		AH,0x02
		INT		0x16 			; keyboard BIOS
		MOV		[LEDS],AL
; 防止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
        IN       AL, 0x60       ; 空读,清空缓冲区内原有数据
		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:
我们一段一段解析这篇关键代码。
BOTPAK	EQU		0x00280000		; 加载bootpack
DSKCAC	EQU		0x00100000		; 磁盘缓存的位置
DSKCAC0	EQU		0x00008000		; 磁盘缓存的位置(实模式)

理解这三个数需要用到原书作者设计的系统分布图(当然了,你也可以设计自己的)。

; 防止PIC接受所有中断
		MOV		AL,0xff
		OUT		0x21,AL
		NOP     ; 部分机型不支持连续执行OUT指令
		OUT		0xa1,AL
		CLI						; 进一步中断CPU

这一步是阻断所有中断,先是来自主PIC(可编程中断控制器)的中断,其次是来自副PIC的中断,最后终止CPU级别的中断。为什么要阻断这些中断?因为我们正在进行模式转换,不能让CPU在模式转换过程中停下来去做任何事。因此,这部分是死代码,不容更改。

; 让CPU支持1M以上内存、设置A20GATE
		CALL	waitkbdout
		MOV		AL,0xd1
		OUT		0x64,AL
		CALL	waitkbdout
		MOV		AL,0xdf			; enable A20
		OUT		0x60,AL
		CALL	waitkbdout
;---分隔线---
waitkbdout:
		IN		 AL,0x64
		AND		 AL,0x02
        IN       AL, 0x60       ; 空读,清空缓冲区内原有数据
		JNZ		 waitkbdout		; AND结果不为0跳转到waitkbdout
		RET

这段代码的目的是初始化键盘并启动A20GATE信号线。为什么要启动这条信号线呢?你可以把它当作《魁拔》里的脉门或者《火影》里的八门遁甲,只有启动了这条线,CPU才可以访问1MB以上的内存,分别是32位模式的代价和优点。

但这段代码还有用到了一个waitkbdout函数。它的功能是等待键盘输入完成,IN AL, 0x64的意思是将0x64号设备(即键盘)的数据读入到AL寄存器当中,恰好是一个字节!

; 保护模式转换
[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

这段代码开始我们正式切换到32位保护模式。首先,程序读取了一个全局描述符表(global descriptor table, GDT)。这个表是32位保护模式的关键。进入32位模式,相当于我们利用八门遁甲彻底打开了查克拉的限制,我们可以操纵极为庞大的内存空间。但是为了兼容先前16位模式,我们在32位模式中沿用了“段”的思想。我们称先前的模式为实模式(real mode),眼下的模式为保护模式(protected mode)。实模式操纵的是实际地址,可以理解为小农村里用人名代替住所地址。而保护模式为了防止内存交叉,需要根据全局描述符表(GDT)进行寻址,比较像小区里单元楼-楼层-房间号的寻址方法。眼下,我们只是读取了一个随意准备的GDT,日后还会详细设计。

接下来我们把CR0,即0号控制寄存器(control register 0)的数据读取到EAX,再将最高位设置为0,最低位设置为1,最后把处理后的值代入回CR0中。如此一来,就完成了模式转换,进入到了不用分页的保护模式。该模式下,普通的应用程序不能修改既定的GDT(大胆住户竟敢肆意修改单元号、楼层和房间号?),操作系统专用的段就得到了CPU充分的保护。

通过代入CR0而切换保护模式时,一定要立即执行JMP指令。这是因为在模式改变后,机器语言的解释就会发生变化。形象的理解方式是16位模式一次处理长度为16个字节的指令,而32位一次处理32个字节,这当然会发生歧义!

; 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

这三段程序的大意就是在搬运数据,其大小是以双字为单位的,所以必须用字节数除以4来指定。

中间一段的大意是将0x7c00处的数据转移到DSKCAC的位置(注意DSKCAC是保护模式下的地址!),数据总量为512字节,即启动扇区的大小。也就意味着将启动扇区复制到了1MB以后的区域。而下面一段的大意是将软盘剩余的数据跟着储存到启动扇区后的区域。

第一段的大意则是将我们未来要用的C语言引导代码装载到BOTPAK地址,大小为512KB,这比我们写的代码体积要大很多,富裕出很多空间。

; 启动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

这段程序会将C语言编写的引导程序从0x10c8字节开始的0x11a8字节复制到0x00310000号地址。这一操作的意义会在最后为我们自制的操作系统编写专用应用程序的时候揭晓!

memcpy:
		MOV		EAX,[ESI]
		ADD		ESI,4
		MOV		[EDI],EAX
		ADD		EDI,4
		SUB		ECX,1
		JNZ		memcpy			; 运算结果不为0跳转到memcpy
		RET

最终我们来到内存复制程序,它先将[ESI]内的数据复制到EAX,再将ESI进四个字节(一个双字),也就是移动指针,之后将EAX复制到[EDI],再将EDI也进四个字节(一个双字),最后将ECX减1,证明完成了一次复制。如果ECX仍不为0,则继续复制。最后RET语句可以返回(return)调用该代码块的地方。

由于接下来文件数量增多,我们先确认你的文件目录大概是如下形状:

也就是你的操作系统文件所在文件夹要和z_tools并列。最后,在操作系统文件夹添加以下两个文件:make.batMakefile,它们两个会帮助我们高效地组织多个文件。

..\z_tools\make.exe %1 %2 %3 %4 %5 %6 %7 %8 %9
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

# 镜像文件生成

ipl.bin : ipl.nas Makefile
	$(NASK) ipl.nas ipl.bin ipl.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

bootpack.bim : bootpack.obj Makefile
	$(OBJ2BIM) @$(RULEFILE) out:bootpack.bim stack:3136k map:bootpack.map \
		bootpack.obj
# 3MB+64KB=3136KB

bootpack.hrb : bootpack.bim Makefile
	$(BIM2HRB) bootpack.bim bootpack.hrb 0

os.sys : asmhead.bin bootpack.hrb Makefile
	copy /B asmhead.bin+bootpack.hrb os.sys

os.img : ipl.bin os.sys Makefile
	$(EDIMG)   imgin:../z_tools/fdimg0at.tek \
		wbinimg src:ipl.bin len:512 from:0 to:0 \
		copy from:os.sys to:@: \
		imgout:os.img

# 其他指令

img :
	$(MAKE) os.img

run :
	$(MAKE) img
	$(COPY) os.img ..\z_tools\qemu\fdimage0.bin
	$(MAKE) -C ../z_tools/qemu

install :
	$(MAKE) img
	$(IMGTOL) w a: os.img

clean :
	-$(DEL) *.bin
	-$(DEL) *.lst
	-$(DEL) *.gas
	-$(DEL) *.obj
	-$(DEL) bootpack.nas
	-$(DEL) bootpack.map
	-$(DEL) bootpack.bim
	-$(DEL) bootpack.hrb
	-$(DEL) *.sys

src_only :
	$(MAKE) clean
	-$(DEL) os.img

此时你的操作系统文件夹里应该是这样的结构:

在系统文件夹打开命令行,输入make img,在VMware中重新指定镜像文件,运行后应当出现一片黑屏,这意味着我们成功启动了32位保护模式,并且导入C语言!

然而,让原书作者难受的是,我们无法使用汇编语言中的HLT来休眠CPU。为此,我们新建一个汇编代码文件naskfunc.nas来存放C语言中可能会用到的汇编指令或函数。

[FORMAT "WCOFF"]    ; 制作目标文件的格式
[BITS 32]           ; 制作32位模式对应的机械语言

[FILE "naskfunc.nas"]        ; 源文件名
    GLOBAL _io_hlt         ; 程序中包含的函数名

[SECTION .text]    ; 以下是函数本体
_io_hlt:
    HLT
    RET

相应地,我们要更新我们的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

# 镜像文件生成

ipl.bin : ipl.nas Makefile
	$(NASK) ipl.nas ipl.bin ipl.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.nas的编译
naskfunc.obj : naskfunc.nas Makefile
	$(NASK) naskfunc.nas naskfunc.obj naskfunc.lst
# 对应地,也要把naskfunc.obj包含进去
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

os.sys : asmhead.bin bootpack.hrb Makefile
	copy /B asmhead.bin+bootpack.hrb os.sys

os.img : ipl.bin os.sys Makefile
	$(EDIMG)   imgin:../z_tools/fdimg0at.tek \
		wbinimg src:ipl.bin len:512 from:0 to:0 \
		copy from:os.sys to:@: \
		imgout:os.img

# 其他指令

img :
	$(MAKE) os.img

run :
	$(MAKE) img
	$(COPY) os.img ..\z_tools\qemu\fdimage0.bin
	$(MAKE) -C ../z_tools/qemu

install :
	$(MAKE) img
	$(IMGTOL) w a: os.img

clean :
	-$(DEL) *.bin
	-$(DEL) *.lst
	-$(DEL) *.gas
	-$(DEL) *.obj
	-$(DEL) bootpack.nas
	-$(DEL) bootpack.map
	-$(DEL) bootpack.bim
	-$(DEL) bootpack.hrb
	-$(DEL) *.sys

src_only :
	$(MAKE) clean
	-$(DEL) os.img

此时,我们就可以在C语言文件中使用汇编语言写的函数了。

void io_hlt(void);

void HariMain(void)
{
fin:
    io_hlt();
    goto fin;
}

命令行运行make img,随后启动虚拟机。再次运行成功!今天已经学了很多了,我们明天会开始进入C语言和画面显示的部分。

posted on 2025-02-06 17:28  溴锑锑跃迁  阅读(112)  评论(0)    收藏  举报