第一步、编写汇编程序(day-02.nas)
; hello-os ; TAB=4 ORG 0x7c00 ; 指明程序的装载地址 ; 以下这段是标准 FAT12 格式软盘专用的代码 JMP entry DB 0x90 DB "HELLOIPL" ; 启动区名称可以是任意字符串 DW 512 ; 每个扇区的大小,必须是 512 字节 DB 1 ; 簇(Cluster)的大小,必须为 1 个扇区 DW 1 ; FAT 的起始位置(一般从第一个扇区开始) DB 2 ; FAT 的个数(必须为 2) DW 224 ; 根目录的大小(一般设成 244 项) DW 2880 ; 该磁盘的大小(必须是 2880 扇区) DB 0xf0 ; 磁盘的种类(必须是 0xf0) DW 9 ; FAT的长度(必须是 9 扇区) DW 18 ; 1 个磁道(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 ; 2 个换行 DB "hello, world" DB 0x0a ; 1 个换行 DB 0 RESB 0x01fe - ($ - $$) ; 使用 0x00 填充,直到 0x01fe 结束 DB 0x55, 0xaa
第二步、编译并运行程序
nasm day-02.nas qemu-system-i386 day-02
介绍文本编辑器
书中推荐 TeraPad 编辑器,我们使用 Emacs 编辑器。
继续开发
改写汇编程序
程序供给四部分,第一部分与第四部分需要磁盘方面的知识。这里我们先探讨第二部分与第三部分:
; hello-os ; TAB=4 ORG 0x7c00 ; 指明程序的装载地址 ; 以下是标准 FAT12 格式软盘专用的代码 JMP entry DB 0x90 --- (中略) --- ; 程序主体 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
汇编指令含义说明
ORG
origin,告诉汇编器在开始执行时机器语言指令装在到内存的哪个地址。如果没有它,有几个指令就无法正确翻译与执行。这里是内存 0x7c00 地址:内存的 0 号地址,是 BIOS 用于实现功能的地方,使用该地址会影响到 BIOS 程序,程序也会出错;内存 0xf0000 号地址也存放着 BIOS 程序,也不能使用。内存地址的使用是有规范的,0x0007c00 – 0x00007dff 是启动区内容的装载地址。
JMP
jump,跳转
entry:、putloop:、fin:、msg:
标签声明,是 JMP 指令跳转地址
MOV
move,赋值,原值不会被删除,所以 move 是不准确的说法。
MOV SI, msg:将 msg 标签的地址赋值给 SI 寄存器,msg 标签的地址是汇编器基于 ORG 指令计算出来的,这里是 0x7c74 地址。JMP entry:同理跳转到 entry 标签的地址,这里是 0x7c50 地址。
MOV AL, [SI]:此时在 SI 中保存的数值被视为内存地址,将在该内存地址中的数值赋予 AL 寄存器(即 AX 的低 8 位);再比如 MOV BYTE [678], 123 表示将 123 保存到内存地址为 678 的地方;再比如 MOV WORD [678], 123 表示将 123 保存到内存地址为 678、679 的地方,因为 WORD 表示 16 位;再比如使用 DWORD 则邻近(地址增加方向) WORD 的两个字节都会成为操作对象。
能进行内存取值操作的寄存器只有 BX、BP、SI、DI,而 AX、CX、DX、SP 未实现该功能。如果在 DX 中保存了内存地址,则需要先移动 MOV BX, DX 后再通过 MOV AL, BYTE [BX] 获取在内存中的数值。另外在 MOV 中,源数据与目的数据必须位数相同,因此 可以省略 BYTE,使用 MOV AL, [SI] 写法。
ADD
加法指令。指令 ADD SI, 1 等价 SI = SI + 1
CMP
比较指令。CPM AL, 0 即将在 AL 中的数值与 0 比较。
JE
条件跳转,jump if equal,如果相等则跳转。JE fin:如果相等则跳转 fin 标签,同要标签代表地址,该地址是由汇编器根据 ORG 指令计算而来。
INT
中断指令,用于调用 BIOS 函数。INT 0x10:调用显卡 BIOS 函数用于显示文字,需要查看官方文档以获取更多信息。
按照下面查找到的方法,然后在调用 INT 0x10 即可显示字符:
AH=0x0e; AL=character code; BH=0 BL=color code; 返回值:无 ; 这就好像向寄存器中保存参数,然后调用函数,函数会去寄存器中取值。
HLT
halt,让 CPU 停止动作,进入休眠状态,当发生外部变化时(比如键盘按键、鼠标单击等等),CPU 会被唤醒,程序继续执行。如果不使用 HLT 指令,CPU 会进入毫无意义的无限循环。使用 HLT 使 CPU 休眠,防止 CPU 进入毫无意义的无限循环。
在使用 C 语言改写后的程序(节选)
entry: AX = 0 SS = AX SP = 0x7c00 DS = AX ES = AX SI = msg putloop: AL = BYTE [SI] SI = SI + 1 if (AL == 0) { goto fin;} AH = 0x0e BX = 15 INT = 0x10 goto putloop fin: HTL; goto fin;
有了这个程序才能将在 msg 中的数据打印出来,当数据变成 0 后,停止显示,进入 HLT 休眠。
先制作启动区
考虑到以后的开发,我们不再使用汇编器制作整个磁盘镜像,而是只制作启动区(前面 512 字节)。
1)ipl.nas => ipl.bin,并附带输出的 ipl.lst 文件(描述汇编语言翻译为机器语言的过程)
2)利用磁盘镜像管理工具,将 ipl.bin 写入到空镜像中,得到 helloos.img 镜像。
Makefile 入门