# 3.8 代码：exec

`exec`是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。`exec`(***kernel/exec.c***:13)使用`namei` (***kernel/exec.c***:26)打开指定的二进制`path`，这在第8章中有解释。然后，它读取ELF头。Xv6应用程序以广泛使用的ELF格式描述，定义于(***kernel/elf.h***)。ELF二进制文件由ELF头、`struct elfhdr`(***kernel/elf.h***:6)，后面一系列的程序节头（section headers）、`struct proghdr`(***kernel/elf.h***:25)组成。每个`proghdr`描述程序中必须加载到内存中的一节（section）；xv6程序只有一个程序节头，但是其他系统对于指令和数据部分可能各有单独的节。

> [!NOTE]
> ELF文件格式：在计算机科学中，是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储格式文件。ELF是UNIX系统实验室（USL）作为应用程序二进制接口（Application Binary Interface，ABI）而开发和发布的，也是Linux的主要可执行文件格式。ELF文件由4部分组成，分别是ELF头（ELF header）、程序头表（Program header table）、节（Section）和节头表（Section header table）。实际上，一个文件中不一定包含全部内容，而且它们的位置也未必如同所示这样安排，只有ELF头的位置是固定的，其余各部分的位置、大小等信息由ELF头中的各项值来决定。

第一步是快速检查文件可能包含ELF二进制的文件。ELF二进制文件以四个字节的“幻数”`0x7F`、“`E`”、“`L`”、“`F`”或`ELF_MAGIC`开始(***kernel/elf.h***:3)。如果ELF头有正确的幻数，`exec`假设二进制文件格式良好。

`exec`使用`proc_pagetable` (***kernel/exec.c***:38)分配一个没有用户映射的新页表，使用`uvmalloc` (***kernel/exec.c***:52)为每个ELF段分配内存，并使用`loadseg` (***kernel/exec.c***:10)将每个段加载到内存中。`loadseg`使用`walkaddr`找到分配内存的物理地址，在该地址写入ELF段的每一页，并使用`readi`从文件中读取。

使用`exec`创建的第一个用户程序`/init`的程序节标题如下：

```bash
 # objdump -p _init 
 user/_init: file format elf64-littleriscv 
 Program Header: 
     LOAD off 0x00000000000000b0 vaddr 0x0000000000000000 
                                    paddr 0x0000000000000000 align 2**3 
          filesz 0x0000000000000840 memsz 0x0000000000000858 flags rwx 
     STACK off 0x0000000000000000 vaddr 0x0000000000000000 
                                    paddr 0x0000000000000000 align 2**4 
          filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw- 
```

程序节头的`filesz`可能小于`memsz`，这表明它们之间的间隙应该用零来填充（对于C全局变量），而不是从文件中读取。对于***/init***，`filesz`是2112字节，`memsz`是2136字节，因此`uvmalloc`分配了足够的物理内存来保存2136字节，但只从文件***/init***中读取2112字节。

现在`exec`分配并初始化用户栈。它只分配一个栈页面。`exec`一次将参数中的一个字符串复制到栈顶，并在`ustack`中记录指向它们的指针。它在传递给`main`的`argv`列表的末尾放置一个空指针。`ustack`中的前三个条目是伪返回程序计数器（fake return program counter）、`argc`和`argv`指针。 

`exec`在栈页面的正下方放置了一个不可访问的页面，这样试图使用超过一个页面的程序就会出错。这个不可访问的页面还允许`exec`处理过大的参数；在这种情况下，被`exec`用来将参数复制到栈的函数`copyout`(***kernel/vm.c***:355) 将会注意到目标页面不可访问，并返回-1。 

在准备新内存映像的过程中，如果`exec`检测到像无效程序段这样的错误，它会跳到标签`bad`，释放新映像，并返回-1。`exec`必须等待系统调用会成功后再释放旧映像：因为如果旧映像消失了，系统调用将无法返回-1。`exec`中唯一的错误情况发生在映像的创建过程中。一旦映像完成，`exec`就可以提交到新的页表(***kernel/exec.c***:113)并释放旧的页表(***kernel/exec.c***:117)。

`exec`将ELF文件中的字节加载到ELF文件指定地址的内存中。用户或进程可以将他们想要的任何地址放入ELF文件中。因此`exec`是有风险的，因为ELF文件中的地址可能会意外或故意的引用内核。对一个设计拙劣的内核来说，后果可能是一次崩溃，甚至是内核的隔离机制被恶意破坏（即安全漏洞）。xv6执行许多检查来避免这些风险。例如，`if(ph.vaddr + ph.memsz < ph.vaddr)`检查总和是否溢出64位整数，危险在于用户可能会构造一个ELF二进制文件，其中的`ph.vaddr`指向用户选择的地址，而`ph.memsz`足够大，使总和溢出到0x1000，这看起来像是一个有效的值。在xv6的旧版本中，用户地址空间也包含内核（但在用户模式下不可读写），用户可以选择一个与内核内存相对应的地址，从而将ELF二进制文件中的数据复制到内核中。在xv6的RISC-V版本中，这是不可能的，因为内核有自己独立的页表；`loadseg`加载到进程的页表中，而不是内核的页表中。

内核开发人员很容易省略关键的检查，而现实世界中的内核有很长一段丢失检查的历史，用户程序可以利用这些检查的缺失来获得内核特权。xv6可能没有完成验证提供给内核的用户级数据的全部工作，恶意用户程序可以利用这些数据来绕过xv6的隔离。
