从零写 OS 内核-第九篇:用户态程序加载 —— ELF 解析与简易文件系统

"你的 OS 能 fork/exec,但程序从哪来?
今天,我们构建第一个文件系统,让内核能从‘磁盘'加载用户程序!"

在上一篇中,我们实现了 forkexec,但用户程序仍需硬编码在内核镜像中
这显然不现实——真正的操作系统必须能从存储设备加载任意程序

而这一切,依赖两大核心模块: ✅ 文件系统 —— 组织和管理磁盘上的文件
ELF 加载器 —— 解析并加载可执行文件到内存

今天,我们就来实现一个极简但可用的文件系统(Initramfs 风格),并完成 ELF 程序加载,让你的 exec("/bin/ls") 真正工作!


🗂️ 一、为什么需要文件系统?

没有文件系统,你的 OS 就像一台没有硬盘的电脑——
所有程序必须编译进内核,无法动态扩展。

文件系统的核心职责:

  • 命名:通过路径(如 /bin/ls)访问文件
  • 存储:将文件内容映射到物理存储(磁盘/内存)
  • 元数据:记录文件大小、权限、类型等

💡 初期无需复杂磁盘驱动——我们可以用 Initramfs(初始内存文件系统)
将所有文件打包进内核镜像,启动时加载到内存。


📦 二、Initramfs:最简文件系统实现

Initramfs 是 Linux 启动初期常用的内存文件系统,非常适合我们的场景。

设计思路:

  1. 将所有用户程序打包成一个二进制 blob
  2. 定义简单目录结构(扁平或树形)
  3. 内核启动时解析 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=binaryuser_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 加载器

步骤:

  1. 从文件系统读取 ELF 文件到内核缓冲区
  2. 验证 ELF 魔数和类型
  3. 遍历 Program Headers,加载 PT_LOAD
  4. 设置入口地址

代码框架:

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)