3.8: 系统调用exec的实现分析(Code:exec)
什么是 exec
?
exec
是一个系统调用,用于将一个进程的用户地址空间替换为从可执行文件(binary 或 executable file)中读取的数据。可执行文件通常是编译器和链接器的输出,包含机器指令和程序数据。
exec
的工作流程
1. 打开文件
调用
namei
(kernel/exec.c:36
)打开指定路径的二进制文件。
if((ip = namei(path)) == 0){
end_op();
return -1;
}
文件格式为 ELF(Executable and Linkable Format),这是一个被广泛使用的二进制文件格式。
2. 读取 ELF 头
ELF 文件结构:
ELF 头:
struct elfhdr
(定义在kernel/elf.h:6
)。
#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
),描述需要加载到内存的程序部分。
// 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
)。
if((pagetable = proc_pagetable(p)) == 0)
为每个 ELF 段分配内存(调用
uvmalloc
,kernel/exec.c:65
),并加载到内存中:
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
调用
loadseg
(kernel/exec.c:10
)逐页加载 ELF 文件数据。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 有一些检查,但未涵盖所有用户输入验证:
恶意程序可能利用缺陷绕过隔离。
总结
exec
的作用: 替换当前进程的用户地址空间,加载一个新的程序。实现流程: 打开 ELF 文件 -> 解析段头 -> 分配页表和内存 -> 加载段内容 -> 设置用户栈。
安全设计: 分离内核与用户页表,进行边界检查以防止溢出攻击。
潜在风险: 检查的遗漏可能被恶意程序利用,造成系统漏洞。