Lab traps: Traps
Backtrace (moderate)
For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.
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:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
After bttest
exit qemu. In your terminal: the addresses may be slightly different but if you run addr2line -e kernel/kernel
(or riscv64-unknown-elf-addr2line -e kernel/kernel
) and cut-and-paste the above addresses as follows:
$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D
You should see something like this:
kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85
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.
Some hints:
Add the prototype for backtrace to
kernel/defs.h
so that you can invokebacktrace
insys_sleep
.The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to
kernel/riscv.h
:static inline uint64 r_fp() { uint64 x; asm volatile("mv %0, s0" : "=r" (x) ); return x; }
and call this function in
backtrace
to read the current frame pointer. This function uses in-line assembly to read s0.These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
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)
andPGROUNDUP(fp)
(seekernel/riscv.h
. These number are helpful forbacktrace
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.
题目要求通过调用栈帧打印出函数调用地址以便追踪
kernel/defs.h
// printf.c
void printf(char*, ...);
void panic(char*)__attribute__((noreturn));
void printfinit(void);
void backtrace(void); //声明堆栈追踪函数
kernel/printf.c
void
panic(char *s)
{
pr.locking = 0;
printf("panic: ");
printf(s);
printf("\n");
backtrace(); //调用堆栈追踪函数
panicked = 1; // freeze uart output from other CPUs
for(;;)
;
}
// 堆栈追踪函数实现
void
backtrace(void){
// 获取当前堆栈指针
uint64 fp = r_fp();
// 计算页上限与下线
uint64 high = PGROUNDUP(fp),low = PGROUNDDOWN(fp);
printf("backtrace:\n");
// 打印堆栈内容
while (fp<=high&&fp>=low){
printf("%p\n",*((uint64 *)(fp-8)));
fp = *((uint64 *)(fp-16));
}
}
kernel/riscv.h
//获取s0寄存器的值
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
kernel/sysproc.c
uint64
sys_sleep(void)
{
//调用backtrace函数
backtrace();
int n;
uint ticks0;
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
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.
You should add a new sigalarm(interval, handler)
system call. If an application calls sigalarm(n, fn)
, then after every n
"ticks" of CPU time that the program consumes, the kernel should cause application function fn
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 calls sigalarm(0, 0)
, the kernel should stop generating periodic alarm calls.
You'll find a file user/alarmtest.c
in your xv6 repository. Add it to the Makefile. It won't compile correctly until you've added sigalarm
and sigreturn
system calls (see below).
alarmtest
calls sigalarm(2, periodic)
in test0
to ask the kernel to force a call to periodic()
every 2 ticks, and then spins for a while. You can see the assembly code for alarmtest in user/alarmtest.asm, which may be handy for debugging. Your solution is correct when alarmtest
produces output like this and usertests also runs correctly:
$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$
When you're done, your solution will be only a few lines of code, but it may be tricky to get it right. We'll test your code with the version of alarmtest.c in the original repository. You can modify alarmtest.c to help you debug, but make sure the original alarmtest says that all the tests pass.
test0: invoke handler
Get started by modifying the kernel to jump to the alarm handler in user space, which will cause test0 to print "alarm!". Don't worry yet what happens after the "alarm!" output; it's OK for now if your program crashes after printing "alarm!". Here are some hints:
You'll need to modify the Makefile to cause
alarmtest.c
to be compiled as an xv6 user program.The right declarations to put in
user/user.h
are:int sigalarm(int ticks, void (*handler)()); int sigreturn(void);
Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow
alarmtest
to invoke the sigalarm and sigreturn system calls.For now, your
sys_sigreturn
should just return zero.Your
sys_sigalarm()
should store the alarm interval and the pointer to the handler function in new fields in theproc
structure (inkernel/proc.h
).You'll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process's alarm handler; you'll need a new field in
struct proc
for this too. You can initializeproc
fields inallocproc()
inproc.c
.Every tick, the hardware clock forces an interrupt, which is handled in
usertrap()
inkernel/trap.c
.You only want to manipulate a process's alarm ticks if there's a timer interrupt; you want something like
if(which_dev == 2) ...
Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user's alarm function might be 0 (e.g., in user/alarmtest.asm,
periodic
is at address 0).You'll need to modify
usertrap()
so that when a process's alarm interval expires, the user process executes the handler function. When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?It will be easier to look at traps with gdb if you tell qemu to use only one CPU, which you can do by running
make CPUS=1 qemu-gdb
You've succeeded if alarmtest prints "alarm!".
test1/test2(): resume interrupted code
Chances are that alarmtest crashes in test0 or test1 after it prints "alarm!", or that alarmtest (eventually) prints "test1 failed", or that alarmtest exits without printing "test1 passed". To fix this, you must ensure that, when the alarm handler is done, control returns to the instruction at which the user program was originally interrupted by the timer interrupt. You must ensure that the register contents are restored to the values they held at the time of the interrupt, so that the user program can continue undisturbed after the alarm. Finally, you should "re-arm" the alarm counter after each time it goes off, so that the handler is called periodically.
As a starting point, we've made a design decision for you: user alarm handlers are required to call the sigreturn
system call when they have finished. Have a look at periodic
in alarmtest.c
for an example. This means that you can add code to usertrap
and sys_sigreturn
that cooperate to cause the user process to resume properly after it has handled the alarm.
Some 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).
Have
usertrap
save enough state instruct proc
when the timer goes off thatsigreturn
can correctly return to the interrupted user code.Prevent re-entrant calls to the handler----if a handler hasn't returned yet, the kernel shouldn't call it again.
test2
tests this.
Once you pass test0
, test1
, and test2
run usertests
to make sure you didn't break any other parts of the kernel.
kernel/proc.c
static struct proc*
allocproc(void)
{
...
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// 开辟一个新的trapframe页面以便保存
if((p->savetrapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
...
}
static void
freeproc(struct proc *p)
{
...
// 释放页面
if(p->savetrapframe)
kfree((void*)p->savetrapframe);
p->savetrapframe = 0;
...
}
kernel/proc.h
struct proc {
...
int ticks; // 每ticks时间片调用一次handler
void(*handler)() ; // 被调用的handler
int lefttick; // 剩余的tick时间片
int ishandler; // 标志位 sigalarm进入时为1,sigreturn退出时为0
struct trapframe savetrapframe; // 保存trapframe,待退出sigalarm是恢复寄存器状态
};
kernel/syscall.c
// 声明系统函数
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
// 添加系统函数调用
static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};
kernel/syscall.h
//声明系统调用数
#define SYS_sigalarm 22
#define SYS_sigreturn 23
kernel/sysproc.c
uint64
sys_sigalarm(void){
if(argint(0,&(myproc()->ticks))<0){
return -1;
}
if(argaddr(1, (uint64 *) &(myproc()->handler)) < 0){
return -1;
}
if(myproc()->ishandler==1) {
return 0;
}
myproc()->lefttick = myproc()->ticks;
return 0;
}
uint64
sys_sigreturn(void){
if(myproc()->ishandler==1){
// 保存页面
memmove(myproc()->trapframe,myproc()->savetrapframe,sizeof (struct trapframe));
myproc()->ishandler = 0;
}
return 0;
}
kernel/trap.c
void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt. 如果是时间片打断
if(which_dev == 2){
// 如果已经退出handler且定时不为0
if(myproc()->ishandler==0&&myproc()->ticks!=0){
// 剩余时间片减一
myproc()->lefttick--;
// 如果剩余时间片为0
if(myproc()->lefttick==0){
// 保存trapframe
memmove(p->savetrapframe,p->trapframe,sizeof (struct trapframe));
// 设置退出寄存器
myproc()->trapframe->epc = (uint64) myproc()->handler;
// 重置时间片计时
myproc()->lefttick = myproc()->ticks;
// 进入handler
myproc()->ishandler = 1;
}
}
yield();
}
usertrapret();
}
Makefile
ifeq ($(LAB),traps)
UPROGS += \
$U/_call\
#添加文件
$U/_bttest\
$U/_alarmtest
endif
Q.E.D.