# Initial process that execs /init.
# This code runs in user space.
#include "syscall.h"
# exec(init, argv)
.globl start
start:
la a0, init
la a1, argv
li a7, SYS_exec
ecall
# for(;;) exit();
exit:
li a7, SYS_exit
ecall
jal exit
# char init[] = "/init\0";
init:
.string "/init\0"
# char *argv[] = { init, 0 };
.p2align 2
argv:
.long init
.long 0
在这段代码中,通过ecall指令进入内核。
然后,会执行uservec、usertrap和syscall函数。
暂时先看syscall
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
因为a7中保存的是sys_exec,所以这里会调用sys_exec函数
(通过函数指针表)
uint64
sys_exec(void)
{
char path[MAXPATH], *argv[MAXARG];
int i;
uint64 uargv, uarg;
if(argstr(0, path, MAXPATH) < 0 || argaddr(1, &uargv) < 0){
return -1;
}
memset(argv, 0, sizeof(argv));
for(i=0;; i++){
if(i >= NELEM(argv)){
goto bad;
}
if(fetchaddr(uargv+sizeof(uint64)*i, (uint64*)&uarg) < 0){
goto bad;
}
if(uarg == 0){
argv[i] = 0;
break;
}
argv[i] = kalloc();
if(argv[i] == 0)
goto bad;
if(fetchstr(uarg, argv[i], PGSIZE) < 0)
goto bad;
}
int ret = exec(path, argv);
for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
kfree(argv[i]);
return ret;
bad:
for(i = 0; i < NELEM(argv) && argv[i] != 0; i++)
kfree(argv[i]);
return -1;
}
这个sys_exec
的返回值会被syscall
保存在p->trapframe->a0
内核将控制权返回给用户空间,用户空间的exec()调用根据a0中的值得到结果。
内核函数argint、argaddr和argfd从陷阱帧中检索第 n 个系统调用参数,分别作为整数、指针或文件描述符。它们都调用argraw来检索适当的已保存用户寄存器(位于kernel/syscall.c:35)。
static uint64
argraw(int n)
{
struct proc *p = myproc();
switch (n) {
case 0:
return p->trapframe->a0;
case 1:
return p->trapframe->a1;
case 2:
return p->trapframe->a2;
case 3:
return p->trapframe->a3;
case 4:
return p->trapframe->a4;
case 5:
return p->trapframe->a5;
}
panic("argraw");
return -1;
}
这里的意思是:内核开发者在写一个内核函数时,会指定一些参数,那么这些参数就是这个内核函数的调用约定。这些参数需要在调用这个内核函数之前就提前放置在寄存器中。例如下面这段代码,用户在调用系统调用sys_exec
之前需要知道这个系统调用的调用约定,然后根据约定把参数放置在正确的寄存器中。然后调用ecall指令来进入内核.
start:
la a0, init
la a1, argv
li a7, SYS_exec
ecall
然后在内核函数被调用时,通过argint等函数来从寄存器中进行读取。
int sys_my_syscall(void) {
int arg1, arg3;
char *arg2;
// 从陷阱帧中提取参数
argint(0, &arg1);
argaddr(1, (uint64 *)&arg2);
argint(2, &arg3);
// 在这里处理您的系统调用逻辑
return 0;
}
某些系统调用传递指针作为参数,内核必须使用这些指针来读取或写入用户内存。例如,exec系统调用将指向用户空间字符串参数的指针数组传递给内核。这些指针带来了两个挑战。首先,用户程序可能存在缺陷或恶意行为,并可能向内核传递无效指针或用于欺骗内核访问内核内存而非用户内存的指针。其次,xv6 内核页表映射与用户页表映射不同,因此内核无法使用普通指令从用户提供的地址加载或存储数据。
内核实现了将数据安全地从用户提供的地址传输到内核并返回的函数。fetchstr是一个例子(位于kernel/syscall.c:25)。诸如exec之类的文件系统调用使用fetchstr从用户空间检索字符串文件名参数。fetchstr调用copyinstr来完成繁重的工作。
copyinstr(位于kernel/vm.c:398)从用户页表pagetable中的虚拟地址srcva复制最多max字节的数据到dst。
由于pagetable不是当前页表,copyinstr使用walkaddr(通过调用walk)在pagetable中查找srcva,得到物理地址pa0。内核将每个物理 RAM 地址映射到相应的内核虚拟地址,因此copyinstr可以直接从pa0复制字符串字节到dst。
这段话的意思是,walkaddr在用户页表中查找指定的指针地址srcva
,并且通过页表寄存器计算出物理地址pa0
。
然后将物理地址映射到内核的虚拟地址中。所以就完成了从用户指定的虚拟地址指针srcva
将数据拷贝到内核虚拟地址中。
walkaddr(位于kernel/vm.c:104)检查用户提供的虚拟地址是否是进程的用户地址空间的一部分,因此程序无法欺骗内核去读取其他内存。类似地,copyout函数将数据从内核复制到用户提供的地址。
总结起来,内核处理用户空间传递的参数和指针的方法如下
-
通过argint、argaddr和argfd函数从陷阱帧中获取系统调用参数。
-
对于涉及传递指针的系统调用,内核实现了安全地将数据从用户提供的地址传输到内核并返回的函数。例如,
fetchstr
函数用于从用户空间检索字符串文件名参数。 -
fetchstr
调用copyinstr
来实际执行数据的拷贝工作。copyinstr
从用户页表pagetable中的虚拟地址srcva复制数据到dst。 -
copyinstr
使用walkaddr
在pagetable中查找srcva,得到物理地址pa0,并确保用户提供的虚拟地址是进程的用户地址空间的一部分,以防止程序欺骗内核访问其他内存。 -
copyinstr
可以直接从pa0复制字符串字节到dst,因为内核将每个物理 RAM 地址映射到相应的内核虚拟地址。 -
另一个函数
copyout
用于将数据从内核复制到用户提供的地址。