6.S081 Lab Traps 笔记
这个Lab跟其他的比起来挺简单的,主要是汇编要去翻翻手册,简单说一下吧。顺便讲一下中断之类的东西。
xv6的Trap
xv6的trap包括interrupt这些,就是大部分切入内核态的东西都算作中断。以write
系统调用为例,一般是ecall
指令进入特权模式,然后uservec
保存 寄存器进入内核态,然后usertrap
, 然后syscall执行16号sys_write
, 执行完之后usertrapret
调用userret
返回用户空间
相关的寄存器和指令
RISC-V的汇编指令和ARM的还是有挺多不一样的地方,我一开始凭着本科学的那点ARM汇编的印象,搞错了好几次(最后还是翻手册的时候才意识到我错了,太坑了,说好的RISC-V和ARM指令差不多呢)
通用寄存器和ARM差不多,a0-a2这些是函数的参数,t0-t9和s0-s7这些都是保存临时值的(saved registers),区别是t开头的是caller-saved,可以被其他函数返回值更改,s开头的是callee-saved,不会被函数返回值之类的的东西更改。
ECALL会进入特权模式,这个syscall的时候讲过了不多说了。risc-v跟中断有关的寄存器比较多,建议下面看到什么直接去翻手册:RISC-V 手册
在RISC-V中,下面这些跟中断有关的寄存器属于CSR(Control and Status Registers)寄存器
SP寄存器: 栈寄存器,不说了
PC寄存器: 程序计数器,不说了
SATP寄存器: 用来控制分页的特权模式
STVEC寄存器:中断向量基址寄存器
SEPC寄存器:记录触发中断的向量地址,用来trap返回时恢复
SSRATCH寄存器:一般通过csrrw指令交换,在用户态保存内核栈的地址,在内核态值为0(可以通过这个判断是否处于内核态)
SCAUSE寄存器:中断类型和原因
STVAL寄存器:用来存储其他异常有关的东西
SIE寄存器:Supervisor Interrupt Enable, 中断使能
SIP寄存器:记录每种中断是否被触发
SSTAUTS寄存器:也是记录状态
再简单说一下用到的指令:
sd指令(Store Double)
sd rs2, offset(rs1)
将 x[rs2]中的 8 字节存入内存地址 x[rs1]+sign-extend(offset)。
ld指令(load double,双字加载)
ld rd, offset(rs1) x[rd] = M[x[rs1] + sext(offset)][63:0]
双字加载 (Load Doubleword). I-type, RV32I and RV64I.
从地址 x[rs1] + sign-extend(offset)读取八个字节,写入 x[rd]。
读写CSR寄存器的指令:
1 | # 控制状态寄存器 |
还有csrs和csrc,用来将csr指定位置的值置为0或1。此外还有csrrs、csrrc这些,用来读取(read)一个csr的值然后, 将csr中特定的bit置为0/1,trap的相关操作没有涉及到,暂时不用管。
然后是中断切换的指令
ecall:切换特权模式触发中断
sret:从S MODE(内核态)返回用户态,将PC的值设置为SEPC
ebreak:断点(调试的时候用)
mret:从M MODE(机器态)返回内核态,将PC置为MEPC(这个不用管,这个Lab没涉及到)
然后是用来保证内存屏障的指令,用来在更改页表的时候刷新内存屏障的
sfemce.vma rs1, rs2
虚拟内存屏障(Fence Virtual Memory). R-type, RV32I and RV64I 特权指令。
根据后续的虚拟地址翻译对之前的页表存入进行排序。当 rs2=0 时,所有地址空间的翻译都
会受到影响;否则,仅对 x[rs2]标识的地址空间的翻译进行排序。当 rs1=0 时,对所选地址
空间中的所有虚拟地址的翻译进行排序;否则,仅对其中包含虚拟地址 x[rs1]的页面地址翻
译进行排序。
Trap种类
1 | // xv6中用户态trap的代码 |
Trap在xv6里面指的是用户空间和内核空间的切换,包括系统调用、page fault、中断等。(xv6这里的Trap是一个统称,跟其他地方的称呼习惯不一样,比如CSAPP和一些linux的书习惯把syscall之类的东西叫trap,而把键盘I/O这种interrupt叫做中断,大部分其他的书认为trap、interrupt、fault是三种不同的东西)
一般Trap干了这些事:
保存32个用户寄存器
保存pc和sp寄存器
切到supervisor mode
从user page table切到kernel page table
进入内核态执行程序
完成这些过程的就是kernel/trampoline.S
中的uservec
函数,加了点注释,直接看注释吧。
1 | .globl uservec |
与之对应的是userret
,用来在返回userspace的时候恢复这些寄存器
1 | .globl userret |
除了上面的usertrap,还有内核态引发的trap
1 | // interrupts and exceptions from kernel code go here via kernelvec, |
还有一个trap就是时钟,这个东西做过单片机的应该非常熟悉,就是用来做时钟嘀嗒的(ticks)。一般板子上有晶振等东西产生时钟信号,然后在固定的时间每次都出发一个时钟中断,用来做基准时间。(在linux里面就是会变成我们熟悉的那个jiffies)
这个比较特殊,直接用的比较少,不多说了。
详见kernel/kernelvec.S
中的timervec函数。需要注意的是这个玩意虽然也算作interrupt,但是它是不归中断处理函数管的。我们在一些spinlock等操作关中断的时候intr_off
是不会把时钟中断也关掉的。
1 | void |
此外还有来自设备的trap,就是别的系统习惯成为外部中断(external interrupt)的东西,定义在kernel/trap.c
的devintr
函数,这个后面说道驱动的时候再讲。
ps:外部中断的优先级是很高的,比syscall等其他的trap都高
Lab
RISC-V assembly (easy)
1 | int g(int x) { |
1 | int g(int x) { |
给你一个call.c
,然后make fs.img
之后会生成一个call.asm
,下面问了你几个汇编的问题(然而我懒,我直接掏出gdb回答)
这个简单,翻手册就完事了
Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
a0-a7这些a开头的呗,没啥好说的。第二问, 我们直接打开call.asm
,然后找到main
函数,发现main函数第45行赫然写着li a2,13
那就是a2呗。
(这直接cat user/call.asm | rg 13
搜索13不就好了, 逃)
Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)
我一开始傻乎乎的找调用没找到,一看提示才发现直接内联了。第46行,li a1,12
,紧挨刚才那个13,f(8)+1 = 8 + 3 + 1 = 12,这个12就是了。(你比编译器聪明系列
At what address is the function printf located?
就是pc的address,就是ra寄存器的值呗,gdb看一下就知道了。算了算了不偷懒了,看一下汇编,接着刚才的12向下面找,
查手册发现auipc rd, immediate
,是左移12 immediate立即数加到rd上
jalr指令: jalr rd, offset(rs1)
, 把 pc 设置为 x[rs1] + sign-extend(offset),把计算出的地址的最低有效位设为 0,并将原 pc+4的值写入 f[rd]。rd 默认为 x1。 大概就是跳转到rs1, 然后将下一条指令(pc+4)保存到rd里面。
auipc和jalr这一对指令就相当于jal跳转,参见stackoverflow的这个问题Understanding the auipc+jalr sequence used for function calls
说人话就是jalr 1528(ra) # 628 <printf>
,在jalr这个无条件跳转的时候,将printf的pc的值(628)放到了ra里面
答案就是628
Run the following code.
unsigned int i = 0x00646c72;
printf(“H%x Wo%s”, 57616, &i);What is the output?
这一看就是hello, world啦
结果HE110 World
。直接查表就好了(唯一的小坑是大小端的问题,我这是小端,所以你还得倒过来查,不过这个坑很隐蔽,因为57616不管是大端还是小端结果都是一样的e110
,所以第二问的回答是不需要改57616,i要倒过来,改成0x00726c64
)
In the following code, what is going to be printed after ‘y=’? (note: the answer is not a specific value.) Why does this happen?
printf(“x=%d y=%d”, 3);
call.asm
是这样,对比printf("x=%d y=%d", 3, 4);
, 只少了一行li a2,4
, 推测这个printf函数打印出的y的值是寄存器a2中的值,由于我们没有赋值所以打印出来是原来的a2
1 | printf("x=%d, y=%d", 3); |
运行结果y=5301,然后我们用gdb验证一下呗,断点打到file user/_call.c
, 然后b mian
, 然后info registers
发现a2寄存器是5301,好了,问答环节结束,我们看下面的Lab
多说一下printf的实现,这个在csapp的第八章里面有讲,大概就是 调用 vspringf 然后调用sys_write, 然后去跟显示驱动打交道
Backtrace (moderate)
实现这个方便panic的时候看到backtrace结果,这个搞懂他想让我们干什么就很简单了,我们只要读出fp寄存器然后打印就好了。
怎么读 frame poiner提示已经告诉我们了,编辑kernel/riscv.h
:
1 | static inline uint64 |
在kernel/printf.c
那里加一个backtrace函数,然后输出照抄printint
和printptr
用consputc就好了。
提示甚至连栈顶和栈底怎么获取都告诉你了(PGROUNDDOWN(fp) and PGROUNDUP(fp)), 那就直接把这一段地址中间的所有东西打印出来就好了
Alarm (hard)
In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.
这个也不是很难,有点繁琐。就是添加一个syscall而已,做法跟syscall那个Lab的内容差不多。大概思路就是先trap进内核态看中断然后返回用户态打印,搞清楚流程照着做就行了。
保存和恢复寄存器这个我一开始是照抄uservec,全部保存的,后来看到Lab的Hints, 又看了一遍讲义
Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).
这里只要保存那些caller寄存器就可以了,像s开头的callee寄存器可以不管。Caller寄存器需要调用者主动保存,不然会被函数返回值覆盖(比如临时寄存器),Callee寄存器在函数调用的过程中不需要调用者保存,会由被调用者保存 。
只保存和恢复这些就可以了(注意sp寄存器也是callee寄存器)
1 | sd ra, 40(a0) |
那s0-s11是谁保存的?是被调用者保存的,比如进程的context上下文保存的就是这些callee寄存器,保存和恢复context就是操作这些callee-saved registers。哦,扯远了,到multithreading那个Lab再说
1 | // Saved registers for kernel context switches. |