开始日期:22.4.7
操作系统:Ubuntu20.0.4
Link:Lab Traps
个人博客:Memory Dot
目录- Lab Traps
- 写在前面
- vscode+wsl2+unbuntu20.04
- 只使用
gdb-multiarch
进入qemu-gdb - 参考链接
- 实验内容
- RISC-V assembly
- Backtrace
- Alarm
- test0: invoke handlers
- test1/test2(): resume interrupted code
- 总结
- 写在前面
-
由于使用虚拟机,可能是分配的内存不够,遇到了内存泄露,故更换为vscode+wsl2+unbuntu20.04
-
参考链接:
Windows10开发环境搭建(2) | Terminal和VS Code
WSL2 安装教程
-
由于笔者mircosoft store无法使用,要自己下载windows terminal 和 ubuntu20.04
-
成功了
gdb-multiarch
进入qemu-gdb
-
此方案是在学习群一位群友提出的,事实上,按之前的方式,输入
gdb-multiarch kernel/kernel
之后就有「提示」了For help, type "help". Type "apropos word" to search for commands related to "word". warning: File "/home/duile/xv6-labs-2021/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load". To enable execution of this file add add-auto-load-safe-path /home/duile/xv6-labs-2021/.gdbinit line to your configuration file "/home/duile/.gdbinit". To completely disable this security protection add set auto-load safe-path / line to your configuration file "/home/duile/.gdbinit".
-
使用第一个
.gitinit
文件使得/
目录安全,该文件位于/home/user/.gitinit
,
该文件要「自己手动创建」,文件内容只有一句,如下set auto-load safe-path /
-
/
路径安全之后,就可以安全地执行第二个位于/home/user/xv6-labs-2021/.gdbinit
的文件,文件内容在git clone
的时候已经有了。set confirm off set architecture riscv:rv64 target remote 127.0.0.1:26000 symbol-file kernel/kernel set disassemble-next-line auto set riscv use-compressed-breakpoints yes
- 可以注意到,语句
target remote 127.0.0.1:26000
,笔者之前就使用过了,这里的操作就是让Ubuntu系统自动执行语句。
- 可以注意到,语句
-
那么即可改为如下命令,即可进入qemu-gdb
-
one windows $ make CUPS=1 qemu-gdb another windows $ gdb-multiarch
效果如下:
- Lab4: Traps
-
以下是笔者的txt文件,仅供参考
1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf? main() : a0 = pc value or ret value a1 = 12, a2 = 13 f() : a0 = ret value g() : a0 = ret value 2. 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.) 2.1 no call f()(and no call g()) make inline optimization that a1 = 12 2.2 the compiler inline func (look at line:32) 26 int f(int x) { 27 e: 1141 addi sp,sp,-16 28 10: e422 sd s0,8(sp) 29 12: 0800 addi s0,sp,16 30 return g(x); 31 } 32 14: 250d addiw a0,a0,3 33 16: 6422 ld s0,8(sp) 34 18: 0141 addi sp,sp,16 35 1a: 8082 ret 3. At what address is the function printf located? line:630 4. What value is in the register 'ra' just after the 'jalr' to 'printf' in 'main'? ra = pc+4 = 0x34 + 0x4 = 0x38 5. 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 change 57616 to a different value? 5.1 I run this process in win10, because ubuntu has warning(win10 also use little endian and I make some changes) #include<stdio.h> int main(){ unsigned int i = 0x00646c72; printf("H%x Wo%s \n", 57616, &i); printf("H%x Wo%x \n", 57616, i); } output: He110 World He110 Wo646c72 5.2 in ASCII: 0x72->r, 0x6c->l, 0x64->d if RISC-V is big endian, we must change i (i = 0x00646c72 => i = 0x00726c64) because we would the value of fisrt addr is 0x72 in big endian No change 57616 because we only use it hexadecimal value 6.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); I also run this process in win10 and I also make some changes #include<stdio.h> int main(){ printf("x=%d y=%d \n"); printf("x=%d y=%d \n", 3); } output: x=11364944 y=11356192 x=3 y=434198848 before run this process, x belong to a register that value is 11364944 y belong to another register that value is 434198848 before run this process, x belong to a register that value be changed to 3 y belong to another register that value also is 434198848
-
笔者重点讲一下第4题,首先代码:
30: 00000097 auipc ra,0x0 34: 610080e7 jalr 1552(ra) # 640 <printf>
-
然后是参考文件:riscv-spec.pdf
P36
AUIPC (add upper immediate to pc) uses the same opcode as RV32I. AUIPC is used to buildpc-relative addresses and uses the U-type format. AUIPC forms a 32-bit offset from the U-immediate,filling in the lowest 12 bits with zeros, sign-extends the result to 64 bits, adds it to the address of the AUIPC instruction, then places the result in register rd.
P37
Note that the set of address offsets that can be formed by pairing LUI with LD, AUIPC with JALR
-
由此得知,auipc的作用是「把高位立即数加到pc上」即执行
ra <- PC += (imm << 12)
,
看第一行代码30: 00000097 => 30: 0000000000000000 00001 0010111
,那么有
pc = 0x30
,imm = 0x0
,rd = 00001 -> ra
,opcode = 001011 -> auipc
(这些操作或者寄存器的「对应代码」,也可以在riscv-spec.pdf查到,我这里就不列出来了)
从而ra <- 0x30 += (0x0 << 12)
得出ra = 0x30
P21
The indirect jump instruction JALR (jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1, then settingthe least-significant bit of the result to zero. The address of the instruction following the jump(pc+4) is written to register rd. Register x0 can be used as the destination if the result is not required.
-
由此得知,jalr的作用是「跳转到偏移地址并将下一条pc指令存储到寄存器当中」
即执行jump to address: base + offest
和ra <- pc + 0x4
看第二行代码,34: 00000097 => 30: 011000010000 00001 000 00001 1100111
,那么有
pc = 0x34
,offest = 1552 = 0x610
,rs1 = rd = 00001 -> ra
,opcode = 001011 -> jalr
在第一行代码,我们已经算出ra = 0x30
从而jump to address: ra + offest = 0x30 + 0x610 = 0x640
,
ra <- pc + 0x4 = 0x34 + 0x4 = 0x38
-
综上,在跳转到printf函数后,
ra = 0x38
-
题意:在一个程序栈的多个帧中实现返回地址的「回溯」,在这个过程中,把这些返回地址打印出来
-
参考hints即可写出,是回溯时,这个循环的结束条件是什么?
-
hint提示到:一个stack的大小是一个页面即
4kb
,从而得知,这个页面上下限也是固定的地址。那么无论栈指针fp
地址被改变成多少,靠fp
计算出的PGROUNDUP(fp)
和PGROUNDDOWN(fp)
都是不变的,笔者将其打印出来是:void backtrace(void) { uint64 fp = r_fp(); printf("%p, %p\n", PGROUNDUP(fp), PGROUNDDOWN(fp)); } output: 0x3fffff9000 0x3fffffa000
从而,得出循环条件:
while (PGROUNDUP(fp) == 0x3fffffa000 && PGROUNDDOWN(fp) == 0x3fffff9000)
,结合PGSIZE
也可以写成while ((PGROUNDUP(fp) - PGROUNDDOWN(fp)) == PGSIZE)
-
取出
fp
的值时,要先转为指针,再解除指针拿到里面的值,「因为fp
本身是一个地址,不能直接用」,参考代码如下:void backtrace(void) { uint64 fp = r_fp(); printf("backtrace:\n"); uint64 ret_addr; while ((PGROUNDUP(fp) - PGROUNDDOWN(fp)) == PGSIZE){ ret_addr = *((uint64*)(fp-8)); printf("%p\n", ret_addr); fp = *((uint64*)(fp-16)); } }
-
-
hints的其它内容别忘了
-
题意:CPU的每经历一个tick就会触发一次timer interrupt,我们需要安装一个
sigalarm(n, fn)
(原题意是调用,笔者认为理解为安装更好),使得xv6在n
个ticks
之后就能在CPU触发的timer interrupt中调用一次fn
,这个fn
是handler
。void test0() { int i; printf("test0 start\n"); count = 0; /* install sigalrm */ sigalarm(2, periodic); /* wait timer interrupt after 2 ticks */ for(i = 0; i < 1000*500000; i++){ if((i % 1000000) == 0) write(2, ".", 1); if(count > 0) break; } /* unstall sigalrm */ sigalarm(0, 0); if(count > 0){ printf("test0 passed\n"); } else { printf("\ntest0 failed: the kernel never called the alarm handler\n"); } }
-
按照hints一步步来
-
实现调用一次
handler
,也就是调用一次periodic()
,因为可以看到test0
只输出了一个alarm!
test0 start ........alarm! test0 passed
-
在实现调用一次
handler
之前,我们先回顾一下一次syscall
是怎么发生的:- 硬件执行一些行为,做准备 [CPU]
- 汇编指令准备[vector]
- 在C代码中处理trap[handler]
- 返回原来的mode(kernel/mode)
-
而在这段过程中,需要思考它的
program count: PC
是怎么变化,我们假设是一次syscall,如下When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?
-
ecall:
sret
是硬件指令,会将SEPC
设置为PC
,即SEPC <- PC <- ecall
,SEPC
此时为ecall
-
userver: no operation about any PC
-
usertrap:
p->trapfram->epc = r_sepc();
,将SEPC
传给epc
,epc
此时为ecall
-
usertrap:
p->trapframe->epc += 4;
,+4
指向下一条指令,epc
此时为ret
.global sigalarm sigalarm: li a7, SYS_sigalarm ecall ret
-
usertrapret:
w_sepc(p->trapframe->epc);
,将epc
传给SEPC
,SEPC
此时为ret
-
userret:
sret
是硬件指令,可以将PC
设置为SEPC
,PC
此时为ret
# return to user mode and user pc. # usertrapret() set up sstatus and sepc. sret
-
在用户态执行
PC
即ret
-
-
回到
test0
,我们需要满足,n
个ticks
之后,在CPU触发的timer interrupt中调用一次handler
,参照上面的PC
变化,以及提示说要修改usertrap
,不难想出:在usertrap
中添加p->trapfram->epc = handler;
,使得最终处于用户态会去执行handler
函数。这条语句当然需要写在if(which_dev == 2)
之下,因为这是timer interrupt -
完成前五个hints
-
proc.h
添加new field,passticks
是为了配合ticks
,满足n
个ticks
之后,才调用一次handler
// these are private to the process, so p->lock need not be held. int ticks; //ticks of alarm void (*handler)(); //handler function int passticks; //ticks from the last handler to the current handler
-
记得在allocproc,freeproc中分配,释放相关内容。
allocproc() ... found: p->passticks = 0; p->ticks = 0; p->handler = 0; ... freeproc() ... p->passticks = 0; p->ticks = 0; p->handler = 0; ...
-
编写
sys_sigalarm
,获得入参ticks
和(void*)handler
,handler
通过argaddr()
传进来时是类型是uint64
,要转型为(void*)
uint64 sys_sigalarm(void) { int ticks; uint64 handler; if(argint(0, &ticks) < 0) return -1; if(argaddr(1, &handler) < 0) return -1; struct proc *p = myproc(); p->ticks = ticks; p->handler = (void*)handler; return 0; }
-
修改
usertrap()
,满足n
个ticks
之后,才调用一次handler
- 一定要注意,我们这是在timer interrupt中
// give up the CPU if this is a timer interrupt. if(which_dev == 2){ p->passticks = p->ticks; while(p->ticks){ p->ticks--; if(p->ticks == 0) p->trapframe->epc = (uint64)p->handler; } p->ticks = p->passticks; yield(); }
-
先考虑
test1
,CPU触发timer interrupt之后,调用了一次handler
即periodic()
void periodic() { count = count + 1; printf("alarm!\n"); sigreturn(); }
-
它会接着执行一次syscall即
sigreturn()
,还是一样:- 硬件执行一些行为,做准备 [CPU]
- 汇编指令准备[vector]
- 在C代码中处理trap[handler]
- 返回user mode
-
等到它回归user mode时,它的
trapfram
是属于periodic()
的,但我们希望返回user mode时,它的trapfram
是属于timer interrupt的。
那我们就需要在调用periodic()
之前先保存好timer_trapfram
,在sys_sigreturn()
返回user mode之前用timer_trapfram
替换periodic()
的trapfram
即可 -
同时,为了满足test2,Prevent re-entrant calls to the handler----if a handler hasn't returned yet, the kernel shouldn't call it again. 我们引入一个标志:
handler_execute
-
proc.h
添加new field// these are private to the process, so p->lock need not be held. int ticks; //ticks of alarm void (*handler)(); //handler function int passticks; //ticks from the last handler to the current handler struct trapframe *timer_trapframe; // saves registers to resume in sigret int handler_execute; // handler executing => 1, handler no executing => 0
-
记得在allocproc,freeproc中分配,释放相关内容。
allocproc() ... found: p->passticks = 0; p->ticks = 0; p->handler = 0; p->handler_execute = 0; // Allocate a timer_trapframe page. if((p->timer_trapframe = (struct trapframe *)kalloc()) == 0){ freeproc(p); release(&p->lock); return 0; } ... freeproc() ... p->passticks = 0; p->ticks = 0; p->handler = 0; if(p->timer_trapframe) kfree((void*)p->timer_trapframe); p->timer_trapframe = 0; p->handler_execute = 0; ...
-
修改
usertrap()
,在调用periodic()
之前先保存好timer_trapfram
,同时满足periodic()
的执行没有结束之前,不能调用它- 注意不要使用
p->timer_trapframe = p->trapframe
,它只能获得地址,而不是内容
// give up the CPU if this is a timer interrupt. if(which_dev == 2){ p->passticks = p->ticks; while(p->ticks){ p->ticks--; if(p->ticks == 0 && p->handler_execute == 0){ // we can't do it beacese we only get address of trapframe but not get content // p->timer_trapframe = p->trapframe; memmove(p->timer_trapframe, p->trapframe , sizeof(struct trapframe)); p->handler_execute = 1; p->trapframe->epc = (uint64)p->handler; } } p->ticks = p->passticks; yield(); }
- 注意不要使用
-
修改
usertrap()
,在sys_sigreturn()
返回user mode之前用timer_trapfram
替换periodic()
的trapfram
,同时,此时已经执行periodic()
可以视作执行结束,要将handler_execute
置为0
int sys_sigreturn(void) { struct proc *p = myproc(); memmove(p->trapframe, p->timer_trapframe, sizeof(struct trapframe)); p->handler_execute = 0; return 0; }
-
成果图
- 结束日期:22.4.10
- 本次实验在过程中,没有很好地把已学知识联系起来,尤其是在
PC
变化那一块,我是知道的,但没有完全联系起来,当然我虽然知道,但确实没有很清晰,写完实验才弄清楚了 trapfram
的地址和内容是有区别,传递时要注意,到底是一个页面内容,还是这个页面的地址- proc的新字段,在allocproc,freeproc中都需要处理
- 最近在听【A-SOUL/向晚】顶碗先生