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: 解析参数和路径

传入的 pathargv 是用户虚拟地址,内核无法直接使用。

  • 后续需通过 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.memszph.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 保持不变。