Lab 3
1 实验准备
- 阅读xv6[https://pdos.csail.mit.edu/6.S081/2022/xv6/book-riscv-rev1.pdf]的第2章以及第4章的4.3和4.4小节,以及相关的源文件:
- 用户空间的"stubs",它们将系统调用路由到内核中,在user/usys.S中,当你运行make时,它是由user/usys.pl生成的。声明在user/user.h中。
- 在内核空间的代码,它将系统调用路由到实现它的内核函数,在kernel/syscall.c和kernel/syscall.h中。
- 与进程相关的代码在kernel/proc.h和kernel/proc.c中。
- 开始实验前,切换到syscall分支:
$ git fetch
$ git checkout syscall
$ make clean
- 任务要求:添加必要的系统调用和stubs,使make grade这个命令能够正常工作
2 实验一:使用gdb
遇到的问题:按照实验步骤在一个窗口中输入make qemu-gdb,另一个窗口中输入gdb-multiarch,当在gdb模式下为syscall设置断点时,输入c,提示:the program is not being running。原因是未将待运行文件的.gdbinit加入到用户主目录下的.gdbinit,在运行gdb-mutiarch时才能识别出qemu程序。
解决:执行命令
echo "add-auto-load-safe-path ~/xv6Lab/xv6LabResourceCode/xv6-labs-2022/.gdbinit " >> ~/.gdbinit.
然后,运行make qemu-gdb
![[Pasted image 20231030151500.png]]
另一个窗口运行 gdb-mutiarch并进入到内核文件,输入c正常运行
![[Pasted image 20231030151729.png]]
问题一:Looking at the backtrace output, which function called syscall?
![[Pasted image 20231030152326.png]]
问题二:What is the value of p->trapframe->a7 and what does that value represent? (Hint: look user/initcode.S, the first user program xv6 starts.)
![[Pasted image 20231030154255.png]]
通过查看kernel/syscall.h文件中,7对应的是exec系统调用
![[Pasted image 20231030154339.png]]
问题三: What was the previous mode that the CPU was in?
![[Pasted image 20231030154801.png]]
main函数到shell命令执行的过程:
![[Pasted image 20231030155712.png]]
问题四:Write down the assembly instruction the kernel is panicing at. Which register corresponds to the varialable num?
语句解读:
(int*)0
:这将整数0
转换为指针,指向内存地址0
。*(int*)0
:这将尝试从内存地址0
处获取一个整数值。*
是一个解引用操作,它获取该指针指向的值- 试图读取内存地址
0
处的一个整数值并将其赋给num
变量 - 这通常是一种危险的操作,因为大多数操作系统不允许应用程序直接访问地址
0
(通常被视为无效或null地址),因此尝试这样做可能会导致程序出现段错误或访问违规错误。
gdb模式下的运行过程:
![[Pasted image 20231030160511.png]]
qemu的输出,scause代表发生故障的原因(需要查阅特权指令表),sepc表示造成故障的代码段的地址
![[Pasted image 20231030160533.png]]
在kernel/kernel.asm中找到对应的汇编指令
![[Pasted image 20231030161421.png]]
验证:
![[Pasted image 20231030162409.png]]
问题五:Why does the kernel crash? Hint: look at figure 3-3 in the text; is address 0 mapped in the kernel address space? Is that confirmed by the value in scause above? (See description of scause in RISC-V privileged instructions)
图:
![[Pasted image 20231030163008.png]]
内核崩溃的原因:
1. 由于虚拟地址0未映射到任何的物理地址(找不到实际的物理空间),故加载指令(lw)不能翻译虚拟地址0, 导致load页故障。
2. 该页故障异常发生在内核中,xv6没有对内核态下的异常进行处理,故导致直接崩溃。
验证:
scause 0x000000000000000d,查看特权指令表得到
![[Pasted image 20231030163618.png]]
对应的异常代码为13,确实是加载页表故障
问题六:What is the name of the binary that was running when the kernel paniced? What is its process id (pid)?
判断故障时,可以找到哪个进程正在运行
![[Pasted image 20231030164014.png]]
补充
什么是$sstatus
是一个特定的寄存器或变量,表示RISC-V的sstatus
寄存器,该寄存器保存有关特权模式和中断状态的信息,根据RISC-V privileged instructions中对特权指令 sstatussstatus 的描述
![[Pasted image 20231030155141.png]]
主要看的是riscv64中的spp位,若spp为0则处于用户态,否则为内核态,通过输出22,二进制为00010110,推断出SPP位为0,所以cpu之前的状态是用户态。
GDB模式下的layout命令
在GDB中,layout asm
命令用于打开TUI(文本用户界面)模式,并显示汇编代码的窗口。这对于想要在调试过程中查看汇编代码的开发者非常有用,特别是当他们正在进行底层或汇编级调试时。
当你在GDB中执行layout asm
命令后,你会看到一个分割的屏幕。其中一部分将显示当前执行的汇编代码,高亮显示当前的执行点。这样你可以直观地看到程序如何在汇编级别上执行。
此外,你还可以使用layout src
来查看高级语言源代码(例如C或C++),或使用layout split
来同时查看源代码和汇编代码。在TUI模式下,你可以使用方向键(上/下/左/右)来导航,并使用其他TUI命令来控制和定制视图。
退出layout模式的快捷键:Ctrl-x,然后a
总结
实验一的主要目的是要熟悉gdb的使用,然后设置可执行文件,设置断点,在运行。最后是在调试过程中如何判断故障的原因,可以通过错误提示 scause代码找到故障原因,sepc代码+layout split命令找到出错代码段的位置,还可以用过p->name找到正在运行的进程名称或者id(p->pid)
3 实验二:tracing
实验目的:编写一个新的系统调用函数trace(),功能是跟踪调用了哪些系统调用函数然后输出,可以与gdb结合使用
实验过程:
$U/_trace
添加到Makefile,qemu能够编译trace.c文件- 用户空间下提供的接口原型
int trace(int);
添加到user/user.h中,该接口的定义在内核态。调用过程中需要进行转换:用户态下的trace:int trace(int)
->将调用的参数和系统调用名称传递给内核态->转变为SYS_trace(void){argint(0,&mask)}(函数体内接收参数) - 在 user/usys.pl 为用户空间添加系统调用 tracetrace 的存根(真正的用户态下调用系统调用的接口),目的就是提供一个友好的接口,能够在用户态下调用系统调用而不关系内核实现的细节
- 注:user/usys.pl是一个Perl脚本,它按照固定的模板,生成在用户空间调用系统调用的汇编代码。 usys.pl经过编译得到汇编文件usys.S,从而产生系统调用号SYS_${name}
- ![[Pasted image 20231103092104.png]]
- 存根函数将用户传递的参数和系统调用号传递给内核,并执行实际的系统调用。这简化了应用程序开发,因为开发人员只需要调用这些存根函数,而不必直接与系统调用的底层实现交互。这有助于提高代码的可读性和可维护性。
- 用户态下产生的系统调用号需要在内核态下注册:添加 trace 的系统调用号,在kernel/syscall.h文件末尾加上
#define SYS_trace 22
- kernel/syscall.c是负责调用系统调用的(每次执行一个系统调用都可以在syscall.c中找到该系统调用的信息以及调用该系统调用的进程信息):所以,需要添加系统调用 trace 的函数声明
extern uint64 sys_trace(void);
用于实现系统调用函数的实际功能。以及需要添加系统调用号与系统调用函数的映射(静态数组)[SYS_trace] sys_trace
。目的是将系统调用号与实际的系统调用函数关联起来,以便内核可以根据系统调用号查找并执行相应的系统调用。 - trace函数的具体实现过程:trace函数的功能是提供一个整型参数mask(确定跟踪的哪些系统调用),来跟踪本进程及其子进程中指定的系统调用。所以需要在进程间传递参数,但是trace 在内核态中执行,使用pipe进行进程间通信很复杂且不知何时会fork子进程,无法确定子进程的系统调用函数。所以选择修改进程结构体,为其添加一个变量mask,对文件kernel/proc.h进行修改。
struct proc {
...
int mask;
};
- 为了实现进程间传递参数,需添加对mask的拷贝,在kernel/proc.c的fork定义中。子进程也要跟踪这些系统调用
int
fork(void)
{
...
// Cause fork to return 0 in the child.
np->trapframe->a0 = 0;
np->mask = p->mask;
...
}
- 接下来就是要去完成系统调用 tracetrace 的函数定义,在该系统调用会接收用户态传递的参数,并将其赋值给mask,我们将定义写在kernel/sysproc.c中:
uint64
sys_trace(void){
int n;
argint(0,&n);//接收参数
myproc()->mask = n;//赋给mask
return 0;
}
- 为了方便信息输出为其定义一个字符串数组,修改的文件为kernel/syscall.c:
char *str[]={
[SYS_fork] "syscall fork",
[SYS_exit] "syscall exit",
[SYS_wait] "syscall wait",
[SYS_pipe] "syscall pipe",
[SYS_read] "syscall read",
[SYS_kill] "syscall kill",
[SYS_exec] "syscall exec",
[SYS_fstat] "syscall fstat",
[SYS_chdir] "syscall chdir",
[SYS_dup] "syscall dup",
[SYS_getpid] "syscall getpid",
[SYS_sbrk] "syscall sbrk",
[SYS_sleep] "syscall sleep",
[SYS_uptime] "syscall uptime",
[SYS_open] "syscall open",
[SYS_write] "syscall write",
[SYS_mknod] "syscall mknod",
[SYS_unlink] "syscall unlink",
[SYS_link] "syscall link",
[SYS_mkdir] "syscall mkdir",
[SYS_close] "syscall close",
[SYS_trace] "syscall trace",
};
void
syscall(void)
{
int num;
struct proc *p = myproc();
num = p->trapframe->a7; //系统调用号
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// Use num to lookup the system call function for num, call it,
// and store its return value in p->trapframe->a0
p->trapframe->a0 = syscalls[num](); //系统调用的返回值
if((p->mask >> num) & 1) //若该系统调用被跟踪
printf("%d: %s -> %d\n",p->pid,str[num],p->trapframe->a0);//输出信息
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
存在的问题:
-
输入命令grep hello README时也会打印关于系统调用的信息
![[Pasted image 20231102223513.png]]
原因:修改proc.c中freeproc没有释放或者重置trace_mask
![[Pasted image 20231102224511.png]] -
trace 2 usertests forkforkfork,如何跟踪父进程及其所有子进程
按照hints: Modify fork() (see kernel/proc.c) to copy the trace mask from the parent to the child process.测试通过
![[Pasted image 20231102225444.png]] -
trace all syscalls时,missing trace()
原因:在没有调用系统调用函数之前就取了mask,而此时的mask只是一个初始化的mask,默认值为0
![[Pasted image 20231103111038.png]]
将语句调换,即调用了系统调用函数之后,mask参数才能存储在进程控制块trace_mask中,否则默认为0
测试通过:
- 测试所有系统调用
![[Pasted image 20231102225812.png]] - 测试一个系统调用
![[Pasted image 20231102225824.png]] - 测试fork()系统调用
![[Pasted image 20231102225923.png]] - 测试分数
![[Pasted image 20231103110834.png]]
4 实验三:sysinfotest
关键问题:
- 如何将结构体复制回用户空间
答:使用内核态下提供的copyout
函数,其原型为:
// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len);
// 在内核态中,src开始指向的地方到len这一块的内容复制给用户态的虚拟地址dstva开始的地方,且位于当前进程下的页表中
-
如何获取正在运行的进程数
答:可以借鉴kernel/proc.c中的allocproc()函数,核心是遍历进程结构体数组然后判断每个进程的状态,并对当前使用的进程进行计数
![[Pasted image 20231106100943.png]] -
如何获取内存空间
答:首先需要知道系统中每一物体内存页的单位PGSIZE=4096,然后查看系统中空闲物理内存的管理方式,在kernel/kalloc.c中,它是以一个单链表的形式管理空闲内存页,所以只需遍历该单链表获取空闲页数,每页计一个PGSIZE即可。分配一个空闲内存的过程如下:
![[Pasted image 20231106101224.png]]
实验过程:
- 按照编写系统调用trace()的过程,编写系统调用函数sysinfo()
答:用户态下:user/sysinfotest.c -> user/user.h(系统调用的声明) -> Makefile(将添加的应用程序加入到编译列表) ——>usys.pl(编译后形成用户态到内核态的接口)
内核态下:kernel/syscall.h(添加系统调用编号)——> kernel/syscall.c(添加系统调用函数声明和系统调用号与系统调用函数的映射)——> kernel/proc.c(添加系统调用函数) - 编写获得当前系统进程数和获取当前空闲内存的函数
uint64 get_sued_proc()
和uint64 get_free_memory()
// kernel/kalloc.c
uint64
// 获取当前系统空闲内存的大小
get_free_memory(void)
{
uint64 n = 0;
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist; // 指向第一个空闲列表的空间
while(r)
{
n += PGSIZE; // 每次都加上一个空闲单位
r = r->next;
}
release(&kmem.lock);
return n;
}
// kernel/proc.c
// 获取当前系统正在运行的进程数
uint64
get_used_proc(void)
{
struct proc *p;
uint64 n = 0;
for(p = proc; p < &proc[NPROC]; p++) // 遍历进程结构体数组
{
acquire(&p->lock);
if(p->state != UNUSED) n++; // 当前进程正在运行,则计数器++
release(&p->lock);
}
return n;
}
- 编写系统调用函数sysinfo(),使用copyout()函数
// kernel/syspro.c
uint64
sys_sysinfo(void)
{
uint64 st; // 接收用户态下传入的地址
argaddr(0, &st);
struct sysinfo p; // 存储内核态下获取的sysinfo状态
p.nproc = get_used_proc();
p.freemem = get_free_memory();
if(copyout(myproc()->pagetable, st, (char *)&p, sizeof(p)) < 0) // 内核态下的sysinfo状态复制给用户态下的虚拟地址处
return -1;
return 0;
}
实验结果:
![[Pasted image 20231106112850.png]]
注意的问题:
出现隐式函数声明的问题,需要在当前文件最开始的地方加入函数声明