# 3.8: 系统调用exec的实现分析(Code:exec) ## 什么是 `exec`? `exec` 是一个系统调用,用于将一个进程的用户地址空间替换为从可执行文件(binary 或 executable file)中读取的数据。可执行文件通常是编译器和链接器的输出,包含机器指令和程序数据。 ## `exec` 的工作流程 ### 1. 打开文件 - 调用 `namei`(`kernel/exec.c:36`)打开指定路径的二进制文件。 ```c if((ip = namei(path)) == 0){ end_op(); return -1; } ``` - 文件格式为 ELF(Executable and Linkable Format),这是一个被广泛使用的二进制文件格式。 ### 2. 读取 ELF 头 - ELF 文件结构: - ELF 头:`struct elfhdr`(定义在 `kernel/elf.h:6`)。 ```c #define ELF_MAGIC 0x464C457FU // "\x7FELF" in little endian // File header struct elfhdr { uint magic; // must equal ELF_MAGIC, four-byte “magic number” 0x7F, ‘E’, ‘L’, ‘F’, uchar elf[12]; ushort type; ushort machine; uint version; uint64 entry; uint64 phoff; uint64 shoff; uint flags; ushort ehsize; ushort phentsize; ushort phnum; ushort shentsize; ushort shnum; ushort shstrndx; }; ``` - 程序段头表:`struct proghdr`(定义在 `kernel/elf.h:25`),描述需要加载到内存的程序部分。 ```c // Program section header struct proghdr { uint32 type; uint32 flags; uint64 off; uint64 vaddr; uint64 paddr; uint64 filesz; uint64 memsz; uint64 align; }; ``` - 通常有两种段: - 指令段 - 数据段 - 首先检查 ELF 文件是否有效: - ELF 文件以 "magic number"(`0x7F 'E' 'L' 'F'`)开头。 ### 3. 分配页表与内存 - `exec` 为进程创建新的页表(调用 `proc_pagetable`,`kernel/exec.c:49`)。 ```c if((pagetable = proc_pagetable(p)) == 0) ``` - 为每个 ELF 段分配内存(调用 `uvmalloc`,`kernel/exec.c:65`),并加载到内存中: ```c if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0) ``` - 调用 `loadseg`(`kernel/exec.c:10`)逐页加载 ELF 文件数据。 ```c static int loadseg(pde_t *, uint64, struct inode *, uint, uint); ``` - 使用 `walkaddr` 查找物理地址。 - 使用 `readi` 从文件中读取内容。 ### 4. 加载段 `/init` 是通过 `exec` 系统调用创建的第一个用户程序,其程序段头表如下: ``` # objdump -p user/_init user/_init: file format elf64-little Program Header: 0x70000003 off 0x0000000000006bb0 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2 ** 0 filesz 0x000000000000004a memsz 0x0000000000000000 flags r-- LOAD off 0x0000000000001000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2 ** 12 filesz 0x0000000000001000 memsz 0x0000000000001000 flags r-x LOAD off 0x0000000000002000 vaddr 0x0000000000001000 paddr 0x0000000000001000 align 2 ** 12 filesz 0x0000000000000010 memsz 0x0000000000000030 flags rw- STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2 ** 4 filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw- ``` 关于段的关键字段的解释 |字段| 含义| |-----|------| |off |段在文件中的偏移量。| |vaddr |段在内存中的虚拟地址(加载后的起始地址)。| |paddr |段在内存中的物理地址(在现代系统中一般与 vaddr 相同,可能被忽略)。| |align |段的对齐要求,通常是 2 的幂。| |filesz |段在文件中的大小(字节数)。| |memsz |段在内存中的大小(字节数)。| |flags |段的权限标志:r(读),w(写),x(执行)。| 上述的内容中, `0x70000003`为自定义段 (Type: 0x70000003),`LOAD`为可执行段或读写段(数据) 系统调用(例如 exec)会根据 Program Header 中的描述,将 LOAD 段映射到进程的虚拟内存中。特殊段(如 STACK)由内核动态管理,通常不直接对应文件内容。 - ELF 段头中的 `filesz` 和 `memsz` 指示加载逻辑: - 当 `filesz < memsz` 时,剩余部分用零填充(例如 C 的全局变量)。 - 示例:`/init` 的数据段 `filesz = 0x10`,`memsz = 0x30`,分配 0x30 字节内存,但只从文件中读取 0x10 字节。 ### 5. 设置用户栈 - 分配一页作为用户栈,并将参数字符串逐一复制到栈顶。 - 记录参数指针到 `ustack`,在末尾添加空指针(作为 `argv` 的结束)。 - 参数通过系统调用返回路径传递: - `argc` 存储在 `a0`。 - `argv` 存储在 `a1`。 - 栈下方分配一个不可访问页面: - 防止程序超限使用多页栈。 - 如果参数过大,`copyout` 会检测到目标页不可访问并返回 `-1`。 ### 6. 错误处理 - 如果 `exec` 发现错误(如无效段),跳转到 `bad` 标签: - 释放新内存映像,返回 `-1`。 - 直到确保系统调用成功前,不会释放旧内存映像。 ## 安全性考虑 ### 1. ELF 文件地址的风险 - ELF 文件可能包含恶意地址: - 地址(`ph.vaddr`)加上大小(`ph.memsz`)的和可能溢出,导致危险。 - xv6 使用检查,例如 `if(ph.vaddr + ph.memsz < ph.vaddr)`,防止溢出。 ### 2. 分离的内核地址空间 - 在 RISC-V 架构的 xv6 中: - 内核和用户空间有独立的页表。 - `loadseg` 仅加载到用户页表,无法直接写入内核地址空间。 ### 3. 检查缺失的隐患 - 尽管 xv6 有一些检查,但未涵盖所有用户输入验证: - 恶意程序可能利用缺陷绕过隔离。 ## 总结 1. `exec` 的作用: 替换当前进程的用户地址空间,加载一个新的程序。 2. 实现流程: 打开 ELF 文件 -> 解析段头 -> 分配页表和内存 -> 加载段内容 -> 设置用户栈。 3. 安全设计: 分离内核与用户页表,进行边界检查以防止溢出攻击。 4. 潜在风险: 检查的遗漏可能被恶意程序利用,造成系统漏洞。