6.S081-Lab4 traps实验笔记
RISC-V assembly
There is a file user/call.c
in your xv6 repo. make
fs.img compiles it and also produces a readable assembly version of the
program in user/call.asm
.
我们启动qemu可以看到有call.c文件,执行make fs.img
可以生成汇编文件
1 |
|
Which registers contain arguments to functions? For example, which register holds 13 in main's call to
printf
?那几个寄存器保存了函数的参数,例如main函数里面那个寄存器保存了printf的入参13这个值
1 | void main(void) { |
从上面可以看出li a2,13 。将13加载到了a2寄存器中,从下图中可以看出,a2寄存器一般是用来传递函数的参数的
Where is the call to function
f
in the assembly code for main? Where is the call tog
? (Hint: the compiler may inline functions.)没有这样的代码。 g(x) 被内链到 f(x) 中,然后 f(x) 又被进一步内链到 main() 中
At what address is the function
printf
located?
可以看出printf的入口地址是0x630
1 | 0000000000000630 <printf>: |
What value is in the register
ra
just after thejalr
toprintf
inmain
?
1 | 34: 600080e7 jalr 1536(ra) # 630 <printf> |
jalr,参考riscv指令集发现,其功能是
- 把pc + 4 的值记为t
- 把pc的值设置成 $ra + 1536
- 把ra寄存器的值设置成t
所以要跳转的位置是ra+1536
可以看出。ra寄存器里面存的是0x30,1536在16进制下就是0x600.所以最后就是跳转到printf的入口函数地址
Run the following code.
1
2
3 unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
What is the output? Here's an ASCII table that maps bytes to characters.
The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set
i
to in order to yield the same output? Would you need to change57616
to a different value?
执行上述代码的输出是HE110 World
为了知道ox00646c72是怎么代表字母的:
大端序:高位字节在低地址。
小端序:低位字节在低地址。
RSIC-V是小端序的所以高字节在低地址显示出来就是rld
要是大端序的话需要换一下顺序:ox00726c64
Backtrace
在调试的过程中,有一个发生错误的点之上的堆栈上的函数调用列表通常很有用
Implement a backtrace()
function in
kernel/printf.c
. Insert a call to this function in
sys_sleep
, and then run bttest, which calls
sys_sleep
. Your output should be as
follows。将写好的backtrace函数加入到sys_sleep系统调用里面,方便后面的测试。
1 | backtrace: |
The compiler puts in each stack frame a frame pointer that holds the
address of the caller's frame pointer. Your backtrace
should use these frame pointers to walk up the stack and print the saved
return address in each stack frame.
栈帧
The compiler puts in each stack frame a frame pointer that holds the address of the caller's frame pointer. Your
backtrace
should use these frame pointers to walk up the stack and print the saved return address in each stack frame.编译器在每一个栈帧里面存放一个栈的指针,这个栈的指针保存了调用这个栈帧的栈帧的地址
- 函数调用栈(Stack)
- 由高地址往低地址增长
- 在xv6里,栈有一页大小(4KB)
- 栈指针(stack pointer)保存在sp寄存器里
gdb调试一下理解栈帧的调用
- 栈帧(Stack Frame)
- 当前栈帧的地址保存在 s0/fp寄存器 里
- 当前栈帧的地址也叫栈帧的指针(frame pointer, fp),指向该栈帧的最高处
- 栈帧指针往下偏移8个字节是函数返回地址 return address
- 往下偏移16个字节是上一个栈帧的栈帧指针(previous frame pointer)
栈从高地址向低地址增长,每个大的box叫一个stack frame(栈帧),栈帧由函数调用来分配,每个栈帧大小不一定一样,但是栈帧的最高处一定是return address
sp是stack pointer,用于指向栈顶(低地址),保存在寄存器中
fp是frame pointer,用于指向当前帧底部(高地址),保存在寄存器中,同时每个函数栈帧中保存了调用当前函数的函数(父函数)的fp(保存在to prev frame那一栏中)
这些栈帧都是由编译器编译生成的汇编文件生成的
首先我们在backtrace函数打断点后,启动gdb调试。此时我们info frame
可看到当前的栈帧情况:
可以看出现在的栈帧的地址是
我们可以看出这个栈的上一个栈在
这个上一栈的地址是存在fp寄存器里面的:

比如我们打印栈帧1,可以看出ra里面存的返回寄存器里面存的栈针的内容就是syscall函数的地址
hints
Add the prototype for backtrace to
kernel/defs.h
so that you can invokebacktrace
insys_sleep
.在 defs.h 中添加声明
1
2
3
4
5
6
void printf(char*, ...);
void panic(char*) __attribute__((noreturn));
void printfinit(void);
void backtrace(void); // new function for backtraceThe GCC compiler stores the frame pointer of the currently executing function in the register s0. GCC编译器把当前执行函数的栈帧指针存储在寄存器s0里面
Add the following function to kernel/riscv.h:
1
2
3
4
5
6
7static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}asm volatile("mv %0, s0" : "=r" (x) );
:这是内联汇编的部分。asm volatile
是内联汇编的开始标记,mv
是 MIPS 汇编指令,用于将寄存器s0
的值移动到%0
中。%0
是内联汇编语法中的占位符,表示第一个输出操作数(output operand)。=
表示这是一个输出操作数。(x)
表示要将结果存储到变量x
中。r
表示使用寄存器约束,告诉编译器将变量x
放置在一个通用寄存器中。and call this function inbacktrace to read the current frame pointer. This function usesin-line assembly to read s0.
实现 backtrace 函数
小技巧,因为是栈指针往下偏移8个字节是函数返回地址,为了更好偏移8个字节,可以指定为uint_64指针,这样一个就是8个字节,因栈的地址是从高地址向低地址生长的所以是-1
Xv6 allocates one page for each stack in the xv6 kernel at
PAGE-aligned address. You can compute the top and bottom address of the
stack page by using PGROUNDDOWN(fp)
and
PGROUNDUP(fp)
(see kernel/riscv.h
. These
number are helpful for backtrace
to terminate its loop.
可以通过判断是否超过一页的大小判断栈指针是否越界来终止循环
函数实现:
Once your backtrace is working, call it from panic
in
kernel/printf.c
so that you see the kernel's backtrace when
it panics.
实现后,将backtrace函数放到panic里面,当函数出现问题的时候就能看到backtrace
1 | void |
trap 的原理
Alarm
In this exercise you'll add a feature to xv6 that periodically alerts a process as it uses CPU time.
给XV6内核增添一个周期性报警的功能来限制进程使用CPU的时间
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可以用相似的方式去处理页错误.
You should add a new
sigalarm(interval, handler)
system call.新增一个系统调用,功能:当应用程序调用sigalarm(n, fn)
then after every
n
"ticks" of CPU time that the program consumes, the kernel should cause application functionfn
to be called. (有点类似软中断的定时器功能)When
fn
returns, the application should resume where it left off. A tick is a fairly arbitrary unit of time in xv6, determined by how often a hardware timer generates interrupts. If an application callssigalarm(0, 0)
, the kernel should stop generating periodic alarm calls.首先修改Makefile*以使alarmtest.c*被编译为xv6用户程序.
到user/user.h声明
1 | int sigalarm(int ticks, void (*handler)()); |
- 更新user/usys.pl(此文件生成user/usys.S)、kernel/syscall.h和kernel/syscall.c以允许
alarmtest
调用sigalarm
和sigreturn
系统调用。然后在sysproc.c里面实现这两个系统调用的函数体
invoke handler(调用处理程序)
- 你的
sys_sigalarm()
应该将报警间隔和指向处理程序函数的指针存储在struct proc
的新字段中(位于*kernel/proc.h*)。 - 你也需要在
struct proc
新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;您可以在*proc.c*的allocproc()
中初始化proc
字段。 - 每一个滴答声,硬件时钟就会强制一个中断,这个中断在kernel/trap.c中的
usertrap()
中处理。 - 如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
1 | if(which_dev == 2) ... |
仅当进程有未完成的计时器时才调用报警函数。请注意,用户报警函数的地址可能是0(例如,在user/alarmtest.asm中,
periodic
位于地址0)。您需要修改
usertrap()
,以便当进程的报警间隔期满时,用户进程执行处理程序函数。当RISC-V上的陷阱返回到用户空间时,什么决定了用户空间代码恢复执行的指令地址?首先我们知道,32个通用寄存器的值都保存在了proc结构体的trapframe里面
struct trapframe *trapframe; // data page for trampoline.S
用户程序的pc指针保存在epc里面.所以我们想让trap恢复后去执行handler的函数只需要把这里的epc指向handler就可以
注意:如果您告诉qemu只使用一个CPU,那么使用gdb查看陷阱会更容易,这可以通过运行
1 | make CPUS=1 qemu-gdb |
在 proc 结构体的定义中,增加 alarm 相关字段:
- alarm_interval:时钟周期,0 为禁用
- alarm_handler:时钟回调处理函数
- alarm_ticks:下一次时钟响起前还剩下的 ticks 数
- alarm_trapframe:时钟中断时刻的 trapframe,用于中断处理完成后恢复原程序的正常执行
- alarm_goingoff:是否已经有一个时钟回调正在执行且还未返回(用于防止在 alarm_handler 中途闹钟到期再次调用 alarm_handler,导致 alarm_trapframe 被覆盖)
函数句柄,定义一个函数指针来保存:
void(*alarm_handler)();
1 | uint64 |
在allocproc的时候进行初始化工作:
1 | // Allocate a trapframe page for alarm_trapframe. |
freefroc的时候同样要进行操作
1 | if(p->alarm_trapframe) |
在 usertrap() 函数中,实现时钟机制:
1 | if(which_dev == 2){ |
执行完handel后恢复原来的执行流现场:
1 | uint64 |