MIT6.S081 lab4 Trap

注明: 都是我自己做的笔记,读者可能看不懂,所以看不懂就找其他博主的看好了

读完整个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原因)

调试的时候,脑子要灵活,多去试错,不要死板,不要死磕

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值