MIT6.S081 Lab3

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?
语句解读

  1. (int*)0:这将整数0转换为指针,指向内存地址0
  2. *(int*)0:这将尝试从内存地址0处获取一个整数值。*是一个解引用操作,它获取该指针指向的值
  3. 试图读取内存地址0处的一个整数值并将其赋给num变量
  4. 这通常是一种危险的操作,因为大多数操作系统不允许应用程序直接访问地址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;
      }
    }

存在的问题

  1. 输入命令grep hello README时也会打印关于系统调用的信息
    ![[Pasted image 20231102223513.png]]
    原因:修改proc.c中freeproc没有释放或者重置trace_mask
    ![[Pasted image 20231102224511.png]]

  2. 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]]

  3. trace all syscalls时,missing trace()
    原因:在没有调用系统调用函数之前就取了mask,而此时的mask只是一个初始化的mask,默认值为0
    ![[Pasted image 20231103111038.png]]
    将语句调换,即调用了系统调用函数之后,mask参数才能存储在进程控制块trace_mask中,否则默认为0

测试通过

  1. 测试所有系统调用
    ![[Pasted image 20231102225812.png]]
  2. 测试一个系统调用
    ![[Pasted image 20231102225824.png]]
  3. 测试fork()系统调用
    ![[Pasted image 20231102225923.png]]
  4. 测试分数
    ![[Pasted image 20231103110834.png]]

4 实验三:sysinfotest

关键问题:

  1. 如何将结构体复制回用户空间
    答:使用内核态下提供的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开始的地方,且位于当前进程下的页表中
  1. 如何获取正在运行的进程数
    答:可以借鉴kernel/proc.c中的allocproc()函数,核心是遍历进程结构体数组然后判断每个进程的状态,并对当前使用的进程进行计数
    ![[Pasted image 20231106100943.png]]

  2. 如何获取内存空间
    答:首先需要知道系统中每一物体内存页的单位PGSIZE=4096,然后查看系统中空闲物理内存的管理方式,在kernel/kalloc.c中,它是以一个单链表的形式管理空闲内存页,所以只需遍历该单链表获取空闲页数,每页计一个PGSIZE即可。分配一个空闲内存的过程如下:
    ![[Pasted image 20231106101224.png]]

实验过程

  1. 按照编写系统调用trace()的过程,编写系统调用函数sysinfo()
    答:用户态下:user/sysinfotest.c -> user/user.h(系统调用的声明) -> Makefile(将添加的应用程序加入到编译列表) ——>usys.pl(编译后形成用户态到内核态的接口)
    内核态下:kernel/syscall.h(添加系统调用编号)——> kernel/syscall.c(添加系统调用函数声明和系统调用号与系统调用函数的映射)——> kernel/proc.c(添加系统调用函数)
  2. 编写获得当前系统进程数和获取当前空闲内存的函数 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;
}
  1. 编写系统调用函数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]]

注意的问题
出现隐式函数声明的问题,需要在当前文件最开始的地方加入函数声明

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值