注明: 都是我自己做的笔记,读者可能看不懂,所以看不懂就找其他博主的看好了
读完整个lecture的总结:
关于寄存器
a0/a1用来存储返回值或者函数参数
SEPC 寄存器用来存储指令地址(比如汇编指令),或者要执行的函数地址
STVAL寄存器的内容是出错时,虚拟地址
SCAUSE寄存器存储出错的原因:
trap:
ecall 其实是汇编指令,完成用户态到内核态的切换,trap
与其相反的指令是sret,完成内核态到用户态的切换
trap以后,从用户态user mode切换到supervisior mode,特权模式可以使用PTE_U为0的PTE,而用户是不能使用的,相应的页表也从user pagetable切换到kernel pagetable, 不过kernel pagetable和user table有两个页是相同的,那就是trapframe,
看一下
切换到kernerl以后,开始执行trappoline.S里面的汇编代码,首先是uservec(trappoline.S) -> usertrap(trap.c) -> syscall -> sys_write -> usertrapret(trap.c) -> userret(trappoline.S) -> 跳转到ecall下面的指令,那么是如何跳到ecall的呢?
其实在user.pl里面会有如下代码:
# Generate usys.S, the stubs for syscalls.
print "# generated by usys.pl - do not edit\n";
print "#include \"kernel/syscall.h\"\n";
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
entry("fork");
根据pl文件,会生成汇编文件usys.S,用户态程序会执行usys.S的代码,然后调用ecall,切换到内核态,去执行内核代码
int g(int x) {
// sp是stack pointer,栈指针
0: 1141 addi sp,sp,-16 // addi $1,$2,100 意思是$1 = $2 + 100
// sp减16,是为新的Stack Frame创建了16字节的空间
2: e422 sd s0,8(sp) // sd 把两个字节的数据从寄存器存储到存储器中, (sp)应该对应内存地址,8对应偏移
// s0 是用来存储当前的fp,fp(frame pointer)是栈顶,指向return address,sp是栈底
4: 0800 addi s0,sp,16
return x+3;
}
6: 250d addiw a0,a0,3 // +3 操作,不过a0的值是谁复制给他的呢
8: 6422 ld s0,8(sp)
a: 0141 addi sp,sp,16 //删除新建的Stack Frame,而后返回
c: 8082 ret
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
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)
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
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0 // AddUpperImmediate toPC,把符号位扩展的 20 位(左移 12 位)立即数加到 pc 上, 结果存入a0
2c: 7b050513 addi a0,a0,1968 # 7d8 <malloc+0xea>
//ra存入的是pc的值
30: 00000097 auipc ra,0x0
34: 600080e7 jalr 1536(ra) # 630 <printf> // 0x630 = 1584
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0 // 其实这边根本没加吧,没加还要调用auipc干啥?
3e: 27e080e7 jalr 638(ra) # 2b8 <exit>
which register holds 13 in main's call to printf? 当然是a2寄存器
At what address is the function printf located? 应该是 1536
Backtrace
1. 在kernel/riscv.h添加如下汇编代码
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
2. 在kernel/printf.c里面添加backtrace(),当然还要在kernel/defs.h里面声明,这个我懒得写了
void backtrace() {
uint64 fp = r_fp(); //fp是一个地址,页表地址,或者说一个指针
uint64 up = PGROUNDUP(fp); //为什么用UP而不同down,见下面saved_pointer
printf("backtrace:\n");
while(fp < up) {
//uint64* val = (uint64 *) fp; //获取fp所指定的地址上的值
uint64* return_address = (uint64*) (fp - 8); // 获取fp - 8多指定的地址上的值,其实就是return_address
uint64* saved_pointer = (uint64*) (fp - 16);
/*** saved_pointer指向调用本子方法的父方法,因为栈的分配是由高到低的,
所以父方法的栈会在高处,也就是地址比较大,
所以其实一直向上找的过程,但是不能越过这个页表的最高处(变量up)*/
printf("%p\n", *return_address);
fp = *saved_pointer;
}
}
3. defs.h添加backtrace
void backtrace(void);
所谓的返回地址(return address),"一般是紧邻函数调用语句的下一条语句的地址, 因为函数调用结束后程序要继续执行, 所以先把这个地址压入堆栈, 等函数调用结束以后, 把这个地址从堆栈里面弹出来, 接着执行"
而至于save_pointer,就是父方法的指针(也即父方法在栈中的地址)
4. make qemu以后如图:
5.复制上述地址,运行 addr2line -e kernel/kernel,并粘贴地址:
Alarm
1. 修改Makefile
2. 在user/user.h里面添加
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
这个handle其实就是函数的意思,不过我现在不太清楚,怎么调用
3. 在user/user.pl里面添加
entry("sigalarm");
entry("sigreturn");
3.在syscall.h里面添加
#define SYS_sigalarm 22
#define SYS_sigreturn 23
4. 修改kernel/syscall.c
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
5.在kernel/sysproc.c里面添加
uint64
sys_sigalarm(void) {
int n;
uint64 handler;
if(argint(0, &n) < 0)
return -1;
if (argaddr(1, &handler) < 0)
return -1;
myproc()->interval = n;
myproc()->handler = (void(*)()) handler;
return 0;
}
uint64
sys_sigreturn(void) {
return 0;
}
6.给proc.h添加结构体
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
void (*handler)(); //函数指针
};
6.修改kernel/trap.c的usertrap()函数
话说usertrap和kerneltrap有啥区别?
a0到a7寄存器是用来作为函数的参数。如果一个函数有超过8个参数,我们就需要用内存了 看来函数参数过多,还会影响性能?
给alarmtest.c添加pediodic函数如下:
volatile static int count;//alarmtest自带的变量,文件内方法共享变量
void
periodic()
{
count = count + 1;
printf("alarm!\n");
sigreturn();
}
sigalarm的调用例子:
void
test0()
{
int i;
printf("test0 start\n");
count = 0;
sigalarm(2, periodic);
for(i = 0; i < 1000*500000; i++){
if((i % 1000000) == 0)
write(2, ".", 1);
if(count > 0)
break;
}
sigalarm(0, 0);
if(count > 0){
printf("test0 passed\n");
} else {
printf("\ntest0 failed: the kernel never called the alarm handler\n");
}
}
在trap.c文件中添加:
} else if((which_dev = devintr()) != 0){
// ok
if(which_dev == 2) {
p->spend = p->spend + 1;
if(p->spend == p->interval) {
p->trapframe->epc = (uint64)p->handler;
}
}
} else {
有一个问题,就是process里面存储了函数指针,难道进入了process,就会执行函数指针对应的函数? 这,好奇怪,这个进程的寄存器值被保存到process的trapflame里了吗?
答: 这里epc就是pc寄存器的值,当usertrap return时,会将进程的trapframe->epc的值,放到sepc寄存器中,下一次要执行的指令地址就在trapframe->epc开始
输出的结果一般是: ....alarm! 不过前面的.号个数是不固定的,我觉得原因是因为指令执行顺序每次不见得一样,所以count + 1出现的顺序可能在前,可能在后,在前.号个数就少,在后就大
为什么sigalarm(0, 0)就啥也不干了? 因为这个时候,p->internal ==0, p->spend 永远不会等于p->internal,上面p->trapframe那句永远不会执行
关于trap,当中断、异常或者用户空间发起系统调用,会进入usertrap,此时,我们会把用户程序的pc指针保存至当前进程的trapframe中:
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;
+4是为了跳到下一条指令,既然如此,当当前进程执行完时,应该把trapframe保存的pc值放到sepc寄存器
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.
# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero
# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0
关于上述user return的解读, a0存储的是TRAPFRAME, a1存储的是用户页表地址
satp 是页表地址,可能是用户的,也可能是内核页表地址
ld t0, 112(a0) 就是a0寄存器指向的地址,偏移112以后存储的值,赋给t0寄存器,至于为什么是112,我也不知道。。。。哦,这个好像对应进程proc的proc->trapframe->a0字段, trapframe对应的结构如下,注释112对应的是a0:
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // saved user program counter
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
/* 48 */ uint64 sp;
/* 56 */ uint64 gp;
/* 64 */ uint64 tp;
/* 72 */ uint64 t0;
/* 80 */ uint64 t1;
/* 88 */ uint64 t2;
/* 96 */ uint64 s0;
/* 104 */ uint64 s1;
/* 112 */ uint64 a0;
/* 120 */ uint64 a1;
/* 128 */ uint64 a2;
/* 136 */ uint64 a3;
/* 144 */ uint64 a4;
/* 152 */ uint64 a5;
/* 160 */ uint64 a6;
/* 168 */ uint64 a7;
/* 176 */ uint64 s2;
/* 184 */ uint64 s3;
/* 192 */ uint64 s4;
/* 200 */ uint64 s5;
/* 208 */ uint64 s6;
/* 216 */ uint64 s7;
/* 224 */ uint64 s8;
/* 232 */ uint64 s9;
/* 240 */ uint64 s10;
/* 248 */ uint64 s11;
/* 256 */ uint64 t3;
/* 264 */ uint64 t4;
/* 272 */ uint64 t5;
/* 280 */ uint64 t6;
};
csrw 指令就是用来交换两个寄存器的值
ld t4, 264(a0)
ld t5, 272(a0)
ld t6, 280(a0)
# restore user a0, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0
这里最后一行,就是交换a0和sscratch的值,交换以后,sscratch变成TRAPFRAME的地址,而a0则是,用户的返回值?
7. 修改proc.h
char name[16]; // Process name (debugging)
void (*handler)();
int spend;
int interval;
struct trapframe *trapframeSave;
int waitReturn;
};
8.在kernel的proc.c添加分配trapframe内存的代码
allocproc()
p->spend = 0;
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
freeproc()
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
if(p->trapframeSave)
kfree((void*)p->trapframeSave);
9.修改kernel/trap.c
void switchTrapframe(struct trapframe* trapframe, struct trapframe* trapframeSave) {
trapframe->kernel_satp = trapframeSave->kernel_satp;
trapframe->kernel_sp = trapframeSave->kernel_sp;
trapframe->epc = trapframeSave->epc;
trapframe->kernel_hartid = trapframeSave->kernel_hartid;
trapframe->ra = trapframeSave->ra;
trapframe->sp = trapframeSave->sp;
trapframe->gp = trapframeSave->gp;
trapframe->tp = trapframeSave->tp;
trapframe->t0 = trapframeSave->t0;
trapframe->t1 = trapframeSave->t1;
trapframe->t2 = trapframeSave->t2;
trapframe->s0 = trapframeSave->s0;
trapframe->s1 = trapframeSave->s1;
trapframe->a0 = trapframeSave->a0;
trapframe->a1 = trapframeSave->a1;
trapframe->a2 = trapframeSave->a2;
trapframe->a3 = trapframeSave->a3;
trapframe->a4 = trapframeSave->a4;
trapframe->a5 = trapframeSave->a5;
trapframe->a6 = trapframeSave->a6;
trapframe->a7 = trapframeSave->a7;
trapframe->s2 = trapframeSave->s2;
trapframe->s3 = trapframeSave->s3;
trapframe->s4 = trapframeSave->s4;
trapframe->s5 = trapframeSave->s5;
trapframe->s6 = trapframeSave->s6;
trapframe->s7 = trapframeSave->s7;
trapframe->s8 = trapframeSave->s8;
trapframe->s9 = trapframeSave->s9;
trapframe->s10 = trapframeSave->s10;
trapframe->s11 = trapframeSave->s11;
trapframe->t3 = trapframeSave->t3;
trapframe->t4 = trapframeSave->t4;
trapframe->t5 = trapframeSave->t5;
trapframe->t6 = trapframeSave->t6;
}
以及
if(which_dev == 2) {
p->spend = p->spend + 1;
if(p->spend == p->interval) {
switchTrapframe(p->trapframeSave, p->trapframe);
p->spend = 0;
10.在defs.h里面添加:
struct superblock;
struct trapframe;
void switchTrapframe(struct trapframe*, struct trapframe*);
11. 在kernel/sysproc.c修改sigreturn:
proc* proc = myproc();
switchTrapframe(p->trapframe, p->trapframeSave);
查找指定内容所在文件
grep -r uservec ./
上述命令是用来,递归查询(recursive),当前目录(./), 内容中出现uservec的所有文件名
satp的用处,就是告诉我们,页表的地址在哪,或者说我们该用哪张页表
sscratch保存的,我觉得总是TRAMFRAME程序的地址
从用户空间进入内核空间时,是需要页表切换的
12. proc添加字段
uint64 lastEpc;
13. trap.c在usertrap里面添加
p->lastEpc = p->trapframe->epc;
14. kernel/sysproc.c里面添加代码
p->trapframe->epc = p->lastEpc;
15 修改sigreturn以后老是报错
把代码都注释掉,只留下 return 0
uint64
sys_sigreturn(void) {
// struct proc* p= myproc();
// switchTrapframe(p->trapframe, p->trapframeSave);
// p->trapframe->epc = p->lastEpc;
return 0;
}
还是不行
干脆把trap.c 里面switch也删掉:
if(p->spend == p->interval) {
//switchTrapframe(p->trapframeSave, p->trapframe);
p->spend = 0;
p->lastEpc = p->trapframe->epc;
p->trapframe->epc = (uint64)p->handler;
修改sysproc.c如下
extern char trapframe_alarm[512];
uint64
sys_sigreturn(void) {
struct proc* p= myproc();
memmove(p->trapframe, trapframe_alarm[512]);
// p->trapframe->epc = p->lastEp;
return 0;
}
修改trap.c 如下:
if(which_dev == 2) {
if (p->interval != 0) {
p->spend = p->spend + 1;
if(p->spend == p->interval) {
//switchTrapframe(p->trapframeSave, p->trapframe);
p->spend = 0;
//p->lastEpc = p->trapframe->epc;
memmove(trapframe_alarm,p->trapframe,512);
p->trapframe->epc = (uint64)p->handler;
}
}
}
test2截图:
test2()
{
int i;
int pid;
int status;
printf("test2 start\n");
if ((pid = fork()) < 0) {
printf("test2: fork failed\n");
}
if (pid == 0) {
count = 0;
sigalarm(2, slow_handler);
printf("pid: %d\n", getpid());
for(i = 0; i < 1000*500000; i++){
if((i % 1000000) == 0)
write(2, ".", 1);
if(count > 0)
break;
}
if (count == 0) {
printf("\ntest2 failed: alarm not called\n");
exit(1);
}
exit(0);
}
wait(&status);
if (status == 0) {
printf("test2 passed\n");
}
}
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++) {
asm volatile("nop"); // avoid compiler optimizing away loop
}
sigalarm(0, 0);
sigreturn();
}
注释掉 allocproc的内容:
/***
p->spend = 0;
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
***/
以及freeproc
// if(p->trapframeSave)
// kfree((void*)p->trapframeSave);
p->trapframe = 0;
还是报错,改掉下面p->spend = 0,这一行就能通过了
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
if(which_dev == 2) {
if (p->interval != 0) {
p->spend = p->spend + 1;
if(p->spend == p->interval) {
//switchTrapframe(p->trapframeSave, p->trapframe);
//p->spend = 0;
//p->trapframe->epc += 4;
memmove(trapframe_alarm,p->trapframe,512);
p->trapframe->epc = (uint64)p->handler;
}
而且对which_dev == 2的情况,是不需要 p->trapframe->epc += 4的,(ecall的sys call其实是会自增4的)具体原因我也不知道
trap 流程总结
其实这个Lab中,运行alarmtest,就相当于开启了一个进程,然后时间中断会触发操作系统进入trap.c这段代码,进入 if(which_dev == 2) { 这个条件内,然后trapframe的epc被赋值为proc的函数地址,epc最终会赋值给pc寄存器,并开始执行proc的handler函数,详细流程如下:
sigalarm --> uservec(trampoline.S)保存寄存器到p->frapframe --> usertrap(trap.c) 定时器中断 --> usertrapret(trap.c) --> userret((trampoline.S 利用p->trapframe将寄存器恢复) --> 在用户程序中执行alam handler
--> sigret --> uservec(trapframe.S)保存寄存器到p->frapframe --> usertrap(trap.c) 定时器中断 --> usertrapret(trap.c) --> userret((trapframe.S 利用p->trapframe将寄存器恢复)
再次进入sigalarm时寄存器内容已经被改变,但是中断结束之后返回用户程序的位置应该和调用中断之前相同(包括寄存器内容)
exectest没过,其他的都过了,暂时到此为止,做不下去,太恶心了,,,,
心得总结
写博客记录自己的操作过程以后,可以比较方便的debug,排除,比如那个p->spend = 0导致test2多次走进slowhandler,也是回头看博客,才能发现自己哪里写错了(当然如果看git提交记录可能也找得到bug原因)
调试的时候,脑子要灵活,多去试错,不要死板,不要死磕