这一章做完,我们的系统就有了完整的用户程序执行链路:写一个独立的程序,编译成 ELF,放到磁盘上,Shell 里输入 run hello.elf,内核加载并执行它。


ELF 是什么

ELF(Executable and Linkable Format)是 Linux 可执行文件的格式。你编译出来的每个程序、/bin/ls/usr/bin/python 都是 ELF 文件。

结构很简单:

ELF Header        → 魔数、架构、入口地址
Program Headers   → 每个段加载到内存哪里、从文件哪里读
.text             → 代码
.data             → 数据
.bss              → 未初始化数据(文件里不占空间,加载时清零)

加载一个 ELF,本质上就是:按 Program Header 的指示,把文件里的数据复制到内存里,然后跳到入口地址


先写用户程序

用户程序不能调内核函数,只能通过 syscall 和内核通信:

BITS 64
section .text
global _start

_start:
    mov rax, 1              ; syscall 1 = print
    lea rdi, [rel msg]      ; RIP 相对寻址
    syscall

    mov rax, 2              ; syscall 2 = exit
    syscall

section .data
msg: db "Hello from ELF!", 13, 10, 0

链接时指定加载地址 0x400000(避开内核占用的低地址区域):

ENTRY(_start)
SECTIONS {
    . = 0x400000;
    .text : { *(.text) }
    .data : { *(.data) }
}

ELF 加载器

加载流程:

uint64_t elf_load(const char *filename) {
    // 1. 读 ELF Header,验证魔数
    // 2. 遍历 Program Headers
    for each PT_LOAD segment:
        // 3. 分配物理页,建立虚拟地址映射
        for va in p_vaddr .. p_vaddr+p_memsz:
            map_page(va, pmm_alloc(), 0x3)
        // 4. 从文件读数据写到 p_vaddr
        // 5. .bss 部分清零
    // 6. 返回 e_entry
}

关键在第3步:用户程序的地址(0x400000)在内核页表里默认没有映射,直接写会 Page Fault。必须先用 pmm_alloc 分配物理页,再用 map_page 建立映射。

Shell 里执行:

uint64_t entry = elf_load(argv[1]);
void (*fn)(void) = (void (*)(void))entry;
fn();   // 跳转执行

跑起来

> ls
hello.elf
hello.txt

> run hello.elf
loading hello.elf...
Hello from ELF!
[kernel] process exited

>

从 ext2 读 ELF → 解析 Program Header → 分配物理页 → 写入内存 → 跳转 → syscall 打印 → 返回 Shell。整条链路通了。


两个坑

坑1:Page Fault,写 0x400000

ELF 加载地址是 0x400000,内核页表没有这个地址的映射,写入直接 fault。

修法:加载每个段之前,遍历 p_vaddr ~ p_vaddr+p_memsz,每 4KB 分配一个物理页并映射。

坑2:文件名被截断(h.elf 而不是 hello.elf

串口缓冲区里有上次命令残留的字节,readline 开始时把它们读走,导致后续字符错位。

修法:readline 前调 serial_flush(),把缓冲区里已到达的字符清掉。


和真实 Linux 的差距

我们跑用户程序是在 Ring 0(内核态),共用内核页表。真实 Linux:

  • 用户程序跑在 Ring 3,权限受限
  • 每个进程有独立页表,进程间内存隔离
  • ELF 支持动态链接(.so),大部分程序不是静态链接的

但核心逻辑是一样的:读 Program Header,按指示加载,跳入口。


下一章

十六章走完,整个系统的主要组件都有了。下一章做整体回顾:从第1章到第16章,我们建了什么,每一层的作用是什么。

源码:github.com/tongpengfei/learn-with-ai