前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >6.S081/6.828: xv6源码分析--trap机制

6.S081/6.828: xv6源码分析--trap机制

原创
作者头像
冰寒火
修改2022-11-26 05:01:58
8790
修改2022-11-26 05:01:58
举报
文章被收录于专栏:软件设计软件设计

一、汇编基础

1 汇编

编译阶段
编译阶段

处理器只能识别机器指令,不能识别汇编指令。汇编语言是直接面向处理器的程序设计语言,并且操作的对象不是数据,而是寄存器、内存。

汇编语言是一门非常底层的语言,许多其他语言,比如C++,都会编译成汇编语言。运行任何编译型语言之前都需要先生成汇编语言。

RISC-V精简指令集,优势在于指令少、简单。使用x86 CISC而不是RISC-V的唯一优势就是能得到性能的提升,但是这里的性能是以复杂度和潜在的安全为代价的。

2 常见寄存器

sscratch:保存了trapframe page地址0x3fffffe000,RISCV-V提供了csrrw可以交换任意两个寄存器的值。

scause:trap的原因,8表示系统调用。

stvec:trampoline中断上半部处理代码地址,uservec、userret。

sepc:状态切换时用来暂存pc的,最终会保存到trapframe.epc。

sstatus:设置控制寄存器sstatus,SPP bit为0表示下次sret指令返回user mode,而不是supervisor mode。SPIE bit表示sret后是否要打开中断。

tp:保存CPU核编号,hartid。

s0:就是fp。

satp:页表顶级目录物理地址。

常见寄存器
常见寄存器

3 函数调用

栈帧
栈帧

return address总在栈帧的第一位,第二位是前一栈帧的指针。

有关Stack Frame中有两个重要的寄存器,第一个是SP(Stack Pointer),它指向Stack的底部并代表了当前Stack Frame的位置。第二个是FP(Frame Pointer),它指向当前Stack Frame的顶部。因为Return address和指向前一个Stack Frame的的指针都在当前Stack Frame的固定位置,所以可以通过当前的FP寄存器寻址到这两个数据。

Caller Saved寄存器在函数调用的时候不会保存,比如a调用函数b时,a不会保存,b有可能会重写这个寄存器。

二、中断流程

1 中断介绍

中断源包括系统调用、设备等,甚至还能嵌套中断。中断分两个部分,上半部由汇编实现,主要是关中断、保存pc、保存用户寄存器以及切换内核stack、内核页表等。下半部是C语言实现的,主要是中断的具体逻辑,会先开中断,然后执行内核函数,此时可以再次被中断。

中断流程
中断流程

与usertrap类似,还有一个kerneltrap,专门处理来自内核的中断。

2 ecall

ecall做了三件事:

  1. 将user mode修改为supervisor mode。
  2. 将PC值写入SEPC,将STVEC的值写入到PC。
  3. 跳转到STVEC指向的代码,也就是trampoline page中,这个是从supervisor mode返回user mode时设置的。

至于保存用户寄存器、设置内核需要的寄存器,都在trampoline.S中做。ecall指令做的不是很多,RISC-V希望硬件完成较少的事情,其余的交给软件完成,给操作系统程序员最大的灵活性。

ecall之前
ecall之前

3 trampoline.uservec

之前讲解过用户页表的布局,虚拟地址最高处的几页比较特殊,trampoline是trap代码,系统调用进入内核的必经之路,此时会执行trampoline.S中uservec代码进行保存用户寄存器到trapframe,并将trapframe中的内核参数设置到指定寄存器,trampoline这一页每个进程都有,共享同一个物理页,但是trampoline这一页没有PTE_U,不能被用户态执行,所以ecall要从user mode切换到supervisor mode。

用户页表布局
用户页表布局

进程的trapframe页中保存的是以下结构体的一个对象,暂存用户寄存器、内核寄存器,因为寄存器是cpu的,而这些参数是进程的,如果再次切换就可能被覆盖了。

代码语言:c
复制
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;
};
  1. 寄存器sscratch存放的是trapframe地址,将其与a0交换,其他用户寄存器可以按照偏移量保存到trap frame页中,最后将a0的值也放到trap frame中。
  2. 从trapframe中恢复kernel stack到sp中,kernel pagetable到satp中,并刷新TLB,完成切换的过程。
  3. 跳转到内核的usertrap函数处。
代码语言:c
复制
.globl trampoline
trampoline:
.align 4
.globl uservec
uservec:    
	#
        # 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 process's p->trapframe is
        # mapped into user space, at TRAPFRAME.
        #
        
	# swap a0 and sscratch
        # so that a0 is TRAPFRAME
        # sscratch存放的是trampoline frame虚拟地址
        # 交换a0和sscratch,那么后面的寄存器都会被保存到trampoline frame中。
        csrrw a0, sscratch, a0

        # 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
        csrr t0, sscratch
        sd t0, 112(a0)

        # restore kernel stack pointer from p->trapframe->kernel_sp
        ld sp, 8(a0)

        # make tp hold the current hartid, from p->trapframe->kernel_hartid
        //cpu核编号
        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

        # a0 is no longer valid, since the kernel page
        # table does not specially map p->tf.

        # jump to usertrap(), which does not return
        jr t0

问题:为什么要用trap frame保存用户寄存器,而不是用户栈?

因为栈和编程语言强绑定,编程语言定义的栈有所不一样,甚至有些语言没有栈,内核不能依赖这个。

4 usertrap

内核中处理trap的逻辑。

代码语言:c
复制
void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  //取决于trap来自于用户态还是内核态,两者处理方式不一样,来自于内核态可以少做很多事
  //在内核中,先要将STVEC设置为kernelvec,trampoline是处理用户态的trap。
  w_stvec((uint64)kernelvec);
  //先前将cpu核编号hartid保存在tp寄存器,先从cpu数组里拿到cpu,再从cpu里拿到proc
  struct proc *p = myproc();
  
  // save user program counter.
  //寄存器是cpu的,为了避免切换到另外一个进程,覆盖了sepc寄存器,将sepc值写入到trapframe
  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.
    //返回用户态后需要执行ecall后一条指令,而不是重新执行ecall
    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
  } else {
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    p->killed = 1;
  }

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();
 //返回
  usertrapret();
}

5 syscall

代码语言:c
复制
void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

所有系统调用都会返回一个uint64表示执行状态,也有可能是结果,放在a0中。

6 usertrapret

执行完syscall后就会调用usertrapret函数,它的主要流程是:

  1. 关中断。
  2. 设置stvec为uservec,便于下次从用户态到内核态的切换处理。
  3. 保存kernel_stap、kernel_sp、kernel_trap(usertrap函数地址)、kernel_hartid到trapframe中,因为从用户态切换到内核态时需要给对应寄存器设置内核参数。
  4. 设置控制寄存器sstatus,SPP bit为0表示下次sret指令返回user mode,而不是supervisor mode。SPIE bit表示sret后是否要打开中断
  5. 将sepc设置为trapframe中的epc,用于后面设置pc值。
  6. 获取trapoline.userret地址并执行。
代码语言:c
复制
void
usertrapret(void)
{
  struct proc *p = myproc();

  // we're about to switch the destination of traps from
  // kerneltrap() to usertrap(), so turn off interrupts until
  // we're back in user space, where usertrap() is correct.
  intr_off();

  // send syscalls, interrupts, and exceptions to trampoline.S
  w_stvec(TRAMPOLINE + (uservec - trampoline));

  // set up trapframe values that uservec will need when
  // the process next re-enters the kernel.
  p->trapframe->kernel_satp = r_satp();         // kernel page table
  p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
  p->trapframe->kernel_trap = (uint64)usertrap;
  p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()

  // set up the registers that trampoline.S's sret will use
  // to get to user space.
  
  // set S Previous Privilege mode to User.
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x);

  // set S Exception Program Counter to the saved user pc.
  w_sepc(p->trapframe->epc);

  // tell trampoline.S the user page table to switch to.
  uint64 satp = MAKE_SATP(p->pagetable);

  // jump to trampoline.S at the top of memory, which 
  // switches to the user page table, restores user registers,
  // and switches to user mode with sret.
  uint64 fn = TRAMPOLINE + (userret - trampoline);
//执行trampoline.userret代码段,这两个参数分别放到a0、a1寄存器中
  ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}
代码语言:c
复制
.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.
        # 返回值写入到sscratch,而sscratch中原来保存的是trapframe page地址
        ld t0, 112(a0)
        csrw sscratch, t0

        # restore all but a0 from TRAPFRAME
        ld ra, 40(a0)
        ld sp, 48(a0)
        ld gp, 56(a0)
        ld tp, 64(a0)
        ld t0, 72(a0)
        ld t1, 80(a0)
        ld t2, 88(a0)
        ld s0, 96(a0)
        ld s1, 104(a0)
        ld a1, 120(a0)
        ld a2, 128(a0)
        ld a3, 136(a0)
        ld a4, 144(a0)
        ld a5, 152(a0)
        ld a6, 160(a0)
        ld a7, 168(a0)
        ld s2, 176(a0)
        ld s3, 184(a0)
        ld s4, 192(a0)
        ld s5, 200(a0)
        ld s6, 208(a0)
        ld s7, 216(a0)
        ld s8, 224(a0)
        ld s9, 232(a0)
        ld s10, 240(a0)
        ld s11, 248(a0)
        ld t3, 256(a0)
        ld t4, 264(a0)
        ld t5, 272(a0)
        ld t6, 280(a0)

	# restore user a0, and save TRAPFRAME in sscratch
        csrrw a0, sscratch, a0
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

sret指令是我们在kernel中的最后一条指令,当执行完后会有以下效果:

  1. 修改mode,重回user mode。
  2. SEPC会被赋值到PC中。
  3. 开中断。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、汇编基础
    • 1 汇编
      • 2 常见寄存器
        • 3 函数调用
        • 二、中断流程
          • 1 中断介绍
            • 2 ecall
              • 3 trampoline.uservec
                • 4 usertrap
                  • 5 syscall
                    • 6 usertrapret
                    相关产品与服务
                    图片处理
                    图片处理(Image Processing,IP)是由腾讯云数据万象提供的丰富的图片处理服务,广泛应用于腾讯内部各产品。支持对腾讯云对象存储 COS 或第三方源的图片进行处理,提供基础处理能力(图片裁剪、转格式、缩放、打水印等)、图片瘦身能力(Guetzli 压缩、AVIF 转码压缩)、盲水印版权保护能力,同时支持先进的图像 AI 功能(图像增强、图像标签、图像评分、图像修复、商品抠图等),满足多种业务场景下的图片处理需求。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档