Orange S:一个操作系统的实现

1.1 Hello, OS world!

	org	07c00h			; 告诉编译器程序加载到7c00处
	mov	ax, cs
	mov	ds, ax
	mov	es, ax
	call	DispStr			; 调用显示字符串例程
	jmp	$			; 无限循环
DispStr:
	mov	ax, BootMessage
	mov	bp, ax			; ES:BP = 串地址
	mov	cx, 16			; CX = 串长度
	mov	ax, 01301h		; AH = 13,  AL = 01h
	mov	bx, 000ch		; 页号为0(BH = 0) 黑底红字(BL = 0Ch,高亮)
	mov	dl, 0
	int	10h			; 10h 号中断
	ret
BootMessage:		db	"Hello, OS world!"

times 	510-($-$$)	db	0	; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 	0xaa55				; 结束标志

编译:

nasm boot.asm -o boot.bin

org指令文档:https://nasm.us/doc/nasmdoc7.html

The function of the ORG directive is to specify the origin address which NASM will assume the program begins at when it is loaded into memory.

org 07c00h会让编译器假定这段代码会被加载到07c00h处。之后如果有需要以绝对地址跳转等情况,编译器就会计算当这段代码从07c00h开始,那么对应的位置是多少。

反汇编:

ndisasm -b 16 boot.bin > boot.bin.txt

boot.bin.txt

00000000  8CC8              mov ax,cs
00000002  8ED8              mov ds,ax
00000004  8EC0              mov es,ax
00000006  E80200            call 0xb
00000009  EBFE              jmp short 0x9
0000000B  B81E7C            mov ax,0x7c1e
0000000E  89C5              mov bp,ax
00000010  B91000            mov cx,0x10
00000013  B80113            mov ax,0x1301
00000016  BB0C00            mov bx,0xc
00000019  B200              mov dl,0x0
0000001B  CD10              int 0x10
0000001D  C3                ret
0000001E  48                dec ax
0000001F  656C              gs insb
00000021  6C                insb
00000022  6F                outsw
00000023  2C20              sub al,0x20
00000025  4F                dec di
00000026  53                push bx
00000027  20776F            and [bx+0x6f],dh
0000002A  726C              jc 0x98
0000002C  642100            and [fs:bx+si],ax
0000002F  0000              add [bx+si],al
00000031  0000              add [bx+si],al
...省略...
000001F9  0000              add [bx+si],al
000001FB  0000              add [bx+si],al
000001FD  0055AA            add [di-0x56],dl

留意mov ax,0x7c1e,这里显然是根据07c00h偏移0x1e计算得出的。
这条指令对应NASM伪指令的mov ax, BootMessage
在汇编的世界里,绝大多数类似路径的概念都是绝对路径。我们已经到达软件世界的最底层,没有更底层的第三方软件帮我们将相对路径转换为绝对路径。我们只能靠自己,这一点请牢记。
如果没有汇编器的帮助,我们将只能手动计算从mov ax, BootMessagedb "Hello, OS world!"首字母的距离,加上07c00h,得出mov ax,0x7c1e。一旦中间有任何代码变更,你将不得不重新计算所有受影响的地址。想象一下你在1000000行汇编代码的开头增加一个无关紧要的nop指令,你可能因此必须重新计算后面100000个地址。为了解决这种毫无意义的机械劳动,你写了工具帮你计算地址,bang,这就是汇编器的雏形。

重新反汇编,让偏移地址为07c00h:

ndisasm -b 16 -o07c00h boot.bin > boot.bin.txt

boot.bin.txt:

00007C00  8CC8              mov ax,cs
00007C02  8ED8              mov ds,ax
00007C04  8EC0              mov es,ax
00007C06  E80200            call 0x7c0b
00007C09  EBFE              jmp short 0x7c09
00007C0B  B81E7C            mov ax,0x7c1e
00007C0E  89C5              mov bp,ax
00007C10  B91000            mov cx,0x10
00007C13  B80113            mov ax,0x1301
00007C16  BB0C00            mov bx,0xc
00007C19  B200              mov dl,0x0
00007C1B  CD10              int 0x10
00007C1D  C3                ret
00007C1E  48                dec ax
00007C1F  656C              gs insb
00007C21  6C                insb
00007C22  6F                outsw
00007C23  2C20              sub al,0x20
00007C25  4F                dec di
00007C26  53                push bx
00007C27  20776F            and [bx+0x6f],dh
00007C2A  726C              jc 0x7c98
00007C2C  642100            and [fs:bx+si],ax
00007C2F  0000              add [bx+si],al
00007C31  0000              add [bx+si],al
...省略...
00007DF9  0000              add [bx+si],al
00007DFB  0000              add [bx+si],al
00007DFD  0055AA            add [di-0x56],dl

调试:

<bochs:1> b 0x7c00
<bochs:2> c
(0) Breakpoint 1, 0x0000000000007c00 in ?? ()
Next at t=14040244
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): mov ax, cs                ; 8cc8
<bochs:3> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00000000000f9a37, limit=0x30
idtr:base=0x0000000000000000, limit=0x3ff
<bochs:4> n
Next at t=14040245
(0) [0x000000007c02] 0000:7c02 (unk. ctxt): mov ds, ax                ; 8ed8
<bochs:5> n
Next at t=14040246
(0) [0x000000007c04] 0000:7c04 (unk. ctxt): mov es, ax                ; 8ec0
<bochs:6> n
Next at t=14040247
(0) [0x000000007c06] 0000:7c06 (unk. ctxt): call .+2 (0x00007c0b)     ; e80200
<bochs:7> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=1
	Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00000000000f9a37, limit=0x30
idtr:base=0x0000000000000000, limit=0x3ff

这里是为了确保ds和es与cs一致。从规范来说,cs作为代码段寄存器,它的值必然是0x0000,但是ds和es的值是没有严格限制的。某些BIOS会把ds和es设为奇怪的值。

00007C06  E80200            call 0x7c0b
00007C09  EBFE              jmp short 0x7c09

我们计划打印Hello, OS world!之后,就让电脑死机。
00007C06调用00007C0B,当它return时,00007C09会不断跳转到自身,实现死循环。

后面的代码要一起看:

00007C0B  B81E7C            mov ax,0x7c1e
00007C0E  89C5              mov bp,ax
00007C10  B91000            mov cx,0x10
00007C13  B80113            mov ax,0x1301
00007C16  BB0C00            mov bx,0xc
00007C19  B200              mov dl,0x0
00007C1B  CD10              int 0x10
00007C1D  C3                ret

在屏幕上显示字符串的方式是调用BIOS的10h中断,并且要求此刻AH为13h、AL为写入模式、BH为页号、BL为颜色、CX为字符串长度、DH为行、DL为列、ES:BP为字符串的偏移。
调用中断和高级编程语言中调用函数是类似的,细微差别是调用中断时你必须按照中断的要求把内容放到对应的寄存器中。在这个过程中,你可以各种挪腾寄存器,只要在执行int指令时,一切如中断要求即可。
这里我们先把ES:BP搞定:
ES在前面已经被CS初始化为0x0000,不用动。
BP:00007C1E开始存储着字符串"Hello, OS World!",00007C0B将这个字符串的首地址放入ax,再挪入bp。
CS:手动数出来字符串"Hello, OS World!"的长度为0x10,如果你数错了,那打印出来的内容肯定是错误的。
AH和AL:设置AL为01h,表示字符串中只包含字符,属性统一为BL,并且光标会跟随字符向后挪一位。
BL:BIOS中断能显示的颜色非常有限,通过查表可知0xc对应的颜色为亮红色。
DH和DL:设置为0x0,表示从0行、0列开始打印。
一切准备就绪,00007C1B正式触发了int 10h中断,屏幕打印出亮红色的"Hello, OS world!"内容。
00007C1D执行return,并在00007C09死循环。

在这段简单的代码中,主体结构是调用int 10h中断,其他内容都是为此准备的。

00007C1E开始的内容为"Hello, OS World!",建议逐字对照ASCII表看一遍。

最后:

times 	510-($-$$)	db	0	; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw 	0xaa55				; 结束标志

我们让汇编器填充了一大堆0,距离00007C00的510字节处,填充55AA作为启动扇区的魔法值。