想要进行Linux内核开发,就必须要对计算机的启动流程有一个大致的了解,知道系统开机后,它的内部到底做了什么事,我们的系统是怎么跑起来的,同时还需要学习MBR的作用,开机后BIOS检测内存、显卡等外设信息,完成初始化硬件的工作后,就将接力棒交由MBR来加载我们的内核,本博客的初步工作就是编写一个简单的MBR。
MBR(主引导记录)是存储在计算机硬盘的第一个扇区中的一个特殊的引导扇区。它包含了启动计算机所需的信息,例如分区表和引导程序。当计算机开机时,BIOS会读取MBR中的引导程序,并将控制权交给它。引导程序再根据分区表信息加载操作系统的引导程序,最终启动操作系统。因此,MBR是计算机启动的关键。
目录
1.BIOS
当我们按下计算机的power键,然后计算机上电启动,运行BIOS(基本输入输出系统),BIOS的存放位置在哪呢?其实这和实模式下的1MB布局有关。
BIOS程序是主板自带的,出厂之前烧录到主板的ROM里面的
众所周知,Intel 8086有20条地址线,即可以访问1MB的内存空间,其中1MB=2的20次方=1048576,也就是16进制下的0x00000—0xFFFFF。
这里比较重要的是画红框的两个地址,后面会讲到。
从以上的内存分布可以看到,地址0—0x9FFFF处是动态随机访问内存DRAM(动态随机访问内存,断电即丢失),也就是我们插在主板的内存条,但是,内存条的内存不等于全部的物理内存,而0xF0000—0xFFFFF这64KB内存则是ROM(只读存储),其中存放BIOS的代码(其实这里存储的只是跳转指令)。是 BIOS 的入口地址,此处的内容是 跳转指令 jmp f000: e05b
。其中 其中 0xFFFF0—0xFFFFF
这段 16 字节,BIOS 的作用是 检测、初始化硬件 和 建立中断向量表,这里的建立操作对硬件来说就是 IO 操作。
BIOS主要用来检测和初始化硬件,建立中断向量表。BIOS程序在内存最开始的位置(即 0x00000) 用1KB的内存空间(0x00000 ~ 0x003FF)构建中断向量表, 并在紧挨着它的位置用256 B的内存构建BIOS 数据区(0x00400 ~ 0x004FF),在大约56KB以后的位置(0x0E2CE)加载 8KB 左右的与中断向量表相应的若干中断服务程序。
实模式(实地址模式)
计算机刚加电时处于实模式下
程序按照8086寻址方式访问0h-FFFFFh(1MB)空间
寻址方式:物理地址(20位)=段地址:偏移地址
CPU单任务运行
保护模式计算机启动成功后处于保护模式下
寻址方式:段(32位)和偏移量(32位),寻址4GB空间
段页式寻址机制(段,页)
虚拟地址,进程,封闭空间
应用程序和操作系统的运行环境都被保护
CPU支持多任务
2.BIOS的启动
BIOS作为计算机上第一个启动的软件,是由只读存储器ROM加载的,其入口地址就是0xFFFF0;当CPU去执行BIOS时,其cs:ip值会组合成0xFFFF0。CPU访问内存采用的是分段访问机制,即段地址+偏移地址;由于在实模式下,段地址需要乘以16再和偏移地址相加,从而得到物理地址。而在开机的一瞬间,CPU的cs:ip寄存器就会被强制初始化为0xF000:0xFFF0,就此得到BIOS的入口地址0xFFFF0。由于BIOS是在实模式下运行的,而实模式只能访问1MB的空间,而0xFFFF0离1MB只有16字节了,并不足够存放BIOS的代码,因此物理地址0xFFFF0处存放的内容实际上是跳转指令(jmp f000:e05b),让CPU的执行流跳转到BIOS代码真正开始的地方。紧接着BIOS便会开始检测内存、显卡等外设信息,然后初始化硬件,在0x000—0x3FF处建立数据结构、中断向量表IVT并填写中断例程。到此,BIOS便完成了自己的使命,接下来进行接力,将执行权交给MBR。
3.地址0x7c00
BIOS的最后一项工作是校验启动盘中位于0盘0道1扇区(也就是主引导记录MBR)的内容,如果此扇区末尾两个字节是0x55和0xaa,BIOS便会认为此扇区中确实存在可执行程序(包括内核程序),然后将可执行程序加载到地址0x7c00,也就是说,这些可执行程序(包括内核程序)就放在内存物理地址为0x7c00的位置上。
MBR主引导扇区位于磁盘的第一个扇区,即0号扇区,主要由引导代码、分区表、结束标志(0x55 0xaa)三部分构成,总共占512字节。
4.MBR简介
首先明确一点:主引导记录(MBR,Master Boot Record)是装有Linux系统的硬盘的第一个扇区,即C/H/S地址的0柱面0磁头1扇区,也叫做MBR扇区(来源百度百科)。所以MBR应该是一个硬件。
在这里我们简单介绍一下MBR。
计算机接电后运行的是BIOS,它完成检测和初始化工作后就会处理器使用权交给MBR。MBR位于整个硬盘最开始的扇区,称为MBR主引导扇区,其内容是:
446字节的引导程序和参数;
64字节的分区表;
2字节结束标级0x55和0xaa。
在得到控制权后,MBR开始寻找“次引导程序”(内核加载程序),然后再移交控制权。
MBR找到第一个活动分区后,会加载活动分区的代码。通常这段代码所在位置也是活动分区的第一个扇区,即512KB,这段代码通常被称为boot代码(boot.bin),这个扇区的最后两个字节也是0x55aa。最后MBR会将权限交给boot.bin,实际上boot.bin可以直接就是内核代码本身。
可以用下面这张简图,描述一下内核程序的加载运行流程。
5.自己写一个内核程序
本部分将做一个实验,验证上面的一系列说法。
MBR的大小必须是512字节,我们使用bochs模拟器,它模拟的是x86平台。
1.制作一个镜像
Linux命令行下输入bximage指令:输入1,回车——>输入fd,回车——>我选择的默认大小1.44M,直接回车——>输入镜像名:boot.img。
创建成功后,目录下就会生成boot.img文件,同时Linux会输出以上信息,注意上面画红框的内容,意思是说:“floppya:image="boot.img", status=inserted”这句话要出现在bochsrc配置文件中。
2.bochsrc配置文件
下面创建bochsrc配置文件,文件内容如下:
# 下面两个配置可以打开寄存器监视窗口
# magic_break: enabled=1
# display_library: x, options="gui_debug"
megs: 32
romimage: file=$BXSHARE/BIOS-bochs-latest
vgaromimage: file=$BXSHARE/VGABIOS-lgpl-latest
floppya: 1_44=boot.img, status=inserted
boot: a
floppya:image="boot.img", status=inserted
log: run.log
mouse: enabled=0
keyboard: keymap=$BXSHARE/keymaps/x11-pc-us.map
下一步,就开始些内核文件,命名为boot.asm。内容简单写一下,大概作用就是让内核程序打印出一句话——“hello,Linux!”。
ORG 0x7c00 ;程序开始的地址
mov ax,cs ;使用cs初始化其他寄存器
mov ds,ax ;通过jmp 0:0x7c00到的MBR开始地址
mov es,ax ;此时ax为0,也就是用0初始化其他寄存器
call MyPrint ;打印函数
jmp $ ;让程序死循环
MyPrint:
mov ax,msg
mov bp,ax
mov cx,16
mov ax,0x01301
mov bx,0x000c
mov dl,0
int 10h ;中断
ret
msg: db "hello,Linux!"
times 510 - ($ - $$) db 0 ;填充剩下的空间,使生成的二进制代码为512字节
db 0x55, 0xaa ;MBR结束标志
$和$$是nasm编译器中的关键字,用来表示当前行和本section的地址,而该地址是由编译器确定的,默认情况下它们的值是相对于本文件开头的偏移量。
org是一个伪指令,这些伪指令并不是真正的汇编代码,而是给编译器使用的。
section定义的段
(PS:2.8段的概念_10000h到100ff单元组成的一个段,该段的所有有关信息有哪些-CSDN博客)
$:代码当前位置
$$: 段当前的起始位置。
3.boot.asm编译
下面开始对boot.asm进行编译
nasm boot.asm -o boot.o
dd if=boot.o of=boot.img bs=512 count=1 conv=notrunc
dd是Linux的一个命令,用于磁盘操作,键入man dd可以看到该语句的帮助文件,简单来说该语句主要内容是:
if 指定要读取的文件
of 指定把数据输出到哪个文件
bs 指定块的大小
count 指定拷贝的块数
conv 指定如何转换文件
4.运行和验证
然后我们就可以运行这个MBR程序了!输入bochs -q -f bochsrc,可以看到:
继续输入c,则会看到内核会打印出“hello,Linux!”。
按ctrl+c,退出输入行,再输入exit,退出程序。
如果在bochsrc配置文件中开启下面这两个配置,就会开启寄存器监控窗口:
magic_break: enabled=1
display_library: x, options="gui_debug"
继续运行bochs -q -f bochsrc,就能看到如下窗口,每个寄存器的数据信息就显示出来了,这对于后期继续调试寄存器很重要。
CPU在运行程序时,一般是从内存中将数据读入到合适的寄存器,在寄存器中计算好结果,再将结果写会内存。
点击左上角的Continue,依然会显示:
同时我们也寄存器窗口看到有这样一段信息: jmpf 0xf000:e05b。
jmp f000:e05b
它的意思是跳转到了(f000 << 4) + e05b = fe05b处,这里的段基址左移四位的原因是,在实模式下段基址寄存器只有16位,想一下,16位的寄存器最多访问2^16=64KB的空间,我们想访问实模式下1MB的空间的话就需要将段基址左移4位,自然就可以访问到1MB的空间了,这么做的原因也是出于兼容性而采取的曲线救国方式,虽然我们现在的OS都已经到了64位,它还得向下兼容。
当我们的电脑加电的一瞬间cs:ip就会被强制置位f000:e05b了,接下来就对内存,显卡等外设进行检查,做好它的初始化工作之后就完成它的任务了,在最后的时候,BIOS会通过绝对远跳,将控制权交给MBR,让它来加载内核。
jmp 0:0x7c00
现在来验证一下:我们启动bochs模拟器,然后最开是左侧模拟器窗口完全是黑的,我们在右侧输入行不停输入:n,会看到随着输入的n个数变多,左侧模拟器窗口的设备图标变多了,这个过程应该就是系统在进行BIOS自检的过程。
(输入4个n)
(输入68个n)
随着输入n的次数变多,可以看到地址也在发生变化,这就是BIOS的自检过程,我们省略了中间的步骤,直接在输入行输入b 0x7c00,让程序运行地址直接来到0x7c00,然后再按c,继续连续按n,我们看到模拟器窗口打印出了“hello,Linux!”,而且也输出了我们编写的boot.asm代码。
最后我们来验证一下,boot.o文件的确为512字节:
然后我们查看一下boot.o的内容,由于它是一个二进制文件,所以我通过clion中的BinEd插件查看(没有的话可以在插件商店下载一个)。
打开boot.o文件后,我们看到了,最后两个字节的确是55AA,文件前半部分是我们编写的代码,中间填充的都是0:
以上,基本的MBR程序就写完了。