引言
有关project2的实验,对应课程网址如下6.1810 / Fall 2024
system call tracing(moderate)
前置知识
-
本实验使用位图mask记录需要追踪的系统调用,如果对应位置为1,就会监听对应的系统调用
-
xv6下系统调用的流程(后续的每一个操作,几乎都可以在源码层面进行体现,需细心品读.c或者.S文件)
-
用户态下调用系统调用函数,系统调用参数会被放到a0 - a6的寄存器当中
-
执行user/usys.S中对应系统调用函数的汇编指令
- 将系统调用号放在寄存器a7当中
- 调用ecall指令,ecall会进行的硬件操作
- 保存中断使能状态,
- 关中断,
- 切换到内核态,
- 跳转执行执行kernel/trampoline.S中的uservec中(陷阱处理入口)的指令
-
kernel/trampoline.S中uservec的执行
-
先保存a0寄存器 (csrw sscratch, a0) ,让a0寄存器可以空出来保存TRAPFRAME
-
a0放TRAPFRAME (li a0, TRAPFRAME),TRAPFRAME是proc中的一个结构体,保存用户寄存器状态
-
暂存用户态寄存器 (sd ra, 40(a0) ……)
-
将原来的a0也保存到TRAPFRAME当中
csrr t0, sscratch sd t0, 112(a0)
-
后续的一些过程
1 2 3 4 5 6 7 8 9 10 11# initialize kernel stack pointer, from p->trapframe->kernel_sp ld sp, 8(a0) # make tp hold the current hartid, from p->trapframe->kernel_hartid ld tp, 32(a0) # load the address of usertrap(), from p->trapframe->kernel_trap ld t0, 16(a0) # fetch the kernel page table address, from p->trapframe->kernel_satp. ld t1, 0(a0) -
再往后
1 2 3 4 5 6 7 8 9 10 11 12 13 14# wait for any previous memory operations to complete, so that # they use the user page table. # 主要是刷新TLB sfence.vma zero, zero # 确保 CPU 已完成所有未完成的用户态访存操作,防止切换页表后丢失写入。 # install the kernel page table. csrw satp, t1 # flush now-stale user entries from the TLB. sfence.vma zero, zero # 清除 TLB 中缓存的用户页表项,避免后续内核代码使用错误的地址翻译。 # jump to usertrap(), which does not return jr t0
-
-
执行kernel/trap.c中的usertrap()函数
- 检查是否之前是用户态,陷阱向量更改为内核陷阱向量,(当前内核态,下次陷阱走内核陷阱向量)
- 保存用户的pc程序计数器
- 陷入原因是系统调用,将原来用户态pc值 + “1”
- 开中断
- 调用kernel/syscall.c中的syscall()
-
kernel/syscall.c中syscall()的调用过程
- 获取系统调用号执行对应操作,将结果放在a0寄存器当中
- p->trapframe->a0 = syscalls[num]()
-
kernel/syscall.c中syscalls[]数组成员执行,从a0等寄存器中获取系统调用的参数,参数传递到对应的操作当中
-
执行完成后调用usertrapret函数
- 关中断
- 陷阱向量改为用户的陷阱向量trampoline_uservec
- 初始化陷阱帧,保存内核相关信息
- 配置返回状态
- 挑战到trampoline.S中的userret
-
tramploline.S中的userret的执行
-
切换用户页表
# switch to the user page table. sfence.vma zero, zero csrw satp, a0 sfence.vma zero, zero -
恢复寄存器
1 2 3 4 5 6 7 8li a0, TRAPFRAME # restore all but a0 from TRAPFRAME ld ra, 40(a0) ...... # restore user a0 ld a0, 112(a0) -
返回用户态和复原用户pc
-
-
具体实现
了解了系统调用的过程,我们要做的就是根据trace_mask来追踪每一次的系统调用过程
-
首先需要在proc中定义trace_mask,用于之后的使用
-
调用trace系统调用的时候,将参数传递给trace_mask,这一步在kernel/sysproc.c中实现sys_trace()来完成
-
上面两步实现了通过trace系统调用在proc中添加trace_mask
-
下面还要实现通过系统调用号打印对应的系统调用
-
kernel/syscall.c中syscall可以获得系统调用号,与trace_mask判断本次系统调用是否被监听了,如果监听了,就进行打印。
-
注意对于fork的调用,也需要父进程的p->trace_mask传给子进程的p->trace_mask,否则子进程监听就不对了
-
最后实现trace程序
- trace程序接受trace_mask和监听命令作为参数
- 先调用trace系统调用初始化trace_mask
- 再通过exec执行后面的命令及看监听指定系统调用
-
kernel/proc.h中struct proc的修改
|
|
- kernel/sysproc.c中的sys_trace()函数
|
|
- kernel/proc.c中fork()的修改
|
|
- syscall.h添加trace系统调用号
|
|
- kernel/syscall.c中syscall的实现
|
|
- user/trace.c的实现
|
|
attack xv6(moderate)
前置知识(具体的分析,完成下一章project再来看可能会更清晰)
比较源码进行分析,效果更好,建议完成下一章再来看,需要对kfree,kalloc,mappages等函数有一定的理解
-
fork时分配物理页面的过程(当前没有写时复制,见kernel/proc.c中的fork())
-
np = allocproc()
-
(p->trapframe = (struct trapframe *)kalloc() 分配了一页作为trapframe,不同进程有各自独立的trapframe
-
p->pagetable = proc_pagetable(p) 创建第一个页表
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19// 创建第一个页表的时候,就要去映射TRAMPOLINE和TRAPFRAME if(mappages(pagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) < 0){ uvmfree(pagetable, 0); return 0; } // TRAMPOLINE存放了处理陷入的汇编代码,空间是内核启动时预先分配好的,物理地址是固定的TRAMPOLINE,这里将用户页表进行映射即可 // map the trapframe page just below the trampoline page, for // trampoline.S. if(mappages(pagetable, TRAPFRAME, PGSIZE, (uint64)(p->trapframe), PTE_R | PTE_W) < 0){ uvmunmap(pagetable, TRAMPOLINE, 1, 0); uvmfree(pagetable, 0); return 0; } // 这两个页面再虚拟地址最顶端相邻两个页,三级页表只需要1个就可以进行映射 - xv6系统使用三级页表结构(可以看xv6book中的页表章节进行了解),所以这里分配了3个页面
-
-
-
uvmcopy(p->pagetable, np->pagetable, p->sz)
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) { ... for(i = 0; i < sz; i += szinc){ // sz是用户空间已经分配的空间大小,这里遍历整个用户空间进行复制 szinc = PGSIZE; szinc = PGSIZE; if((pte = walk(old, i, 0)) == 0) // 找到用户空间的页表项 panic("uvmcopy: pte should exist"); if((*pte & PTE_V) == 0) panic("uvmcopy: page not present"); pa = PTE2PA(*pte); // 获取对应的物理地址 flags = PTE_FLAGS(*pte); if((mem = kalloc()) == 0) // 分配新的物理地址 goto err; memmove(mem, (char*)pa, PGSIZE); // 将就地址内容拷贝到新地址 if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){ // new为新页表,会创建新页表 kfree(mem); goto err; } } ... } -
也就是说,原来父进程有多少用户空间的页面以及页表的页面,子进程就会创建多少页面
-
-
至此,fork分配内存的环节就结束了
-
-
exec调用执行物理页面的变化过程 (见kernel/exec.c)
-
(pagetable = proc_pagetable(p)),fork中分析过了,分配了三个页面映射TRAMPOLINE和TRAPFRAME
- exec执行一个新程序,不要分配新的TRAPFRAME吗?exec虽然是执行新程序,但是是在当前程序上执行,使用当前程序分配的TRAPFRAME即可,不需要新分配。换言之,exec不会改变struct proc *p = myproc(),其只会替换用户空间。
- 注: p以及里面相关地址不会改变,但是内容可能会因为运行新程序而改变。
-
加载elf中文件的load
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19// Load program into memory. for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){ if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph)) goto bad; if(ph.type != ELF_PROG_LOAD) continue; if(ph.memsz < ph.filesz) goto bad; if(ph.vaddr + ph.memsz < ph.vaddr) goto bad; if(ph.vaddr % PGSIZE != 0) goto bad; uint64 sz1; if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0) goto bad; sz = sz1; if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0) goto bad; } -
exec要执行什么程序,就去看对应程序elf文件中有多少个需要load的页面
-
uvmalloc创建用户页面的同时,还会进行映射,创建页表。所以elf文件里面需要load的页面少于512个就会再创建二级和三级页表,也就是2个页面
-
512是根据每个页表页表项个数得到的,看xv6book三级页表结构,为2^9
-
-
分配用户栈以及一个保护页面,总计USERSTACK+1个页面,在xv6中
#define USERSTACK 1-
1 2 3 4 5 6 7 8 9 10 11 12 13// Allocate some pages at the next page boundary. // Make the first inaccessible as a stack guard. // Use the rest as the user stack. sz = PGROUNDUP(sz); uint64 sz1; // 只要用户空间的页面没有超过512个,就不会增加新的页表 if((sz1 = uvmalloc(pagetable, sz, sz + (USERSTACK+1)*PGSIZE, PTE_W)) == 0) goto bad; sz = sz1; uvmclear(pagetable, sz-(USERSTACK+1)*PGSIZE); sp = sz; stackbase = sp - USERSTACK*PGSIZE;
-
-
proc_freepagetable(oldpagetable, oldsz); 释放旧的页表和用户地址空间
-
1 2 3 4 5 6 7 8 9 10void proc_freepagetable(pagetable_t pagetable, uint64 sz) { uvmunmap(pagetable, TRAMPOLINE, 1, 0); // 这里不删除物理空间,只删除页表 uvmunmap(pagetable, TRAPFRAME, 1, 0); // 同上 // 先解除了TRAMPOLINE和TRAPFRAME映射,这样下面就不会把上面两个物理地址一同删除 // 因为上面两个物理地址是用户页表和内核页表共同使用的 uvmfree(pagetable, sz); // 里面uvmunmap最后一个参数是1,说明删除了物理地址空间(都是用户空间) }-
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// Remove npages of mappings starting from va. va must be // page-aligned. The mappings must exist. // Optionally free the physical memory. void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) { uint64 a; pte_t *pte; int sz; if((va % PGSIZE) != 0) panic("uvmunmap: not aligned"); // 根据页表遍历删除,说明用户空间的删除是虚拟地址从低到高依次删除 for(a = va; a < va + npages*PGSIZE; a += sz){ sz = PGSIZE; if((pte = walk(pagetable, a, 0)) == 0) panic("uvmunmap: walk"); if((*pte & PTE_V) == 0) { printf("va=%ld pte=%ld\n", a, *pte); panic("uvmunmap: not mapped"); } if(PTE_FLAGS(*pte) == PTE_V) panic("uvmunmap: not a leaf"); if(do_free){ uint64 pa = PTE2PA(*pte); kfree((void*)pa); } *pte = 0; } } -
1 2 3 4 5 6 7void uvmfree(pagetable_t pagetable, uint64 sz) { if(sz > 0) uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1); //这里删除了物理地址空间 freewalk(pagetable); }
-
-
-
至此exec页面变化过程就结束了
-
-
xv6底层的内存管理
- 通过链表来进行管理,分配的页表从链表头取出来
- 释放的页面放回到链表头
-
程序结束后何时回收用户地址空间
-
在exit函数中没有找到回收用户地址空间的代码,但是会p->state = ZOMBIE;通知父进程进行回收
-
在wait函数中有这样一段代码freeproc(pp);
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18static void freeproc(struct proc *p) { if(p->trapframe) kfree((void*)p->trapframe); // 先删除trapframe的物理地址空间,这里不回收页表 p->trapframe = 0; if(p->pagetable) proc_freepagetable(p->pagetable, p->sz); // 再删除用户地址空间和页表,这个函数在exec分析中有 p->pagetable = 0; p->sz = 0; p->pid = 0; p->parent = 0; p->name[0] = 0; p->chan = 0; p->killed = 0; p->xstate = 0; p->state = UNUSED; }
-
-
也就是说,如果由父进程,用户内存的回收是通过父进程的wait函数来完成的,先回收trapframe的物理内存(不会回收页表),再从低地址到高地址依次进行回收用户空间页面和页表
-
具体实现
- 页面分配涉及elf文件load,attacktest, secret 和 attack的elf文件中都需要加载多少load展示出来
- 在xv6目录中执行
readelf -l user/_attacktest readelf -l user/_secretreadelf -l user/_attack- 可以看出attacktest, secret, attack的elf文件需要加载的页面都是两个
- 在xv6目录中执行
- attacktest分析(结合前置知识的分析)
- attacktest的用户页面是通过exec来分配出来的,总共有四个(两个load,一个用户栈,一个guard),同时需要三级页表来映射
- fork为子进程分配空间
- 三级页表映射到trapframe(一级页表,二级页表,三级页表,trapframe)总共4个页面
- 复制attacktest的用户页面+二级页表+三级页表进行映射,总共6个页面
- 前排提示,上面的三级页表和用户页面+两个页表,总共9个页面,后面执行exec会被释放。trapframe不会被释放
- 子进程执行exec(secret)
- 新建页面映射trapframe(新一级页表,二级页表,三级页表),总共3个页面
- 加载secret的elf文件,总共2个页面,加载同时还要创建二级页表,三级页表映射用户空间(2个页面)
- 用户栈和guard,总共2个页面
- 释放上面旧的9个页面,释放9个页面
- 分配32个页面,在第10个页面上写上secret,申请32个页面
- 子进程结束,父进程wait,依次释放trapframe页面(1页),用户页面(4 + 32页),页表页面(5页)
- 此时空闲链表如下所示
- (头,旧的页表页面5页)-> (用户页面32页,secret在第23页上) -> 用户页面(4页,elf的load,用户栈,guard) -> (trapframe) -> …….
- secret在空闲链表从头开始数第28个页面上
- 执行pipe(fds)
- 追踪pipe系统调用的执行流程kernel/sysfile.c中的uint64 sys_pipe(void)函数,我们可以发现pipe执行了pipealloc函数
- pipealloc中执行了kalloc,又申请了一个页面。
- secret在空闲链表从头开始数第27个页面上
- fork为子进程分配空间,和上面一样,总共10个页面
- exec(attack)分配空间,和上面一样,到guard页申请结束总共申请了9个
- 释放fork中的9个页面,与上面申请的9个页面相抵消
- 27-10 = 17,因此再分配17个页面就可以找到secret
- attack.c代码如下
|
|
- 至此,attack的分析就结束了


