MIT 6.s081 lab2.1–trace
No.1 写在前面的话
我很激动,这个实验当初跟着答案瞎做两遍时依然毫无头绪,现在重新做起,不依靠答案自己想,一路丝滑,顺利通过!进步了!有必要强调的是:一定要理解指导书和提示让参阅的代码!一定要充分理解!
一开始我不理解课上frans教授后面示例说已经足够的意思,现在才知道,提示真的已经很多了。
No.2 实验思路
一开始提示到要利用掩码进行trace的syscall,实验要求写的很清楚,在这里,mask的bits表明了系统调用号。
例如,若现在mask = 2,其二进制为0010,标志位1出于第一位次,那么就是跟踪调用号为1的系统调用。先进行一个初步了解,之后会有作用。
根据实验指导添加trace到各文件的步骤就不再赘述了,主要是下面几步:
2.1 如何利用系统调用?
提示要求,【通过将参数保存到proc结构体里的一个新变量来实现系统调用】,在这里便出现两个问题:新变量怎么来?如何利用新变量进行系统调用?
首先看看kernel/proc.h中结构体proc的代码:
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
}
可以看出,这里提供了进程的信息,分为锁和不锁,在这里并没有所谓的新变量,那么就需要自己添加。而题目中要求利用mask进行系统调用,很自然的就想到了添加mask。
因为对于一段进程而言,它利用栈和名字这些比较私有的东西不需要上锁,mask也属于这一类,并不需要特别保护,所以加入到最下面:
char name[16]; // Process name (debugging)
int mask; //查了查,掩码位数有31位,示例中2147483647将所有31个低位置为1,算作边界值。
接下来解决第二个问题:如何利用mask进行调用?提示在kernel/sysproc.c中可以查看使用示例,一起来看看:
uint64
sys_wait(void)
{
uint64 p;
if(argaddr(0, &p) < 0)
return -1;
return wait(p);
}
首先需要清楚,argaddr是什么东西。官方原文档的Traps, interrupts, and drivers章节中的Code: System calls写的很清楚,总的来说:argint/argaddr/argfd表示从陷阱框架中检索第n个系统调用参数并以int、address、fd的形式保存。我们再追溯一下:
首先,参数p进入argaddr;
int
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
return 0;
}
搜索argraw;
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;
}
可以看到,我们传入的0指示当前进程进入trapframe获取寄存器a0的值,然后ip指针将它的地址记录,传回if判断,如果<0,说明寄存器中没有值,返回-1,否则调用wait(),那么我们的sys_trace就好写了:
uint64
sys_trace(void){
int n;
if(argint(0, &n) < 0)//因为trace的返回值为int类型,所以利用argint
return -1;
myproc()->mask = n;//将a0寄存器中的值保存到proc.h里新定义的mask来实现新的系统调用,myproc为当前进程,proc.h有定义。
return 0;
}
之后根据提示,查看fork()函数,我们只需看前面一小块:
// Create a new process, copying the parent.
// Sets up child kernel stack to return as if from fork() system call.
int
fork(void)
{
int i, pid;
struct proc *np;
struct proc *p = myproc();
// Allocate process.
if((np = allocproc()) == 0){
return -1;
}
因为子进程要复制父进程,那么就得进行分配空间之类的设置,可以看出,np为子进程,p为当前进程。那么来到下面,根据提示将掩码从父进程复制到子进程就好写了:
np->sz = p->sz;
np->mask = p->mask;//跟踪掩码从父copy到子
np->parent = p;
走到这里,实验已经完成了三分之二。
2.2 syscall()
提示要求打印跟踪输出,添加系统调用名称数组建立索引,到这里先是一愣:打印?怎么打印?在哪打印?什么叫建立索引?哈哈,往下继续探索,我们先看看syscall()。
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
//NELEM:
//number of elements in fixed-size array
//固定大小数组中的元素数
//defs.h #define NELEM(x) (sizeof(x)/sizeof((x)[0]))
//也就是说,如果系统调用号>0,并且在syscall的数组中存在,在范围之内
//那么就将a7中存储的调用号复制到a0.
p->trapframe->a0 = syscalls[num]();
}
} else {
//否则就是一个未知的系统调用
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
那么,我们的trace还没有加入syscall数组中,如果运行必然会导致错误;因此,修改syscall()之上的数组和extern:
extern uint64 sys_trace(void);
[SYS_trace] sys_trace,
接下来继续解决一开始的问题。思路进行到这里突然中断,那么就返回起点重新开始梳理。
No.3 系统调用的过程
到目前为止,跟着提示下来补补写写已经快完成,但思路依旧是乱的,何时陷入,何时开始,那么,重新回顾一下课程上frans教授所演示的第一个进程如何发生【视频节点在1:13:14】:
-
汇编
# exec(init, argv) .globl start start: la a0, init la a1, argv li a7, SYS_exec ecall
将exec系统调用加载到a7,然后ecall进入内核。
-
userinit创建初始进程,返回到用户空间,执行上面汇编,发现系统调用,回到内核空间
-
进入syscall,num读取对应系统调用整数,然后传给a0,syscall[num]对应入口函数
-
第一个进程通过exec调用init完成。
trace的实现如下图:
那么接下来,就知道怎么完成了。
题目要求每个系统调用即将返回时打印出一行。该行应该包含进程id、系统调用的名称和返回值,首先printf:
printf("%d: syscall %s -> %d\n", p->pid, ?, p->trapframe->a0);
但是中间的系统调用名称如何确定?那么就用到了提示中的“添加系统调用名称数组建立索引”:
char *syscall_names[] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace",};
//需要注意的是,调用号从1开始
马上就要完成了,但是如何跟踪呢?回顾一下,trace的作用是跟踪正在调用的syscall,而题目一开始提示利用mask指定跟踪的syscall。
那么,如果自己输入的系统调用号进行掩码转换后,如果与当前进程调用的一致,就可以完成跟踪。整个syscall的完全修改如下:
void
syscall(void)
{
int num;
struct proc *p = myproc();
char *syscall_names[] = {
"", "fork", "exit", "wait", "pipe",
"read", "kill", "exec", "fstat", "chdir",
"dup", "getpid", "sbrk", "sleep", "uptime",
"open", "write", "mknod", "unlink", "link",
"mkdir", "close", "trace",};
//需要注意的是,调用号从1开始。
num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
//何为跟踪?就是跟踪正在or已使用的系统调用
//那么如何判断一个进程的系统调用可被跟踪呢?
//如果当前进程调用的掩码与生成实际的系统调用存根user/usys.S的调用号一致
if((1 << num) & p->mask){
//在这里我犯蠢了,用==而不是用&运算
//要通过与运算按位判断标志位是否相等,如果用==意义为两边都不是空值
printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
完美解决。【用时半天】