Appearance
代码:exec
exec
是一个系统调用,它用从文件中读取的数据(称为二进制文件或可执行文件)替换进程的用户地址空间。二进制文件通常是编译器和链接器的输出,包含机器指令和程序数据。exec
使用 namei
打开名为 path
的二进制文件,这在第 8 章中有解释。然后,它读取 ELF 头。Xv6 二进制文件采用广泛使用的 ELF 格式进行格式化,该格式在 kernel/elf.h
中定义。一个 ELF 二进制文件由一个 ELF 头 struct elfhdr
,后跟一系列程序段头 struct proghdr
组成。每个 proghdr
描述了必须加载到内存中的应用程序的一个段;xv6 程序有两个程序段头:一个用于指令,一个用于数据。
第一步是快速检查文件是否可能包含 ELF 二进制文件。一个 ELF 二进制文件以四字节的“魔数” 0x7F
、'E'
、'L'
、'F'
或 ELF_MAGIC
开头。如果 ELF 头有正确的魔数,exec
就假定该二进制文件格式正确。
exec
使用 proc_pagetable
分配一个没有用户映射的新页表,使用 uvmalloc
为每个 ELF 段分配内存,并使用 loadseg
将每个段加载到内存中。loadseg
使用 walkaddr
查找分配的内存的物理地址,以写入 ELF 段的每一页,并使用 readi
从文件中读取。
/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-
我们看到文本段应该加载到内存中的虚拟地址 0x0(没有写权限),其内容来自文件中的偏移量 0x1000。我们还看到数据应该加载到地址 0x1000,这是一个页边界,并且没有执行权限。
程序段头的 filesz
可能小于 memsz
,这表明它们之间的差距应该用零填充(对于 C 全局变量),而不是从文件中读取。对于 /init
,数据 filesz
是 0x10 字节,memsz
是 0x30 字节,因此 uvmalloc
分配了足够的物理内存来容纳 0x30 字节,但只从文件 /init
中读取 0x10 字节。
现在 exec
分配并初始化用户栈。它只分配一个栈页。exec
一次一个地将参数字符串复制到栈顶,并将指向它们的指针记录在 ustack
中。它在将传递给 main
的 argv
列表的末尾放置一个空指针。值 argc
和 argv
通过系统调用返回路径传递给 main
:argc
通过系统调用返回值传递,该返回值进入 a0
,argv
通过进程的陷阱帧的 a1
条目传递。
exec
在栈页的正下方放置一个不可访问的页面,这样试图使用多于一个页面的程序就会出错。这个不可访问的页面也允许 exec
处理过大的参数;在这种情况下,exec
用来将参数复制到栈的 copyout
函数会注意到目标页面不可访问,并返回 -1。
在准备新内存映像期间,如果 exec
检测到错误,如无效的程序段,它会跳转到标签 bad
,释放新映像,并返回 -1。exec
必须等到它确定系统调用会成功后才能释放旧映像:如果旧映像不见了,系统调用就无法向其返回 -1。exec
中唯一的错误情况发生在创建映像期间。一旦映像完成,exec
就可以提交到新页表并释放旧页表。
exec
将字节从 ELF 文件加载到由 ELF 文件指定的地址的内存中。用户或进程可以将任何他们想要的地址放入 ELF 文件中。因此 exec
是有风险的,因为 ELF 文件中的地址可能会意外地或故意地引用内核。对于一个不警惕的内核来说,后果可能从崩溃到恶意颠覆内核的隔离机制(即安全漏洞利用)。Xv6 执行了许多检查来避免这些风险。例如 if(ph.vaddr + ph.memsz < ph.vaddr)
检查和是否溢出一个 64 位整数。危险在于用户可以构造一个 ELF 二进制文件,其中 ph.vaddr
指向一个用户选择的地址,而 ph.memsz
足够大,以至于和溢出到 0x1000,这看起来像一个有效值。在旧版本的 xv6 中,用户地址空间也包含内核(但在用户模式下不可读/写),用户可以选择一个对应于内核内存的地址,从而将数据从 ELF 二二进制文件复制到内核中。在 RISC-V 版本的 xv6 中,这不会发生,因为内核有自己独立的页表;loadseg
加载到进程的页表中,而不是内核的页表中。
内核开发人员很容易忽略一个关键的检查,而现实世界的内核有很长的历史,都存在着被用户程序利用以获取内核权限的缺失检查。xv6 很可能没有完全验证提供给内核的用户级数据,恶意用户程序可能会利用这一点来规避 xv6 的隔离。