Appearance
exec.c
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "spinlock.h"
#include "proc.h"
#include "defs.h"
#include "elf.h"
static int loadseg(pde_t *, uint64, struct inode *, uint, uint);
TIP
将 ELF 程序头中的段标志 (flags) 转换为页表项 (PTE) 的权限位。 ELF flags -> PTE flags
int
flags2perm(int flags)
{
int perm = 0;
if(flags & ELF_PROG_FLAG_EXEC)
perm = PTE_X;
if(flags & ELF_PROG_FLAG_WRITE)
perm |= PTE_W;
return perm;
}
TIP
exec 系统调用:加载并执行一个新程序。 exec(path, argv) 会加载并执行名为 path 的可执行文件, 并将 argv 作为命令行参数传递给它。 它会用新程序的内存映像替换当前进程的内存, 但会保留相同的文件描述符、进程 ID 和父进程。 成功时,它不会返回到调用者进程,而是从新程序的 main 函数开始执行。 失败时,返回 -1。
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pagetable_t pagetable = 0, oldpagetable;
struct proc *p = myproc();
begin_op();
TIP
- 打开路径对应的可执行文件 inode
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
TIP
- 读取并验证 ELF 头部 从 inode 的偏移量 0 处读取 ELF 文件头
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
TIP
检查 ELF 魔数 (magic number),确认这是否是一个有效的 ELF 文件
if(elf.magic != ELF_MAGIC)
goto bad;
TIP
- 为新程序创建一个新的用户页表 这个页表将映射新程序的地址空间
if((pagetable = proc_pagetable(p)) == 0)
goto bad;
TIP
- 加载 ELF 文件的程序段 (segments) 到内存 遍历 ELF 文件中的所有程序头 (program header)
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){TIP
从 inode 中读取一个程序头
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
TIP
我们只关心需要加载到内存的段 (type == ELF_PROG_LOAD)
if(ph.type != ELF_PROG_LOAD)
continue;
TIP
一些健全性检查,确保段的定义是合理的
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
if(ph.vaddr % PGSIZE != 0)
goto bad;
TIP
为当前段分配内存,并设置页表映射 uvmalloc 会分配物理内存并将其映射到指定的虚拟地址范围
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
goto bad;
sz = sz1;
TIP
将段的内容从文件加载到刚刚分配好的内存中
if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
iunlockput(ip);
end_op();
ip = 0;
p = myproc();
uint64 oldsz = p->sz;
TIP
- 分配并设置用户栈 首先,将内存大小向上取整到页边界
sz = PGROUNDUP(sz);
uint64 sz1;TIP
分配两页:一页作为实际的栈空间,另一页作为保护页 (guard page) 防止栈溢出
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
goto bad;
sz = sz1;
uvmclear(pagetable, sz - 2*PGSIZE);
sp = sz;
stackbase = sp - PGSIZE;
TIP
- 将命令行参数 (argv) 压入用户栈 首先,将参数字符串本身复制到栈顶
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp -= sp % 16;
if(sp < stackbase)
goto bad;TIP
将参数字符串从内核空间复制到用户栈上
if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[argc] = sp;
}
ustack[argc] = 0;
TIP
接下来,将指向这些字符串的指针数组 (argv) 本身压入栈中
sp -= (argc+1) * sizeof(uint64);
sp -= sp % 16;
if(sp < stackbase)
goto bad;TIP
将 ustack 中的指针数组从内核复制到用户栈
if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
goto bad;
TIP
- 准备新程序的初始执行状态 user main(argc, argv) 的参数 argc 将通过 a0 寄存器传递 (由 syscall wrapper 完成)
p->trapframe->a1 = sp;
TIP
为了调试方便,从路径中提取程序名并保存到进程结构中
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(p->name, last, sizeof(p->name));
TIP
- 提交新的内存映像,正式替换旧的进程映像
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry;
p->trapframe->sp = sp;
proc_freepagetable(oldpagetable, oldsz);
TIP
exec 成功。返回的 argc 会被 syscall handling code 放入 a0 寄存器。 当从内核返回到用户空间时,这个值会成为 main 函数的第一个参数。
return argc;
bad:
if(pagetable)
proc_freepagetable(pagetable, sz);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}
TIP
loadseg: 将一个程序段加载到指定的虚拟地址 从 inode 的 offset 处读取 sz 字节的数据, 并将其加载到 pagetable 中虚拟地址为 va 的位置。 va 必须是页对齐的,并且 va 到 va+sz 的虚拟地址空间 必须已经通过 uvmalloc 分配好了。 成功返回 0,失败返回 -1。
static int
loadseg(pagetable_t pagetable, uint64 va, struct inode *ip, uint offset, uint sz)
{
uint i, n;
uint64 pa;
if((va % PGSIZE) != 0)
panic("loadseg: va must be page aligned");
TIP
遍历该段所占用的所有页
for(i = 0; i < sz; i += PGSIZE){TIP
找到当前虚拟地址 va+i 对应的物理地址 pa uvmalloc 应该已经为这些地址创建了映射,所以 walkaddr 不应该失败
pa = walkaddr(pagetable, va + i);
if(pa == 0)
panic("loadseg: address should exist");
TIP
计算本次循环需要从文件中读取多少字节
if(sz - i < PGSIZE)
n = sz - i;
else
n = PGSIZE;
TIP
从 inode 的指定偏移量读取数据,直接写入到物理地址 pa
if(readi(ip, 0, (uint64)pa, offset+i, n) != n)
return -1;
}
return 0;
}