实现X86_64架构下的BootLoader(一)Boot引导原理

July 17, 2019 2 min read Author: Yu

作为唤醒机器拉起系统内核的第一步,需要用到BootLoader的地方太多了,并且实现原理也完全不同,本次实现的是X64架构下基于BIOS引导的BootLoader,目前主流的引导方式是UEFI,这个以后有机会的话会实现,传统BIOS和UEFI的流程如下: img.png

如果用一段话简短概括整个流程,那就是在整个机器启动时,BIOS进行自检,自检结束后根据设置的启动顺序来检测对应的磁盘,检测的内容是读取目标磁盘的第0磁头第0磁道第1扇区中的最后两个字节,并判断是否为0x55和0xaa (固定值) ,如果是,则认为该扇区内有引导程序,将该扇区的内容加载到内存中,并初始化CPU,将CS:IP初始化为0000<7c00>(固定值),在CPU以实模式运行时通过CS寄存器和IP寄存器配合来执行内存中的指令,过程如下图:

img_1.png

BIOS将扇区中的内容加载至内存,并将CS,然后启动CPU,本质上就是将控制权由BIOS交给程序,也就是说,在第一扇区存储的的程序必须满足三个要求:

  • 大小为512字节
  • 以55和aa结尾(16进制)
  • 指令从0x7c00开始执行
  • 根据这些特性,我们可以写出如下代码:
org 0x7c00
BaseOfStack equ 0x7c00
Label_Start:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
;清屏
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 0184fh
int 10h
;置光标位置为初始值
mov ax, 0200h
mov bx, 0000h
mov dx, 0000h
int 10h
;显示数
mov ax, 1301h
mov bx, 000fh
mov dx, 0000h
mov cx, 10
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartBootMessage
int 10h
;要显示的为10个字符,所以前面CX计数器置10
StartBootMessage: db "start boot"
times 510 - ($ - $$) db 0
dw 0xaa55

上面的指令有两个地方需要注意,一个是进行显示相关操作的时候用到了BIOS中断,这是一份BIOS中断调用表,在里面记录着部分中断调用,在产生中断调用(INT)时,根据各个寄存器里预置的不同值,产生不同的处理结果, 在这个程序中只用到了显示中断调用,也就是INT 10h中的内容。还有一个需要注意的地方是最后的times 510 - ($ - $$) db 0$表示的是当前指令段编译后的地址,$$表示当前Section编译后的起始地址,所以整句的含义是将文件用0填充至510字节(注意,times是伪指令,在编译时就会被解释),最后补上结束符0xaa55

将上面的汇编程序编译,得到二进制文件,文件大小为512B,如果不是那么应该是times那里写的有问题。

如何测试这段程序呢?这段程序可以在任何兼容x86架构的物理机或虚拟机运行,这次我们用最常用的虚拟机+虚拟3.5寸软盘实现,其实本来想用真东西玩玩的,结果翻出来了一堆软盘,就是没翻出来软驱,而且后续程序不会再在软盘环境测试,还是不费那个功夫了。那么就需要将这512B的文件装在一个软盘映像文件里,注意,这个映像文件不要用现有的任何例如UltraISO这样的第三方程序生成,因为这样生成的都是自带文件系统的,这些文件系统的定义和代码一般在开头,导致我们的程序无法在物理位置放置于软盘的第一扇区,所以这个文件需要自己来生成。我比较推荐的方法是借助现有工具从原理上实现,例如以下这个Java程序:

public static void main(String[] args) throws IOException {
if (args.length != 2) {
System.out.println("param error.");
return;
}
InputStream in = new FileInputStream(args[0]);
byte[] b = new byte[512];
in.read(b);
in.close();
byte[] b2 = new byte[1474560];
System.arraycopy(b, 0, b2, 0, 512);
in.close();
File f = new File(args[1]);
if (!f.exists()) {
f.createNewFile();
}
OutputStream out = new FileOutputStream(f);
out.write(b2);
out.close();
}

整个过程很简单,读512字节数据,复制进长度为1.4410001024的字节数组中,并写入目标文件,这样目标文件的大小就刚好为1.44MB,并且以512B的Boot程序作为开始。当然这里想用什么语言实现都可以,只要保证大小以及Boot程序位置正确就行了,注意mb换算kb的比值是1000不是1024。如果在这一步你不打算用任何编程语言也是可以完成的,找一款能运行在你的平台下的十六进制编辑器,也可以完成以上内容,打开编译好后的二进制文件:

img_2.png

复制所有内容到一个新二进制文件中,或者在原文件末尾追加0,一直填充到16<8000h之前就行了>,相信如果你真的这么做了,就会发现写一个java或python程序来帮你填充确实是个不错的选择。

将处理好的软盘映像文件(为了方便后面叫boot.img)插入虚拟机就可以使用了,具体怎么配置,不管是boch还是vmware都大同小异,这里懒得说了直接给图:

img_3.png

然后就可以启动虚拟机了:

img_4.png

这个Boot除了显示了个字符串之外什么都没有做,后续会编写一个简单的文件系统,并且编写loader程序然后用boot拉起loader。