mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3971 字
11 分钟
启动镜像:系统集成与自检
2024-05-16

在之前的章节中,我们逐步构建了操作系统的各个子系统:从基础的 MBR 引导、保护模式设置、虚拟内存管理,到中断系统、内存管理、任务调度、同步机制、用户空间、设备驱动、文件系统,以及 Shell 和用户工具。本章将把所有这些组件整合成一个完整的、可启动的操作系统镜像。

经过 18 章的开发,我们有了引导程序、内核、调度器、文件系统、Shell……但它们还散落在各个目录中。本章要把所有组件集成为一个可启动的完整系统:自定义引导链(MBR→Loader→Kernel)、VHD 磁盘镜像打包、子系统初始化顺序、系统自检,这是整个系列的“毕业典礼”。

启动镜像概述#

集成所有组件的必要性#

在操作系统开发过程中,我们通常采用模块化的方式开发各个子系统。每个章节专注于实现特定的功能,如内存管理、任务调度、文件系统等。然而,这些分散的模块最终需要被整合成一个统一的系统,才能形成完整可用的操作系统。

启动镜像(Boot Image)的作用包括:

  1. 统一启动流程:将引导加载程序、内核、文件系统等所有必需组件打包到一个镜像文件中,确保系统能够从零开始完整启动
  2. 便于分发和测试:单个镜像文件可以方便地在不同环境中运行,便于测试和演示
  3. 系统集成验证:创建启动镜像的过程本身就是对系统集成的一次全面验证,可以发现各模块之间的接口问题
  4. 版本管理:每个镜像对应一个特定的系统版本,便于版本管理和发布

传统操作系统 vs 教学操作系统#

商业操作系统(如 Linux、Windows)通常使用复杂的引导加载程序(如 GRUB、LILO)和成熟的文件系统(如 ext4、NTFS)。我们的教学操作系统采用了更简洁但同样有效的设计:

  • 自定义引导链:MBR → Loader → 内核,而不是依赖 GRUB
  • 内存文件系统:SimpleFS 运行在内存中,简单高效
  • VHD 磁盘镜像:虚拟硬盘格式,便于 QEMU 直接启动
  • 集成自检:系统启动时自动进行功能验证

这种设计既保持了教育价值,又确保了系统的完整性和可靠性。

启动镜像的核心组成#

自定义引导链#

我们为什么不使用 GRUB 这样的成熟引导加载程序?原因有几个:

  1. 学习目的:自定义引导链能让你深入理解计算机启动的底层机制
  2. 完全控制:不依赖外部工具,从 MBR 到内核的每一步代码都是自己编写的
  3. 简化流程:避免了 GRUB 的复杂性,更适合教学环境
  4. 独立性:整个系统完全自包含,不依赖第三方软件

我们的引导链由三个阶段组成:

graph TD A[BIOS 启动] -->|加载扇区0| B[MBR<br/>512字节] B -->|加载到0x7C00| C[MBR代码执行] C -->|读取扇区1-8| D[Loader<br/>4KB] D -->|加载到0x8000| E[Loader代码执行] E -->|读取扇区9+| F[Kernel<br/>ELF格式] F -->|解析ELF| G[加载到内存] G -->|跳转到入口点| H[Kernel main函数]

MBR(Master Boot Record):位于磁盘第一个扇区(扇区 0),大小为 512 字节。BIOS 启动后会将 MBR 加载到内存地址 0x7C00 并执行。MBR 的主要任务是:

  • 通过 E820 获取内存映射
  • 从磁盘读取 Loader 程序
  • 将 Loader 加载到内存并跳转执行
  • 在扇区末尾写入 0x55AA 引导签名供 BIOS 检查

Loader:位于扇区 1-8(共 8 个扇区,4KB)。Loader 完成更复杂的任务:

  • 进入保护模式
  • 设置 GDT(全局描述符表)
  • 启用分页机制
  • 加载 ELF 格式的内核
  • 跳转到内核入口点

Kernel:从扇区 9 开始存储,包含完整的操作系统内核。内核以 ELF 格式存储,Loader 负责解析 ELF 并加载到正确的内存位置。

VHD 磁盘镜像#

VHD(Virtual Hard Disk)是虚拟磁盘的通用格式,具有以下优势:

  1. 标准格式:被 QEMU、VirtualBox 等主流虚拟机支持
  2. 固定大小:可以预先分配固定大小的磁盘空间
  3. 便于管理:单个文件包含完整的磁盘内容
  4. 可移植性:方便在不同机器之间传输

VHD 镜像的布局(按文件偏移,单位为字节):

区域偏移扇区大小说明
MBR0x00000512B引导扇区,BIOS 加载到 0x7C00
Loader0x02001-84KB8 个扇区,加载到 0x8000
Kernel0x12009+ELF 镜像内核二进制,最高 1MB
VHD Footer文件末尾-512BVPC 格式尾,标识镜像结束

VHD 文件由 qemu-img create -f vpc -o subformat=fixed 创建为固定大小镜像(注意必须显式指定 subformat=fixed-f vpc 默认是动态磁盘,会把 footer 副本放在文件开头,dd 写 MBR 时会破坏它)。VPC 格式的标识 footer 在文件末尾而非开头,因此 MBR 直接从文件偏移 0 开始写入。

构建过程:

  1. 编译 MBR、Loader、Kernel,生成二进制文件
  2. qemu-img create -f vpc 创建固定大小的空 VHD 镜像
  3. dd ... seek=0 将 MBR 写入偏移 0x0000(扇区 0)
  4. dd ... seek=1 将 Loader 写入偏移 0x0200(扇区 1)
  5. dd ... seek=9 将 Kernel 写入偏移 0x1200(扇区 9)
  6. (可选)写入文件系统数据

系统集成与初始化#

操作系统从引导加载程序接管控制权后,需要按正确顺序初始化各个子系统:

  1. 依赖关系:某些子系统依赖于其他子系统(如任务调度依赖定时器)
  2. 资源分配:需要在早期初始化阶段分配关键资源(如内核堆)
  3. 错误处理:初始化失败时需要能够报告错误
  4. 性能优化:某些操作在启动阶段一次性完成,避免运行时开销

完整的初始化流程:

flowchart TD A[Kernel 入口] --> B[VGA 显示初始化] B --> C[GDT 全局描述符表] C --> D[TSS 任务状态段] D --> E[IDT 中断描述符表] E --> F[启用中断] F --> G[定时器初始化] G --> H[Boot 信息解析] H --> I[PMM 物理内存管理] I --> J[VMM 虚拟内存管理] J --> K[KHeap 内核堆] K --> L[设备框架初始化] L --> M[键盘驱动] M --> N[ATA 硬盘驱动] N --> O[文件系统初始化] O --> P[SimpleFS 挂载] P --> Q[任务子系统] Q --> R[进程子系统] R --> S[系统调用初始化] S --> T[系统自检] T --> U[创建文件系统内容] U --> V[启动 Shell 任务] V --> W[进入调度器]

每个初始化步骤都有其特定的职责:

  • 早期初始化:GDT、IDT、TSS 必须首先完成,它们为后续操作提供基础
  • 内存管理:PMM 和 VMM 必须在需要动态内存分配之前初始化
  • 设备驱动:硬件驱动需要在需要硬件功能之前初始化
  • 文件系统:文件系统必须在用户进程使用文件之前挂载
  • 任务和进程:这是系统启动的最后阶段,因为它们依赖于前面的所有子系统

系统自检(Self-Test)#

系统自检在启动阶段自动验证关键功能是否正常工作:

  1. 快速诊断:在用户交互前发现明显的问题
  2. 教学价值:展示如何编写测试代码
  3. 增强信心:让开发者知道系统启动成功
  4. 文档作用:自检输出可以作为系统功能的简要说明

我们的自检包括三个基本测试:

static int self_test(void)
{
int pass = 0;
// 测试1:内存分配
void *p = kmalloc(256);
if (p) { pass++; kfree(p); }
vga_printf(" [%s] Memory allocation\n", p ? "PASS" : "FAIL");
// 测试2:中断启用
uint32_t fl; __asm__ volatile("pushfl; popl %0":"=r"(fl));
if (fl & 0x200) pass++;
vga_printf(" [%s] Interrupts\n", (fl & 0x200) ? "PASS" : "FAIL");
// 测试3:定时器工作
uint32_t t1 = getTick();
for (volatile int i = 0; i < 50000; i++);
if (getTick() >= t1) pass++;
vga_printf(" [%s] Timer\n", getTick() >= t1 ? "PASS" : "FAIL");
return pass;
}
  • 内存分配测试:验证内核堆能够正常分配和释放内存
  • 中断测试:验证中断标志位(IF)已启用,意味着中断系统工作正常
  • 定时器测试:验证定时器中断在增加 tick 计数

自检输出示例:

Self-test:
[PASS] Memory allocation
[PASS] Interrupts
[PASS] Timer
Result: 3/3 passed

代码实现#

文件结构#

19.boot-image

boot

mbr.SMBR 引导代码

loader.SLoader 引导代码

kernel

arch/x86

context_switch.S上下文切换汇编

usermode.S用户模式切换

drivers

ata.cATA 硬盘驱动

device.c设备框架

keyboard.c键盘驱动

vga.cVGA 显示驱动

fs

disk_fs.c磁盘文件系统接口

fs.c虚拟文件系统

pipe.c管道实现

simple_fs.cSimpleFS 实现

include所有头文件(types.h、io.h、memory.h 等)

interrupt

interrupt.S中断处理汇编

interrupt.c中断处理

syscall.c系统调用

timer.c定时器

lib

bitmap.c位图操作

hash_table.c哈希表

id_pool.cID 分配池

linked_list.c链表

math.c数学函数

ordered_array.c有序数组

rand.c随机数

string.c字符串操作

mem

boot_info.c启动信息解析

gdt.cGDT 管理

gdt.SGDT 加载汇编

kheap.c内核堆

pmm.c物理内存管理

vmm.c虚拟内存管理

sync

lock.S锁原语汇编

mutex.c互斥锁

semaphore.c信号量

spinlock.c自旋锁

yieldlock.c让出锁

task

exec.cexec 系统调用

fork.cfork 系统调用

process.c进程管理

runqueue.c运行队列

scheduler.c调度器

signal.c信号处理

task.c任务管理

tss.cTSS 管理

kernel.c内核主函数(含 Shell)

link.ld内核链接脚本

Makefile构建配置

关键初始化流程#

内核主函数 main() 完成所有初始化:

void main()
{
vga_clear();
vga_printf(" ===================================\n");
vga_printf(" | MyOS - Teaching OS v1.0 |\n");
vga_printf(" | Chapter 19: Final Boot Image |\n");
vga_printf(" ===================================\n\n");
vga_printf("Initializing kernel subsystems...\n");
// 硬件基础设施
vga_printf(" GDT..."); gdt_init(); vga_printf("OK\n");
vga_printf(" TSS..."); tss_init(); vga_printf("OK\n");
vga_printf(" IDT..."); idt_init(); vga_printf("OK\n");
vga_printf(" Interrupts..."); enable_interrupts(); vga_printf("OK\n");
vga_printf(" Timer..."); init_timer(TIMER_FREQUENCY); vga_printf("OK\n");
// 内存管理
vga_printf(" Memory...");
boot_info_init();
pmm_init(&boot_info);
vmm_init();
init_kheap();
vga_printf("OK\n");
// 设备驱动
vga_printf(" Devices...");
device_framework_init();
keyboard_init();
ata_init();
vga_printf("OK\n");
// 文件系统
vga_printf(" Filesystem...");
fs_init();
simple_fs_init();
vga_printf("OK\n");
// 任务和进程管理
vga_printf(" Tasks...");
task_subsystem_init();
process_subsystem_init();
syscall_init();
vga_printf("OK\n");
// 系统自检
vga_printf("\nSelf-test:\n");
int pass = self_test();
vga_printf(" Result: %d/3 passed\n", pass);
// 预创建文件系统内容
fs_mkdir("/home", FS_PERM_READ | FS_PERM_WRITE);
fs_mkdir("/bin", FS_PERM_READ | FS_PERM_EXEC);
fs_mkdir("/tmp", FS_PERM_READ | FS_PERM_WRITE);
int fd = fs_open("/readme.txt", FS_OPEN_WRITE | FS_OPEN_CREATE);
if (fd >= 0) {
const char *txt = "Welcome to MyOS!\nA complete teaching operating system.\n\nFeatures:\n- Preemptive multitasking\n- Virtual memory\n- File system\n- Device drivers\n- System calls\n- Interactive shell\n";
fs_write(fd, txt, strlen(txt));
fs_close(fd);
}
// 启动 Shell
vga_printf("\nStarting shell...\n");
task_create_kernel("shell", shell_task, NULL, 5);
// 触发首次调度,然后进入低功耗等待循环
schedule();
while (1) {
if (schedule_needed()) {
schedule_yield();
}
__asm__ volatile("hlt");
}
}

MBR 引导代码(boot/mbr.S)#

下面是 mbr.S 的真实代码节选(省略部分辅助函数)。MBR 运行在 16 位实模式,由 BIOS 加载到 0x7C00 执行。

; mbr.S:真实代码节选(NASM 语法,bits 16)
; 通过 %include 引入 bootmacros.inc 中的常量与宏
bits 16
org 0x7c00
%include "../common/bootmacros.inc"
loader_base_addr equ 0x8000 ; loader 所处的段地址
loader_sectors equ 8 ; 需要读取的扇区数
E820_COUNT equ 0x8FF0 ; 存储E820条目数量的地址
; 初始化寄存器
mov ax, cs
mov ds, ax
mov ss, ax
mov fs, ax
mov ax, 0
mov es, ax
mov sp, 0x7c00 ; 将 0x7c00 以下的区域作为栈
mov ax, 0xb800
mov gs, ax ; 显存段位置
call cls ; 清空屏幕
mov bx, hello_msg
call print ; 输出启动信息
call get_memory_map ; 通过 E820 获取内存映射
; 读 loader(8 个扇区)
mov dh, loader_sectors
mov dl, 0x80 ; 第一个硬盘
mov bx, loader_base_addr
call disk_load
jmp 0:loader_base_addr ; 跳转至 loader(物理 0x8000)
hello_msg: db "HELLO, MBR!", 0
; --- disk_load 含 3 次重试机制(节选) ---
disk_load:
pusha
push dx
mov byte [.retry_count], 3
.retry:
mov ah, 0x02 ; int 0x13 功能:读扇区
mov al, dh ; 读取的扇区数
mov cl, 0x02 ; 从扇区 2 开始(扇区 1 是 MBR)
mov ch, 0x00 ; 柱面 0
mov dh, 0x00 ; 磁头 0
int 0x13
jnc .disk_ok
dec byte [.retry_count] ; 失败则重试
jz disk_error
mov ah, 0x00 ; 重置磁盘驱动器后重试
int 0x13
jmp .retry
.disk_ok:
pop dx
cmp al, dh ; 校验实际读取的扇区数
jne sectors_error
popa
ret
.retry_count: db 0
; get_memory_map:通过 int 0x15, ax=0xE820 获取内存映射,存入 E820_BUFFER
; (省略实现细节,最多 16 个条目)
; --- 扇区末尾的引导签名 ---
times 510 - ($ - $$) db 0
db 0x55, 0xaa ; 写入引导签名,供 BIOS 检查

解析:MBR 是系统启动的第一步。它的工作包括:

  1. 设置段寄存器和栈指针(栈位于 0x7C00 以下)
  2. 清屏并打印启动信息
  3. 调用 get_memory_map 通过 E820 获取物理内存映射,供后续 PMM 使用
  4. 调用 disk_load 读取 Loader(8 个扇区,含 3 次重试机制)
  5. jmp 0:loader_base_addr(即 jmp 0:0x8000)跳转到 Loader
  6. 在扇区末尾写入 0x55, 0xaa 引导签名,BIOS 启动时会检查该签名是否存在

MBR 的限制是只有 512 字节,所以必须简洁。复杂的初始化交给 Loader 完成。

Loader 引导代码(boot/loader.S)#

下面是 loader.S 的真实代码节选。Loader 加载到 0x8000,负责进入保护模式、启用分页(含内核高地址映射)、解析 ELF 加载内核,最终跳转到内核入口。

; loader.S:真实代码节选(NASM 语法)
; 通过 %include 引入 bootmacros.inc 中的 GDT 宏、分页常量、内核地址常量
[bits 16]
org 0x8000
%include "../common/bootmacros.inc"
_start:
jmp loader_start
; 用宏定义标准 GDT(null / 内核代码段 / 内核数据段 / 视频段)
DEFINE_STANDARD_GDT
loader_start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
SET_STACK_AT_MBR
call cls
mov bx, MSG_REAL_MODE
call print_bios
; 启用 A20 地址线(依次尝试 BIOS、键盘控制器、0x92 端口三种方法)
; ...(省略 A20 启用细节)
; 加载 GDT 并进入保护模式
lgdt [gdt_descriptor]
mov eax, cr0
or eax, 1 ; 设置 PE 位
mov cr0, eax
jmp SELECTOR_CODE32:init_pm ; 远跳刷新 CS
[bits 32]
init_pm:
mov ax, SELECTOR_DATA32
mov ds, ax
mov es, ax
mov fs, ax
mov ss, ax
mov ax, SELECTOR_VIDEO
mov gs, ax
SET_STACK_AT_PM ; 切换栈到 0x90000
call clear_screen
; 打印保护模式信息(省略)
call setup_page ; 建立页目录与页表,启用分页
; 打印分页启用信息(省略)
call init_kernel ; 解析 ELF,加载内核段,返回入口地址于 eax
cli
.hang:
hlt
jmp .hang
; --- setup_page:建立页目录与页表(节选) ---
; 关键映射:
; - 0xC0000000 的第一个 4MB:低 1MB 恒等映射 + 内核页表
; - 0xC0400000 起:内核所有页表(254 个 PDE)
; - 0xC0800000 起:内核代码/数据物理起点 0x200000
setup_page:
; 清零页目录(位于 0x101000)
; ...
; 第 0 个 PDE 与第 768 个 PDE 指向同一页表(低 1MB 恒等映射)
mov eax, PAGE_DIR_PHYSICAL_ADDR - PAGE_SIZE
or eax, PTE_USER | PTE_WRITE | PTE_PRESENT
mov [PAGE_DIR_PHYSICAL_ADDR + 0], eax
mov [PAGE_DIR_PHYSICAL_ADDR + 768 * 4], eax
; 第 769 个 PDE 指向内核页表区
; 第 770-1023 共 254 个 PDE 指向内核后续页表
; ...
call enable_page ; 设置 CR3、置 CR0.PG、重新加载高地址 GDT
ret
; --- init_kernel:解析 ELF 程序头,加载段,返回入口地址 ---
init_kernel:
call allocate_pages_for_kernel ; 为内核映像、内核段、内核栈建立映射
call load_hd_kernel_image ; 从磁盘读取内核映像到 KERNEL_BIN_LOAD_VIRTUAL_ADDR
call do_load_kernel ; 遍历 PT_LOAD 段,memcpy 到 p_vaddr
finit
mov esp, KERNEL_STACK_TOP - 16 ; 内核栈顶 0xF0000000
mov ebp, esp
jmp eax ; 跳转到 ELF 入口点 e_entry
ret

解析:Loader 的职责比 MBR 复杂得多:

  1. 加载 GDT:通过 DEFINE_STANDARD_GDT 宏定义空描述符、内核代码段、内核数据段、视频段,是保护模式的基础
  2. 启用 A20 与保护模式:依次尝试三种方法启用 A20 地址线,然后设置 CR0 的 PE 位并远跳
  3. 启用分页setup_page 建立页目录与页表,关键映射包括低 1MB 恒等映射、内核页表区(0xC0400000 起)、内核代码区(虚拟 0xC0800000 映射到物理 0x200000);enable_page 置 CR0.PG 后重新加载高地址 GDT
  4. 加载内核init_kernel 先为内核映像、内核段、内核栈建立页表映射,再用 load_hd_kernel_image 从磁盘扇区 9 起读取内核映像,最后 do_load_kernel 遍历 ELF 程序头,将 PT_LOADmemcpy 到各自的 p_vaddr
  5. 跳转执行do_load_kernel 把 ELF 入口 e_entry 放入 eaxinit_kerneljmp eax 跳转到内核入口

内核链接在虚拟地址 0xC0800000(物理 0x200000),因此 Loader 必须先建立高地址映射,才能跳转到内核入口。

Shell 命令解析#

static int parse_cmd(char *line, char *argv[])
{
int argc = 0;
char *p = line;
while (*p && argc < MAX_ARGS - 1) {
while (*p == ' ') p++; // 跳过空格
if (!*p) break;
argv[argc++] = p; // 记录参数起始位置
while (*p && *p != ' ') p++; // 找到参数结束
if (*p) *p++ = '\0'; // 用空字符替换空格
}
argv[argc] = NULL; // 参数数组以 NULL 结尾
return argc;
}
static void shell_cmd(char *line)
{
char *argv[MAX_ARGS];
int argc = parse_cmd(line, argv);
if (argc == 0) return;
// 命令分发
if (strcmp(argv[0], "help") == 0) {
vga_printf("Commands: help clear echo ls cat mkdir touch write pwd cd\n");
vga_printf(" ps mem uptime version pipe signal diskls exit\n");
} else if (strcmp(argv[0], "ls") == 0) {
const char *p = argc > 1 ? argv[1] : cwd;
int d = fs_opendir(p);
if (d < 0) {
vga_printf("ls: cannot open '%s'\n", p);
return;
}
dirent_t *de;
while ((de = fs_readdir(d))) {
vga_printf(" %s%s\n", de->name,
de->type == FILE_TYPE_DIRECTORY ? "/" : "");
}
fs_closedir(d);
} else if (strcmp(argv[0], "cat") == 0) {
if (argc < 2) {
vga_printf("Usage: cat <file>\n");
return;
}
char path[128];
if (argv[1][0] == '/') {
strncpy(path, argv[1], 127);
} else {
sprintf(path, "%s/%s",
strcmp(cwd, "/") == 0 ? "" : cwd,
argv[1]);
}
int f = fs_open(path, FS_OPEN_READ);
if (f < 0) {
vga_printf("cat: not found\n");
return;
}
char b[128];
ssize_t n;
while ((n = fs_read(f, b, 127)) > 0) {
b[n] = 0;
vga_printf("%s", b);
}
fs_close(f);
}
// ... 更多命令 ...
}

解析:Shell 实现了基本的命令行界面:

  1. 命令解析parse_cmd() 函数将输入行分解为参数数组
  2. 命令分发:使用 strcmp 匹配命令名并调用相应的处理函数
  3. 文件操作:通过 VFS 接口调用文件系统功能
  4. 路径处理:支持绝对路径和相对路径

Shell 是一个内核任务,通过键盘输入获取命令,然后解析并执行。它直接调用内核文件系统 API,没有经过系统调用,这是教学简化。真正的 Shell 应作为用户态进程通过 int 0x80 与内核交互。

构建系统(Makefile)#

CHAPTER := 19.boot-image
MODULES := mbr loader kernel
# MBR 模块
mbr_SRC := boot/mbr.S
mbr_OFFSET := 0
# Loader 模块
loader_SRC := boot/loader.S
loader_OFFSET := 1
# Kernel 模块
kernel_SRC := kernel
kernel_LDFILE := kernel/link.ld
kernel_OFFSET := 9
# 包含通用 Makefile
include ../Makefile.inc

解析:Makefile 定义了三个模块:

  1. mbr:从 boot/mbr.S 编译,写入扇区 0
  2. loader:从 boot/loader.S 编译,写入扇区 1
  3. kernel:从 kernel/ 目录编译,写入扇区 9 及之后

../Makefile.inc 提供了通用的构建逻辑,包括:

  • 编译汇编和 C 源文件
  • 链接内核
  • 创建 VHD 镜像
  • 将各个模块写入正确的扇区偏移

构建命令:

cd 19.boot-image
make all # 构建所有模块并创建镜像
make run # 在 QEMU 中运行
make clean # 清理构建产物

系统启动流程图#

sequenceDiagram participant BIOS participant MBR participant Loader participant Kernel participant Shell BIOS->>MBR: 加载扇区 0 到 0x7C00 BIOS->>MBR: 跳转执行 MBR->>MBR: 设置段寄存器和栈 MBR->>MBR: 读取扇区 1-8 MBR->>Loader: 跳转到 0x8000 Loader->>Loader: 加载 GDT Loader->>Loader: 启用保护模式 Loader->>Loader: 设置页表 Loader->>Loader: 启用分页 Loader->>Loader: 读取内核 Loader->>Kernel: 跳转到内核入口 Kernel->>Kernel: 初始化 GDT/TSS/IDT Kernel->>Kernel: 启用中断 Kernel->>Kernel: 初始化定时器 Kernel->>Kernel: 初始化内存管理 Kernel->>Kernel: 初始化设备驱动 Kernel->>Kernel: 初始化文件系统 Kernel->>Kernel: 初始化任务和进程 Kernel->>Kernel: 执行系统自检 Kernel->>Kernel: 预创建文件系统内容 Kernel->>Shell: 创建 Shell 任务 Kernel->>Kernel: 启动调度器 Shell->>Shell: 显示提示符 Shell->>Shell: 等待键盘输入 Shell->>Shell: 解析命令 Shell->>Kernel: 执行系统调用

运行与验证#

编译运行#

# 进入章节目录
cd 19.boot-image
# 清理旧构建
make clean
# 构建镜像
make all
# 在 QEMU 中运行
make run
# 或者带 VNC 显示运行
make run-vnc
# 或者带调试支持运行
make run-vnc.debug

VHD 镜像本身是标准虚拟磁盘格式,理论上可以 dd 写入 U 盘或移动硬盘,在真实 PC 上通过 BIOS 启动。本系列的镜像在 QEMU 上验证通过,真机启动受限于具体主板的 BIOS 兼容性和磁盘控制器支持,未在所有机型上逐一验证,建议以 QEMU 为主要运行环境。

预期输出#

启动时会看到以下输出:

===================================
| MyOS - Teaching OS v1.0 |
| Chapter 19: Final Boot Image |
===================================
Initializing kernel subsystems...
GDT...OK
TSS...OK
IDT...OK
Interrupts...OK
Timer...OK
Memory...OK
Devices...OK
Filesystem...OK
Tasks...OK
Self-test:
[PASS] Memory allocation
[PASS] Interrupts
[PASS] Timer
Result: 3/3 passed
Starting shell...
MyOS Shell v1.0 - Type 'help' for commands.
/> help
Commands: help clear echo ls cat mkdir touch write pwd cd
ps mem uptime version exit
/> ls
home/
bin/
tmp/
readme.txt
/> cat readme.txt
Welcome to MyOS!
A complete teaching operating system.
Features:
- Preemptive multitasking
- Virtual memory
- File system
- Device drivers
- System calls
- Interactive shell
/> pwd
/
/> mkdir /home/user
/> cd /home/user
/home/user> touch test.txt
/home/user> write test.txt Hello MyOS!
/home/user> cat test.txt
Hello MyOS!
/home/user> ps
TID NAME STATE
3 shell RUNNING
/home/user> uptime
Uptime: 0 seconds
/home/user> version
MyOS v1.0 - Complete Teaching OS
/home/user> exit
System halted.

功能验证清单#

启动后,可以验证以下功能:

  1. 引导流程:系统从 MBR 成功启动,经过 Loader 到内核
  2. 内存管理mem 命令显示 E820 内存映射信息
  3. 中断和定时器uptime 命令显示系统运行时间,证明定时器工作正常
  4. 文件系统lscatmkdirtouchwrite 等命令证明文件系统工作正常
  5. 任务调度ps 命令显示当前任务状态
  6. Shell:可以交互式执行命令
  7. 键盘驱动:可以正常输入命令
  8. 自检功能:所有自检测试通过

踩坑记录#

问题 1:系统启动后没有输出#

可能原因

  • VGA 显示驱动初始化失败
  • 中断未正确启用,导致后续初始化卡住
  • 内核加载位置错误

解决方案

  1. 检查 vga_clear() 是否在初始化序列的最前面
  2. 确认 enable_interrupts()idt_init() 之后调用
  3. 检查内核链接脚本 kernel/link.ld 中的入口地址是否正确
  4. 使用 make run-vnc.debug 启动 QEMU 并连接 GDB 调试

问题 2:键盘输入无响应#

可能原因

  • 键盘驱动未初始化
  • 中断未启用
  • 键盘缓冲区处理有问题

解决方案

  1. 确认 keyboard_init() 已被调用
  2. 检查 keyboard_has_char()keyboard_getchar() 函数实现
  3. 验证键盘中断处理程序是否正确注册到 IDT
  4. 添加调试输出,检查键盘扫描码是否被接收到

问题 3:文件系统命令失败#

可能原因

  • SimpleFS 未正确初始化
  • 文件系统未挂载
  • VFS 接口调用错误

解决方案

  1. 确认 simple_fs_init() 已被调用
  2. 检查文件系统初始化时的错误返回值
  3. 验证文件操作(fs_openfs_readfs_write)的返回值
  4. 添加调试输出,跟踪文件系统调用链

问题 4:构建失败或镜像不完整#

可能原因

  • 模块偏移配置错误
  • 链接脚本问题
  • 编译器或链接器版本不兼容

解决方案

  1. 检查 Makefile 中的 *_OFFSET 配置是否正确
  2. 验证 kernel/link.ld 中的内存布局
  3. 确认使用正确的交叉编译工具链(i386-elf-gcc
  4. 使用 make view 查看磁盘镜像的十六进制内容

至此,从第 01 章的系统加电到第 19 章的完整启动镜像,一个教学操作系统从零到一的旅程结束了。这个系统虽然简化,但涵盖了操作系统的基本概念:引导、内存管理、调度、同步、特权级隔离、文件系统、进程管理和用户交互。

参考#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

启动镜像:系统集成与自检
https://blog.souloss.cn/posts/os/boot-image-integration/
作者
Souloss
发布于
2024-05-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时