笔记08 - HW3: xv6 system calls

Part One: System call tracing

任务:修改syscall.c的syscall()函数,输出系统调用函数名和执行结果。
解决方案如下:
根据xv6源码分析,可以知道inidcode.S执行movl $SYS_exec, %eax把trapno放到%eax里面,之后int的时候会跳转到alltraps,其会执行pushal将包括eax在内的通用寄存器进栈(其实是保存到proc->tf),之后在syscall函数就可以通过proc->tf->eax获取trapno了。而系统调用的执行结果也放在了eax中。从proc->tf->eax中可以拿到syscall number,然后做个映射表输出就可以了。
另外,syscall.c提供argint、argptr和argstr等工具函数,用于在进程用户空间获得第 n 个系统调用参数。argint用于获取第n个整数,存储在int指针中(通过fetchint工具函数完成)。argptr用于获取第n个整数,将char指针指向它。argstr用于获取第n个整数,该参数是字符串起始地址值,使用char指针指向它,函数本身返回字符串长度(通过fetchstr工具函数完成)。argint利用用户空间的%esp寄存器定位第n个参数:%esp指向系统调用结束后的返回地址,参数就恰好在%esp之上(%esp+4),因此第n个参数就在%esp+4+4*n。
此处不对系统调用参数进行输出,因为每个系统调用所传递的参数个数和类型不同,而如果直接在工具函数中输出的话,则无法判断是地址值还是参数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const char* syscall_name[] = { "fork","exit","wait","pipe","read","kill","exec","fstat","chdir","dup","getpid","sbrk","sleep","uptime","open","write","mknod","unlink","link","mkdir","close" };

void
syscall(void)
{
int num;
num = proc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
proc->tf->eax = syscalls[num]();
//trapno放到%eax里面,系统调用的执行结果也放在了eax中
//打印系统调用名称和结果
//cprintf("\n%s -> %d\n", syscall_name[num], proc->tf->eax);
} else {
cprintf("%d %s: unknown sys call %d\n",
proc->pid, proc->name, num);
proc->tf->eax = -1;
}
}

Part Two: Date system call

系统调用实现思路如下:
有一个.h文件暴露接口,有一个.c文件来实现接口,在x86上实现方法是c语言层内联汇编int指令(或者直接汇编实现),把系统调用号放入eax,内核中有一个系统调用表,根据eax的值来索引这个表得到vectorXXX地址,vectorXXX jmp过去alltraps,进入内核模式,执行trap函数,trap函数将执行系统调用函数(如果系统调用叫xxx,内核对应的函数一般叫sys_xxx)。
在xv6中,想要添加新的系统调用并在shell中调用,需要了解五个文件:

1
2
3
4
5
user.h 定义了可以通过shell调用的函数
usys.S 使用宏定义将用户调用转换为系统调用
syscall.h 定义系统调用的位置向量,从而可以连接到系统调用的实现
syscall.c 根据syscall.h 定义的系统调用位置向量,调用系统调用的实现函数
sysproc.c 增加了系统调用的真正实现

添加系统调用函数date(),返回当前UTC时间。参考cmostime()(defined in lapic.c)函数,其功能是读取实时时钟;参考date.h,该头文件定义了struct rtcdate,该结构体作为cmostime()的指针形参。
技巧:参考uptime的实现,执行grep -n uptime *.[chS]参考其在对应的文件中的定义。然后在Makefile中添加_date到UPROGS中。创建date.c,输入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "types.h"
#include "user.h"
#include "date.h"

int
main(int argc, char *argv[])
{
struct rtcdate r;

if (date(&r)) {
printf(2, "date failed\n");
exit();
}

// your code to print the time in any format you like...
printf(1, "%d/%d/%d %d:%d:%d\n", r.year, r.month, r.day, r.hour, r.minute, r.second);
exit();
}

这样之后编译执行后,在命令行输入date应该能输出时间。
其工作原理是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//see in usys.S
#define SYSCALL(name) .globl name; name: movl $SYS_ ## name, %eax; int $T_SYSCALL; ret
## 操作符将会连接$SYS_和name。
所以,声明SYSCALL(date)将被扩展为:
.globl date; date: movl $SYS_date, %eax; int $T_SYSCALL; ret

如果在控制台中(用户进程)执行date(&r),其中r的定义是struct rtcdate r,将会将r的地址进栈,eip进栈,然后寻找到 date: 标识位置,执行int系统调用。syscall.c根据SYS_date的值调用sys_date()函数。sys_date()函数的实现是:

int
sys_date(void)
{
struct rtcdate *pr;
if (argptr(0, (void*)&pr, sizeof(struct rtcdate)) < 0) {
return -1;
}
cmostime(pr);
return 0;
}

其将会定义struct rtcdate类型的pr指针,然后使用argptr函数获取proc->tf->esp(进程用户栈)指向的第一个32-bit参数(跳过esp指向的eip),将pr指针指向该值,由上述可知这是调用date函数的参数地址,所以存储到pr相当于存储到参数r中。

在Makefile中添加_date到UPROGS中后,将生成_date的可执行文件,其将作为内核的内置函数。

Challenge

add a dup2() system call
实现的方法如上。区别是将sys_dup2(void)的实现放在sysfile.c中,与sys_dup(void)放在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int
sys_dup2(void)
{
struct file *oldfile, *newfile;
int newfd;
//取出第一个参数fd,其应该对应到一个file对象
if (argfd(0, 0, &oldfile) < 0) {
return -1;
}
//取出第二个参数fd
if (argint(1, &newfd) < 0) {
return -1;
}
if(newfd < 0 || newfd >= NOFILE) {
return -1;
}

//newfd文件描述符没有对应的file对象,可以安全使用,使新旧fd指向同一个file对象
if (proc->ofile[newfd] == 0) {
goto final;
} else if (argfd(1, &newfd, &newfile) < 0) { //newfd文件描述符有对应的file对象,取出file对象
return -1;
}
//两个fd指向同个file对象,返回
if (oldfile == newfile) {
return newfd;
}
//关闭文件
if (newfile->ref > 0) {
fileclose(newfile);
}

final:
proc->ofile[newfd] = oldfile;
filedup(oldfile);

return newfd;
}

dup2test.c比较疑惑的是创建的文件不知道在哪里,所以使用了read函数进行测试。(在xv6虚拟的文件系统中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//dup2test.c
/*
* Tests the functionality of the `dup2` system call. The first argument is the
* original file the we are duplicating. If a second argument is passed in we
* open this file, write a string to it, then duplicate it. This tests that our
* kernel is properly closing and duplicating the file. In both cases the original
* file should have the string "foobar\n". If not, then something went wrong
*/

#include "types.h"
#include "stat.h"
#include "user.h"
#include "fcntl.h"

int
main(int argc, char *argv[]) {
int origfd, newfd = 0;
if (argc < 2) {
printf(1, "%s: Not enough arguments\n", argv[0]);
printf(1, "Usage: origfile [newfile]\n");
exit();
}

unlink(argv[1]);

if ((origfd = open(argv[1], O_CREATE|O_RDWR)) < 0) {
printf(1, "Cannot open '%s'\n", argv[1]);
exit();
}
if (argc > 2) {
unlink(argv[2]);
if ((newfd = open(argv[2], O_CREATE|O_RDWR)) < 0) {
printf(1, "Cannot open '%s'\n", argv[2]);
exit();
}
write(newfd, "ignored\n", 8);
}

write(origfd, "foo", 3);
if (dup2(origfd, newfd) < 0) {
printf(1, "dup2 error\n");
}

write(newfd, "bar\n", 4);

close(origfd);
close(newfd);
if ((newfd = open(argv[1], O_RDWR)) < 0) {
printf(1, "Cannot open '%s'\n", argv[2]);
exit();
}
char buf[256];
int n = read(newfd,buf,256);
printf(1, "%d: %s\n", n,buf);

exit();
}

其他问题

如何拿到系统调用的结果,比如dup(fd)?
我们知道,系统调用的结果最终是放在进程tf结构的eax中,然后在trapret阶段弹出到寄存器中。而返回到用户空间后会往eax中拿取结果,这是一种默认约定。




遗留的问题

为什么需要转换成系统调用?实质区别在哪里(关于硬件特权)?
为什么类似printf等不需要实现为系统调用?

显示 Gitment 评论