解析 xv6 exec 函数过程,理解内核中 exec 是如何实现的。
下边是从源代码中拷贝出来的,仅保留关键逻辑的代码。
13 exec(char *path, char **argv)
14 {
17 uint64 argc, sz = 0, sp, ustack[MAXARG+1], stackbase;
21 pagetable_t pagetable = 0, kpagetable = 0, oldpagetable, oldkpagetable;
22 struct proc *p = myproc();
24
25 if((ip = namei(path)) == 0){
26 end_op();
27 return -1;
28 }
30
31 // Check ELF header
32 if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
33 goto bad;
34 if(elf.magic != ELF_MAGIC)
35 goto bad;
36
37 if((kpagetable = proc_kpagetable(p)) == 0)
38 goto bad;
43 for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
44 if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
53 if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
54 goto bad;
58 if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
59 goto bad;
60 }
64
65 p = myproc();
66 uint64 oldsz = p->sz;
67
68 // Allocate two pages at the next page boundary.
69 // Use the second as the user stack.
70 sz = PGROUNDUP(sz);
71 uint64 sz1;
72 if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
73 goto bad;
74 sz = sz1;
75 uvmclear(pagetable, sz-2*PGSIZE);
77 sp = sz;
78 stackbase = sp - PGSIZE;
79
80 // Push argument strings, prepare rest of stack in ustack.
81 for(argc = 0; argv[argc]; argc++) {
82 if(argc >= MAXARG)
83 goto bad;
84 sp -= strlen(argv[argc]) + 1;
85 sp -= sp % 16; // riscv sp must be 16-byte aligned
86 if(sp < stackbase)
87 goto bad;
88 if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
89 goto bad;
90 ustack[argc] = sp;
91 }
92 ustack[argc] = 0;
93
94 // push the array of argv[] pointers.
95 sp -= (argc+1) * sizeof(uint64);
96 sp -= sp % 16;
97 if(sp < stackbase)
98 goto bad;
99 if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
100 goto bad;
101
102 // arguments to user main(argc, argv)
103 // argc is returned via the system call return
104 // value, which goes in a0.
105 p->trapframe->a1 = sp;
115 p->pagetable = pagetable;
117 p->sz = sz;
118 p->trapframe->epc = elf.entry; // initial program counter = main
119 p->trapframe->sp = sp; // initial stack pointer
120 proc_freepagetable(oldpagetable, oldsz);
127 return argc; // this ends up in a0, the first argument to main(argc, argv)
129 bad:
138 return -1;
139 }
exec 和 fork 函数不同,exec 的函数原型是 exec(BIN, argv...)
。exec 并不额外创建进程,
而是在当前进程中直接执行对应的命令和函数。那么就要替换进程的内容。其中最终要的,就是替换新的地址空间及内容。
首先读取可执行文件的在文件系统上的内容,做一些合法校验。ip(inode path)。
25 if((ip = namei(path)) == 0){
26 end_op();
27 return -1;
28 }
然后创建一个新的地址空间,实际就是创建新的页表
40 if((pagetable = proc_pagetable(p)) == 0)
41 goto bad;
根据用户地址空间的布局,这里需要初始化三种,一种是二进制自带的指令和数据段,统称一下 text,用户运行的栈 stack,还有一块跳转代码 trampoline。其中 trampoline 已经由 proc_pagetable 初始完成,置于虚拟地址空间最高处,分配一页内存。接下来就是完成二进制数据文件的布局 text。
这里先进行页表关联,然后将数据加载到对应的物理内存中。
43 for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
44 if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
53 if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
54 goto bad;
58 if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
59 goto bad;
60 }
接下来分配栈空间,分配栈空间细分三个步骤,首先需要分配对应的物理内存和进行页表关联。
这里栈地址首先进行页对齐,然后分配两个物理页,将高地址的那一个页作为栈,低地址那一个干什么?保护页吗,还是扩展页呢?
70 sz = PGROUNDUP(sz);
71 uint64 sz1;
72 if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
73 goto bad;
74 sz = sz1;
75 uvmclear(pagetable, sz-2*PGSIZE);
77 sp = sz;
78 stackbase = sp - PGSIZE;
从栈的高地址开始,依次放置 0~argc 的参数值。每一个值都是 16 字节对齐的,这里可以用 sp & 16 进行计算。
80 // Push argument strings, prepare rest of stack in ustack.
81 for(argc = 0; argv[argc]; argc++) {
82 if(argc >= MAXARG)
83 goto bad;
84 sp -= strlen(argv[argc]) + 1;
85 sp -= sp % 16; // riscv sp must be 16-byte aligned
86 if(sp < stackbase)
87 goto bad;
88 if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
89 goto bad;
90 ustack[argc] = sp;
91 }
92 ustack[argc] = 0;
然后再将这些参数地址作为一个数组,放在接下来地址对齐的地址上。这里会多4个空字节。不清楚为什么。
95 sp -= (argc+1) * sizeof(uint64);
96 sp -= sp % 16;
97 if(sp < stackbase)
98 goto bad;
99 if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
100 goto bad;
至此,新的页表和地址空间分配完毕,进行切换即可。