前情提要
在上一部分的内容中,我们已经做好了模拟仿真的整个准备工作,这一节我们就先把计算机启动起来。
一、Bochs配置
1.1、配置启动盘
创建硬盘的工具是bximage,这个工具在我们的Bochs目录下,使用命令行创建硬盘
第一个选择 1,即创建新的硬盘或者软盘
第二个选择 hd,即创建硬盘
第三个选择 flat,即创建flat格式的硬盘,还有一些诸如sparse, growing, vpc or vmware4格式的硬盘。
第四个选择 512,即一个扇区有512字节
第五个选择 60,即硬盘的大小为60MB
第六个选择 hd60M.img,这个是硬盘的名字
1.2、配置启动文件
在bin目录下新建一个启动文件 bochsrc.disk
# 设置内存为32MB
megs: 32
# 设置对应机器的BIOS
romimage: file=/home/lyj/bochs/share/bochs/BIOS-bochs-latest
# 设置对应机器的VGA BIOS
vgaromimage: file=/home/lyj/bochs/share/bochs/VGABIOS-lgpl-latest
# 选择启动盘符,默认为从软盘启动,但是这里设置为硬盘,软盘太古老了
boot: disk
# 设置日志文件的输出
log: bochs_out.log
# 关闭鼠标功能,打开键盘功能,并且设置键盘的映射
mouse: enabled=0
keyboard: keymap=/home/lyj/bochs/share/bochs/keymaps/x11-pc-us.map
# 硬盘设置
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=disk, path="/home/lyj/bochs/bin/hd60M.img", mode=flat
其中的大部分设置都做了详细的注释,这里我们对硬盘部分做出一些更详细的解释
-
enabled=1:表示 ATA0 控制器被启用。这意味着 Bochs 将会模拟一个 ATA0 控制器,并允许虚拟机使用这个控制器来访问硬盘或光驱设备。
-
ioaddr1=0x1f0:表示 ATA0 控制器的 I/O 端口地址。在计算机体系结构中,I/O 端口用于与外部设备进行数据交换。这里指定了 ATA0 控制器的主要 I/O 端口地址为 0x1f0。
-
ioaddr2=0x3f0:表示 ATA0 控制器的备用(secondary)I/O 端口地址。有些设备可能需要额外的 I/O 端口进行通信,这里指定了 ATA0 控制器的备用 I/O 端口地址为 0x3f0。
-
irq=14:表示 ATA0 控制器的中断请求线(IRQ)。中断请求线用于处理硬件设备发出的中断信号,这里指定了 ATA0 控制器的中断请求线为 14。
ata0可以挂载两个硬盘,其中一个是主盘一个是从盘。这部分内容我们会在讲解硬盘时详细说。
1.3、启动Bochs
./bochs -f bochsrc.disk
在bochs的命令行中先输入 6,即开始仿真
在输入 c,即开始执行
然后我们就看到了下面的画面
系统提示没有可引导的启动设备,也就是我们设置的硬盘是一个空的硬盘,里面并没有程序可以执行,所以直接报错退出了。接下来我们让整个程序先能跑起来。
二、MBR主引导记录
好了,我们要开始写代码了,在写代码之前先普及一点小知识,x86架构的处理器,由于历史原因,一开始是先进入实模式,再进入保护模式的。实模式下,只有20根地址线,因此只能访问到2的20次方Byte的数据,也就是1MB。这1MB又被分为了几个部分,我们可以看一下
这一部分如果我们学过《汇编语言》或者《微机原理》之类的课程是比较好理解的
2.1、BIOS启动
开机的一瞬间,BIOS被启动,BIOS中的代码就存在于 0xF0000~0xFFFFF
这个地址了,但实际上这个地址不是内存,是的,你没听错,不是内存,是一块ROM。
为了统一管理计算机中的端口,内存,ROM等等设备,计算机将这些东西统一了,就像访问内存那样去访问这些资源。
所以在开机的一瞬间,CPU的cs:ip
寄存器被强制初始化为0xF000:0xFFF0
,这样就可以直接执行BIOS中的程序。BIOS开始检测内存,显卡等外设信息,当外设准备好后,开始在 0x000~0x3FF
处建立中断向量表,这些中断就是BIOS提供的基础功能,可以帮助我们操作系统做一些基础操作。
做完这些,BIOS的使命就要结束了,最后的一点任务,校验启动盘中位于0盘0道1扇区(也就是LBA地址为0的扇区)的内容,如果此扇区末尾的两个字节分别是魔数 0x55
和 0xaa
,BIOS便认为此扇区中确实存在可执行的程序,并把这个扇区的内容加载到物理地址 0x7c00
的内存中,一个扇区的大小我们在上一节中设置硬盘扇区大小中设置过了,是512B。
最后BIOS通过跳转指令 jmp 0:0x7c00
跳转到 0x7c00
的地址开始执行用户的程序,这里用户的程序也就是我们的操作系统了。
问:为什么是魔数
0x55
和0xaa
?答:BIOS中设置好的,也是大家公认的,如果你自己写BIOS的话,也可以设置为别的,但是这就不通用了。
问:为什么是
0x7c00
的地址?答:这也是一个写死了的地址,王八的屁股,规定。
2.2、编写MBR,并编译
这里我们还是用到了BIOS中自带的中断。代码如下所示
assembly
; os/src/boot/mbr.s
; 设置开始的地址,并且初始化寄存器
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
; 利用0x06号功能实现清理屏幕
; AL = 0x06 功能号
; AL 上卷的行数(如果为0,表示全部)
; BH 上卷行属性
; (CL,CH) = 窗口左上角的(X,Y)位置,这里是 (0,0)
; (DL,DH) = 窗口右下角的(X,Y)位置,这里是 (80,25)
mov ah, 0x06
mov al, 0x00
mov bh, 0x7
mov bl, 0x00
mov cx, 0
mov dx, 0x184f
int 0x10 ; int 0x10
; 获取光标位置
; AL = 0x03 功能号
; bh 寄存器存储的是待获取光标的页号
mov ah, 3
mov bh, 0
int 0x10
; 打印字符串
; BP 字符串地址,BP需要通过AX访问,不能直接给BP内存数据
; AL = 0x13 功能号
; AL = 0x01 显示字符串,光标跟随移动
; CX = 0x03 字符串长度
; BH = 0x00 要显示的页号
; BL = 0x02 字符属性,属性黑底绿字
mov ax, message
mov bp, ax
mov ax, 0x1301
mov cx, 0x03
mov bx, 0x02
int 0x10
; 循环等待
jmp $
; 要显示的字符串
message db "MBR"
; 将510个字节中剩余的空间填充为0
; $ 是当前地址
; $$ 是本节开头地址,也就是0x7c00
times 510-($-$$) db 0
db 0x55,0xaa
其实还是很简单的,就像是调用函数,然后我们往那些指定的寄存器中添加数据就可以了。
编译一下
shell
nasm -o bin/mbr.bin src/boot/mbr.s
查看一下确实是512字节
然后我们将这个文件直接通过dd指令存放到我们之前生成的虚拟硬盘中的0盘0道1扇区
dd if=./bin/mbr.bin of=~/bochs/bin/hd60M.img bs=512 count=1 conv=notrunc
然后运行Bochs
~/bochs/bin/bochs -f ~/bochs/bin/bochsrc.disk
可以看到,我们成功的运行起来了
2.3、改进MBR,直接操作显卡
我们将输出字符串直接通过操作显存来完成。通过上表我们可以得知,显存是从0xB8000~0xBFFFF
,范围是32KB,一屏可以显示2000个字符,显示器上的每个字符占2字节大小,故每屏字符实际占用4000字节。这样,我们的32KB的显存可以容纳8屏的数据。
屏幕上每个字符的低字节是字符的ASCII码,高字节是字符属性元信息。
其中各个位的搭配为
我们将程序修改为
assembly
; os/src/boot/mbr.s
; 设置开始的地址,并且初始化寄存器
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
; 利用0x06号功能实现清理屏幕
; AL = 0x06 功能号
; AL 上卷的行数(如果为0,表示全部)
; BH 上卷行属性
; (CL,CH) = 窗口左上角的(X,Y)位置,这里是 (0,0)
; (DL,DH) = 窗口右下角的(X,Y)位置,这里是 (80,25)
mov ah, 0x06
mov al, 0x00
mov bh, 0x7
mov bl, 0x00
mov cx, 0
mov dx, 0x184f
int 0x10 ; int 0x10
mov byte [gs:0x00],'M' ; 字符为M的ascii值
mov byte [gs:0x01],0x0F ; 00001111b 即背景色为黑,字体为白,不闪烁
mov byte [gs:0x02],'B' ;
mov byte [gs:0x03],0x0F ;
mov byte [gs:0x04],'R' ;
mov byte [gs:0x05],0x0F ;
; 循环等待
jmp $
; 要显示的字符串
message db "MBR"
; 将510个字节中剩余的空间填充为0
; $ 是当前地址
; $$ 是本节开头地址,也就是0x7c00
times 510-($-$$) db 0
db 0x55,0xaa
之前的编译,写入硬盘,启动程序,这里也可以简单点写个脚本
shell
# os/run.sh
# 编译mbr
nasm -o bin/mbr.bin src/boot/mbr.s
# 复制mbr二进制程序到硬盘
dd if=./bin/mbr.bin of=/home/lyj/bochs/bin/hd60M.img bs=512 count=1 conv=notrunc
# 启动仿真
/home/lyj/bochs/bin/bochs -f /home/lyj/bochs/bin/bochsrc.disk
直接启动脚本
结束语
好了第二章已经结束了,这一章我们生成了需要的硬盘以及写了一个简单的MBR引导程序,但是我们的内核还在硬盘中(实际上还没有写),下一章我们将在MBR中实现一个内核加载器出来。