在CPU上电后,会自动将cs:ip置为f000:fff0,下图就是一个计算机刚上电的模拟:
ffff00这里开始的代码是BIOS自检,检查计算机的硬件完备性,做完这一切后将第一个扇区的内容复制到0x7c00的位置,并从0x7c00位置执行代码:
0x7c00开始就放着我们的主引导扇区的代码。因为我的第一句代码是mov ax,0x0003,是占用3个字节,因此实际代码将会从0x7c02开始。
因此我们能接触的第一个操作系统代码就是编写这个主引导扇区。主引导扇区的功能主要是要将内核加载器加载到内存中。因此我们此次实验就模拟一个内核加载器,内核加载器的功能我们将慢慢完善。
;boot.asm
[org 0x7c00] ;告诉编译器代码从0x7c00位置开始
xchg bx,bx
mov ax, 3 ;清空屏幕
int 0x10
; 初始化段寄存器
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
mov si, booting
call print
mov edi, 0x1000; 读取的目标内存
mov ecx, 2; 起始扇区
mov bl, 4; 扇区数量
call read_disk
cmp word [0x1000], 0x55aa
jnz error
jmp 0:0x1002 ; 没问题则跳转到内核加载器
; 阻塞
jmp $
read_disk:
; 设置读写扇区的数量
mov dx, 0x1f2
mov al, bl
out dx, al
inc dx; 0x1f3
mov al, cl; 起始扇区的前八位
out dx, al
inc dx; 0x1f4
shr ecx, 8
mov al, cl; 起始扇区的中八位
out dx, al
inc dx; 0x1f5
shr ecx, 8
mov al, cl; 起始扇区的高八位
out dx, al
inc dx; 0x1f6
shr ecx, 8
and cl, 0b1111; 将高四位置为 0
mov al, 0b1110_0000;
or al, cl
out dx, al; 主盘 - LBA 模式
inc dx; 0x1f7
mov al, 0x20; 读硬盘
out dx, al
xor ecx, ecx; 将 ecx 清空
mov cl, bl; 得到读写扇区的数量
.read:
push cx; 保存 cx
call .waits; 等待数据准备完毕
call .reads; 读取一个扇区
pop cx; 恢复 cx
loop .read
ret
.waits:
mov dx, 0x1f7
.check:
in al, dx
jmp $+2; nop 直接跳转到下一行
jmp $+2
jmp $+2
and al, 0b1000_1000
cmp al, 0b0000_1000
jnz .check
ret
.reads:
mov dx, 0x1f0
mov cx, 256; 一个扇区 256 字
.readw:
in ax, dx
jmp $+2; ;类似与nop空指令 只不过时钟周期会更长
jmp $+2
jmp $+2
mov [edi], ax
add edi, 2
loop .readw
ret
print:
mov ah, 0x0e
.next:
mov al, [si]
cmp al, 0
jz .done
int 0x10
inc si
jmp .next
.done:
ret
booting:
db "Loading XJC_OS", 10, 13, 0; \n\r
error:
mov si, .msg
call print
hlt; 让 CPU 停止
jmp $
.msg db "Booting Error!!!", 10, 13, 0
; 填充 0
times 510 - ($ - $$) db 0
; 主引导扇区的最后两个字节必须是 0x55 0xaa
; dw 0xaa55
db 0x55, 0xaa
上述代码是一个完整的主引导扇区,它主要实现如下功能:
1,清空屏幕
2,在屏幕上输出一句加载内核
3,调用硬盘读写功能,将硬盘中的第二个扇区开始的内核加载器写入内存0x1000的位置
4,校检内核加载器的完备性,如果出现错误则跳转到输出错误语句
5,如果成功将控制权交给内核加载器。
这样主引导扇区的使命就完成了。接下来我们模拟实现一个小型的内核加载器
;loader.asm
[org 0x1000] ;程序将从0x1000开始执行
dw 0x55aa
mov si,loading
call print1
jmp $
print1:
mov ah, 0x0e
.next:
mov al, [si]
cmp al, 0
jz .done
int 0x10
inc si
jmp .next
.done:
ret
loading:
db "os loading success!!!", 10, 13, 0; \n\r
这个内核加载器并没有实现很多功能,只是模拟主引导扇区是否成功加载内核加载器。如果成功加载将会在屏幕上输出一句“os loading success!!!”。我们编译这个asm看看二进制文件的样子:
这是编译完的内核加载器的二进制文本,我们去验证一下是否加载到0x1000的内存位置:
屏幕输出了内核加载器的内容,并且确实加载进来了,因此实验成功。