一、本章说明

1. 前置知识

请先阅读 操作系统学习9 引导程序加载和运行系统文件。本章在第 9 章「引导程序读盘 + 跳转执行」的基础上,把被加载的程序从纯汇编换成 C 语言。

2. 本章目标

完成本章后,你将能够:

  • i686-elf-gcc16 位实模式下编译 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.binkernel.binos.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 installdd seek=33kernel.bin 写入该位置。

nasm + ld 0x7C00

gcc + ld 0xC200

dd 扇区0

dd seek=33

loader.s

loader.bin

kernel.c + utilities.s

kernel.bin

os.img

BIOS 加载 0x7C00

INT 13h 读盘到 0x8200

JMP 0xC200

clear_screen → io_hlt 循环


二、实现步骤

  1. 编写引导程序 loader.s(FAT12 BPB + INT 13h 读盘 + 跳转)
  2. 编写汇编子程序 utilities.s(清屏、HLT
  3. 编写头文件 utilities.h,供 C 声明调用
  4. 编写 C 入口 kernel.c
  5. 编写 Makefile,用工具链编译、链接、写入镜像
  6. 第一次运行:先注释掉清屏,确认 C 内核已加载
  7. 第二次运行:恢复清屏,对比两次屏幕现象

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 工具链统一。

核心流程:

  1. 初始化段寄存器与栈(SP = 0x7C00
  2. 从柱面 0、磁头 0、扇区 2 起,用 INT 13h 读 10 个柱面到 ES:BX,起始 ES = 0x8200
  3. 屏幕输出 Loading NotOneOS...Error
  4. 读盘成功后跳转到 0xC200 执行内核;失败则打印 Error 后停机

end 处根据 DH 分支:DH = 1JE 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,清屏后返回 C
  • io_hltHLTRET,供 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 位实模式代码,不链接 libc
  • goto 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 两步:

  1. conv=notruncloader.bin 写入镜像扇区 0(不截断原镜像)
  2. seek=33kernel.bin 写入第 33 扇区(数据区起点)

五、构建与运行

lesson10/ 目录下执行:

make          # 生成 loader.bin、kernel.bin
make install  # 写入 ../target/os.img(依赖上述 bin,可单独执行)
make run      # Bochs 运行

若只执行 make install../target/ 下还没有 loader.binkernel.bin,Windows 版 dd 会报 Error opening input file: 2(这里的 2 是 Windows 错误码「找不到文件」,不是文件名)。先 makeinstall,或直接 make install(Makefile 已声明依赖,会自动编译)。

预期现象(恢复清屏后的最终版本):

  1. 屏幕短暂显示 Loading NotOneOS...
  2. 随后清屏(黑屏 + 闪烁光标)
  3. 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 处,内容可能仍为空——这是预期行为。

dd 写入示意


七、常见问题

现象 可能原因
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

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐