xv6 lab4 traps

xv6 lab4 traps 0.前置1. 5个核心控制寄存器寄存器核心作用stvec陷阱入口地址trap 发生时硬件自动把 PC 设为这个值sepc保存断点 PCsret返回时写回 PCscause记录 trap 原因码系统调用 / 页故障 / 定时器中断等sscratch临时中转寄存器专门给陷阱入口用sstatus状态寄存器•SIE位全局中断总开关•SPP位标记 trap 来自用户态还是内核态trap指程序运行时触发某种事件中断异常系统调用去让内核来解决问题。但不一定全是用户态到内核态trap有俩种链路usertap和kerneltrap。这里主要讲用户态触发ecall指令后提高特权跳转到trampoline.S汇编入口的硬件操作部分。清除SIE位关闭中断防止处理中被新中断打断如果中断已关闭则忽略→把当前 PC 保存到sepc→把当前特权模式存入sstatus的SPP位→把 trap 原因码写入scause→切换到内核监管模式 →把stvec加载到 PC跳转到陷阱入口 →开始执行入口代码。问题trap期间中断一直被关闭吗 在硬件操作部分被硬件关闭用户寄存器还没存页表、栈指针还没换当跳板页工作作为后进入 usertrap() 且确认安全后内核会主动执行 intr_on()重新开启中断。2.源码解析trampoline.Suservec: # # trap.c sets stvec to point here, so # traps from user space start here, # in supervisor mode, but with a # user page table. # # sscratch points to where the processs p-trapframe is # mapped into user space, at TRAPFRAME. # # swap a0 and sscratch # so that a0 is TRAPFRAME csrrw a0, sscratch, a0 //sscratch是临时中转寄存器预存着当前进程陷阱帧基地址交换a0和sscratch的值 # save the user registers in TRAPFRAME 保存用户寄存器到陷阱帧中 sd ra, 40(a0) sd sp, 48(a0) sd gp, 56(a0) sd tp, 64(a0) sd t0, 72(a0) sd t1, 80(a0) sd t2, 88(a0) sd s0, 96(a0) sd s1, 104(a0) sd a1, 120(a0) sd a2, 128(a0) sd a3, 136(a0) sd a4, 144(a0) sd a5, 152(a0) sd a6, 160(a0) sd a7, 168(a0) sd s2, 176(a0) sd s3, 184(a0) sd s4, 192(a0) sd s5, 200(a0) sd s6, 208(a0) sd s7, 216(a0) sd s8, 224(a0) sd s9, 232(a0) sd s10, 240(a0) sd s11, 248(a0) sd t3, 256(a0) sd t4, 264(a0) sd t5, 272(a0) sd t6, 280(a0) # save the user a0 in p-trapframe-a0 单独保存用户a0寄存器的值到陷阱帧中 csrr t0, sscratch sd t0, 112(a0) #从陷阱帧中恢复内核寄存器的值陷阱帧提前由allocproc填充 # restore kernel stack pointer from p-trapframe-kernel_sp ld sp, 8(a0) # make tp hold the current hartid, from p-trapframe-kernel_hartid ld tp, 32(a0) # load the address of usertrap(), p-trapframe-kernel_trap ld t0, 16(a0) # restore kernel page table from p-trapframe-kernel_satp切换到内核页表 ld t1, 0(a0) csrw satp, t1 sfence.vma zero, zero #刷新TLB # a0 is no longer valid, since the kernel page # table does not specially map p-tf. # jump to usertrap(), which does not return 跳到usertrap()处理系统调用异常中断不会返回 jr t0 #链路用户触发陷阱 → 硬件跳 uservec → csrrw 交换拿到 TRAPFRAME → 保存全部用户寄存器 → 加载内核栈 /hartid/ 内核页表 /usertrap 地址 → 切换内核页表刷新 TLB → 调用 usertrapuserret内核返回用户态反着来。usertrapret 准备 sepc/sstatus调用 userret (TRAPFRAME, 用户页表) → 切用户页表 → 预存用户 a0 到 sscratch → 恢复其余寄存器 → 交换恢复 a0 → sret 退回用户程序。trap.c1.用户程序触发陷阱链路用户程序异常 /ecall → uservec (跳板汇编) → usertrap () → 系统调用 / 中断处理 → usertrapret () → userret (跳板汇编) → sret 返回用户2.内核运行触发陷阱链路内核代码中断、异常内核代码中断 / 异常 → kernelvec.S 内核专用陷阱汇编→ kerneltrap ()处理内核陷阱 → devintr 分发中断处理 → yield (可选时钟中断切换进程) → sret 回到内核1.RISC-V assembly0000000000000000 g: #include kernel/param.h #include kernel/types.h #include kernel/stat.h #include user/user.h //前置代码编译生成机器码0指的是相对于函数开头的偏移量pc指针指向这条指令1141则是机器码16进制2子节在后面链接阶段会进行地址重定位。栈是向下增长。 int g(int x) { 0: 1141 addi sp,sp,-16 //压栈把栈顶指针向下移动16字节开辟栈空间addi格式目的寄存器源寄存器立即数即spsp-16 2: e422 sd s0,8(sp) //保护把旧栈底指针放入上栈空间上半部分 4: 0800 addi s0,sp,16 //把s0指向这次函数调用栈底 return x3; } 6: 250d addiw a0,a0,3 //把a0寄存器的值加3a0寄存器是第一个参数x也是函数返回值寄存器 a0a03addiw指只对寄存器里的低 32 位进行加法计算完成后再把结果进行符号扩展到 64 位。 8: 6422 ld s0,8(sp) //恢复旧栈底指针 a: 0141 addi sp,sp,16 //出栈恢复16字节栈空间 c: 8082 ret //返回ret指令会跳转到ra寄存器的值也就是调用函数的下一条指令地址 000000000000000e f: 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 //这里编译器进行了优化没有jal g 16: 6422 ld s0,8(sp) 18: 0141 addi sp,sp,16 1a: 8082 ret 000000000000001c main: void main(void) { 1c: 1141 addi sp,sp,-16 1e: e406 sd ra,8(sp) //把main的返回地址保存调用printf时ra会改变 20: e022 sd s0,0(sp)//保存旧栈底指针在栈顶处 22: 0800 addi s0,sp,16 printf(%d %d\n, f(8)1, 13); 24: 4635 li a2,13 //li是 Load Immediate 缩写表示讲13加载到a2寄存器中a2是printf的第三个参数,13是函数内联编译器计算出的 26: 45b1 li a1,12 28: 00000517 auipc a0,0x0 //auipc指令是Add Upper Immediate to PC的缩写取pc寄存器高位 2c: 7a050513 addi a0,a0,1952 # 7c8 malloc0xe8 //把pc寄存器高位加上偏移量得到字符串%d %d\n的地址放入a0寄存器a0是printf的第一个参数 30: 00000097 auipc ra,0x0 //取当前pc寄存器高位放入ra寄存器 34: 5f8080e7 jalr 1528(ra) # 628 printf //跳转到printf函数执行jalr格式返回地址寄存器, 偏移量(基址寄存器)target0x300x5f815280x628ra寄存器保存返回地址0x38 exit(0); 38: 4501 li a0,0 //把0放入a0寄存器a0是exit的参数 3a: 00000097 auipc ra,0x0 //取当前pc寄存器高位放入ra寄存器 3e: 274080e7 jalr 628(ra) # 2ae exit //跳转到exit函数执行jalr格式返回地址寄存器, 偏移量(基址寄存器)target0x3e0x2746280x2aera寄存器保存返回地址0x441.这个是链接结束后可执行文件的反汇编编译器知道函数及代码数据的位置所以它知道printf及exis函数位置。2.jalr执行完成后ra寄存器的值是 jalr 指令的下一条指令的地址—— 也就是 printf 执行完返回后程序要继续执行的那条指令的地址。3.小端输出大端如何得到相同输出unsigned int i 0x00646c72; printf(H%x Wo%s, 57616, i);%x指将57616转成了16进制0xe110所以第一个字段是He110%s指将i 的内存起始地址当作字符串首地址逐个字节读取直到遇到\0才结束。因为RISV-V默认小端序输出低字节存在低内存地址高字节存在高内存地址。所以i内存从低到高排序0x720x6c0x640x00\0查ASCII表得rld\0最终输出 He110 world大端是高字节存在低内存地址。所以i要反过来i0x726c64004.printf(x%d y%d, 3);输出 y 后面是什么为什么不确定取决于a2寄存器的值。2.Backtrace明确几个概念栈帧函数的栈空间参考上面那个反汇编由栈顶指针sp所指的位置到s0存储的fp栈底位置的空间。看下栈帧结构每个栈空间里都会存储上个栈帧的栈底及ra函数执行完下一条指令。高地址 (前一个函数的栈帧底)-------------------| ... || (Caller 的栈帧) |s0 -- ------------------- -- 此时的 s0 指向这里 (当前栈帧的底部)| Saved RA | [s0 - 8] (当前函数的返回地址)| Saved s0 (fp) | [s0 - 16] (前一个函数的 s0 地址)| 局部变量 ... || Saved Regs ... |sp -- ------------------- -- 当前栈顶 (向下生长)低地址 (当前函数的栈帧顶)要求实现一个backtrace函数顺着函数调用栈逐层打印每一层函数的返回地址内核发生panic崩溃时自动调用帮助定位崩溃的调用路径。思路hint给出函数可以得当前函数的栈底指针根据栈底指针位置我们可以得到ra及上一层栈底递归到处内核页。1.根据hint要求在risvc.h添加函数用来得到当前函数栈底。//GCC编译器将当前正在执行的函数的帧指针保存在s0寄存器 //将s0寄存器的值移到当前函数变量x中 static inline uint64 r_fp() { uint64 x; asm volatile(mv %0, s0 : r (x) ); return x; }2.在printf.c添加backtrace函数void backtrace(void) { printf(backtrace:\n); //读取当前函数栈底指针fp(还是uint64数值) uint64 fp r_fp(); //读取内核栈起始地址 uint64 stack_page PGROUNDDOWN(fp); while (1) { uint64 ra *(uint64*)(fp - 8);//取返回地址ra先转换成指针类型再取地址内容 printf(%p\n, ra); uint64 old_fp *(uint64*)(fp - 16);//取上一层栈底指针 if (old_fp 0 || old_fp stack_page || old_fp stack_page PGSIZE) { break; } if(PGROUNDDOWN(old_fp) ! stack_page) //最开始的栈底指针是零指针 break; fp old_fp; } }3.在defs.h声明函数在sys_sleep return前调用backtrace。3.alarm前置时钟中断是操作系统硬件中断一直存在每隔10ms会中断一次用来记录时间以及进程调度的时间片应用以及一些休眠程序。PILC中断控制器当发生中断时pilc将中断信号分发给cpu中断引脚。中断链路用户态方式中断和系统调用链路相似区别在由硬件发送中断信号经中断控制器发送给cpu然后陷入trap也通过usertrap进入devintr()中断统一入口分发给中断处理程序。中断上下半部为了解决响应速度当出现中断内核必须极速响应处理容量假设网卡发来 100 个包内核必须进行复杂的校验、解包、路由分发这需要消耗大量 CPU 时间。所以分成上半部上半部要严格关闭中断快速读取硬件寄存器把硬件缓冲区里的数据拷贝到内存中向硬件设备发送确认收到ACK信号往内核的调度队列里登记一个“下半部任务”立刻退出中断重新开启中断。下半部完全开启中断执行耗时业务。硬件中断是由外部硬件触发可以对应中断上半部中断状态关闭经由pilc。软中断是软件代码指令在 CPU 内部主动触发的。ecall指令可以看作广义的软中断。实验要求:编写一个alarm定时器来记录当前进程接收到的时钟中断次数设置一个报警阈值当中断次数达到阈值的时候执行用户程序进行报警。思路时钟中断由俩条链路一个是usertrap一个是kerneltrap因为定时器在用户态触发也得返回用户态所以在usertrap里的时钟中断下执行我们的系统调用。看下alarmtest部分volatile static int count;//计数器设置为不被编译器优化的静态变量 void periodic() //回调函数 { count count 1; //ticks满足要求时计数器1 printf(alarm!\n); sigreturn();//恢复中断上下文回到原程序继续执行 } void __attribute__ ((noinline)) foo(int i, int *j) { //foo用来最后判断上下文是否一致 if((i % 2500000) 0) { write(2, ., 1); } *j 1; } void test1() { int i; int j; printf(test1 start\n); count 0; j 0; sigalarm(2, periodic); //设置定时器每个2个ticks内核执行一次回调函数 for(i 0; i 500000000; i){ //大循环在这里面时钟中断 if(count 10) break; foo(i, j); } if(count 10){ printf(\ntest1 failed: too few calls to the handler\n); } else if(i ! j){ printf(\ntest1 failed: foo() executed fewer times than it was called\n); } else { printf(test1 passed\n); } }sigalarm的作用是将俩个参数写入内核PCB结构体当ticks达到要求时会备份当前的trapfram将epc改成回调函数地址返回用户态执行回调函数count1在系统调用返回之前把备份trapframe写回。注意硬件时钟中断每次tick都是大循环用户态执行走的都是usertrap里的定时器中断链路。1.首先扩展PCB进程块让进程有自己的定时器状态以及备份的上下文。proc.h添加int alarm_interval; // 定时间隔单位时钟滴答0 表示未开启 int alarm_passed; // 距离上次回调已经过的滴答数 uint64 alarm_handler; // 用户态回调函数的地址 struct trapframe *alarm_tf; // 备份中断前的完整用户上下文 int in_alarm; // 防重入标记1正在执行回调0未在回调中2.初始化进程处初始化进程结构体字段。proc.c/allocproc//添加初始化进程结构体字段 if((p-alarm_tf (struct trapframe *)kalloc()) 0){ release(p-lock); return 0; } p-alarm_interval 0; p-alarm_passed 0; p-alarm_handler 0; p-in_alarm 0;3.释放进程处proc.c/freeproc//释放进程结构体里定义的alarm相关字段 if(p-alarm_tf) kfree((void*)p-alarm_tf); p-alarm_interval 0; p-alarm_passed 0; p-alarm_handler 0; p-in_alarm 0;4.实现sys_sigalarm系统调用作用就是传参进去把结构体里的定时间隔及回调函数地址写好。uint64 sys_sigalarm(void) { int ticks; uint64 handler; struct proc *p myproc(); // 从 trapframe 里取出两个参数 // 第一个参数是 ticksint 类型 if(argint(0, ticks) 0) return -1; // 第二个参数是 handler 函数指针地址类型 if(argaddr(1, handler) 0) return -1; // 保存到进程结构体 p-alarm_interval ticks; p-alarm_handler handler; p-alarm_passed 0; // 重置计数器 p-in_alarm 0; return 0; }5.在trap.c里的usertrap下的时钟中断添加作用是判断是否到达间隔备份完整的用户上下文修改sepc地址每次时钟中断执行完都会通过usertrapret()返回用户态执行回调函数然后等待下一次时钟中断。if(which_dev 2) { //添加 if(p-alarm_interval 0 p-in_alarm 0){ p-alarm_passed; // 达到间隔触发回调 if(p-alarm_passed p-alarm_interval){ // 1. 备份当前完整的用户上下文 *p-alarm_tf *p-trapframe; // 2. 修改返回地址让 trap 返回时跳转到用户回调函数 p-trapframe-epc p-alarm_handler; // 3. 重置计数器 p-alarm_passed 0; // 4. 标记正在回调中防止重入 p-in_alarm 1; } }//添加结束 yield();//进程主动让出CPU把进程状态设置成就绪态调用系统调用器 }6..sys_sigreturn的作用回调执行完调用它内核把备份的上下文恢复回去让程序回到中断前的状态。uint64 sys_sigreturn(void) { struct proc *p myproc(); // 恢复备份的完整用户上下文 *p-trapframe *p-alarm_tf; // 清除回调中标记允许下一次触发 p-in_alarm 0; return 0; }7.俩个系统调用在user.h usys.pl syscall.h syscall.c声明makefile添加alarmtest。链路test1进程创建时内核会在其 PCB 中预先分配好陷阱帧、alarm 相关成员及其他进程字段用户执行sigalarm(2, periodic)触发系统调用陷入内核内核将传入的滴答阈值与回调函数地址存入当前进程 PCB 后返回用户态程序进入大循环持续运行运行期间若发生用户态时钟中断会走usertrap处理链路进入定时器分支每次中断将alarm_passed自增未达到设定阈值时直接返回用户态继续执行循环当计数等于阈值时内核先备份当前完整用户上下文至alarm_tf修改trapframe中待返回 PC 为回调函数地址、重置计数器并标记进程处于回调状态随后返回用户态执行periodic回调回调内自增计数变量后调用sigreturn再次陷入内核内核将备份的完整上下文写回trapframe、清除回调标记程序恢复至中断打断处继续循环循环持续重复上述定时器触发、回调、恢复上下文的流程直至回调触发满 10 次跳出循环最终通过对比循环变量i与计数变量j校验上下文保存与恢复是否完整其中foo函数通过频繁调用使用各类寄存器用来检测中断恢复后寄存器、执行位置是否未被破坏。