Appearance
traps
开启新实验
git fetch
git checkout traps
make clean
RISC-V assembly
The code in call.asm
for the functions g
, f
, and main
.
The instruction manual for RISC-V is on the reference page.
Q1
Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
RISC-V的函数调用过程参数优先使用寄存器传递,即a0~a7共8个寄存器。返回值可以放在a0和a1寄存器。main函数printf调用的13保存在a2。
Q2
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.)
汇编代码中直接将12移入a2作为printf的参数,12已经是f(8)+1的结果了。
Q3
At what address is the function printf located?
30: 00000097 auipc ra,0x0
34: 5f8080e7 jalr 1528(ra) # 628 <printf>
auipc
指令将当前指令地址加上一个立即数(0x0)存储到ra
寄存器。auipc
指令的全称是 “Add Upper Immediate to PC”,用于计算当前 PC 地址的偏移。
jalr
指令是 “Jump And Link Register”,它将跳转到 ra + 1528
的地址,并将返回地址存储到 ra
寄存器。结合 auipc
和 jalr
,我们知道这实际上是一个跳转到 printf
函数的调用。
当前 PC 地址是 0x30
,那么 auipc ra,0x0
将 ra
设置为 0x30 + 0x0
(即 0x30
)。jalr 1528(ra)
将跳转到 0x30 + 1528
,这就是printf
函数的实际地址。
0x30 + 1528 = 0x30 + 0x5f8 = 0x628
因此,printf
函数的地址是0x628
。
Q4
What value is in the register ra just after the jalr to printf in main?
在main
函数中调用printf
之后,ra
(返回地址寄存器)中将存储返回到main
函数中的地址。这是因为jalr
指令的作用是跳转到目标地址并将返回地址存储到ra
中。
在执行jalr
指令时,ra
寄存器将被设置为跳转指令的下一条指令的地址,也就是printf
调用后的下一条指令的地址。
根据代码,jalr
指令的下一条指令是:
38: 4501 li a0,0`
因此,ra
寄存器将在执行jalr
指令后包含0x38
,这是printf
返回时程序将继续执行的地址。
Q5
Run the following code.
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?Here's a description of little- and big-endian and a more whimsical description.
printf
语句将打印两个格式化的字符串:
"H%x Wo"
- 打印字符H
,然后是十六进制格式的数字57616
,再是字符串Wo
"%s"
- 打印字符串,该字符串从i
的地址开始
57616
的十六进制表示为 0xe110
。
小端
i
地址由低到高分别是:72 6c 64 00
从 i
的地址开始的字符串,转换为 ASCII 字符为:rld
所以输出He110 World
。
大端
要保持输出不变,i
地址由低到高应该依旧是:72 6c 64 00
在大端模式下,这个数字是:0x726c6400
即i
应该设置为0x726c6400
来保持与小端相同的输出。
对于 57616
,它的值不受大端或小端影响,因为它只是一个直接的数值参数,不涉及内存存储顺序,所以不需要更改。
Q6
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);
应该打印出寄存器a2的值,因为printf会从a2寄存器中读取第三个参数作为y的值。
Backtrace
对于调试,回溯跟踪通常很有用:在错误发生点上方的堆栈上调用函数的列表。
任务描述:在kernel/printf.c
中实现backtrace()
函数。并在sys_sleep
中插入对此函数的调用。
- GCC 编译器将当前正在执行的函数的帧指针存储在寄存器 s0 中。在
kernel/riscv.h
中添加以下函数
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
r_fp()
这个函数读出了 s0
这个寄存器的值,然后储存在 x
中,最后又把 x
返回了。
但是我们要读取的明明是fp
这个寄存器,为什么这个函数里写的是s0
呢,具体可以看看下面这个表:
在 ABI Name 那一列,可以看到 s0 其实就是 fp 的别名。
- 在
kernel/printf.c
中实现backtrace()
编译器在每个堆栈帧中放置一个帧指针,该指针保存调用方帧指针的地址。backtrace()
应使用这些帧指针在堆栈中向上移动,并在每个堆栈帧中打印保存的返回地址。
请注意,返回地址位于与堆栈帧的帧指针的固定偏移量 (-8) 处,而上一个函数的帧指针保存于距当前帧指针的固定偏移量 (-16) 处。
Xv6 在 xv6 内核的每个堆栈中与 PAGE 对齐的地址处分配一个页面。可以使用 PGROUNDDOWN(fp) 和 PGROUNDUP(fp) 计算堆栈页面的顶部和底部地址。
需要使用 PGROUNDDOWN
和 PGROUNDUP
是因为,一连串的函数调用最多放在一个页中。那么如果我们在递归打印的时候,超出了这一页的范围,就可以说明已经是最底层的函数,可以停止了。
void backtrace(void)
{
printf("backtrace:\n");
uint64 *fp = (uint64*)r_fp();
uint64 *top = (uint64*)PGROUNDUP((uint64)fp);
uint64 *bot = (uint64*)PGROUNDDOWN((uint64)fp);
while(fp < top && fp > bot){
printf("%p\n", fp[-1]);
fp = (uint64*)fp[-2];
}
}
类型转换好麻烦
可以看到这里用了一些很奇怪的写法,好像是负数下标的数字,其实这个 fp[-1]
等价于 *(fp - 1)
。并且,因为这里 fp
是六十四位的指针,所以 *(fp - 1)
是读取 fp
前八个字节位置的数据。
- 将
backtrace()
添加到kernel/defs.h
中,以便在sys_sleep
中调用。
void backtrace(void);
- 在
kernel/sysproc.c
的sys_sleep
函数中插入对backtrace()
的调用。
Alarm
任务描述:向 xv6 添加一个功能,该功能会在进程使用 CPU 时间时定期发出警报。添加一个新的sigalarm(interval, handler)
系统调用,消耗每interval
个 CPU 时间之后,内核应该导致应用程序函数handler
被调用,返回时从中断的地方继续。此外还要实现一个sigreturn()
系统调用,如果时间到了handler
调用了sigreturn()
,就应该停止执行handler
,然后恢复正常的执行顺序。如果说 sigalarm
的两个参数都为 0,就代表停止执行handler
函数。
test0:invoke handler
user/alarmtest.c
已经实现,将其添加到 Makefile。在
user/user.h
添加如下声明
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
- 在
user/usys.pl
进行添加
entry("sigalarm");
entry("sigreturn");
- 在
kernel/syscall.h
中添加以下定义
#define SYS_sigalarm 22
#define SYS_sigreturn 23
- 修改
kernel/syscall.c
,用 extern 全局声明新的内核调用函数,并且在 syscalls 映射表中,加入从前面定义的编号到系统调用函数指针的映射。
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
}
- 修改
kernel/sysproc.c
,sys_sigreturn()
暂时只需返回0
uint64 sys_sigreturn(void)
{
return 0;
}
- 在
kernel/proc.h
中,struct proc
需要添加一些新成员
struct proc{
...
int interval;
uint64 handler;
int passed;
}
- 修改
kernel/sysproc.c
,sys_sigalarm()
需要保存一下相关信息
uint64 sys_sigalarm(void)
{
struct proc *p = myproc();
int interval;
uint64* handler;
if(argint(0, &interval) < 0) return -1;
if(argaddr(1, &handler) < 0) return -1;
p->interval = interval;
p->handler = handler;
p->passed = 0;
return 0;
}
- 修改
kernel/proc.c
,在allocproc()
和freeproc()
中的初始化和释放
p->interval = 0;
p->handler = 0;
p->passed = 0;
- 修改
kernel/trap.c
中的usertrap()
,以便当进程的警报间隔到期时,用户进程将执行处理程序函数。
每个时钟周期,硬件时钟都会强制中断,该中断在usertrap()
中处理。
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if(p->interval > 0){
p->passed++;
if(p->passed > p->interval){
p->passed = 0;
p->trapframe->epc = p->handler;
}
}
yield();
}
这样我们就能顺利的跳转到handler
,并且通过 test0,当然也毫无悬念的报错了。
报错的主要原因是还没实现 sys_sigreturn()
,这样在执行完handler
函数之后就不知道返回哪里了。
test1/test2(): resume interrupted code
- 在
struct proc
再加一个struct trapframe
类的属性,用于备份执行 handler 前的环境
// kernel/proc.h
struct trapframe *alarmframe;
- 修改
kernel/proc.c
,在allocproc()
和freeproc()
中的初始化和释放
// allocproc()
if((p->alarmframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// freeproc()
if(p->alarmframe)
kfree((void*)p->alarmframe);
p->alarmframe = 0;
- 在
kernel/trap.c
里的usertrap()
需要执行handler
的时候,先备份一下环境,然后再执行
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if(p->interval > 0){
p->passed++;
if(p->passed > p->interval){
p->passed = 0;
*p->alarmframe = *p->trapframe;
p->trapframe->epc = p->handler;
}
}
yield();
}
- 修改
kernel/sysproc.c
,在sys_sigreturn()
按照alarmframe
恢复trapframe
uint64 sys_sigreturn(void){
struct proc *p = myproc();
*p->trapframe = *p->alarmframe;
return 0;
}
到这里,再去运行alarmtest
,会发现还是不能过。
如果 handler 执行的特别慢,自从上次调用 handler 已经过去了规定的时钟周期,但是 handler 还没执行好,这个时候我们又去改一遍 epc,这个 handler 又从头开始执行了,那着不就出大问题了,因为我们每次都会去改 epc,然后就永远执行不完 handler 了。
所以我们需要在 struct proc
里再加一个属性,就是 alarm_state
。如果这个属性为 1,就表示,handler 程序正在执行,这个时候就算又过了 tick 个时钟周期,我们也不能去改 epc 让 handler 重复执行。
- 在
kernel/proc.h
添加属性 - 在
kernel/proc.c
添加初始化与释放
这两步很简单就跳过了,还要修改kernel/trap.c
里的usetrap()
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if(p->interval > 0){
p->passed++;
if(p->passed > p->interval && !p->alarm_state){
p->passed = 0;
*p->alarmframe = *p->trapframe;
p->alarm_state = 1;
p->trapframe->epc = p->handler;
}
}
yield();
}
只有alarm_state
为0才允许跳转到handler
,跳转时将其置1,标志着handler
正在执行。
最后,修改kernel/sysproc.c
中的sigreturn()
。
uint64 sys_sigreturn(void){
struct proc *p = myproc();
*p->trapframe = *p->alarmframe;
p->alarm_state = 0;
return 0;
}
不再执行handler
时调用sigreturn()
,所以alarm_state
要恢复为0。
The End
复习了系统调用,了解了一点点汇编,学习了xv6的Traps。感觉自己并不是非常的理解陷入的过程,只是在一点一点地跟着实验指导做。而且还是没去仔细扒一下xv6的源码,对于一些结构不是很清楚都储存了什么信息,一些方法也不太会用,所以在实现上还是有点茫然。