Lab4为函数调用以及陷入相关的实验
该部分为一系列RISC-V汇编相关的问题,阅读user/call.asm
对应的call.asm
汇编文件,回答下面问题:
- 哪些寄存器包含函数的参数,例如,对于
main
调用printf
函数,哪个寄存器存参数13?
对于RISC-V,前8个参数会放置在a0-a7寄存器,a2放置参数13,如下代码所示:
void main(void) {
1c: 1101 addi sp,sp,-32
1e: ec06 sd ra,24(sp)
20: e822 sd s0,16(sp)
22: 1000 addi s0,sp,32
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0
2c: 7f850513 addi a0,a0,2040 # 820 <malloc+0xea>
30: 00000097 auipc ra,0x0
34: 648080e7 jalr 1608(ra) # 678 <printf>
main
汇编中哪里调用了函数f
和g
?
由上述代码可以看到,main
中直接得到了12,并放到了a1寄存器,可见编译器进行了优化,直接得到了结果
printf
的地址在哪?
由汇编文件可以看到printf
的地址为0x630,当做完alarm后,该汇编文件会发生变化,printf
的地址也会变化
- 当要进入
main
中printf
函数,执行jalr
指令后ra寄存器的值是多少?
ra应为函数调用中断点出的地址,也即jalr
下一条指令的地址,为0x38
- 运行下列代码:
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
输出依赖于RISC-V是小端系统,如果RISC-V是大端,i应如何设置得到相同结果,是否需要改变57616的值?
小端即低字节放置在低地址,输出为:"HE110 World",如果为大端i为0x726c6400,57616不需要改变
- 下列代码:
printf("x=%d y=%d", 3);
y=
将要输出什么,为什么会这样?
输出结果为x=3,但y是一个不确定的值,实际可能为a2寄存器的值
在kernel/printf.c
实现backtrace()
函数,用于打印函数调用过程,在sys_sleep
中插入该函数,之后运行测试
- 内联汇编读取s0寄存器,即fp栈指针的值
代码如下所示,fp(s0)寄存器用于保存当前函数栈帧的首地址:
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
- 栈帧结构
如下所示,fp寄存器为当前栈帧的首地址,fp-8为上级函数的返回地址,fp-16为上级栈帧的首地址,一直沿着上级栈帧的地址,可以打印出整个栈的调用过程
Stack
.
.
+-> .
| +-----------------+ |
| | return address | |
| | previous fp ------+
| | saved registers |
| | local variables |
| | ... | <-+
| +-----------------+ |
| | return address | |
+------ previous fp | |
| saved registers | |
| local variables | |
+-> | ... | |
| +-----------------+ |
| | return address | |
| | previous fp ------+
| | saved registers |
| | local variables |
| | ... | <-+
| +-----------------+ |
| | return address | |
+------ previous fp | |
| saved registers | |
| local variables | |
$fp --> | ... | |
+-----------------+ |
| return address | |
| previous fp ------+
| saved registers |
$sp --> | local variables |
+-----------------+
backtrace
函数
不断通过fp = fp-16获取栈帧地址,对于每个栈帧,打印上级函数的返回地址
void
backtrace()
{
uint64 cur_fp = r_fp();
printf("backtrace:\n");
for (uint64 fp = cur_fp; fp < PGROUNDUP(cur_fp); fp = *((uint64 *)(fp - 16)) ) {
printf("%p\n", *((uint64 *)(fp - 8)));
}
}
该节你将为xv6添加对于进程使用CPU时间时能够周期性地发出警报的功能,这对于计算密集型进程限制使用CPU时间或者进程希望周期性地执行某个动作很有用;更进一步,你将实现一个初级形式的用户态中断/故障处理程序,和处理应用中的页错误类似
首先添加sigalarm(interval, handler)
系统调用,如果程序调用sigalarm(n, fn)
,则程序每消耗n个ticks,内核调用程序的fn
函数,当fn
返回,程序应该在之前中断的地方恢复执行;一个tick是xv6的一个计时单元,由硬件时钟生成中断;如果一个应用调用sigalarm(0, 0)
,内核应停止周期性地执行alarm调用
- 添加
sys_sigalarm
、sys_sigreturn
两个系统调用定义
按之前lab添加系统调用的方式即可
- 在
proc.h/struct proc
添加alarm相关的成员变量
如下所示,alarm_ticks
为alarm的周期,alarm_handler_addr
为alarm处理函数的地址,该地址为用户进程的虚拟地址,这两个由sys_sigalarm
系统调用参数设置;ticks
为当前进程消耗的CPU时间,last_ticks
为上一次执行alarm处理函数的开始CPU时间,alarm_regs
为执行处理函数时保存与需要恢复的寄存器组值,alarm_running
用来标记是否该进程正在执行处理函数中
int alarm_ticks; // lab alarm
uint64 alarm_handler_addr; // lab alarm
uint64 ticks; // lab alarm
uint64 last_ticks; // lab alarm
struct alarm_regs regs; // lab alarm
int alarm_running; // lab alarm
- 初始化
proc
结构体alarm相关变量
在proc.c/allocproc
函数中对上述定义的相关变量进行初始化,初始ticks为0
// Init ticks for lab alarm
p->ticks = 0;
p->last_ticks = 0;
p->alarm_running = 0;
- 添加
sys_sigalarm
系统调用实现
sys_sigalarm
对进程proc
结构体的alarm_ticks
、alarm_handler_addr
变量进行设置,同时设置last_ticks
为当前ticks
,也即从当前开始计时
uint64
sys_sigalarm(void)
{
int ticks;
uint64 handler_addr;
if (argint(0, &ticks) < 0 || argaddr(1, &handler_addr) < 0)
return -1;
struct proc *p = myproc();
p->alarm_ticks = ticks;
p->alarm_handler_addr = handler_addr;
p->last_ticks = p->ticks;
return 0;
}
- 保存与恢复上下文
当进程从当前运行地方切换到处理函数入口地址时,应保存切换时的CPU寄存器值,这里在proc.h
定义struct alarm_regs
结构体,其需要保存的寄存器基本和trapframe
一致
struct alarm_regs
{
uint64 epc;
uint64 ra;
uint64 sp;
.....
uint64 s10;
uint64 t5;
uint64 t6;
};
当执行sys_sigalarm
系统调用进入内核后,当前进程用户态的上下文,也即执行ecall指令时的状态,保存在进程结构体的trapframe
中,由于sys_sigalarm
系统调用返回后强制使得该进程跳转到了处理函数去执行,执行完成后在通过sys_sigreturn
系统调用恢复,在这个处理函数执行过程中trapframe
已经发生了很大变化,因此需要保证sys_sigalarm
进入时的trapframe
和sys_sigreturn
返回时的trapframe
一致,即可恢复到执行处理函数之前的位置继续执行
在trap.c
中定义保存与恢复上下文如下所示:
void
save_regs(struct proc *p)
{
p->regs.epc = p->trapframe->epc;
p->regs.ra = p->trapframe->ra;
p->regs.sp = p->trapframe->sp;
.........
p->regs.t3 = p->trapframe->t3;
p->regs.t4 = p->trapframe->t4;
p->regs.t5 = p->trapframe->t5;
p->regs.t6 = p->trapframe->t6;
}
void restore_regs(struct proc *p)
{
p->trapframe->epc = p->regs.epc;
p->trapframe->ra = p->regs.ra;
p->trapframe->sp = p->regs.sp;
p->trapframe->gp = p->regs.gp;
.......
p->trapframe->t5 = p->regs.t5;
p->trapframe->t6 = p->regs.t6;
}
- 处理时钟中断
每当进程因为时钟中断陷入后,进程的CPU时间ticks
增加,当alarm处理程序未在运行,并且设置了alarm周期时间,则当到期后就开始执行处理函数
首先p->last_ticks = p->ticks
设置最后调用处理函数的开始时间为当前ticks
save_regs(p)
保存了将要调用处理函数之前的CPU寄存器状态
p->trapframe->epc = p->alarm_handler_addr
将陷入后的返回地址设置为处理函数的地址,当trampoline.S/userret
最后的sret
指令执行后,即将PC设置为了处理函数地址,也即执行该处理函数
p->alarm_running = 1
表示该进程的处理函数正在执行
// give up the CPU if this is a timer interrupt.
// lab alarm
if(which_dev == 2) {
p->ticks++;
if (p->alarm_ticks != 0 && p->alarm_running == 0) {
if (p->last_ticks + p->alarm_ticks <= p->ticks) {
p->last_ticks = p->ticks;
save_regs(p);
p->trapframe->epc = p->alarm_handler_addr;
p->alarm_running = 1;
}
}
- 添加
sys_sigreturn
系统调用实现
在sysproc.c
中添加sys_sigreturn
实现,主要作用是用户的alarm处理函数执行后,恢复到处理函数执行前的状态
uint64
sys_sigreturn(void)
{
struct proc *p = myproc();
restore_regs(p);
p->alarm_running = 0;
return 0;
}