How computer boots up#
首先是按下电源键进入 BIOS。这时会进行自检(POST or power-on self-test),如果此时出现蜂鸣声,则代表失败。以前蠢作者的 HP 台式机出现过这种情况,这种蜂鸣声是有节奏的,可以去查对应的蜂鸣代码列表来排查故障。没有出现问题的话,BIOS 会根据设置来选择一个设备(U盘、硬盘)。通常,Linux 都是从硬盘上引导的,其中主引导记录(MBR)中包含主引导加载程序。MBR 是一个 512 字节大小的扇区,位于磁盘上的第一个扇区中(0 道 0 柱面 1 扇区)。当 MBR 被加载到内存中之后,BIOS 就会将控制权交给 MBR。MBR 以两个特殊的字节 0xAA55 结束。BIOS 便是根据这两个字节来判断此设备是否可以用于启动;如果不是则会尝试去启动启动顺序表中的下一个设备
PS 0xAA55 的值为 0b1010101001010101,貌似是因为这种和谐的格式。另外这 512 字节是载入到内存的 0x7c00 处
由于 MBR 的容量限制无法容纳整个 bootloader 程序(可使用的仅有 446 字节,另外分区表占 64 字节)。以 GRUB 为例,分为三个启动阶段
stage1:安装在 MBR 中 stage1.5:存放在 MBR 之后的扇区中,可以帮助识别文件系统 stage2:存放在磁盘分区上的,一般都在/boot/grub/目录下
如果是双系统,可以在 stage2 时看到系统选择界面。之后便是载入内核映像文件
更详细的介绍可以戳这里
计算机是如何启动的? - 阮一峰的网络日志
Linux 的启动流程 - 阮一峰的网络日志
Multiboot#
GRUB 实现了 Multiboot 的规范 PDF,只要我们的遵循这份规范,我们便可以使用 GRUB 来载入内核。一个好处就是它可以帮助我们从 real mode 切换到 protected mode
编写 multiboot_header.asm
section .multiboot_header
header_start:
dd 0xe85250d6 ; magic number (multiboot 2)
dd 0 ; architecture 0 (protected mode i386)
dd header_end - header_start ; header length
; checksum
dd 0x100000000 - (0xe85250d6 + 0 + (header_end - header_start))
; insert optional multiboot tags here
; required end tag
dw 0 ; type
dw 0 ; flags
dd 8 ; size
header_end:
需要解释一下 checksum,规范中是这样表述的
The field checksum is a 32-bit unsigned value which, when added to the other magic fields (i.e. magic, architecture and header_length), must have a 32-bit unsigned sum of zero.
即 checksum + magic_number + architecture + header_length = 0
,所以 checksum = -(magic_number + architecture + header_length)
因为是一个 u32
,所以这里使用了溢出的 trick
接下来需要进行汇编生成 multiboot_header.o
文件
nasm -f elf64 multiboot_header.asm
Say Hello#
编写 boot.asm
文件
global start
section .text
bits 32
start:
; print `Hello` to screen
mov WORD [0xb8000], 0x1f48;
mov WORD [0xb8002], 0x1f65;
mov WORD [0xb8004], 0x1f6c;
mov WORD [0xb8006], 0x1f6c;
mov WORD [0xb8008], 0x1f6f;
hlt
start
label 是 GRUB 一个约定,kernel 的入口(类似 main 函数)。bits 32
指定接下来的指令是 32 位的。[0xb8000]
是一个特殊的地址,向其后的 32KB 内存中写入数据,会打印至屏幕。原理是 memory-mapped,IO 设备上的设备内存和寄存器都已经被映射到内存空间的某个地址。这样设备内存也可以通过内存访问指令来完成读写。写入需要按照一定的格式,每个输出字符占用两个字节,一个字节是 ASCII 码,另一个字节是属性。比如 Hello
的输出
0x000b8000: 'H', colour_for_H
0x000b8002: 'e', colour_for_e
0x000b8004: 'L', colour_for_L
0x000b8006: 'l', colour_for_l
0x000b8008: 'o', colour_for_o
属性字节的最高位取决于硬件,接下来的 3 位是背景色,最后 4 位是前景色,详细的戳这里 VGA text buffer
接下来进行汇编
nasm -f elf64 boot.asm
Linking#
我们将两个 .o
文件进行连接,就像 PPAP 那样。编写 linker.ld
文件
ENTRY(start)
SECTIONS {
. = 1M;
.boot :
{
/* ensure that the multiboot header is at the beginning */
*(.multiboot_header)
}
.text :
{
*(.text)
}
}
linker script 的语法可以参考 这里
ENTRY
用于指定入口,需要和前面保持一致,设置为 start
. = 1M
指定将此 section 放于 1M 处的内存空间中,这是一个传统的位置。低于 1M 的内存空间有可能用于 memory-mapped
剩下的就是指定嵌入的 section
进行连接时需要指定 -n
参数禁止 page align,这会影响到 GRUB 的识别
ld -n -o kernel.bin -T linker.ld multiboot_header.o boot.o
Making ISO#
建立如下的工程目录,将刚才生成的 kernel.bin
拷贝至指定位置
isofiles/
└── boot
├── grub
│ └── grub.cfg
└── kernel.bin
编写 grub.cfg
文件,你可以观察一下你自己 Linux 中的 /boot/grub/grub.cfg
文件
set timeout=0
set default=0
menuentry "kurumi" {
multiboot2 /boot/kernel.bin
echo 'kurumi is booting...'
boot
}
如果你使用过双系统,并用 GRUB 作为引导。那么你开机时应该会看到一个菜单,让你选择启动的系统,set timeout = 0
正是设置此菜单的超时时间。set default = 0
用于设置默认启动的启动项。menuentry
用于设置每一个启动项的详细信息,有几个系统就会出现几个 menuentry
block
下面的命令用于生成 iso
镜像
grub-mkrescue -o os.iso isofiles
使用 qemu 启动
qemu-system-x86_64 -cdrom os.iso
Reference#
Inside the Linux boot process
Printing To Screen - OSDev Wiki
A minimal x86 kernel | Writing an OS in Rust