RISC-V assembly
以下内容摘自知乎用户rocketeerli
首先,执行 make fs.img
指令,进行编译。然后查看生成的 user/call.asm 文件,其中的 main 函数如下:
这部分没有需要写的代码,主要根据这个编译生成的代码,回答几个问题。
这里直接按照中文翻译了。
问题一
Q: 哪些寄存器存储了函数调用的参数?举个例子,main 调用 printf 的时候,13 被存在了哪个寄存器中? A: A1. a0-a7, a2.
根据第 45 行代码,可以看到 13 被放到了 a2 寄存器中。猜测是 a0-a7 寄存器保存参数。
问题二
Q: main 中调用函数 f 对应的汇编代码在哪?对 g 的调用呢? (提示:编译器有可能会内联(inline)一些函数) A: nowhere, compiler optimization by inline function.
其实是没有这样的代码。 g(x) 被内联到 f(x) 中,然后 f(x) 又被进一步内联到 main() 中。所以看到的不是函数跳转,而是优化后的内联函数。
问题三
Q: printf 函数所在的地址是? A: 0x0000000000000630 (ra=pc=0x30, 1536(ra)=0x0000000000000630).
其实,直接在 user/call.asm 代码中一直找,就能找到 printf 函数的地址。
也可以通过计算得到,首先将当前程序计数器的值赋给 ra 寄存器。auipc ra, 0x0
,是指将当前立即数向右移动12位,然后加上 pc 寄存器的值,赋给 ra 寄存器,由于立即数为 0,因此 ra 的值即为 pc 的值。当前指令在0x30处,因此 pc = 0x30。1528(ra) 是指 1528 加上 ra 寄存器的值,1536 转为16进制再加上0x30 即为 0x0000000000000628。刚好是 printf 的地址。
问题四
Q: 在 main 中 jalr 跳转到 printf 之后,ra 的值是什么? A: 0x38(ra=pc+4).
jalr 指令会将 pc + 4 赋给当前寄存器,刚好是其下一条指令的地址。
问题五
Q: 运行下面的代码
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
输出是什么? 如果 RISC-V 是大端序的,要实现同样的效果,需要将 i 设置为什么?需要将 57616 修改为别的值吗?
A: He110 World, 0x726c6400, no change for 57616.
%x 表示以十六进制数形式输出整数,57616 的16进制表示就是 e110,与大小端序无关。 %s 是输出字符串,以整数 i 所在的开始地址,按照字符的格式读取字符,直到读取到 '\0' 为止。当是小端序表示的时候,内存中存放的数是:72 6c 64 00,刚好对应rld。当是大端序的时候,则反过来了,因此需要将 i 以16进制数的方式逆转一下。
问题六
Q: 在下面的代码中,'y=' 之后会答应什么? (note: 答案不是一个具体的值) 为什么?
printf("x=%d y=%d", 3);
A: print the value of a2 register.
printf 接收到了两个参数,但实际需要三个参数,最后一个参数是放在 a2 寄存器中,由于没有输入第三个参数,因此 a2 寄存器中目前有啥就输出啥。
Backtrace (moderate)
这个实验是让我们完成一个函数调用跟踪
首先我们阅读题目获取信息,题目让我们先将backtrace()
这个函数的定义加到kernel/defs.h
里面,以便于后面sys_sleep()
函数获取他。
接着,根据hint,我们知道当前的帧指针被保存在s0里面,我们想要获取就得使用内联汇编函数,不过题目已经给出,我们按照要求将其加入到kernel/riscv.h
即可。
//kernel/riscv.h
...
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
...
xv6会为每个进程分配一页的栈,其中包含着许多栈帧(stack frame),每个frame里面都会包含此函数的一些信息。如下图所示
高地址
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 ------+ <-- 如果是 x86,那么 bp 指针会指向这里
| saved registers |
$sp --> | local variables |
+-----------------+
低地址(增长方向)
所以我们只需要不断的找栈帧内保存的上一个frame pointer的数据就可以实现函数调用顺序的打印了。
请注意,在RISCV中,返回地址位于距当前stack frame的帧指针的固定偏移量 (-8) 处,而保存的帧指针位于距当前stack frame的帧指针的固定偏移量 (-16) 处。
Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address.一连串的函数调用最多放在一个页中。那么如果我们在递归打印的时候,可以通过宏定义的PGROUNDDOWN
和PGROUNDUP
来限定页的范围,超出了这一页的范围,就可以说明已经是最底层的函数,可以停止了。
由以上信息不难写出
void
backtrace()
{
uint64 fp = r_fp();
uint64 top = PGROUNDUP(fp);
uint64 bottom = PGROUNDDOWN(fp);
printf("backtrace:\n");
for (;bottom < fp && fp < top;) {
printf("%p\n", *(uint64 *)(fp - 8));
fp = *(uint64*)(fp -16);
}
}
Alarm (hard)
这个task是实现一个用户级的timer?(不知道这么叫是否合适),大概意思就是设定两个参数,间隔n和函数handler,代表着每间隔n个tick,就会调用一次handler()函数。
在写代码之前,我们先按照要求添加这两个系统调用,具体过程和lab syscalls一样,这里不再赘述。
我们这次主要要修改的地方在kernel/sysproc.c
(实现两个系统调用的地方),kernel/trap.c
(利用hardware clock配合实现的系统调用完成功能),kernel/proc.h
(为proc定义新的有用的数据)里面。
根据我个人写实验总结的习惯,我还是会将我的思考路线尽可能还原出来。
test0
我们首先来看test0,首先hint告诉我们先不用管sys_sigreturn()
,直接让其return 0即可,我们按照提示加代码就行了
//kernel/sysproc.c
int
sys_sigreturn(void)
{
return 0;
}
接着,hint提示我们,在sys_sigalarm()
函数中,应该像proc结构体里面存放间隔和指向handler()的指针。
虽然现在还不知道有什么用,但是在kernel/proc.h
的proc结构体里面加入如下两行。
//kernel/proc.h
struct proc {
...
int nticks // 用来存放间隔
uint64 addFunc // 用来存放函数指针
...
}
接着在sys_sigalarm()
里面增加存储过程
//kernel/sysproc.c
int
sys_sigalarm(void)
{
struct proc* p = myproc();
int ticks;
uint64 addFunc; // 函数指针也是地址,所以我们定义uint64用来接收
argint(0, &ticks);
argaddr(1, &addFunc);
p->nticks = ticks;
p->addFunc = addFunc;
return 0;
}
这样就好了。不过我们还得在kernel/proc.c
的allocproc()
里面添加这两个值的初始化。都将其设置为0即可
static struct proc*
allocproc(void)
{
...
found:
...
p->nticks = 0;
p->addFunc = 0;
...
}
riscv 的硬件(其实我不太确定是哪个硬件)会每过一个时钟周期都产生一个时钟中断,而 trap.c
会处理这个中断。以此我们可以通过不断的进入usertrap()
来确定我们到底过去了多少个ticks,现在是不是可以慢慢理解为什么要像proc结构体里面加入nticks这个数据了?猜测是为了检测是否进入了usertrap()
这么多次,所以我们还需要往结构体里面加一个sinceticks的参数,用于记录在上一次调用handler过后,我们经历了多少个ticks,如果经历sinceticks = nticks,那我们就继续调用handler
You only want to manipulate a process's alarm ticks if there's a timer interrupt; you want something like
if(which_dev == 2) ...
一开始不明白什么意思,但是配合usertrap()
的代码一起理解,就可以发现这是干什么的。变量 int which_dev = 0; 在 usertrap()
函数中的作用是用于记录设备中断的类型,帮助确定当前陷阱是否由设备中断引起,以及具体是由哪个设备触发的。
//kernel/trap.c
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// save user program counter.
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();
syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
于是综合以上信息,我们可以在usertrap()
里面写出如下逻辑
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if (p->alarmlock == 0 && p->nticks > 0) {
p->sinceticks ++;
if (p->sinceticks == p->nticks) {
p->sinceticks = 0;
// 调用handler,但是此时我们还不知道怎么调用
}
}
现在有点困惑,如何调用handler呢?我一开始想的是在里面调用sys_sigreturn()
,因为这个函数名听起来像是返回东西的函数(?)但是继续阅读hint便可以发现
When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?
恍然大悟,在trap的时候,系统会保存发生trap的pc位置,当trap执行usertrapret()
返回的时候,便会将保存下来的pc赋给pc寄存器,这样就可以正确的返回了。但是我们现在并不想回到发生trap的位置,而是想进入handler函数处理。RFTM之后,我们发现sepc这个寄存器保存着要返回的pc信息。
一开始我是这么写的
w_stepc(p->addFunc);
发现根本没用,感觉很奇怪,但是转念又想,这个操作应该是在usertrapret()
函数里面被完成的(不要问我为什么,想一想就知道了),于是过去查看代码,发现这段代码之后恍然大悟。原来是通过读取trapframe里面的值来设置sepc
//kernel/trap.c
// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);
知道了这一点之后,我们只需要修改p->trapframe->epc就可以了
思考一个问题,为什么usertrapret()会从p->trapframe->epc恢复epc寄存器?这说明了什么?
我们来看一下以下代码
//kernel/trap.c/usertrap()
if(r_scause() == 8){
// system call
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();
syscall();
}
注意,进入trap的时候系统中断会被关闭,防止在执行trap的时候又有中断进入,影响一些寄存器等数据的保存。
当需要强制执行陷阱时,RISC-V硬件对所有陷阱类型(计时器中断除外)执行以下操作:
-
如果陷阱是设备中断,并且状态SIE位被清空,则不执行以下任何操作。
-
清除SIE以禁用中断。
-
将pc复制到sepc。
-
将当前模式(用户或管理)保存在状态的SPP位中。
-
设置scause以反映产生陷阱的原因。
-
将模式设置为管理模式。
-
将stvec复制到pc。
-
在新的pc上开始执行。
-
由于syscall()
这个操作可能会非常耗时,所以我们在执行syscall之前就显性的打开了系统中断,但是如果又有别的程序调用了系统调用怎么办?所以我们就需要将一些关键的寄存器保存下来,于是变有了以上的设置。将数据保存到进程的trapframe页里面。
所以现在我们的usertrap()
里面是这样的
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if (p->alarmlock == 0 && p->nticks > 0) {
p->sinceticks ++;
if (p->sinceticks == p->nticks) {
p->sinceticks = 0;
p->trapframe->epc = p->addFunc; // 这里卡了好久,以知不知道怎么执行,之前写成了w_stepc(p->addFunc),知道看了usertrapret()的代码才发现问题所在
}
}
}
这样的话,应该就可以通过test0了。
test1/2:resume interrupted code
这两个test要求我们在完成handler之后,恢复我们之前代码中断时的状态。这就要求着我们储存一些必要的信息以便于恢复的时候用。
首先我想肯定要保存epc,因为这是我们回到中断代码的必要条件,如果回不到之前的代码,那一切都无从谈起了。除此之外,我们还需要恢复寄存器的状态,因为我们知道,跳转到别的函数的时候有些寄存器是会丢失的,所以我们必须把它存起来,也就是我们说的恢复中断时的状态。
所以我们在proc.h里面加上一个trapframe结构体(后面我会解释为什么定义成struct trapframe,而不是和trapframe一样定义成指针),用于恢复寄存器
...
uint64 alarmepc // 用于备份epc
struct trapframe alarmframe // 用于备份trapframe
...
然后继续在usertrap()
和sys_sigreturn()
这两个函数里面完成备份和恢复的逻辑
//usertrap()
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if (p->alarmlock == 0 && p->nticks > 0) {
p->sinceticks ++;
if (p->sinceticks == p->nticks) {
p->sinceticks = 0;
p->alarmepc = (uint64)r_sepc;
p->alarmframe = *p->trapframe;
p->trapframe->epc = p->addFunc; // 这里卡了好久,以知不知道怎么执行,之前写成了w_stepc(p->addFunc),知道看了usertrapret()的代码才发现问题所在
}
}
}
//sys_sigreturn()
uint64
sys_sigreturn(void) {
struct proc* p = myproc();
w_sepc(p->alarmepc);
*p->trapframe = p->alarmframe;
return 0
但是到这里,我们发现还是不能通过测试,照理来说应该已经没问题了呀?
试想这样一个情况,如果 handler 执行的特别慢,自从上次调用 handler 已经过去了规定的时钟周期,但是 handler 还没执行好,这个时候我们又去改一遍 epc,这个 handler 又从头开始执行了,那着不就出大问题了,因为我们每次都会去改 epc,然后就永远执行不完 handler 了。
测试程序里就包括了这个情况:
void
slow_handler()
{
count++;
printf("alarm!\n");
if (count > 1) {
printf("test2 failed: alarm handler called more than once\n");
exit(1);
}
for (int i = 0; i < 1000*500000; i++) { // 超慢的 handler
asm volatile("nop"); // avoid compiler optimizing away loop
}
sigalarm(0, 0);
sigreturn();
}
所以我们需要在 struct proc
里再加一个属性,就是 alarmlock
。如果这个属性为 1,就表示,handler 程序正在执行,这个时候就算又过了 tick 个时钟周期,我们也不能去改 epc 让 handler 重复执行。
//usertrap()
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if (p->alarmlock == 0 && p->nticks > 0) {
p->sinceticks ++;
if (p->sinceticks == p->nticks) {
p->sinceticks = 0;
p->alarmepc = (uint64)r_sepc;
p->alarmframe = *p->trapframe;
p->alarmlock = 1; // 加锁
p->trapframe->epc = p->addFunc; // 这里卡了好久,以知不知道怎么执行,之前写成了w_stepc(p->addFunc),知道看了usertrapret()的代码才发现问题所在
}
}
}
//sys_sigreturn()
uint64
sys_sigreturn(void) {
struct proc* p = myproc();
w_sepc(p->alarmepc);
*p->trapframe = p->alarmframe;
p->alarmlock = 0; // 释放锁
return 0
但是接下来我将要说一下为什么不把alarmframe定义成结构体指针的原因。
首先来看为什么trapframe定义成了指针,因为在系统初始化分配空间的时候,allocproc()
这个函数就给trapframe分配了一段空间,所以我们只需要用指针引用即可,但是如果我们也定义成了指针,还没有分配空间的话,就会出错。如果我们不用指针,就是直接在结构体proc里面给alarmframe分配空间,这样这段空间就会随着结构体的释放而释放,不需要我们手动freeproc()
的。
allocproc()
的这段代码代码给trapframe分配了空间,并将proc结构体里的指针指向这段空间。
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}