操作系统实践 第一章 主引导程序

192 阅读7分钟

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 = 上卷行数(我们使用0BH = 行属性(这里的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(表示的是当前的页号,我们这里使用的是0BL = 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指定的端口中

CHS和LBA