在计算机启动过程中,引导程序(Bootloader)是连接硬件和操作系统的关键桥梁。当按下电源键时,主板固件(如BIOS或UEFI)会首先加载并执行存储设备上的引导程序,再由引导程序负责初始化硬件环境、加载操作系统内核。市面上常见的GRUB、Syslinux等工具虽然功能强大,但隐藏了大量实现细节。
自制x86引导程序的意义在于:
注意:本实验需要物理x86计算机或支持硬件虚拟化的虚拟机(如QEMU),普通VMware/VirtualBox可能无法正确模拟实模式环境。
bash复制# 编译工具
sudo apt install nasm gcc binutils
# 模拟器
sudo apt install qemu-system-x86
# 磁盘工具
sudo apt install mtools
code复制bootloader/
├── boot.asm # 主引导记录(MBR)代码
├── loader.asm # 第二阶段加载器
├── Makefile # 构建脚本
└── kernel.bin # 示例内核(占位文件)
x86 CPU启动时处于16位实模式,内存访问遵循以下规则:
典型内存映射:
code复制0x00000-0x003FF IVT(中断向量表)
0x00400-0x004FF BDA(BIOS数据区)
0x7C00-0x7DFF MBR加载位置
0xA0000-0xFFFFF 硬件保留区域
主引导记录包含三部分:
示例MBR汇编框架:
nasm复制[org 0x7C00] ; 设置加载地址
[bits 16] ; 16位实模式
start:
cli ; 禁用中断
xor ax, ax ; 清零段寄存器
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00 ; 设置栈指针
sti ; 启用中断
mov si, msg
call print_string
jmp $
print_string:
lodsb ; 加载SI指向的字符
or al, al ; 检测NULL终止符
jz .done
mov ah, 0x0E ; BIOS显示功能
int 0x10 ; 调用BIOS中断
jmp print_string
.done:
ret
msg db "Booting...", 0
times 510-($-$$) db 0 ; 填充剩余空间
dw 0xAA55 ; 魔数
BIOS提供INT 0x13中断进行磁盘操作:
示例加载第二阶段的代码:
nasm复制load_stage2:
mov ax, 0x1000 ; 加载到ES:0x1000
mov es, ax
xor bx, bx
mov ah, 0x02 ; 读功能
mov al, 4 ; 读取4个扇区
mov ch, 0 ; 柱面0
mov cl, 2 ; 从第2扇区开始
mov dh, 0 ; 磁头0
int 0x13
jc disk_error ; 错误处理
jmp 0x1000:0x0000 ; 跳转到第二阶段
关键注意事项:
功能扩展:
A20启用代码示例:
nasm复制enable_a20:
cli
call .wait_input
mov al, 0xAD
out 0x64, al ; 禁用键盘
call .wait_input
mov al, 0xD0
out 0x64, al ; 读状态
call .wait_output
in al, 0x60
push eax
call .wait_input
mov al, 0xD1
out 0x64, al ; 写状态
call .wait_input
pop eax
or al, 2 ; 设置A20位
out 0x60, al
call .wait_input
mov al, 0xAE
out 0x64, al ; 启用键盘
sti
ret
.wait_input:
in al, 0x64
test al, 2
jnz .wait_input
ret
.wait_output:
in al, 0x64
test al, 1
jz .wait_output
ret
GDT示例:
nasm复制gdt_start:
dq 0x0000000000000000 ; NULL描述符
gdt_code:
dw 0xFFFF ; 段界限
dw 0x0000 ; 基地址低位
db 0x00 ; 基地址中位
db 0x9A ; 访问权限
db 0xCF ; 标志位+界限高位
db 0x00 ; 基地址高位
gdt_data:
dw 0xFFFF
dw 0x0000
db 0x00
db 0x92
db 0xCF
db 0x00
gdt_end:
gdtr:
dw gdt_end - gdt_start - 1
dd gdt_start
makefile复制ASM=nasm
CC=gcc
LD=ld
QEMU=qemu-system-x86_64
all: boot.bin loader.bin kernel.bin
dd if=/dev/zero of=disk.img bs=1M count=10
dd if=boot.bin of=disk.img conv=notrunc
dd if=loader.bin of=disk.img seek=1 conv=notrunc
dd if=kernel.bin of=disk.img seek=5 conv=notrunc
boot.bin: boot.asm
$(ASM) -f bin -o $@ $<
loader.bin: loader.asm
$(ASM) -f bin -o $@ $<
kernel.bin: kernel.c
$(CC) -ffreestanding -c $< -o kernel.o
$(LD) -Ttext 0x10000 --oformat binary -o $@ kernel.o
run: all
$(QEMU) -drive format=raw,file=disk.img -monitor stdio
clean:
rm -f *.bin *.o disk.img
-s -S参数启动调试服务器bash复制gdb -ex "target remote localhost:1234" \
-ex "set architecture i8086" \
-ex "break *0x7C00" \
-ex "continue"
info registers:查看寄存器状态x/10i $eip:反汇编当前指令x/8xb 0x7C00:查看内存数据可能原因:
解决方案:
nasm复制mov ax, 0
mov ds, ax ; 确保DS=0
sti ; 启用中断
mov ah, 0x0E ; 正确功能号
可能原因:
调试方法:
info block验证磁盘状态备选方案:
nasm复制mov ax, 0x2401
int 0x15
nasm复制in al, 0x92
or al, 2
out 0x92, al
实现FAT32解析以加载内核文件:
切换VBE模式示例:
nasm复制mov ax, 0x4F01 ; 获取模式信息
mov cx, 0x4115 ; 1024x768x24bpp
mov di, mode_info
int 0x10
mov ax, 0x4F02 ; 设置视频模式
mov bx, 0x4115
int 0x10
我在实际开发中发现,使用QEMU调试时添加-d cpu_reset参数可以捕获CPU复位事件,这对调试早期启动代码特别有用。另一个实用技巧是在汇编代码中插入xchg bx, bx作为软件断点(Bochs magic breakpoint),这比硬件断点更可靠。