20180808失忆的操作系统内核实现(三) 计算机的启动过程&bootloader的编写
三、PC机的启动过程&bootloader的编写
①传统启动方式
传统计算机的启动主角是BIOS,BIOS(Basic Input/Output System 基本输入输出系统),是一个固话在主板上的软件程序,由主板厂商定义。
启动过程主要分为几个阶段,首先是执行BIOS程序,它会做一堆检查,包括检索可以启动的存储设备——它需要直到"下一阶段的启动程序"放在什么地方;然后它将这个"启动程序"放在内存的一个特定位置,剩下的事情就交给这个"启动程序"了。硬盘的启动程序可能在硬盘的开头MBR(Main Boot Record)里,然后这段程序将加载操作系统内核到内存里,下面的事情就由内核管理了。
(这个不重要,我的电脑既然支持UEFI,就不想编写汇编来启动了)
②目前流行的启动方式
传统的BIOS启动方式没有一个统一的标准,后来Intel和其他厂商牵头搞了一个统一的标准:UEFI(Unified Extensible Firmware Interface统一的可扩展固件接口)。对于我的操作系统内核来说,只需要了解它的API(Application Programming Interface 应用程序接口)怎么调用就行了。
UEFI参考网站:http://wiki.phoenix.com/wiki/index.php/UEFI
需要准备的参考/引用项目有:
①gnu-efi:一个用于编写UEFI app的编译库文件,比巨大的UDK小得多得多,项目地址:https://github.com/vathpela/gnu-efi
②OVMF:可以为虚拟机提供UEFI的虚拟环境,下载地址:https://sourceforge.net/projects/edk2/files/OVMF/
编写uefi app有许多项目,比如UDK2018,但是我嫌它的配置太麻烦,这个系统并不是围绕UEFI展开的。我们需要的是轻便的项目,一个bootloader(启动加载器),它负责:显示一个好看的界面,并寻找硬盘上的内核,加载到内存中来。这里推荐使用gnu-efi。
好了,下面建立一个项目文件夹,预示着项目的开始。文件夹结构如下:
AmnesiaOS #项目文件夹
├── bootloader
│ ├── gnu-efi #gnu-efi项目,可从上述链接下载,也可用git clone命令下载
│ ├── main.c #bootloader主程序
│ └── Makefile
├── OVMF-X64-r15214 #下载的OVMF,解压到这个位置
│ └── OVMF.fd #使用OVMF必须的文件
├── Makefile #Makefile,(一种帮忙执行编译链接命令的程序,写好之后,只需要一个make即可自动执行)
├── ReadMe.txt #说明文档,内容随便写
└── start.sh #启动仿真器的命令
现在,可以开始编写main.c中的内容了,下面是一个bootloader的helloworld,效果是在屏幕上输出"Hello World!",预示着项目的开始。
(/bootloader/main.c)
1 #include <efi.h> 2 #include <efilib.h> 3 4 EFI_STATUS 5 efi_main(EFI_HANDLE image_handle, EFI_SYSTEM_TABLE *systab) 6 { 7 SIMPLE_TEXT_OUTPUT_INTERFACE *conout; 8 9 //gnu-efi的要求,加载系统表systable 10 InitializeLib(image_handle, systab); 11 12 //调用API在屏幕上输出Hello World! 13 conout = systab->ConOut; 14 uefi_call_wrapper(conout->OutputString, 2, conout, (CHAR16 *)L"Hello World!\n\r"); 15 16 //按任意键后退出模拟器 17 WaitForSingleEvent(systab->ConIn->WaitForKey, 0); 18 gRT->ResetSystem(EfiResetShutdown, EFI_SUCCESS, 19 0, NULL); 20 21 return EFI_SUCCESS; 22 }
下面开始对整个bootloader进行编译,首先是gnu-efi的编译,在bootloader目录下执行以下命令:
git clone https://github.com/vathpela/gnu-efi.git #如果已经下载则不需要进行这一步 cd gnu-efi #进入gnu-efi目录 make #执行gnu-efi项目的makefile #这个时候gnu-efi目录下将生成x86_64文件夹,所需的目标文件在这里面 cd .. #回到bootloader文件夹 gcc -Ignu-efi/inc -Ignu-efi/inc/x86_64 -Ignu-efi/inc/protocol \ -Wno-error=pragmas -Wall -Wextra -Werror \ -mno-red-zone -mno-avx \ -fpic -fshort-wchar -fno-strict-aliasing -ffreestanding -fno-stack-protector -fno-stack-check -fno-merge-all-constants \ -DCONFIG_x86_64 -DGNU_EFI_USE_MS_ABI -maccumulate-outgoing-args --std=c11 -D__KERNEL__ \ -g -O2 \ -c main.c -o main.o #这条命令表示调用gcc进行编译,-c表示对main.c进行编译但不链接,-I表示Include包含文件夹,-W表示Warning显示/禁用某些警告,-g生成调试信息,-O2优化程度,-f顾名思义…… ld -nostdlib --warn-common --no-undefined --fatal-warnings --build-id=sha1 -shared -Bsymbolic \ -T gnu-efi/gnuefi/elf_x86_64_efi.lds \ gnu-efi/x86_64/gnuefi/crt0-efi-x86_64.o main.o -o bootloader.so \ -L gnu-efi/x86_64/lib -lefi -L gnu-efi/x86_64/gnuefi -lgnuefi #这条命令表示调用ld进行链接,-T表示指定链接脚本(可以打开内容看看),-L和-l表示加载需要的库文件这里是libefi.a和libgnuefi.a,输出bootloader.so objcopy -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel -j .rela -j .rel.* -j rela.* -j .rela* -j .reloc \ --target efi-app-x86_64 \ bootloader.so bootx64.efi #这条命令表示调用objcopy将so文件转换为所需要的efi文件,-j命令后面接的是so文件中各个段的名称,这些段将按照命令的顺序生成efi文件。
到这一步为止,生成的bootx64.efi是一个合格的UEFI app,如果手头上有U盘,且U盘的文件系统格式为fat,那么在U盘根目录下建立文件夹boot/efi,并将这个文件拷贝到该文件夹里,重启电脑,通过UEFI的U盘启动,即可在屏幕上看到令人兴奋的输出"Hello World!"——
*这里我曾经想过,做一个UEFI app的小游戏,并替换掉当前电脑内的启动文件,当且仅当游戏通过的时候才调用真正的引导文件,才能进入系统,这个似乎比较好玩……
注:UEFI协议中规定,UEFI将从ESP分区(FAT文件系统格式)中的boot/efi或者boot/Microsoft目录中加载启动程序。
而,作为测试,我们需要在支持UEFI的虚拟机里启动这个文件。然而,截至目前为止,WSL中并未加入挂载文件到回环设备(mount as loop device)的功能,这里暂时找不到办法来挂载并修改仿真用的虚拟磁盘。
因此,这里采用折中的办法,进入UEFI Shell(命令行)中手动调用这个启动程序。输入下列命令启动qemu:
export DISPLAY=:0 #确保X服务与Win10的Xming连接 qemu-system-x86_64 -bios ../OVMF-*/OVMF.fd \ -drive file=fat:.,media=disk,format=raw
命令的大意是,指定仿真用的bios为OVMF,并将当前目录仿真一个fat格式的硬盘
此时将启动一个窗口,相当于一个仿真的显示器,等待一会进入UEFI Shell
输入:
fs0:
bootx64.efi
运行后界面截图如下:
上述的过程大致上就是C语言从源文件到可执行文件的编译过程,总结起来如下图所示。
gcc命令将c文件和h文件进行预处理、汇编、编译,生成目标文件o文件,o文件包含了可执行的机器码,也包含未链接的符号、函数名称等等链接所需要的信息。
ld命令将这些目标文件可其他库文件按照链接文件的格式、要求,生成动态链接库文件或者可执行文件,在Linux系统下,可执行文件是ELF格式的,具体格式需查阅相关信息。
而,我们这里需要的是efi格式的文件,它有它自己的格式。objcopy命令就是将上述生成的动态链接库转换成efi格式。
为了以后不需要输入那么多的命令,上述过程可以写成一个Makefile文件,该文件内容如下:
(/bootloader/Makefile)
#这些是变量 SOURCE = main.c TARGET = bootx64.efi EFILIB = gnu-efi/x86_64/gnuefi/crt0-efi-x86_64.o #Makefile的第一项可以由make命令直接执行,该项目也可以由make all命令执行 all: $(TARGET) #表示生成$(TARGET)目标,需要$(SOURCE)和$(EFILIB),下同 $(TARGET): $(SOURCE) $(EFILIB) gcc -Ignu-efi/inc -Ignu-efi/inc/x86_64 -Ignu-efi/inc/protocol \ -Wno-error=pragmas -Wall -Wextra -Werror \ -mno-red-zone -mno-avx \ -fpic -fshort-wchar -fno-strict-aliasing -ffreestanding -fno-stack-protector -fno-stack-check -fno-merge-all-constants \ -DCONFIG_x86_64 -DGNU_EFI_USE_MS_ABI -maccumulate-outgoing-args --std=c11 -D__KERNEL__ \ -g -O2 \ -c main.c -o main.o ld -nostdlib --warn-common --no-undefined --fatal-warnings --build-id=sha1 -shared -Bsymbolic \ -T gnu-efi/gnuefi/elf_x86_64_efi.lds \ gnu-efi/x86_64/gnuefi/crt0-efi-x86_64.o main.o -o bootloader.so \ -L gnu-efi/x86_64/lib -lefi -L gnu-efi/x86_64/gnuefi -lgnuefi objcopy -j .text -j .sdata -j .data -j .dynamic -j .dynsym -j .rel -j .rela -j .rel.* -j rela.* -j .rela* -j .reloc \ --target efi-app-x86_64 \ bootloader.so bootx64.efi $(EFILIB): make -C gnu-efi #通过git下载gnu-efi,该项目可以由make dl_gnuefi执行 dl_gnuefi: git clone https://github.com/vathpela/gnu-efi.git #清楚目录下的生成的文件,由make clean执行 clean: rm *.o *.so *.efi #打开qemu并测试的项目,make test test: export DISPLAY=:0 #确保有X服务,Win10下的Xming需要处于运行状态 qemu-system-x86_64 -bios ../OVMF-*/OVMF.fd \ -drive file=fat:.,media=disk,format=raw
注:编译、链接所需的参数不是我想出来的,而是参考gnu-efi项目中的Makefile得到的