操作系统学习10 x86引导程序加载C语言程序
操作系统学习10 x86引导程序加载C语言程序
一、本章说明
1. 前置知识
请先阅读 操作系统学习9 引导程序加载和运行系统文件。本章在第 9 章「引导程序读盘 + 跳转执行」的基础上,把被加载的程序从纯汇编换成 C 语言。
2. 本章目标
完成本章后,你将能够:
- 用
i686-elf-gcc在 16 位实模式下编译 freestanding C 代码 - 用
nasm -f elf32+i686-elf-ld -Ttext把 C 与汇编链接成裸二进制 - 让引导程序读盘后跳转到
0xC200执行 C 内核 - 在 C 中通过头文件声明调用汇编子程序(清屏、
HLT)
3. 目录结构
lesson10/
├── loader.s # 引导程序(含 FAT12 BPB)
├── Makefile
└── kernel/
├── kernel.c # C 入口
├── utilities.s # 汇编子程序
└── utilities.h # 供 C 调用的声明
编译产物输出到 ../target/(loader.bin、kernel.bin、os.img)。
4. 准备工具
| 工具 | 用途 |
|---|---|
nasm |
汇编 .s 为 ELF 目标文件 |
i686-elf-gcc |
编译 C 内核(裸机,无标准库) |
i686-elf-ld |
链接并输出 flat binary |
dd |
把引导扇区/内核写入 os.img |
make |
一键编译、安装、运行 |
| Bochs | 运行与调试 |
工具已放在开源仓库的 tools/、env/ 目录。Windows 下请将 tools/、tools/bochs/ 加入 PATH。
5. 内存与磁盘布局
| 位置 | 物理地址 | 说明 |
|---|---|---|
| 引导扇区 | 0x7C00 |
BIOS 加载,loader.bin 链接地址 |
| 读盘缓冲区起点 | 0x8200 |
从软盘扇区 2 开始读入 |
| 内核入口 | 0xC200 |
kernel.bin 链接地址,引导程序 JMP 目标 |
| 栈 | SS:SP = 0:0x7C00 |
跳转 C 代码前由引导程序设置 |
FAT12 1.44MB 软盘布局:引导区 1 扇区 + FAT 18 扇区 + 根目录 14 扇区 = 33 扇区,第 33 扇区起为数据区。make install 用 dd seek=33 把 kernel.bin 写入该位置。
二、实现步骤
- 编写引导程序
loader.s(FAT12 BPB + INT 13h 读盘 + 跳转) - 编写汇编子程序
utilities.s(清屏、HLT) - 编写头文件
utilities.h,供 C 声明调用 - 编写 C 入口
kernel.c - 编写
Makefile,用工具链编译、链接、写入镜像 - 第一次运行:先注释掉清屏,确认 C 内核已加载
- 第二次运行:恢复清屏,对比两次屏幕现象
6. 第一次运行 — 注释清屏
临时修改 kernel.c,注释掉 clear_screen():
#include "utilities.h"
void main(void){
// clear_screen(); // 先注释,便于观察引导程序输出
fin:
io_hlt();
goto fin;
}
编译并运行:
make install
make run
预期现象: 屏幕一直保留 Loading NotOneOS...,光标在文字下方闪烁。说明引导程序读盘、跳转到 0xC200、C 代码执行均正常;之后没有新输出是因为只剩 io_hlt() 空循环。
7. 第二次运行 — 恢复清屏
改回 kernel.c:
#include "utilities.h"
void main(void){
clear_screen();
fin:
io_hlt();
goto fin;
}
再次编译并运行:
make install
make run
预期现象: 先短暂看到 Loading NotOneOS...,随后整屏被清掉,只剩黑屏和闪烁光标。两次对比即可确认:黑屏是 clear_screen()(INT 10h)的效果,不是系统卡死。
三、源码讲解
1. loader.s — 引导程序
引导程序不再使用 ORG,加载地址改由链接器 -Ttext 0x7c00 指定,以便与 ELF 工具链统一。
核心流程:
- 初始化段寄存器与栈(
SP = 0x7C00) - 从柱面 0、磁头 0、扇区 2 起,用 INT 13h 读 10 个柱面到
ES:BX,起始ES = 0x8200 - 屏幕输出
Loading NotOneOS...或Error - 读盘成功后跳转到
0xC200执行内核;失败则打印Error后停机
end 处根据 DH 分支:DH = 1 时 JE 0xC200 进入内核,否则 JMP $ 停机,避免读盘失败后仍跳转到无效地址。
BPB 字段使用 NASM 伪指令:DB = 1 字节,DW = 2 字节,DD = 4 字节。
[BITS 16]
global _start
CYLS EQU 10 ; 读 10 柱面 → 0x8200 ~ 0x34fff
_start:
JMP init
; FAT12 BPB(节选)
DB 0x90
DB "NotOneOS" ; OEM,8 字节
DW 512 ; 每扇区 512 字节
; ... 完整 BPB 见 loader.s ...
init:
MOV AX, 0
MOV SS, AX
MOV SP, 0x7c00
MOV DS, AX
MOV AX, 0x0820 ; 读盘目标:物理地址 0x8200
MOV ES, AX
MOV CH, 0 ; 柱面 0
MOV DH, 0 ; 磁头 0
MOV CL, 2 ; 从扇区 2 开始(扇区 1 是引导区)
; readloop / retry / next:INT 13h 读盘,失败重试最多 5 次
; ... 完整读盘循环见 loader.s ...
MOV DH, 1
MOV SI, loading_msg
JMP print
error:
MOV DH, 0
MOV SI, error_msg
print:
; INT 10h 打印以 0 结尾的字符串
; ...
end:
CMP DH, 1 ; 成功则 DH = 1
JE 0xC200 ; 跳转到内核
JMP $ ; 失败则停机
loading_msg: DB "Loading NotOneOS...", 0
error_msg: DB "Error", 0
times 510-($-$$) db 0
DW 0xAA55
loader.s 末尾还有大段 RESB,用于把输出镜像填充为完整 FAT12 软盘格式;与「加载 C 程序」关系不大,详见源码。
2. utilities.s — 汇编子程序
[BITS 16]
global io_hlt
global clear_screen
clear_screen:
mov ax, 0x0003 ; INT 10h 功能 06h/ax=3:清屏
int 0x10
ret
io_hlt:
hlt ; 停机,等待中断
ret ; C 调用约定需要返回
clear_screen:仍在 16 位实模式下调用 BIOS,清屏后返回 Cio_hlt:HLT后RET,供 C 在循环中调用;裸机环境下通常不会真正「返回后继续有意义的工作」
3. utilities.h — C 调用声明
#ifndef __UTILITIES_H_
#define __UTILITIES_H_
void io_hlt(void);
void clear_screen(void);
#endif
4. kernel.c — C 入口
#include "utilities.h"
void main(void){
clear_screen();
fin:
io_hlt();
goto fin;
}
说明:
- 使用
void main而非_start:入口地址由链接器-Ttext 0xC200决定,栈已由引导程序设好 -m16 -ffreestanding:生成 16 位实模式代码,不链接 libcgoto fin+io_hlt():进入空闲循环,避免 C 函数返回后跑飞
四、Makefile 与工具链
1. 关键编译选项
| 选项 | 作用 |
|---|---|
nasm -f elf32 |
输出 ELF 32 位目标文件,供 ld 链接 |
-march=i386 -m16 |
GCC 生成 16 位实模式指令 |
-ffreestanding |
不依赖标准库与 hosted 环境 |
-Ttext 0x7C00 |
引导程序链接地址 |
-Ttext 0xC200 |
内核链接地址(与 JMP 一致) |
--oformat binary |
输出纯二进制,无 ELF 头 |
dd seek=33 |
内核写入 FAT12 数据区首扇区 |
2. Makefile(与仓库一致)
TOOLPATH = ../tools/
TARGET = ../target/
SRC = ./
LD = ../env/bin/i686-elf-ld.exe
CC = ../env/bin/i686-elf-gcc.exe
NASM = $(TOOLPATH)nasm.exe
DD = $(TOOLPATH)dd.exe
LDFLAGS = -m elf_i386
CCFLAGS = -march=i386 -m16 -mpreferred-stack-boundary=2 -ffreestanding
ASFLAGS =
default : kernel.bin loader.bin kernel.o utilities.o loader.o
utilities.o: $(SRC)kernel/utilities.s
$(NASM) $(ASFLAGS) -f elf32 -o $(TARGET)$@ $^
kernel.o: $(SRC)kernel/kernel.c $(SRC)kernel/utilities.h
$(CC) $(CCFLAGS) -o $(TARGET)kernel.o -c $(SRC)kernel/kernel.c
kernel.bin: kernel.o utilities.o
$(LD) $(LDFLAGS) -Ttext 0xC200 --oformat binary -o $(TARGET)$@ $(TARGET)kernel.o $(TARGET)utilities.o
loader.o: $(SRC)loader.s
$(NASM) $(ASFLAGS) -f elf32 -o $(TARGET)$@ $^
loader.bin: loader.o
$(LD) $(LDFLAGS) -Ttext 0x7c00 --oformat binary -o $(TARGET)$@ $(TARGET)$^
install:
$(DD) if=$(TARGET)loader.bin of=$(TARGET)os.img conv=notrunc
$(DD) if=$(TARGET)kernel.bin of=$(TARGET)os.img conv=notrunc bs=512 seek=33
run:
cd $(TOOLPATH) && bochs.bat && cd ..
debug:
cd $(TOOLPATH) && bochsdebug.bat && cd ..
clean:
del "$(TARGET)*"
install 两步:
conv=notrunc把loader.bin写入镜像扇区 0(不截断原镜像)seek=33把kernel.bin写入第 33 扇区(数据区起点)
五、构建与运行
在 lesson10/ 目录下执行:
make # 生成 loader.bin、kernel.bin
make install # 写入 ../target/os.img(依赖上述 bin,可单独执行)
make run # Bochs 运行
若只执行 make install 而 ../target/ 下还没有 loader.bin、kernel.bin,Windows 版 dd 会报 Error opening input file: 2(这里的 2 是 Windows 错误码「找不到文件」,不是文件名)。先 make 再 install,或直接 make install(Makefile 已声明依赖,会自动编译)。
预期现象(恢复清屏后的最终版本):
- 屏幕短暂显示
Loading NotOneOS... - 随后清屏(黑屏 + 闪烁光标)
- CPU 在
io_hlt处反复HLT(Bochs 中可见空闲循环)
若按上文第 6 步注释清屏,则第 2 步不会发生,Loading NotOneOS... 会一直留在屏幕上。
调试:
make debug
在 Bochs 命令行:
b 0xC200
c
可在 0xC205 附近看到对 clear_screen(约 0xC220)的 call,随后在 0xC20b 一带进入 io_hlt 循环。


六、dd 与 VirtualBox 拷贝的对比
推荐方式(本章默认): make install 中的 dd seek=33,一步把内核写入镜像数据区,无需虚拟机。
备选方式: 用 VirtualBox 挂载 os.img,把 kernel.bin 拷进软盘。若 FAT 根目录中有对应文件项,文件内容也会落在数据区,效果与 seek=33 类似。
注意:dd seek=33 只写数据区字节,不会更新 FAT 表和根目录项。因此在虚拟机资源管理器中可能看不到该文件,但引导程序按扇区读取仍然正确。根目录在镜像偏移约 0x2600 处,内容可能仍为空——这是预期行为。

七、常见问题
| 现象 | 可能原因 |
|---|---|
make 报找不到 i686-elf-gcc |
检查 env/bin/ 是否存在,路径是否正确 |
| 运行后花屏或重启 | kernel.bin 未写入镜像,或 seek 不是 33 |
一直显示 Error |
软盘读失败,检查 Bochs 是否挂载 os.img |
| 有 Loading 但未清屏 | 跳转地址与 -Ttext 不一致,或内核未链接 utilities.o |
八、本章小结
- 引导程序仍用汇编 + INT 13h;应用程序首次改用 C 编写
- 汇编与 C 通过 ELF
.o+ 同一链接器合并,再用--oformat binary导出 flat bin - 链接地址
-Ttext必须与引导程序JMP目标、磁盘写入位置三者一致 - C 通过
utilities.h声明、.s实现的方式调用 BIOS 级功能
下一章可在此基础上扩展:规范内核入口(_start)、完善错误分支、或按 FAT 目录项加载文件而非固定扇区。
源码地址:
https://gitee.com/xundh/learn-os
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)