从零自制x86引导程序
一、前言
引导程序(Bootloader)是电脑启动时BIOS交接控制权的第一段代码,是衔接硬件和操作系统的关键。本文基于x86 16位实模式,从零实现“清屏+移动光标+打印文字”的极简引导程序,全程包含核心概念解析、可直接运行的代码、完整实践步骤,并解答新手常见疑问,适合零基础入门汇编和底层启动原理。
二、核心基础概念(新手必懂)
1. 引导程序的本质
- 电脑开机后,CPU先执行BIOS完成硬件自检(POST),随后BIOS会扫描“可引导介质”(U盘/硬盘/软盘);
- 可引导介质的核心特征:第一个扇区(512字节)末尾必须有0xAA55签名,BIOS会把这512字节加载到内存0x7C00地址,并将控制权交给这段代码——这就是引导程序;
- 我们的目标:编写符合规则的512字节汇编代码,验证引导程序的运行流程。
2. 16位实模式与寻址规则
- BIOS交接控制权时,CPU处于16位实模式:仅能访问1MB内存,支持直接调用BIOS中断(无需手动操作硬件);
- 实模式地址计算:物理地址 = 段寄存器值 × 16 + 偏移量(例如DS=0x7C0 + 偏移0x00,对应物理地址0x7C00);
- 段寄存器的“坑”:不能直接赋值立即数(如mov ds, 0x7C0非法),必须通过通用寄存器(如AX)中转。
3. BIOS中断:硬件操作的“快捷方式”
无需直接操作显卡/键盘等硬件,只需给寄存器传参数再触发中断,即可实现对应功能。核心使用int 0x10(视频服务中断),常用子功能如下:
AH值子功能关键寄存器参数0x02设置光标位置BH=视频页号,DX=行号(DH)+列号(DL)0x07清屏/向下滚屏AL=滚动行数(0=清屏),BH=颜色属性,CX/DX=清屏范围0x0ETTY模式打印字符AL=字符ASCII码,BH=视频页号4. 颜色属性编码(BH寄存器)
仅在AH=0x07(清屏)时,BH表示字符颜色属性,编码规则:
- 高4位=背景色,低4位=前景色;
- 常用值:黑色(0x0)、浅灰色(0x7)→ BH=0x07即“黑底浅灰字”。
5. 引导扇区的硬性规则
- 代码总长度必须为512字节;
- 最后2字节必须是0xAA55(BIOS识别可引导的核心标志)。
三、开发环境准备(Linux系统)
只需安装3个工具,执行以下命令(Ubuntu/Debian系,Arch用pacman,CentOS用yum):- sudo apt install nasm bochs bochs-x # 分别为汇编器、虚拟机、调试组件
复制代码
- NASM:将汇编代码编译为二进制文件;
- Bochs:x86虚拟机,测试引导程序(无需物理硬盘,避免误操作真实数据);
- dd(系统自带):制作虚拟磁盘镜像,模拟“写入硬盘”操作。
四、完整引导程序代码(可直接复制)
保存为boot.asm,关键部分已标注详细注释,兼顾“易读性”和“最佳实践”(避免硬编码地址):- bits 16 ; 声明为16位实模式代码
- org 0x7C00 ; 告诉汇编器:程序加载到内存0x7C00,自动计算偏移
- ; 符号常量(避免硬编码,改一处即可全局生效)
- BOOTSECTOR_SIZE equ 512 ; 引导扇区固定512字节
- STACK_BASE equ 0x7C00 + BOOTSECTOR_SIZE ; 栈起始于引导扇区结束后(0x7E00)
- STACK_SIZE equ 0x2000 ; 8KB栈空间(足够使用)
- ; 1. 初始化段寄存器与栈(避免寻址错误)
- xor ax, ax ; 快速置0(比mov ax,0更高效)
- mov ds, ax ; DS=0,配合org 0x7C00实现正确寻址
- mov es, ax ; 附加段ES=0,保持统一
- ; 设置栈段:SS=栈物理地址÷16(右移4位),SP=栈大小
- mov ax, STACK_BASE >> 4
- mov ss, ax
- mov sp, STACK_SIZE
- ; 2. 调用核心功能子程序
- call clearscreen ; 清屏
- push 0x0000 ; 压入光标位置参数(行0,列0)
- call movecursor ; 移动光标到左上角
- add sp, 2 ; 清理栈参数(16位=2字节)
- push msg ; 压入字符串地址参数
- call print ; 打印文字
- add sp, 2 ; 清理栈参数
- cli ; 关闭中断(防止CPU被唤醒)
- hlt ; 暂停CPU(程序结束)
- ; 3. 子程序1:清屏(利用BIOS中断0x10)
- clearscreen:
- push bp ; 保存基址指针(遵循调用规范)
- mov bp, sp
- pusha ; 保存所有通用寄存器(避免破坏调用者数据)
-
- mov ah, 0x07 ; AH=0x07:清屏/向下滚屏
- mov al, 0x00 ; AL=0:清空整个屏幕
- mov bh, 0x07 ; 颜色属性:黑底浅灰字
- mov cx, 0x00 ; 清屏区域左上角(行0,列0)
- mov dh, 0x18 ; 清屏区域右下角行=24(80x25文本模式最后一行)
- mov dl, 0x4F ; 清屏区域右下角列=79(80x25文本模式最后一列)
- int 0x10 ; 触发视频中断执行清屏
-
- popa ; 恢复所有寄存器
- mov sp, bp
- pop bp
- ret ; 子程序返回
- ; 4. 子程序2:移动光标(支持栈传参)
- movecursor:
- push bp
- mov bp, sp
- pusha
-
- mov dx, [bp+4] ; 从栈取参数(BP+4:BP占2字节,返回地址占2字节)
- mov ah, 0x02 ; AH=0x02:设置光标位置
- mov bh, 0x00 ; BH=0:操作第0个视频页(默认即可)
- int 0x10 ; 触发中断移动光标
-
- popa
- mov sp, bp
- pop bp
- ret
- ; 5. 子程序3:打印字符串(接收字符串地址参数)
- print:
- push bp
- mov bp, sp
- pusha
-
- mov si, [bp+4] ; 从栈取字符串地址
- mov bh, 0x00 ; 视频页0
- mov bl, 0x00 ; 文本模式下颜色无效
- mov ah, 0x0E ; AH=0x0E:TTY模式打印字符(自动后移光标)
- .char:
- mov al, [si] ; 取当前字符
- add si, 1 ; 指针后移
- or al, 0 ; 检查是否为字符串结束符(0)
- je .return ; 是结束符则返回
- int 0x10 ; 打印当前字符
- jmp .char ; 循环处理下一个字符
- .return:
- popa
- mov sp, bp
- pop bp
- ret
- ; 6. 定义打印字符串(以0结尾标记结束)
- msg: db "Hello, x86 Bootloader!", 0
- ; 7. 引导扇区收尾(必须!)
- times 510 - ($ - $$) db 0 ; 填充0到510字节($=当前地址,$$=段起始地址)
- dw 0xAA55 ; 引导签名(BIOS识别关键)
复制代码 代码核心解析
- org 0x7C00:让汇编器自动计算物理地址,避免手动硬编码0x7C0;
- 栈设置:栈起始于0x7E00(引导扇区结束后),避免覆盖引导程序代码;
- 栈传参:子程序通过[bp+4]读取栈中参数,调用后用add sp, 2清理栈,符合16位汇编调用规范;
- times 510 - ($ - $$) db 0:用0填充剩余空间,确保前510字节占满,为0xAA55留位置。
五、编译与虚拟镜像制作(无物理硬盘也能测)
核心思路:用“虚拟磁盘镜像”模拟物理硬盘,步骤如下:
步骤1:编译汇编代码为二进制文件
- nasm -f bin boot.asm -o boot.com
复制代码
- 验证:执行ls -l boot.com,文件大小必须为512字节(否则编译失败)。
步骤2:创建1.44MB虚拟软盘镜像
- dd if=/dev/zero of=floppy.img bs=1024 count=1440
复制代码
- 说明:bs=1024 count=1440 总大小=1.44MB,符合标准软盘规格。
步骤3:将引导程序写入镜像第一个扇区
- sudo dd if=boot.com of=floppy.img bs=512 count=1 conv=notrunc
复制代码
- 关键参数conv=notrunc:仅覆盖前512字节(第一个扇区),不截断整个镜像文件;
- 验证:执行hexdump -C -n 512 floppy.img,输出末尾需显示aa 55(签名正确)。
六、Bochs虚拟机运行测试
步骤1:创建Bochs配置文件
新建bochsrc.txt,复制以下内容:- megs: 32 ; 分配32MB内存(实模式仅用1MB,多分配不影响)
- romimage: file=/usr/share/bochs/BIOS-bochs-latest, address=0xfffe0000
- vgaromimage: file=/usr/share/bochs/VGABIOS-lgpl-latest
- floppya: 1_44=floppy.img, status=inserted ; 加载虚拟软盘镜像
- boot: a ; 从软驱A启动
- log: bochsout.txt ; 日志文件(报错时查看)
- mouse: enabled=0 ; 禁用鼠标(无需)
- display_library: x, options="gui_debug" ; 开启GUI调试界面
复制代码 步骤2:启动Bochs并运行
- 启动后弹出Bochs窗口,按c(continue)继续;
- 成功效果:屏幕清空,左上角显示Hello, x86 Bootloader!。
七、新手常见疑问解答
1. times 510 - ($ - $$) db 0 是什么意思?
- times:NASM伪指令,重复执行后续操作;
- $:当前汇编地址,$$:段起始地址,$ - $$:已编写代码的总字节数;
- 作用:填充0至510字节,确保最后2字节为0xAA55,总长度512字节。
2. pusha 后,mov dx, [bp+4] 还能取到参数吗?
能!pusha在mov bp, sp(固定栈帧)后执行,压入的寄存器数据存在BP指向位置的更低地址(栈深处),而参数存在BP指向位置的更高地址(bp+4),两者不重叠。
3. BH到底是页号还是颜色?
取决于int 0x10的子功能(AH值):
- AH=0x02(设置光标):BH=视频页号(默认0);
- AH=0x07(清屏):BH=颜色属性(如0x07=黑底浅灰字);
- AH=0x0E(打印):BH=视频页号(BL=颜色,文本模式下无效)。
4. 没有物理硬盘,如何模拟“写入硬盘”?
除了本文的“虚拟软盘镜像”,还可通过VirtualBox/VMware添加虚拟硬盘:
- 创建Linux虚拟机,添加1GB虚拟硬盘;
- 虚拟机内执行lsblk识别虚拟硬盘(如/dev/sdb);
- 写入引导程序:sudo dd if=boot.com of=/dev/sdb bs=512 count=1;
- 虚拟机设置从虚拟硬盘启动,即可运行。
八、常见问题排查(避坑)
- 编译后boot.com非512字节:检查是否缺少times指令或0xAA55签名,或代码超长(删除多余注释);
- Bochs黑屏/无输出:验证镜像签名(hexdump看aa 55),检查bochsrc.txt中floppy.img路径是否正确;
- 写入镜像权限不足:给dd命令加sudo;
- 打印乱码:字符串未以0结尾,或AH=0x0E参数配置错误(如BH设为颜色)。
参考文章
1、Joe Bergeron. Writing a Tiny x86 Bootloader[EB/OL]. https://www.joe-bergeron.com/posts/Writing a Tiny x86 Bootloader/, 2016-12-27.
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |