第二章 BIOS --> MBR
第二章 BIOS --> MBR
本文是对《操作系统真象还原》第二章学习的笔记,欢迎大家一起交流。
第一棒 BIOS
首先我们要先明白计算机的启动过程,在 x86 模式下,开机的一瞬间,cpu 的 cs:ip 寄存器被强制初始化为 0xF000:0xFFF0 ,此时所指向的地址即 0xf000*16+0xfff0=0xffff0
,而在开机时,cpu 处于实模式下,即内存地址是唯一的,直接操作物理地址,此时内存布局是这样的(这个图很重要,后面还会用到):
可以看到,最大的地址就是 FFF0:FFFF ,即 0xFFFFF,所以我们开机时 cs:ip 的指向就差 16 个字节就到顶了,所以我们 BIOS 的代码肯定不在这里,那么这里也肯定是个跳转指令,跳转到真正的 BIOS 代码处,那么我们去验证一下,去 bochs 里面调一下,在开机一瞬间(x86):
可以清楚的看到 cs:ip 的指向以及所对应的汇编代码,确定是 jmp 指令,并且跳转到 0xfe05b
接着 BIOS 便紧接着检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存中 0x000~0x3FF 处建立数据结构,中断向量表 IVT 并填写中断例程,紧接着到了交接力棒的时候,将接力棒交给 mbr
bios--->(f000:fff0)jmp---> 检测内存、显卡等外设信息,初始化硬件,中断向量--->jmp 0:0x7c00 (mbr 程序)
第二棒 MBR
再说 mbr 之前,继续详细说一下 BIOS 的作用,下面这段话是我抄的 kanshanxd 博主的,他的文章和视频也是很好的参考资料
BIOS有四个主要功能:
1、硬件自检;
2、建立一些需要用到的数据结构与中断向量表;
3、校验启动盘中位于0盘0道1扇区(其实就是0扇区,只不过CHS方法用1开头)的内容,校验这里是不是放着主引导记录MBR,校验方法是检测这个扇区最后两个字节是不是0x55与0xaa(所以我们编写的主引导记录MBR最后两个字节应该是这两个);
4、在3的基础上是,那么就将该扇区内容加载至0x7c00内存处,这个位置是由于历史遗留导致的兼容,由最初的操作系统本身所占内存大小与布局所决定(书p58)。加载完毕后,然后跳转过去执行。
我们可以看到 mbr 所在扇区应该是 0 盘 0 道 1 扇区,并且这个扇区最后两个字节必须是 0x55 和 0xaa,并且扇区最终会被加载到 x7c00 内存处,关于这几个硬编码我们来一个一个解释。
0 盘 0 道 1 扇区以及 0x55 和 0xaa 的由来
首先我们要明确扇区表示法有两种,CHS 和 LBA,我们这里采用的是 CHS,而 CHS 的扇区从 0 开始,所以 0 盘 0 道 1 扇区也是 0 盘 0 道 0 扇区,知道这个意思就行,而两个魔数 0x55 和 0xaa,如果此扇区的最后两个字节是这两个魔数,BIOS 就认为 MBR 就在这个扇区里面,所以 BIOS 要挨个磁盘进行寻找,直到找到魔数,就认为找到了主引导程序(MBR),所以我们的想法是 MBR 和 BIOS 离得越近越好,而 0 盘 0 道 1 扇区就是第一个扇区,所以我们干脆将 MBR 放入这个扇区了。
0x7c00 的由来
首先我们要知道 MBR 不是放到哪里都行的,首先不能覆盖别的数据,也不能被后来的数据过早覆盖,但我们 mbr 交过接力棒之后是可以被覆盖的,我们根据上面的内存布局再来看一下内存情况:
8086CPU 要求物理地址 0x0~0x3FF 存放中断向量表,所以此处不能动了,再选新的地方看看。
按 DOS 1.0 要求的最小内存 32KB 来说,MBR 希望给人家尽可能多的预留空间,这样也是保全自己的作法,免得过早被覆盖。
所以 MBR 只能放在 32KB 的末尾。
MBR 本身也是程序,是程序就要用到栈,栈也是在内存中的,MBR 虽然本身只有 512 字节,但还要为其所用的栈分配点空间,所以其实际所用的内存空间要大于 512 字节,估计 1KB 内存够用了。
结合以上三点,选择32KB中的最后1KB最为合适,那此地址是多少呢?32KB换算为十六进制为0x8000,减去 1KB(0x400)的话,等于 0x7c00。这就是倍受质疑的 0x7c00 的由来,这下清楚了。
可见,加载 MBR 的位置取决于操作系统本身所占内存大小和内存布局。
小总结
1、BIOS 基本输入输出,检查各种设备的情况,并且建立中断向量表,填写中断例程,某些非常实用的中断是由 BIOS 提供的。
BIOS 将 0 柱面 0 磁头 1 扇区的数据转移到了 0x7c00 位置, 并将 CS:IP 指向了该块位置 ,默认认为 0 盘 0 道 1 扇区的就是启动盘, 且校验方式为看最后两个字节的魔数 是否为 0x55 和 0xaa ,若是的话则校验成功。
2、MBR 接到了接力棒, 作为主引导程序, 发挥自己的作用。
代码部分
代码分析
接下来简单实现一个 MBR 的代码,MBR 的作用是加载某个程序,而这个程序就是内核加载器,我们现在先简单的让 MBR 在屏幕打印字符“1 MBR”
先把代码贴上,我们再进行解读
;主引导程序
;------------------------------------------------------------
SECTION MBR vstart=0x7c00 ;起始地址 0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00 ;初始化栈
;这个时候 ds = es = ss = 0 栈指针指向MBR开始位置
;ah = 0x06 al = 0x00 想要调用int 0x06的BIOS提供的中断对应的函数 即向上移动即完成清屏功能
;cx dx 分别存储左上角与右下角的左边 详情看int 0x06函数调用
mov ax,0x0600
mov bx,0x700
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角的(X,Y)位置
mov cx,0
mov dx,0x184f
;调用BIOS中断
int 0x10
;;;;;;;;; 下面这三行代码是获取光标位置 ;;;;;;;;;
;.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设置写字符方式 ah=01: 显示字符串,光标跟随移动
mov bx, 0x2 ; bh存储要显示的页号,此处是第0页,
; bl中是字符属性, 属性黑底绿字(bl = 02h)
int 0x10 ; 执行BIOS 0x10 号中断
;;;;;;;;; 打字字符串结束 ;;;;;;;;;;;;;;;
jmp $ ; 死循环
;字符串声明 db == define byte dw == define word ascii一个字符占一个字节
message db "1 MBR"
;预留两个字节 其余空余的全部用0填满 为使检测当前扇区最后两字节为0x55 0xaa 检测是否为有效扇区
;510 = 512字节-2预留字节 再减去(当前位置偏移量-段开始位置偏移量)求出来的是剩余空间
times 510 - ($ - $$) db 0
db 0x55,0xaa
注释很详细了,这里挑重点说:
首先是这一部分
我们将 vstart 设置为 0x7c00,这代表我们告诉编译器我们这段程序将被放到 0x7c00 处执行,编译器再编译时就会将第一行假装在偏移 0x7c00 处执行,而我们也确定 mbr 会在 0x7c00 处执行。
此时 cs 的值是 0,因为 BIOS 是通过 jmp 0:0x7c00 执行的,cs 的值肯定是 0,所以用它来初始化别的段寄存器
sp 的初始值被初始化 0x7c00,sp 是栈指针,向低地址发展,所以这样我们就在内存中形成了这样的内存布局:
接下来进行清屏,因为 BIOS 会有一些输出,所以要进行清屏。
这里利用了 BIOS 中断,具体可以参考 BIOS 中断大全(表格)_bios 功能调用表格-CSDN 博客,第一行将 ah 的值赋位 0x06,al 的值赋位 0x00,然后结合注释看就可以了
然后获取光标打印字符串,这里用到的都是 BIOS 中断,都是看说明就好了,这里不再解释
看一下最后的处理首先是 jmp $
,而 $ 就是指当前行地址,就是一直重复在这里 jmp,保证我们能看到上面的输出,万一我们的输出又被别的什么东西覆盖了呢
接下来 message db "1 MBR
则是声明了一个字符串,也是打印的字符串,位置就是 message,在上面打印中断时用到了
times 510-($-$$) db 0
则是从当前处到离最后还有两个位置一直填充 0,$-$$
就是当前段的起始地址-当前行的地址,其实就是在当前磁盘已经用了多少字节,然后用 510 减去这个字节数是将当前行到 510 一直填充 0
最后 db 0x55,0xaa
则是填上魔数,让 0x55 和 0xaa 这两个魔数恰好出现在该扇区的最后两个字节处,,让 BIOS 认识我们 MBR 程序.
编译脚本
利用 nasm 进行编译
nasm -o mbr.bin mbr.S
然后利用 dd 命令将我们编译出来的程序放到对应位置,if 代表读取的文件,of 代表写入的文件,bs 代表块大小 512 字节,seek 代表输出到文件时我们想要跳过几个字节,count 代表写入的块数,conv 代表如何转换文件,notrunc 即不打断文件
dd if=./mbr.bin of=../../hd60M.img bs=512 seek=0 count=1 conv=notrunc
去 bochs 中模拟执行,可以得到如下结果