从零写 OS 内核-第九篇:用户态程序加载 —— ELF 解析与简易文件系统
"你的 OS 能 fork/exec,但程序从哪来?
今天,我们构建第一个文件系统,让内核能从‘磁盘'加载用户程序!"
在上一篇中,我们实现了 fork 和 exec,但用户程序仍需硬编码在内核镜像中。
这显然不现实——真正的操作系统必须能从存储设备加载任意程序。
而这一切,依赖两大核心模块: ✅ 文件系统 —— 组织和管理磁盘上的文件
✅ ELF 加载器 —— 解析并加载可执行文件到内存
今天,我们就来实现一个极简但可用的文件系统(Initramfs 风格),并完成 ELF 程序加载,让你的 exec("/bin/ls") 真正工作!
🗂️ 一、为什么需要文件系统?
没有文件系统,你的 OS 就像一台没有硬盘的电脑——
所有程序必须编译进内核,无法动态扩展。
文件系统的核心职责:
- 命名:通过路径(如
/bin/ls)访问文件 - 存储:将文件内容映射到物理存储(磁盘/内存)
- 元数据:记录文件大小、权限、类型等
💡 初期无需复杂磁盘驱动——我们可以用 Initramfs(初始内存文件系统):
将所有文件打包进内核镜像,启动时加载到内存。
📦 二、Initramfs:最简文件系统实现
Initramfs 是 Linux 启动初期常用的内存文件系统,非常适合我们的场景。
设计思路:
- 将所有用户程序打包成一个二进制 blob
- 定义简单目录结构(扁平或树形)
- 内核启动时解析 blob,构建内存中的文件索引
文件条目格式(简化版):
struct initramfs_entry {
char name[64]; // 文件名(如 "bin/ls")
uint32_t offset; // 在 blob 中的偏移
uint32_t size; // 文件大小
};
构建流程(Makefile):
# 将所有用户程序打包
user_files.bin: bin/ls bin/cat bin/sh
echo "bin/ls" | xxd -r -p > $@
wc -c bin/ls | awk '{printf "%08x", $$1}' | xxd -r -p >> $@
cat bin/ls >> $@
# ... 重复处理其他文件
# 链接进内核
kernel.bin: boot.o kernel.o user_files.bin
$(LD) -T linker.ld -o $@ $^ --format=binary user_files.bin --format=default
📌 关键:使用
--format=binary将user_files.bin作为二进制数据链接进内核。
🔍 三、ELF 文件格式速览
ELF(Executable and Linkable Format)是 Unix 系统的标准可执行文件格式。
关键结构:
| 结构 | 作用 | |——|——| | ELF Header | 描述架构、入口地址、程序头偏移 | | Program Header Table | 描述如何加载到内存(PT_LOAD 段) | | Sections | 编译链接用(运行时不需要) |
我们关心的字段(32 位):
// ELF 头
struct elf_header {
uint8_t e_ident[16]; // 魔数: 0x7F "ELF"
uint16_t e_type; // ET_EXEC = 可执行文件
uint32_t e_entry; // 入口虚拟地址
uint32_t e_phoff; // 程序头表偏移
uint16_t e_phnum; // 程序头数量
};
// 程序头
struct elf_program_header {
uint32_t p_type; // PT_LOAD = 需加载
uint32_t p_offset; // 文件内偏移
uint32_t p_vaddr; // 虚拟地址
uint32_t p_filesz; // 文件中大小
uint32_t p_memsz; // 内存中大小
uint32_t p_flags; // R/W/X 权限
};
✅ 加载器只需解析 Program Headers,忽略 Sections!
⚙️ 四、实现 ELF 加载器
步骤:
- 从文件系统读取 ELF 文件到内核缓冲区
- 验证 ELF 魔数和类型
- 遍历 Program Headers,加载
PT_LOAD段 - 设置入口地址
代码框架:
int load_elf_to_user(const char *path, uint32_t cr3_dir) {
// 1. 从 Initramfs 读取文件
void *elf_data = initramfs_read_file(path);
if (!elf_data) return -1;
// 2. 验证 ELF 头
elf_header *ehdr = (elf_header*)elf_data;
if (ehdr->e_ident[0] != 0x7F ||
memcmp(ehdr->e_ident + 1, "ELF", 3) != 0 ||
ehdr->e_type != ET_EXEC) {
return -1;
}
// 3. 遍历程序头
elf_program_header *phdr = (elf_program_header*)((char*)elf_data + ehdr->e_phoff);
for (int i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_type == PT_LOAD) {
// 分配用户物理页
uint32_t vaddr = phdr[i].p_vaddr;
uint32_t memsz = phdr[i].p_memsz;
uint32_t filesz = phdr[i].p_filesz;
// 映射虚拟地址到物理页
map_user_pages(cr3_dir, vaddr, memsz,
(phdr[i].p_flags & PF_W) ? PAGE_RW : 0);
// 复制文件内容
void *phys_addr = get_phys_addr(cr3_dir, vaddr);
memcpy(phys_addr, (char*)elf_data + phdr[i].p_offset, filesz);
// .bss 部分清零
if (memsz > filesz) {
memset(phys_addr + filesz, 0, memsz - filesz);
}
}
}
current_task->user_eip = ehdr->e_entry;
return 0;
}
🔑
map_user_pages:为用户虚拟地址分配物理页并更新页表。
📁 五、文件系统 API:内核如何"找"文件?
我们需要一个简单的 VFS(虚拟文件系统)层,初期只支持 Initramfs:
// 文件操作抽象
struct file_ops {
int (*read)(void *buf, size_t count, off_t offset);
// ... 其他操作(write, open 等暂不实现)
};
// 文件描述符
struct file {
struct file_ops *f_ops;
void *private_data; // 指向 initramfs_entry
off_t f_pos;
};
// 系统调用:open
int sys_open(const char *pathname, int flags) {
struct initramfs_entry *entry = initramfs_find(pathname);
if (!entry) return -1;
struct file *f = alloc_file();
f->f_ops = &initramfs_ops;
f->private_data = entry;
return alloc_fd(f); // 分配文件描述符
}
// 系统调用:read
int sys_read(int fd, void *buf, size_t count) {
struct file *f = get_file(fd);
return f->f_ops->read(f, buf, count, f->f_pos);
}
💡
exec内部会调用open+read读取 ELF 文件,与用户程序共用同一套 VFS!
🧪 六、测试:真正的 exec("/bin/ls")
构建用户程序:
# 编译 ls
i686-elf-gcc -ffreestanding -nostdlib -o bin/ls ls.c -T user.ld
# 打包进 Initramfs
./build_initramfs bin/ls bin/cat bin/sh > user_files.bin
内核中:
void init_process() {
// exec 会自动调用 open("/bin/ls") + read
exec("/bin/ls", (char*[]){"ls", NULL});
}
运行效果:
bin/
cat
ls
sh
✅ 成功从"磁盘"(内存中的 Initramfs)加载并运行用户程序!
⚠️ 七、局限与未来方向
当前实现仍很初级,但为后续打下基础:
| 问题 | 未来方案 |
|---|---|
| 只读 Initramfs | 实现 ext2/FAT32 磁盘文件系统 |
| 无动态链接 | 实现 ELF 动态加载器(ld.so) |
| 无文件权限 | 添加 inode 和权限位 |
| 无目录遍历 | 实现 opendir/readdir |
🌱 下一步:将 Initramfs 替换为真正的磁盘文件系统(如 FAT32),支持从 SD 卡/硬盘加载程序!
💬 写在最后
文件系统与 ELF 加载器,
是连接静态内核与动态用户世界的桥梁。
今天你构建的这个简易 Initramfs,
正是 Linux 启动时 initrd 的雏形。
🌟 从此,你的操作系统真正"活"了起来——它能生长,能进化,能运行任何你编译的程序。
📬 动手挑战:
添加一个新用户程序(如 echo),打包进 Initramfs,并通过 exec 运行它。
欢迎在评论区分享你的文件列表!
👇 下一篇你想看:FAT32 文件系统实战,还是 时钟中断与抢占式调度?
#操作系统 #内核开发 #文件系统 #ELF #Initramfs #用户程序加载 #从零开始
📢 彩蛋:关注后回复关键词 "elf",获取:
- 完整 ELF 加载器代码
- Initramfs 构建脚本(Python)
- 用户程序链接脚本(user.ld)