user/call.c
, 生成可读的汇编程序文件 user/call.asm
$ make fs.img
g()
, f()
和 main()
函数的代码.0000000000000000 :
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int g(int x) {
0: 1141 addi sp,sp,-16 # 栈顶指针下移16字节
2: e422 sd s0,8(sp) # 存栈底指针s0/fp到sp+8的位置
4: 0800 addi s0,sp,16 # 将sp+16即原sp的值作为新的栈帧
return x+3;
}
6: 250d addiw a0,a0,3 # a0=a0+3, a0即为传入参数x又为返回值
8: 6422 ld s0,8(sp) # 从sp+8恢复原栈帧到s0
a: 0141 addi sp,sp,16 # 回收栈顶指针
c: 8082 ret # 返回
000000000000000e : # 与g()函数系统,相当于将g()内联了
int f(int x) {
e: 1141 addi sp,sp,-16
10: e422 sd s0,8(sp)
12: 0800 addi s0,sp,16
return g(x);
}
14: 250d addiw a0,a0,3
16: 6422 ld s0,8(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret
000000000000001c :
void main(void) {
1c: 1141 addi sp,sp,-16 # 栈顶指针下移16字节
1e: e406 sd ra,8(sp) # 存返回地址到sp+8的位置
20: e022 sd s0,0(sp) # 存栈底指针s0/fp到sp的位置
22: 0800 addi s0,sp,16 # 更新栈帧s0
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13 # 加载13到a2寄存器
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0 # 将pc+0加载到a0
2c: 7b050513 addi a0,a0,1968 # 7d8
30: 00000097 auipc ra,0x0 # 将pc+0<<12加载到ra寄存器
34: 600080e7 jalr 1536(ra) # 630 # 将pc设置为ra+1536,并将pc+4写入ra(即进行函数跳转)
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 27e080e7 jalr 638(ra) # 2b8
13
in main’s call to printf
?printf
的 13
存在寄存器 a2 中f
in the assembly code for main? Where is the call to g
? (Hint: the compiler may inline functions.)f(8)+1
的值 12
计算出来了.printf
located?jalr
跳转的地址为 0x30+1536=0x630
, 即函数 printf
的地址为 0x630
ra
just after the jalr
to printf
in main
?jalr
指令的功能, 在刚跳转后 ra
的值为 pc+4=0x34+4=0x38
.unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
What is the output?i
to in order to yield the same output? Would you need to change 57616
to a different value?HE110 World
i
需要设置为 0x726c6400
, 不需要改变 57616
的值(因为他是按照二进制数字读取的而非单个字符).y=
? (note: the answer is not a specific value.) Why does this happen?printf("x=%d y=%d", 3);
A: 根据函数的传参规则, y=
后跟的值应该为寄存器 a2 的值.y=
后确实是寄存器 a2 的值.printf()
的格式字符串的数量和不定参数的数量不一致, 但函数执行时仍然从原本参数应该加载的寄存器取值. 按照 RISC-V 传参规则, 第二个不定参数应该被存于寄存器 a2, 因此在实际输出时也是将 a2 寄存器的值进行输出.backtrace()
, 遍历读取栈帧(frame pointer)并输出函数返回地址.kernel/riscv.h
中添加内联函数 r_fp()
读取栈帧值// read the current frame pointer from s0 register - lab4-2
static inline uint64 r_fp() {
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
kernel/printf.c
中编写函数 backtrace()
输出所有栈帧r_fp()
函数读取寄存器 s0 中的当前函数栈帧 fp. 根据 RISC-V 的栈结构, fp-8 存放返回地址, fp-16 存放原栈帧. 进而通过原栈帧得到上一级栈结构, 直到获取到最初的栈结构.PGROUNDDOWN()
和 PGROUNDUP()
计算得到一个地址所在的页面的最高和最低地址. 初始从寄存器 s0 读取到的栈帧 fp 是在用户栈空间中的地址, 由此可以得到用户栈的页面最高和最低地址作为循环的终止条件.// print the return address - lab4-2
void backtrace() {
uint64 fp = r_fp(); // 获取当前栈帧
uint64 top = PGROUNDUP(fp); // 获取用户栈最高地址
uint64 bottom = PGROUNDDOWN(fp); // 获取用户栈最低地址
for (;
fp >= bottom && fp < top; // 终止条件
fp = *((uint64 *) (fp - 16)) // 获取下一栈帧
) {
printf("%p\n", *((uint64 *) (fp - 8))); // 输出当前栈中返回地址
}
}
上述代码需要注意的是强制类型转换的对象要么为 (uint64 *)(fp-8)
和 (uint64 *)(fp-16)
, 因为 8 和 16 的单位是字节; 或者为 (uint64 *) fp - 1
和 (uint64 *) fp - 2
, 因为此时 1
和 2
是以 (uint64*)
指针大小(8 字节)为单位的.
3. 添加 backtrace()
函数原型到 kernel/defs.h
.
4. 在 kernel/sysproc.c
的 sys_sleep()
函数中添加对 backtrace()
的调用.
5. 在 kernel/printf.c
的 panic()
函数中添加对 backtrace()
的调用.
bttest
, 输出 3 个栈帧的返回地址; 退出 xv6 后运行 addr2line -e kernel/kernel
将 bttest
的输出作为输入, 输出对应的调用栈函数, 如下图所示.backtrace()
函数的所有调用栈的返回地址(函数调用完后的下一代码)./grade-traps backtrace
测试输出.sigalarm(interval, handler)
和 sigreturn()
kernel/proc.h
的 struct proc
中添加新字段, 记录计时间隔(interval), 函数指针(handler), 以及过去的时钟数(passed ticks).sigreturn()
返回 0.user/user.h
中添加两个系统调用的函数原型:user/usys.pl
脚本中添加两个系统调用的相应 entry
, 在 kernel/syscall.h
和 kernel/syscall.c
添加相应声明.sys_sigreturn()
只需要返回 0. 该函数置于了 kernel/sysproc.c
文件中.// lab4-3
uint64 sys_sigreturn(void) {
return 0;
}
kernel/proc.h
中的 struct proc
结构体中添加记录时间间隔, 调用函数地址, 以及经过时钟数的字段sys_sigalarm()
函数, 将 interval
和 handler
的值存到当前进程的 struct proc
结构体的相应字段中.interval
的值需要非负, 根据定义 interval
表示每次调用 handler
函数的周期, 0
特指取消调用, 而负数在这里是没有意义的, 因此将其视为非法参数; 另一方面同时重置了过去的时钟数 p->passedticks
, 此处考虑到可能中途会更新 sigalarm()
的调用参数, 这样之前记录的过去时钟数便失效了, 应该重新计数.// lab4-3
uint64 sys_sigalarm(void) {
int interval;
uint64 handler;
struct proc *p;
// 要求时间间隔非负
if (argint(0, &interval) < 0 || argaddr(1, &handler) < 0 || interval < 0) {
return -1;
}
// lab4-3
p = myproc();
p->interval = interval;
p->handler = handler;
p->passedticks = 0; // 重置过去时钟数
return 0;
}
kernel/proc.c
的 allocproc()
函数负责分配并初始化进程, 此处对上述 struct proc
新增的三个字段进行初始化赋值.kernel/trap.c
中的 usertrap()
函数. 对于时钟中断 which_dev
变量的值为 2
, 由此便可以单独对时钟中断进行操作.handler
函数地址可能为 0, 因此主要通过 interval==0
来判断是否终止定时调用函数.passedticks
加 1, 当达到 interval
时便要调用 handler()
函数, 同时将 passticks
置零用于下次调用定时函数.handler()
. 这里需要注意到, 在 usertrap()
中时页表已经切换为内核页表(切换工作在 uservec
函数中完成), 而 handler
很显然是用户空间下的函数虚拟地址, 因此不能直接调用. 这里实际上并没有直接调用, 而是将 p->trapfram->epc
置为 p->handler
, 这样在返回到用户空间时, 程序计数器为 handler
定时函数的地址, 便达到了执行定时函数的目的.void
usertrap(void)
{
int which_dev = 0;
// ...
if(p->killed)
exit(-1);
// lab4-3
if(which_dev == 2){ // timer interrupt
// increase the passed ticks
if(p->interval != 0 && ++p->passedticks == p->interval){
p->passedticks = 0;
p->trapframe->epc = p->handler; // execute handler() when return to user space
}
}
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}
struct proc
保存足够的状态以可以由 sigreturn
返回到中断前的用户代码handler
函数在返回前重入.sigalarm(interval, handler)
和 sigreturn()
两个函数是配合使用的, 在 handler
函数返回前会调用 sigreturn()
.handler
实际上是通过修改 trapframe->epc
进而在返回到用户空间时调用定时函数. 但这也同时产生了一个问题, 即原本的 epc
已被覆盖, 无法回到中断前的用户代码执行的位置, 同时在执行 handler()
函数后, 相关的寄存器的值也会受到影响.sigalarm()
函数中将寄存器值进行保存, 在 sigreturn()
函数中进行恢复. 这样在执行完 sigreturn()
后程序能够回到原来的执行位置.p->trapframe
中, 而前者由于在 usertrap()
覆盖了 p->trapframe->epc
, 才能够执行定时函数, 执行完后又会导致一些寄存器的值被修改. 因此, 考虑在 struct proc
中保存一个 trapframe
的副本, 在覆盖 epc 之前先保存副本, 然后在 sys_sigreturn()
中将副本还原到 p->trapframe
中, 从而在 sigreturn
系统调用结束后恢复用户寄存器状态时能够将执行定时函数前的寄存器状态进行恢复.trapframe
的副本, 很显然是和 struct proc
结构体关联的, 但具体实现可以有多种形式:
char trapframebuf[288]
: 即在 struct proc
中设立一个缓冲区字段来存放 trapframe
副本.struct trapframe trapframecopy
: 本质上与上述缓冲区副本是一样的, 不过省去了计算开辟缓冲区大小的步骤.struct trapframe *trapframecopy
: 前两者直接将副本存到结构体中, 相当于每个 proc
结构体都预先分配了该副本的空间. 而使用指针指向新分配的虚拟页, 则可以在需要时进行分配. 此处便类似 p->trapframe
指针, 需要借助 kalloc()
和 kfree()
进行页面分配和释放. 虽然是每次使用时再分配使用完释放, 但每次分配直接分配了 1 page(4096B), 也有大部分字节的浪费.trapframe
页面的指针 struct trapframe *trapframecopy
: 这里和前者一样同样使用了指针形式. 但前者的缺点在于每次都需要额外分配释放一个页面. 而由于 struct trapframe
结构体只有 288B, 而一个页面是 4096B, 可以看到为 p->trapframe
分配的页面实际上有大量内存空间未被使用. 此处便是将副本 trapframecopy
与 trapframe
共用一个页面, 既无需在 struct proc
结构体中分配内存, 又无需使用 kalloc()
和 kfree()
额外分配释放页面, 因此笔者最终选择了该方式.trapframecopy
与 trapframe
之间的拷贝操作, 容易想到有两种方式:
=
: 直接进行结构体的赋值memmove()
字节拷贝: 即直接将结构体视为字节流, 进行字节拷贝. 经过实际测试, 发现使用该方式比结构体赋值的速度更快.struct proc
结构体, 添加 trapframe
的副本字段:// Per-process state
struct proc {
// ...
char name[16]; // Process name (debugging)
int interval; // alarm interval - lab4-3
uint64 handler; // pointer to the handler function - lab4-3
int passedticks; // ticks have passed since the last call - lab4-3
struct trapframe* trapframecopy; // the copy of trapframe - lab4-3
};
kernel/trap.c
的 usertrap()
中覆盖 p->trapframe->epc
前做 trapframe
的副本.void
usertrap(void)
{
// ...
// lab4-3
if(which_dev == 2){ // timer interrupt
// increase the passed ticks
if(p->interval != 0 && ++p->passedticks == p->interval){
// 使用 trapframe 后的一部分内存, trapframe大小为288B, 因此只要在trapframe地址后288以上地址都可, 此处512只是为了取整数幂
p->trapframecopy = p->trapframe + 512;
memmove(p->trapframecopy,p->trapframe,sizeof(struct trapframe)); // copy trapframe
p->trapframe->epc = p->handler; // execute handler() when return to user space
}
}
// ...
}
sys_sigreturn()
中将副本恢复到原 trapframe
.sigalarm()
便使用了该系统调用, 那么此时没有副本即 trapframecopy
是无效的, 应避免错误拷贝. 在拷贝后将 trapframecopy
置零, 表示当前没有副本.// lab4-3
uint64 sys_sigreturn(void) {
struct proc* p = myproc();
// trapframecopy must have the copy of trapframe
if(p->trapframecopy != p->trapframe + 512) {
return -1;
}
memmove(p->trapframe, p->trapframecopy, sizeof(struct trapframe)); // restore the trapframe
p->passedticks = 0; // prevent re-entrant
p->trapframecopy = 0; // 置零
return 0;
}
trapframecopy
的一致性, 在初始进程 kernel/proc.c
的 allocproc()
中, 初始化 p->trapframecopy
为 0, 表明初始时无副本.handler
需要防重入, 即在其未返回时不能触发下一次. 这里需要的改动就是将 p->passedticks = 0;
从原本的 usertrap()
移至 sys_sigreturn()
中.usertrap()
中重置则后续 passedticks
则会继续重新递增, 自然可能满足调用 handler
的条件; 而移至 sys_sigreturn()
之后, 即在最后函数返回前才会清零, 按照该系统调用的正确使用方法, sigreturn()
的结束就应该标志着 handler()
的结束, 也就是说在 handler()
还未结束时, passedticks
会继续递增, 从而不会再满足调用 handler
的条件, 自然就可以避免重入. 上述代码已经是已经过修改.