BIOS程序
BIOS是固定在ROM中的程序,计算机通电之后,CPU默认去执行BIOS程序。BIOS程序是固定的,几乎不会改动的,其工作主要就是检测硬件、建立中断程序、加载操作系统。
我们不用太关注BIOS程序的实现原理,只需要知道它就从主引导扇区加载512字节的数据到物理内存0x7c00处,然后将处理器的执行权交给主引导程序。
主引导扇区
现在我们知道了BIOS的工作原理,那么我们自己编写的操作系统,BIOS就是会加载我们的操作系统,然后将处理器的执行权给操作系统,这样操作系统就接管了计算机。
我们编写的第一个程序是存放在主引导扇区中的,因此我们需要知道主引导扇区是什么。主引导扇区就是硬盘中的第一个扇区,每个扇区的大小为512字节,而主引导扇区的标志为0x55、0xaa。我们编写主引导程序,大小必须是512字节,然后将其放到硬盘中,然后再执行这个程序。
内存分段
最开始的计算机是没有操作系的,大佬编写程序,然后排队去执行这个程序,那时候的程序访问地址是绝对的物理地址的,如果这个地址被别人用了,那么就一定要等别人用完了自己才能开始使用。就是当计算机有多个程序在执行,而它们使用了相同的地址,那么这时候着两个程序是不能同时执行的。
后来,为了让计算机可以同时运行多个程序,引入了分段机制,程序员在程序编写的地址只是物理地址,CPU在寻址的时候通过段地址:偏移地址计算,最终得到物理地址。
正是因为有了分段机制,程序在使用内存的时候,可以给段A给程序A使用,段B给程序B使用,就算它们在程序中的地址是相同的,因为段地址不同,它们最终计算出来的物理地址也不一样,所以它们可以同时执行。
我们在编写程序的时候,必须要声明段地址,段地址要使用段寄存器来存放,最常用的段寄存器就是DS、CS、SS等。
INT 0x10中断
在我们编写的主引导程序中,我们会调用BIOS程序提供的INT 0x10中断功能,只要是为了清屏、获取光标和显示字符。其中AH存放的是其功能号,而不同功能需要使用到的寄存器不一样,我们需要参考手册。
- 清屏
-
- AH = 0x06
- AL = 上卷行数(我们使用0)
- BH = 行属性(这里的BX寄存器我们使用0x700)
- (CH、CL)左上角度坐标(0,0)
- (DH、DL)右下脚坐标(80,25)-> 0x184f
- 获取光标
-
- AH = 0x03
- BH = 0(待获取光标页号)
- 打印字符
-
- ES:BP表示的是字符串的首地址(此时ES和CS指向同一个段)
- AH = 0x13(表示的是子功能号)
- AL = 0x01(表示的是光标的方式,我们这里写0x01就可以了)
- BH = 0x00(表示的是当前的页号,我们这里使用的是0)
- BL = 0x02(表示的是字符属性,这里不一定是0x02)
第一版本代码
readme.txt
编写MBR程序的主要逻辑
1:初始化段寄存器
2:清屏操作
3:获取光标
4:显示字符串
5:填充字符
6:运行程序
---------- 初始化段寄存器 ----------
---------- 清理屏幕的操作 ----------
AH = 0x06
AL = 上卷行数(我们使用0)
BH = 行属性(这里的BX寄存器我们使用0x700)
(CH、CL)左上角度坐标(0,0)
(DH、DL)右下脚坐标(80,25)-> 0x184f
---------- 获取光标 ----------
AH = 0x03
BH = 0(待获取光标页号)
---------- 显示字符串 ----------
ES:BP表示的是字符串的首地址(此时ES和CS指向同一个段)
AH = 0x13(表示的是子功能号)
AL = 0x01(表示的是光标的方式,我们这里写0x01就可以了)
BH = 0x00(表示的是当前的页号,我们这里使用的是0)
BL = 0x02(表示的是字符属性,这里不一定是0x02)
---------- 运行程序mbr1.asm ----------
生存硬盘文件
bximage -> master.img 保存下来(ata0-master: type=disk, path="master.img", mode=flat)
bochs -> 4 -> bochsrc -> 7
nasm -f bin -o mbr1.bin mbr1.asm
dd if=mbr1.bin of=master.img bs=512 conv=notrunc count=1
bochs -f bochsrc
mbr1.asm
org 0x7c00
;---------- 初始化段寄存器 ----------
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
;---------- 清理屏幕 ----------
mov ah, 0x06
mov al, 0x00
mov bx, 0x700
mov dx, 0x184f
int 0x10
;---------- 获取光标 ----------
mov ah, 0x03
mov bh, 0x00
int 0x10
;---------- 显示字符串 ----------
mov ax, message
mov bp, ax
mov ah, 0x13
mov al, 0x01
mov bh, 0x00
mov bl, 0x02
mov cx, 13
int 0x10
jmp $
message db "hello mbr !!!"
times 510-($-$$) db 0
db 0x55, 0xaa
run1.sh
#!/bin/bash
nasm -f bin -o mbr1.bin mbr1.asm
dd if=mbr1.bin of=master.img bs=512 conv=notrunc count=1
bochs -f bochsrc
显存显示字符
我们知道实模式下只有1MB的内存空间可以使用,这块空间中不同的区域有不同的功能的,上面我们是通过调用BIOS的中断程序来显示字符,其实还有一种方法可以实现字符的显示。内存中的0xB8000~0xBFFFF是用于访问显存的,就是我们只需要将我们的字符一个一个放到这个区域中,就可以在显存中显示了。
从0xB8000开始,每个字符占用两个字节空间,低字节用于显示字符,高字节用于显示字符的属性。比如,我们要显示"Hello World!!!",我们只需要将一个一个字符逐个放到0xB8000开始的内存空间就可以了。
第二版本代码
mbr2.asm
org 0x7c00
;初始化段寄存器
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
mov ax, 0xb800
mov gs, ax
;清理屏幕
mov ah, 0x06
mov al, 0x00
mov bx, 0x700
mov dx, 0x184f
int 0x10
;显示字符
mov byte [gs:0x00], 'H'
mov byte [gs:0x01], 0x02
mov byte [gs:0x02], 'e'
mov byte [gs:0x03], 0x02
mov byte [gs:0x04], 'l'
mov byte [gs:0x05], 0x02
mov byte [gs:0x06], 'l'
mov byte [gs:0x07], 0x02
mov byte [gs:0x08], 'o'
mov byte [gs:0x09], 0x02
jmp $
times 510-($-$$) db 0
db 0x55, 0xaa
run2.sh
#!/bin/bash
nasm -f bin -o mbr2.bin mbr2.asm
dd if=mbr2.bin of=master.img bs=512 conv=notrunc count=1
bochs -f bochsrc
字符串转移
上面我们在将我们想要显示的字符一个一个传送到显存的区域中,这样做真的太麻烦了。我们可以先在内存的某一个区域中定义一串字符,然后将这串字符传送到显存那里显示。
字符串传送需要用到的寄存器SI存放了源字符的偏移地址,DI存放目的地址的偏移地址,CX表示的是字符串的个数。
如何显示数字和字符串(代办)
第三版本代码
mbr3.asm
org 0x7c00
;初始化段寄存器
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00
mov ax, 0xb800
mov gs, ax
;清理屏幕
mov ah, 0x06
mov al, 0x00
mov bx, 0x700
mov dx, 0x184f
int 0x10
mov ax, message
mov si, ax
mov ax, 0
mov di, ax
mov cx, current - message
show:
mov al, [si]
mov byte [gs:di],al
inc di
mov byte [gs:di], 0x02
inc di
inc si
loop show
jmp $
message db "Hello World"
current:
times 510-($-$$) db 0
db 0x55, 0xaa
run3.sh
#!/bin/bash
nasm -f bin -o mbr3.bin mbr3.asm
dd if=mbr3.bin of=master.img bs=512 conv=notrunc count=1
bochs -f bochsrc
访问外设
前面,我们一直在主引导扇区的512字节编写程序,仅仅是512字节的空间,让我们去完成加载操作系统内存的工作是远远不够的,所以我们先在这里加载Loader程序,然后再在Loader程序加载我们的内核程序。
因为我们需要在MBR程序中从硬盘加载内容,这些内容就是我们编写的Loader程序,在写Loader程序之前,我们必须在MBR程序中编写读取硬盘的逻辑。
与计算机连接的外设有很多类型,CPU访问外设的时候,只需要访问其对应的端口,然后通过这些端口和外设进行数据交互。端口本质上就是寄存器,有的外设需要用到多个端口,操作端口的命令是in/out,寄存器是dx。
;端口读取数据
mov dx, 0x3f8 ; 将端口地址0x3f8存储到dx寄存器中
in al, dx ; 从dx指定的端口读取一个字节,并将其存储到al寄存器中
;端口写数据
mov dx, 0x3f8 ; 将端口地址0x3f8存储到dx寄存器中
mov al, 'A' ; 将字母A存储到al寄存器中
out dx, al ; 将al寄存器中的数据写入到dx指定的端口中