第3章 完善MBR
参考文章:《操作系统真象还原》第三章 ---- 完善MBR 尝汇编先苦涩后甘甜而再战MBR!(内有闲聊)
参考文章:《操作系统真象还原》第三章 ---- 完善MBR 尝汇编先苦涩后甘甜而再战MBR!(内有闲聊)
一、地址、section、vstart(实模式汇编关键)
地址本质:程序中符号(指令/变量)相对于文件/段开头的偏移量,由编译器分配
section(段):汇编中划分代码/数据的逻辑块,用于组织程序、控制地址分配
vstart:指定段的虚拟起始地址,MBR被BIOS加载到 0x7c00 ,所以必须用 vstart=0x7c00 ,让编译出的偏移地址与实际内存地址一致,避免寻址错误
二、实模式CPU与内存(x86 16位)
CPU结构:控制单元(取指/译码)、运算单元(ALU)、存储单元(寄存器组)
核心寄存器:
通用寄存器:AX/BX/CX/DX、SI/DI、BP/SP(栈指针)
段寄存器:CS(存储代码段)、DS(存储数据段)、ES(额外附加的段寄存器)、SS(栈寄存器)
指令寄存器:IP
标志寄存器:FLAGS(用于表示是否进位,为0、溢出)
内存分段寻址:CS:IP取指实模式下地址=段基址<<4 + 偏,实际计算即为:cs的地址末尾加上0,在与IP地址相加,结果即为最终地址
栈操作:SS:SP指向栈顶, push 减SP、 pop 加SP
三、直接操作显卡(绕过BIOS中断)
BIOS只能在16为实模式下才能使用,在32位保护模式下不能直接调用BIOS中断,后面的学习都是基于保护模式下的,因此要绕过BIOS中断,直接对显卡进行读写来输出字符
原理:显卡显存被映射到CPU内存空间(实模式下文本模式默认 0xb8000 )CPU写显存=直接控制显示
显存格式(文本模式):每字符占2字节,其中低字节:表示要输出的字符,高字节表示所输出字符的属性(前景色+背景色+高亮/闪烁)
用到的代码:
用 mov ax,0xb800 → mov es,ax 指向显存段
按 es:偏移 写入字符与属性,实现屏幕输出
四、MBR-->Loader
主引导记录 MBR最多只有512字节要留 2 字节做标识:0x55、0xaa ,真正能用的代码空间只有几百字节,而一个操作系统的内核所需要的空间大小远超几百字节,所以引入Loader这一个中间程序实现从“MBR-->内核”到“MBER-->Loader-->内核”的转变
在加入了Loader后,操作系统的启动流程就变为了:
1. 开机 → CPU 复位 → 硬件找启动盘
2. 加载 MBR 到 0x7c00 并执行
3. MBR 把 Loader 从硬盘读到内存
4. 跳转到 Loader 执行
5. Loader 进行一系列操作
6. Loader 最后把内核加载进来
7. 跳转到内核,操作系统正式启动
五、代码实现--改进MBR,直接操作显卡来显示字符
将文件mbr.s中的代码修改成如下:
SECTION MBR vstart=0x7c00 ;起始地质编译在0x7c00
mov ax,cs ; CS = 代码段寄存器,此时 BIOS 刚进入 MBR,CS=0,把 CS 寄存器的值传给 AX
mov ds,ax ; 让数据段寄存器 DS、附加段寄存器 ES、栈段寄存器 SS、额外段寄存器 FS 指向和 CS 一样的段地址(统一段地址,避免出错)
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ; 设置栈顶指针 sp = 0x7c00,为 MBR 留出安全的栈空间
mov ax,0xb800 ; 文本模式显存起始地址是 0xB8000,因为段寄存器只能存“段基址”,段基址 << 4 才是真实地址,0xB800 << 4 = 0xB8000,正好是显存开始
mov gs,ax ; GS是通用段寄存器,一般可以任意使用,把 GS 指向显存段,之后用 gs:偏移来表示最终的地址从而正确定位到屏幕的位置
mov ax,0600h ;调用BIOS中断,AH=0x06:向上滚屏功能(等于清屏),AL=0x00:整屏清空
mov bx,0700h ;BH=0x07:清屏后默认黑底白字属性
mov cx,0 ;左上角坐标 (0,0)
mov dx,184fh ;右下角坐标 (24,79) → 184f 是十六进制表示
int 0x10 ;调用 BIOS 显示中断.执行清屏
;新增功能 直接操作显存部分
;预设输出LOVE6 OS
mov byte [gs:0x00],'H' ;低位字节储存要输出的字符
mov byte [gs:0x01],0xA4 ;背景储存在第二个字节 含字符与背景属性:高亮粉字
mov byte [gs:0x02],'E'
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'L'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'0'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0A],' '
mov byte [gs:0x0B],0xA4
mov byte [gs:0x0C],'O'
mov byte [gs:0x0D],0xA4
mov byte [gs:0x0E],'S'
mov byte [gs:0x0F],0xA4
jmp $ ;于此处死循环
times 510 - ($ - $$) db 0 ;填充 0,直到代码长度 = 510 字节,$代表当前指令的地址, $$代表当前段的起始地址(也就是0x7c00 )
db 0x55,0xaa ;MBR 有效标志:BIOS 识别这是合法引导扇区
修改完成后,同上一章的操作一样,将mbr.s汇编生成二进制文件、将二进制文件写入虚拟硬盘、进入bochs运行仿真
在bochs内打开终端,输入以下命令
nasm -o mbr.bin mbr.s ;编译生成二进制文件mbr.bin
dd if=“你的mbr.bin文件的位置” of=“你所创建的虚拟硬盘的位置” bs=512 count=1 conv=notrunc
bin/bochs -f bochsrc.disk ;启动bochs
成功运行如下图所示:

六、代码实现--从磁盘读入Loader
对mbr.s文件进行进一步的修改:
%include "boot.inc"
SECTION MBR vstart=0x7c00 ;起始地址编译在0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800 ; ax为文本信号起始区
mov gs,ax ; gs = ax 充当段基址的作用
; ah = 0x06 al = 0x00 想要调用int 0x06的BIOS提供的中断对应的函数
; 即向上移动即完成清屏功能
; cx dx 分别存储左上角与右下角的左边 详情看int 0x06函数调用
mov ax,0600h
mov bx,0700h
mov cx,0
mov dx,184fh
;调用BIOS中断
int 0x10
mov byte [gs:0x00],'H'
mov byte [gs:0x01],0xA4
mov byte [gs:0x02],'E'
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'L'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'O'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0A],' '
mov byte [gs:0x0B],0xA4
mov byte [gs:0x0C],'O'
mov byte [gs:0x0D],0xA4
mov byte [gs:0x0E],'S'
mov byte [gs:0x0F],0xA4
mov eax,LOADER_START_SECTOR ;把要读取的起始扇区号 2 存入 EAX
mov bx,LOADER_BASE_ADDR ;把内存目标地址 0x600 存入 BX,BX 用来存放数据写入的内存地址
mov cx,1 ;读取磁盘数设为1
call rd_disk_m_16 ;执行该函数:从硬盘读取Loader到内存,函数的具体代码在后面给出
jmp LOADER_BASE_ADDR ;跳转到 Loader 执行
;读取硬盘的基本流程:
;1 写入待操作磁盘数
;2 写入LBA 低24位寄存器 确认扇区(LBA:Logical Block Addressing, 逻辑块寻址现代LBA是48位的)
;3 device 寄存器 第4位主次盘 第6位LBA模式 改为1
;4 command 写指令
;5 读取status状态寄存器 判断是否完成工作
;6 完成工作 取出数据
rd_disk_m_16:
;;;;;;;;;;;;;;;;;;;; 1、写入待操作磁盘数
mov esi,eax ;
mov di,cx ;备份 AX、CX,因为后面端口操作会覆盖这些寄存器
mov dx,0x1F2 ;指定要操作的硬盘端口,0x1F2是IDE硬盘控制器的固定端口号,是硬盘“扇区计数”端口
;操作 IO 端口时,端口号必须放在 dx 寄存器里
mov al,cl ;把要读取的扇区数,即“1”个存到 ,通过端口发送数据时,数据只能存在ax、al 里
out dx,al ;向硬盘的 0x1F2 端口,发送数值 1,即:读取一个扇区
mov eax,esi ;恢复 eax = 2(LBA起始扇区号)
;;;;;;;;;;;;;;;;;;;; 2、写入LBA 24位寄存器 确认扇区
mov cl,0x8 ;准备 “右移 8 位” 的步数,后面要反复用 shr 右移 8 位,所以先把 8 存到 cl
;操作 IO 端口时,端口号必须放在 dx 寄存器里
mov dx,0x1F3 ;写0x1F3 端口,0x1F3 端口是 LBA low,即LBA的低8位,
out dx,al ;把 AL 里的值发给硬盘,此时 al = 0x02,所以低 8 位 = 0x02
mov dx,0x1F4 ;写0x1F4 端口,0x1F4 端口是 LBA Mid,即LBA的中8位
shr eax,cl ;将eax右移8位,此时eax=0x0000 al=00
out dx,al ;所以中 8 位 = 0x00
mov dx,0x1F5 ;写0x1F5 端口,0x1F5 端口是 LBA High,即LBA的高8位
shr eax,cl ;将eax右移8位,此时eax=0x0000 al=00
out dx,al ;所以高 8 位 = 0x00
;因此,最终发送给硬盘的24位地址:0x000002
;;;;;;;;;;;;;;;;;;;;;;3、设置 Device 寄存器(开启 LBA 模式)
shr eax,cl ;把 EAX 右移 8 位
and al,0x0f ;与0x0f做与运算,相当于只保留AL低4位
or al,0xe0 ;与0xe0做或运算,相当于AL高4位变为1110
mov dx,0x1F6 ;写0x1F6 端口,0x1F6 端口是Device寄存器的端口号
out dx,al ;将0x1F6设置为1110 0000 ,代表开启LBA模式
;位 7~5 = 111:固定值,必须这样写
;位 4 = 0:表示主硬盘(1 是从盘)
;位 3~0 = LBA 地址的第 24~27 位(我们这里是 0)
;Device寄存器是用来////
;;;;;;;;;;;;;;;;;;;;;4、发送读命令
mov dx,0x1F7 ;写0x1F7 端口,0x1F5 端口是硬盘控制器的端口号
mov ax,0x20 ; 0x20是读命令,相当于告诉硬盘:要读取扇区
;发给硬件的数据必须放在 al/ax/eax里
out dx,al ;将0x1F7设置为0x20 ,代表让硬盘开始读扇区
;;;;;;;;;;;;;;;;;;;;5、循环等待硬盘就绪
.not_ready:
nop ;空操作,延时等待
in al,dx ;dx 此时的值 = 0x1F7,即将此时0x1F7的状态传给al
and al,0x88 ;al与0100 0100 即0x88做与运算
;即保留al第7位(BSY位,是状态位)
;也保留al第3位(DRQ数据就绪位)
;用于判断此时硬盘是否繁忙、数据是否准备好
cmp al,0x08 ;比较al与0000 1000,若第7位为0 表明硬盘不繁忙
; 若第3位为1,表明数据已准备好
jne .not_ready ; 若不相等,则重复循环
;;;;;;;;;;;;;;;;;;;;;6、循环读取数据(一次读 2 字节)
mov ax,di ;di里保存的是要读取的扇区数(之前备份的 cx=1),因此ax=1
mov dx,256 ;1个扇区 = 512 字节,一次读 2 字节,要读256 次
mul dx ;ax = ax · dx=1x256=256
mov cx,ax ;cx 是 loop 循环专用计数器,cx == 0即跳出循环
mov dx,0x1F0 ;写0x1F0 端口,0x1F0 端口是硬盘数据端口的端口号
.go_read_loop:
in ax,dx ;两字节dx 一次读两字节存入 ax
mov [bx],ax ;bx转么用于存储目标内存地址,即把读到的2字节数据写入内存
;bx = 寄存器里存的纯数字(比如 0x600)
;[bx] = 以bx里的数字作为地址,去访问内存
;bx的初始值我们前面设置了是0x600,是loader程序的起始地址
add bx,2 ;内存地址向后移动2字节,准备存下一组数据
loop .go_read_loop
ret ;函数返回,跳转到 call 的下一句
times 510 - ($ - $$) db 0
db 0x55,0xaa
在bochs文件夹下新建include文件夹用于存放头文件

然后在include文件夹内新建头文件boot.inc
LOADER_START_SECTOR equ 2 ;把 LOADER_START_SECTOR 这个符号直接替换成数值 2
;表明Loader 程序在磁盘上的起始扇区号是2扇区
;扇区从0开始计数,第0扇区是MBR,第1扇区是1,所以2就是第3个扇区
LOADER_BASE_ADDR equ 0x600 ;Loader 程序被加载到内存的起始地址是 0x600
;MBR 引导程序在调用 BIOS 中断读取磁盘时,会把 0x600 作为目标内存地址,把 Loader 读到这里,然后跳过去执行
然后将把库目录路径链接给MBR.S,生成二进制文件,并将二进制文件写入虚拟硬盘(仍然是在bochs下打开终端)
nasm -I include/ -o mbr.bin mbr.S ;在include/这个路径里面去寻找头文件,然后进行编译
dd if=“你的mbr.bin文件的位置” of=“你所创建的虚拟硬盘的位置” bs=512 count=1 conv=notrunc
编写简单的loader程序进行验证
%include "boot.inc"
SECTION MBR vstart=LOADER_BASE_ADDR
mov byte [gs:0x00],0x00 ;gs:0x00访问显存第0个位置(屏幕左上角第一个字符)
mov byte [gs:0x01],0xA4 ;这里是写入空字符,即不显示任何字符
;0xA4 = 二进制 1010 0100
;高位 1010:浅绿色背景
;低位 0100:红色前景
mov byte [gs:0x02],'H'
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'E'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'L'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'L'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0A],' '
mov byte [gs:0x0B],0xA4
mov byte [gs:0x0C],'L'
mov byte [gs:0x0D],0xA4
mov byte [gs:0x0E],'O'
mov byte [gs:0x0F],0xA4
mov byte [gs:0x10],'A'
mov byte [gs:0x11],0xA4
mov byte [gs:0x12],'D'
mov byte [gs:0x13],0xA4
mov byte [gs:0x14],'E'
mov byte [gs:0x15],0xA4
mov byte [gs:0x16],'R'
mov byte [gs:0x17],0xA4
mov byte [gs:0x18],' '
mov byte [gs:0x19],0xA4
mov byte [gs:0x1A],'X'
mov byte [gs:0x1B],0xA4
mov byte [gs:0x1C],'P'
mov byte [gs:0x1D],0xA4
mov byte [gs:0x1E],'Y'
mov byte [gs:0x1F],0xA4
jmp $
创建文件夹boot,将mbr.s 、mbr.bin、 ;loader.s放入boot文件夹:

在boot文件夹内进行编译生成二进制文件、将二进制文件写入虚拟硬盘:
nasm -I “你的include文件夹的路径” -o loader.bin loader.s
dd if=“你的loadee.bin文件所在的位置” of=“你的虚拟硬盘所在的位置” bs=512 count=1 seek=2 conv=notrunc
:因为在此之前,Loader程序我们设定好了从第2个扇区开始加载,所以相对于MBR所在的第0个扇区,我们要跳过2个扇区,因此 dd命令中,我们加入了参数“seek=2”
在bochs文件夹下运行仿真,选择选项6,输入c
bin/bochs -f bochsrc.disk ;启动bochs
成功运行结果如下所示:

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)