编写MBR主引导记录
1 计算机的启动过程
- 为什么程序要载入内存?
CPU的硬件电路被设计为只能运行于内存中的程序,这是硬件基因决定的。原因是速度快,容量大。其次操纵系统也可以存储在软盘上,硬盘上,甚至是U盘上。 - 什么是载入内存?
(1)程序被加载器(软件或硬件)加载到内存某个区域。
(2)CPU的 cs:ip 寄存器被指向这个程序的起始地址。
操作系统在加载程序时,需要加载器来将用户程序存储到内存中。
所谓加载器,本质上就是一堆函数组成的模块。
按下电源键后第一个运行的软件是BIOS。
2 软件接力第一棒,BIOS
BIOS全称叫Base Input & Output System,即基本输入输出系统。
2.1 实模式下的1MB内存布局
BIOS是在实模式下运行的,而实模式只能访问1MB空间(20位地址线,2的20次方是1MB)。
先从低地址看,地址0~0x9FFFF处是DRAM(Dynamic Random Acces Menory),即动态随机访问内存,我们所装的物理内存就是DRAM。
地址0~0x9FFFF的空间范围是640KB,这片地址对应到了DRAM,也就是插在主板上的内存条。
顶部的0xF0000~FFFFF,这64KB的内存是ROM。存的是BIOS的代码。BIOS的主要工作是检测、初始化硬件。硬件自己提供了一些初始化的功能调用,BIOS直接调用进行初始化工作。
BIOS还需要建立中断向量表,这样就可以通过“int 中断号”来实现相关的硬件调用,当然BIOS建立的这些功能就是对硬件的IO操作,也就是输入输出,但由于64KB大小的空间,不可能把所有硬件的IO操作实现得面面俱到。
物理内存并不是“全部的内存”
在计算机中,并不是只有咱们插在主板上的内存条需要通过地址总线访问,还有一些外设同样需要通过地址总线来访问。因为这类设备不少,因此并没有将全部地址总线指向物理内存,地址总线会提前预留出来一些地址空间给这些外设使用,留够了以后,地址总线上其余的可用的地址再指向DRAM,也就是插在主板上的内存条、我们眼中的物理内存。
物理内存多大都没有用,主要看地址总线的宽度。还要看地址总线的设计,是不是全部用于访问DRAM。所以说,地址总线是决定我们访问那些、访问什么,已经访问范围的关键。我们平时的机器一般是32位的,上面的内存条并不会全部使用到,按理说内存条超过4GB就没有意义了,超过势必会浪费。即使没有超过地址总线的范围,也不会全部访问到,毕竟要预留一些地址来访问其他外设,所有最终还得看地址指向哪块内存。这就是安装4GB内存显示可用只有3.8GB的原因。
总之,表示地址的那串数字是地址总线的输入,相当于其参数,和内存条没关系。CPU能够访问一个地址,这是地址总线给做的映射,相当于给该地址分配了一个存储单元,而该存储单元要么落在某个rom中,要么落到了某个外设的内存中,要么落到了物理内存条中。可以想象成CPU给地址总线提供数字,在地址总线看来,这串数字就是地址。地址分配电路根据此地址的范围,决定在那个存储介质分配一个存储单元,最后将此地址与存储储单元对应起来。
2.2 BIOS 是如何苏醒的
BIOS是计算机第一个运行的软件,它是有硬件(相当于只读存储器ROM)加载的。
BIOS代码所做的工作是一成不变的,正常情况下,其本身不需要修改。于是BIOS顺理成章的被写进ROM。ROM也是块内存,内存就需要被访问。此ROM被映射在低端1MB内存的顶部,即0xF0000~0xFFFFF处,只有访问此处的地址,就是访问BIOS。这个映射是由硬件完成的。
BIOS作为程序,它的入口地址便是 0xFFFF0。
知道了入口地址,CPU需要 cs:ip 寄存器组合成 0xFFFF0 。
在开机接电的一瞬间,CPU的 cs:ip 寄存器被强制初始化为 0xF000: 0xFFF0。在实模式下0xF000: 0xFFF0等效于入口地址 0xFFFF0
因为BIOS在实模式下运行,而实模式只能访问1MB的空间,超出0xFFFFF就会溢出。0xFFFF0~0xFFFFF只有16字节的空间,所以此处并不是BIOS真正的代码,而是一个跳转指令jmp far f000:e05b
。0xFE05B才是代码真正开始的地方。接下来BIOS便马不停蹄的检测内存、显卡等外设信息,当检测通过,并初始化好硬件后,开始在内存0x000~0x3FFF处建立数据结构,中断向量表IVT并填写中断例程。
2.3 为什么是 0x7c00
BIOS最后一项工作是校验启动盘中位于0盘0道1扇区的内容。如果此扇区末尾的两个字节分别是魔数 0x55 和 0xaa,BIOS便认为此扇区中存在可执行程序(主引导记录MBR),便加载到物理地址0x7c00,随后远跳转jmp 0: 0x7c00
到地址 0x7c00,继续执行。
为什么是0盘0扇区
作者理解为规定,主引导记录mbr是段程序,无论位于软盘、硬盘或者是其他介质,总该有个地方保存它。与其BIOS通过检测存储设备去寻找mbr,不如提高效率,选择离BIOS最近的位置。
为什么是物理地址 0x7c00
“0x7c00”最早出现在IBM公司的个人电脑PC5150的ROM BIOS的INT19H中断处理程序中。通电之后,BIOS处理程序开始自检,随后调用BIOS中断0x19h,即 call int 0x19h。在此中断处理函数中,BIOS要检测这台计算机有多少可用的硬盘或软盘,如果检测到了任何可用磁盘,BIOS就把它第一扇区加载到0x7c00。现在搞清楚了,它是属于BIOS中的规范。
MBR不能随便放,首先不能覆盖已有的数据,其次不能过早的被其它数据覆盖。通常,MBR的任务是加载某个程序(这个程序一般是内核加载器,很少直接加载内核的)到指定位置,并将控制权交给它。所谓交控制权就是jmp过去而已。之前说的过早被覆盖,是指不能让mbr破坏自己,比如加载程序,如内核加载器,其放置的内存位置不能在MBR自己的所在范围。
按DOS 1.0要求的最小内存32KB来说,MBR需要尽可能多的预留空间,这也保证了不会过早被覆盖,所以MBR只能放在32KB的末尾。
MBR本身是程序,程序必须有栈,栈也在内存中,MBR虽然本身只有512字节,但还有为其分配栈空间,所以其实际所用的内存要大于512字节,估计1KB内存够用了。
结合以上三点,选择32KB最后的1KB最为合适,32KB换算为16进制为0x8000,减去1KB(0x400)的话,等于0x7c00。可见加载MBR的位置取决于操作系统本身所占内存大小和内存布局。
2 让MBR先飞一会儿
MBR必须是512个字节,这是为了保证魔数0x55和0xaa恰好在该扇区的最后两个字节处。
2.1 伪指令$和$$,section
$和$$是编译器NASM预留的关键字,表示当前行和本section的地址,起到了标号的作用,相当于伪指令。
伪指令是编译器为了开发人员写代码方便而提供的一些符号,这些符号在编译时,由编译器转换成CPU可识别的东西,如指令或地址等。
section 是伪指令,CPU运行程序是不需要这个东西的,这个只是用来给程序员规划程序用到,有了section,就可以将自己的代码分成一段一段,当然这只是逻辑上的段,实际上编译出来的程序还是完整的一体。
2.2 NASM简单用法
nasm的基本用法:
nasm -f <format><filename> [-o <output>] // -f是用来指定输出文件的格式 -o就是指定输出可执行文件的名称
使用nasm -hf
命令来查看 -f 有多少种有效的输出格式。
一共21种,现在只关注bin和elf格式。
bin是纯二进制文件,纯二进制就是不参杂其他的东西,直接给CPU后就能用,也就是可执行文件什么样,内存就是什么样。我们平时所说的elf或pe格式的二进制文件,那里面有很多和指令无关的东西,里面掺杂了程序的内存布局、位置等信息,这是给操作系统中的程序加载器用的,是属于操心系统规划的范畴了。
下面是作者提供的代码 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号功能,上卷全部行,则可清屏。
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能描述:上卷窗口
;------------------------------------------------------
;输入:
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;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),
; VGA文本模式中,一行只能容纳80个字符,共25行。
; 下标从0开始,所以0x18=24,0x4f=79
int 0x10 ; 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 $ ; 使程序悬停在此
message db "1 MBR"
times 510-($-$$) db 0
db 0x55,0xaa
编写好后,nasm -o mbr.bin mbr.S
回车编译成bin文件。bin文件大小是512字节。
下面需要将bin文件写入0盘0道1扇区中,这里使用Linux的命令:dd
dd if=/root/bochs/mbr.bin of=/root/bochs/hd60M.img bs=512 count=1 conv=notrunc
执行成功会有如下输出:
接下来,启动bochs测试一下,在安装目录下bin/bochs -f bochsrc.disk
,回车后,弹出bochs所模拟的机器,后面的终端的控制台是bochs的控制台,在控制台上输入字符c
,回车开始运行模拟的机器。
MBR运行起来后,效果如下: