BIOS
Base Input & Output System
主要工作是检测、初始化硬件。BIOS还有中断向量表,通过“int 中断号”来实现相关的硬件调用。
BIOS是计算机上第一个运行的软件,由硬件ROM(只读存储器)加载。开机的时候处于实模式。
MBR
MBR在0盘0道1扇区(就是最开始的扇区),被BIOS加载,要求扇区的最后两个是0x55和0xaa。MBR选择在32K的最后1K被存放(0x7c00)
直接和硬件交互
然后mbr里面就可以写一些汇编了,比如一开始的输出字符。
;2023.5.10
;主引导程序
; 第一版
;----------------------------------
; 引导加载程序
; 0x7c00 是启动时的引导程序
SECTION MBR vstart=0x7c00
mov ax,cs ; cs指向加载程序当前的代码段
mov ds,ax ; Data Segment 数据段寄存器
mov es,ax ; Extra Segment 扩展段寄存器
mov ss,ax ; Stack Segment 堆栈段寄存器
mov fs,ax ; F Segment 拓展段寄存器
mov sp,0x7c00 ; 堆栈指针寄存器
; 清屏利用 0x60 号功能,上卷全部行,则可清屏
; ---------------------
; INT 0x10 功能号:0x06 功能描述:上卷窗口
;----------------------
; 输入:
; AH 功能号 = 0x06
; BH = 上卷行属性
; (CL, CH) = 窗口左上角的(X,Y)位置
; (DL, DH) = 窗口右下角的(X,Y)位置
; 无返回值
mov ax, 0x600 ; 一种显示方案
mov bx, 0x700
mov cx, 0 ; 左上角 (0,0)
mov dx, 0x184f ; 右下角 (80,25)
; 一行只能80个字符,共25行
; 下标从0开始,0x18=24, 0x4f=79
int 0x10 ; BIOS的系统调用接口
;;;;;; 下面这三行代码获取光标的位置
; get_cursor获取当前光标位置,在光标位置处打印字符
mov ah, 3 ; 输入: 3号子功能是获取光标位置,需要纯如ah寄存器
mov bh, 0 ; bh寄存器存储的是待获取的光标的页号
int 0x10 ; 输出: ch=光标开始行,cl=光标结束行
; dh=光标所在行号,dl=光标所在列号
;;;;;;;; 获取光标位置结束 ;;;;;;;;;
;;;;;;;; 打印字符串 ;;;;;;;;
; 还是用10h中断,不过这次调用13号子功能打印字符串
mov ax, message
mov bp, ax ; es:bp 为串首地址,es此时通cs一致
; 开头时已经为sreg初始化
; 光标主要用到dx寄存器中内容,cx中的光标位置可忽略
mov cx, 5 ; cx 为串长度,不包括结束符0的字符个数
mov ax, 0x1301 ; 子功能号13是显示字符及属性,要存入ah寄存器
; al 设置谢字符方式al=01: 显示字符串,光标跟随移动
mov bx, 0x2 ; bh 存储要显示的页号,此处是第0页
; bl 中是字符属性, 属性黑底绿字(bl = 02h)
int 0x10 ; 执行BIOS 0x10 号中断
;;;;;;; 打印字符结束 ;;;;;;;
jmp $ ; 使程序悬停在此
message db "1 MBR"
times 510-($-$$) db 0
db 0x55,0xaa
显卡
不过这样直接与硬件打交道还是比较恐怖的,可以通过IO接口。改进下通过显卡操作。
;2023.5.11
;主引导程序
;直接操作显卡
;----------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax ; gs段寄存器,可以使用gs段寄存器来访问显存
; 清屏利用 0x60 号功能,上卷全部行,则可清屏
; ---------------------
; INT 0x10 功能号:0x06 功能描述:上卷窗口
;----------------------
; 输入:
; AH 功能号 = 0x06
; BH = 上卷行属性
; (CL, CH) = 窗口左上角的(X,Y)位置
; (DL, DH) = 窗口右下角的(X,Y)位置
; 无返回值
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角 (0,0)
mov dx, 184fh ; 右下角 (80,25)
; 一行只能80个字符,共25行
; 下标从0开始,0x18=24, 0x4f=79
int 10h
; 输出背景色绿色, 前景色红色, 并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0x20 ; A表示绿色背景闪烁, 4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0x91
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0x96
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0x94
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0x94
jmp $ ; 跳转到当前指令的地址, 通过死循环使程序悬停在此
times 510-($-$$) db 0 ; $代表当前位置指令的地址, $$代表当前节段的起始地址
; MBR 的最后两字节是固定的 0x55 0xaa 牵引扇区签名
db 0x55,0xaa
硬件
给mbr加上读写硬盘的功能。mbr负责加载loader,loader负责为内核准备好环境和加载内核。
把loader放到第2扇区,离mbr这个第0扇区远一点。
更改mbr
;2023.5.11
;主引导程序
;从硬盘加载一个内核程序并跳转到该程序的入口点
;----------------------------------
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax ; gs段寄存器,可以使用gs段寄存器来访问显存
; 清屏利用 0x60 号功能,上卷全部行,则可清屏
; ---------------------
; INT 0x10 功能号:0x06 功能描述:上卷窗口
;----------------------
; 输入:
; AH 功能号 = 0x06
; BH = 上卷行属性
; (CL, CH) = 窗口左上角的(X,Y)位置
; (DL, DH) = 窗口右下角的(X,Y)位置
; 无返回值
mov ax, 0600h
mov bx, 0700h
mov cx, 0 ; 左上角 (0,0)
mov dx, 184fh ; 右下角 (80,25)
; 一行只能80个字符,共25行
; 下标从0开始,0x18=24, 0x4f=79
int 10h
; 输出背景色绿色, 前景色红色, 并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'1'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁, 4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'M'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'B'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'R'
mov byte [gs:0x09],0xA4
mov eax, LOADER_START_SECTOR ; 起始扇区lba地址
mov bx, LOADER_BASE_ADDR ; 写入的地址
mov cx, 1 ; 代写入的扇区数
call rd_disk_m_16 ; rd_disk_m_16是函数,实现了从硬盘读取一个扇区的数据到内存中
; 以下读取程序的起始部分(一个扇区)
jmp LOADER_BASE_ADDR ; 将读取的数据写入
;--------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;--------------------
;eax=LBA 扇区号
;bx=将数据写入的内存地址
;cx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘
;第1步:设置要读取的扇区数
mov dx, 0x1f2
mov al, cl ;cl是cx的低8位
out dx, al ;out用于将数据传输到指定端口
;读取扇区数
mov eax,esi ;恢复eax
;第2步:将LBA地址存入0x1f3~0x1f6
;LBA地址7~0位写入端口0x1f3
mov dx, 0x1f3
out dx, al
;LBA地址15~8位写入端口0x1f4
mov cl, 8
shr eax, cl ;将eax中存储的扇区数量向右移动8位
mov dx, 0x1f4
out dx, al
;LBA地址23~16位写入端口0x1f5
shr eax, cl
mov dx, 0x1f5
out dx, al
shr eax, cl
and al, 0x0f ; lba第24~27位
or al, 0xe0 ; 设置7~4位为1110,表示lba模式
mov dx, 0x1f6
out dx, al
; 第3步:向0x1f7端口写入读命令,0x20
mov dx, 0x1f7
mov al, 0x20
out dx, al
; 第4步:检测硬件状态
.not_ready:
; 同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop ; 不做任何事情,填充下
in al, dx ; 从端口读取数据
and al, 0x88 ; 第3位为1表示硬盘控制器已准备好数据传输
; 第7位为1表示硬盘忙
cmp al, 0x08 ; 相等时ZF被置1 大于CF 1 小于SF 1
jnz .not_ready ; 若未准备好,继续等
; 第5步:从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax
; di为要读取的扇区数,一个扇区有512字节,每次读入一个字
; 共需 di*512/2 次,所以di*256
mov dx, 0x1f0
.go_on_read:
in ax, dx
mov [bx], ax
add bx, 2
loop .go_on_read
ret
times 510-($-$$) db 0 ; $代表当前位置指令的地址, $$代表当前节段的起始地址
; MBR 的最后两字节是固定的 0x55 0xaa 牵引扇区签名
db 0x55,0xaa
这里的loader也只是打印下字。
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
; 输出背景绿色, 前背景红色, 并且跳动的字符串"1 MBR"
mov byte [gs:0x00], '2'
mov byte [gs:0x01], 0xA4 ; A表示绿色背景闪烁, 4表示前景色为红色
mov byte [gs:0x02], ' '
mov byte [gs:0x03], 0xA4
mov byte [gs:0x04], 'L'
mov byte [gs:0x05], 0xA4
mov byte [gs:0x06], 'O'
mov byte [gs:0x07], 0xA4
mov byte [gs:0x08], 'A'
mov byte [gs:0x09], 0xA4
mov byte [gs:0x0a], 'D'
mov byte [gs:0x0b], 0xA4
mov byte [gs:0x0c], 'E'
mov byte [gs:0x0d], 0xA4
mov byte [gs:0x0e], 'R'
mov byte [gs:0x0f], 0xA4
jmp $
头文件里面写的两个宏,loader将被加载到0x900,在第二扇区。
;---------------- loader 和 kernel -----------------
LOADER_BASE_ADDR equ 0x900
LOADER_START_SECTOR equ 0x2