xv6 中 exec 实现详解
一、exec 的作用
exec(path, argv) 是 Unix 系统核心系统调用之一,它的功能是:
- 用一个新的程序 替换当前进程的用户空间;
- 当前进程的 PID、打开的文件描述符保持不变;
- 执行过程常配合
fork()使用,构成fork-exec模式:
int pid = fork();
if(pid == 0){
exec("/ls", argv); // 子进程替换自身为 ls 程序
}
二、源码位置
exec 的实现位于:
kernel/exec.c
函数签名:
int exec(char *path, char **argv);
三、执行过程分析
Step 1: 解析参数和路径
传入的 path 和 argv 是用户虚拟地址,内核无法直接使用。
- 后续需通过
copyin()和copyinstr()安全复制到内核空间使用。
Step 2: 加载 ELF 可执行文件
begin_op();
ip = namei(path); // 通过路径找 inode
ilock(ip); // 加锁 inode,防止并发访问
namei(path):解析路径,返回对应 inode。ilock(ip):加锁,确保该文件不会被其他进程同时读写。
读取 ELF 头并检查合法性:
readi(ip, 0, (uint64)&elf, 0, sizeof(elf));
if(elf.magic != ELF_MAGIC) goto bad;
readi(...):从 inode 读取文件头到内存;ELF_MAGIC:验证该文件是合法的 ELF 可执行文件。
Step 3: 创建新的地址空间(页表)
pagetable = proc_pagetable(p);
- 创建新的空页表;
- 同时映射内核需要的 trampoline 等区域。
然后,遍历 ELF 的 Program Header,加载每个段:
for(...) {
uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, PTE_W);
loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz);
}
uvmalloc(...):分配物理页并建立映射。loadseg(...):从 ELF 文件中读取数据到刚刚映射的内存中。
注意:
ph.vaddr:目标虚拟地址;ph.memsz≥ph.filesz,超出的部分为 BSS 段,清零即可。
Step 4: 创建用户栈
sz = PGROUNDUP(sz);
sz = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W);
uvmclear(pagetable, sz-2*PGSIZE); // 设置 guard page
sp = sz;
- 分配两个页:一页作为实际栈,一页作为 guard;
uvmclear(...):清除页权限,模拟栈溢出保护;sp:初始化用户栈指针。
Step 5: 构造用户栈参数 argv、argc
将字符串复制到用户栈:
for(argc = 0; argv[argc]; argc++) {
sp -= strlen(argv[argc]) + 1;
copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1);
ustack[argc] = sp;
}
ustack[argc] = 0;
然后把 ustack(每个参数字符串的地址)复制到用户栈上:
sp -= (argc+1) * sizeof(uint64);
copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64));
栈布局如下:
| argv[0] string |
| argv[1] string |
| ... |
| argv[] 指针数组 |
| 空指针 (NULL) |
| sp |
Step 6: 最终切换到新程序
p->trapframe->a0 = argc;
p->trapframe->a1 = sp;
p->trapframe->epc = elf.entry;
p->trapframe->sp = sp;
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
proc_freepagetable(oldpagetable, oldsz);
说明:
-
设置 trapframe:
a0是第一个参数,设为 argc;a1是第二个参数,设为 argv[] 的地址;epc(PC)设为 ELF 的入口地址;sp是新的用户栈指针。
-
替换页表并释放旧地址空间;
-
返回后,用户态从新的程序入口开始执行。
四、总结
| 阶段 | 内容 |
|---|---|
| 参数解析 | 从用户态复制路径和参数字符串 |
| ELF 加载 | 读取可执行文件,验证合法性 |
| 地址空间构建 | 创建新页表,加载代码段和数据段 |
| 栈创建 | 分配栈和 guard 页,准备参数 |
| 注册设置 | 设置 trapframe 并切换页表 |
| 最终执行 | 用户态从新程序入口地址开始运行 |
最终效果是:当前进程的用户空间完全被替换为另一个程序的内容,但进程 ID 保持不变。