xv6启动流程详解


前言

本文会对 x v 6 xv6 xv6 系统的启动流程做解析,先对 R I S C V RISCV RISCV 栈帧结构做解释,随后侧重于启动前中断委派,陷阱入口,时钟中断等的设置,对于虚拟内存,控制台 U A R T UART UART 等内容则只简单提及,后续再做详细解释。

R I S C V RISCV RISCV栈帧结构

下图为 R I S C V RISCV RISCV 调用约定:
在这里插入图片描述
其中, s 0 / f p s0/fp s0/fp 为帧指针, s p sp sp 为堆栈指针。参数传递方式:从寄存器 a 0 − a 7 a0-a7 a0a7 为参数传递寄存器,超过8个参数,则通过堆栈传递。

打开反编译的文件,找到函数起始位置和结束位置,其格式一般为如下所示:

addi    sp,sp,-48
sd      ra,40(sp)
sd      s0,32(sp)
sd      s1,24(sp)
sd      s2,16(sp)
sd      s3,8(sp)
addi    s0,sp,48
......
ld      ra,40(sp)
ld      s0,32(sp)
ld      s1,24(sp)
ld      s2,16(sp)
ld      s3,8(sp)
addi    sp,sp,48
ret

函数进入之后栈指针和帧指针变化如下图所示,旧 f p fp fp 应当指向 r e t u r n   a d d r e s s return \ address return address 的下面:
在这里插入图片描述
另外, 64 64 64 位下要求堆栈按照 16 16 16 字节对齐,所以每次 s p sp sp 向上减小的长度都是 16 16 16 的倍数,所以可能会有空余空间。旧的 r e t u r n   a d d r e s s return \ address return address 是在函数调用时通过 a u i p c + j a l r auipc+jalr auipc+jalr 指令配合实现 c a l l call call 指令的效果并把返回地址放入 r a ra ra 寄存器之中,关于 a u i p c auipc auipc j a l r jalr jalr 配合实现 c a l l call call 指令的效果具体见文末知识扩展部分做了解释。
函数结束, s p sp sp f p fp fp 寄存器以及需要 c a l l e e callee callee 保存的寄存器 s 0 − s 3 s0-s3 s0s3 分别恢复旧值,根据 r e t u r n   a d d r e s s return \ address return address 返回上一层调用栈。

打开一个本地变量较多的函数,使用 o b j d u m p objdump objdump 保留原始C语言代码,观察其反汇编之后函数入口及调用别的函数处格式为:

  void
 ls(char *path)
 {
 b4:   d9010113                addi    sp,sp,-624
 b8:   26113423                sd      ra,616(sp)
 bc:   26813023                sd      s0,608(sp)
 c0:   24913c23                sd      s1,600(sp)
 c4:   25213823                sd      s2,592(sp)
 c8:   25313423                sd      s3,584(sp)
 cc:   25413023                sd      s4,576(sp)
 d0:   23513c23                sd      s5,568(sp)
 d4:   1c80                    addi    s0,sp,624
 d6:   892a                    mv      s2,a0
 char buf[512], *p;
 int fd;
 struct dirent de;
 struct stat st;
 ......
 if(fstat(fd, &st) < 0){
 e8:   d9840593                addi    a1,s0,-616
 ec:   00000097                auipc   ra,0x0
 f0:   4aa080e7                jalr    1194(ra) # 596 <fstat>
 f4:   08054163                bltz    a0,176 <ls+0xc2>
 ......

函数本地变量较多,使得该函数在栈上使用了大量空间,大小为 624 624 624
另外,关于本地变量的存取可以看到都是通过帧指针 s 0 / f p s0/fp s0/fp 实现,下面 i f if if 语句中函数调用时 f d fd fd 参数已经位于 a 0 a0 a0 寄存器, e 8 e8 e8 位置指令通过帧指针寄存器定位参数 s t st st 的地址并放入 a 1 a1 a1 ,参数传递完成。而后通过 a u i p c + j a l r auipc+jalr auipc+jalr 实现函数调用。
本地变量都位于帧指针上方的小地址位置。也就是当本地变量较多无法优化为有限的寄存器能够完成的计算时就需要栈来辅助。

综上, R I S C V RISCV RISCV 函数调用栈帧结构如下图所示:
在这里插入图片描述
图中最上面为最新的当前函数栈帧,其堆栈的空间分布在数值上也最小。

e n t r y . S entry.S entry.S

文件 k e r n e l . l d kernel.ld kernel.ld 确保 _ e n t r y \_entry _entry 位于地址 0 x 80000000 0x80000000 0x80000000位置。这个地址是 q e m u qemu qemu 启动后执行内核代码的初始位置,就像 x 86 x86 x86 开机后 C S CS CS设置为 0 x F F F F 0xFFFF 0xFFFF I P IP IP 设置为 0 x 0000 0x0000 0x0000 一样,该位置放置的代码也就是用户自定义内核的第一行代码或者说第一条指令。初始启动,系统出于 M a c h i n e − M o d e Machine-Mode MachineMode 之下。

文件 e n t r y . S entry.S entry.S 汇编语言代码如下所示:

.section .text
.global _entry
_entry:
        # set up a stack for C.
        # stack0 is declared in start.c,
        # with a 4096-byte stack per CPU.
        # sp = stack0 + (hartid * 4096)
        la sp, stack0     # sp此时为stack0初始基地址
        li a0, 1024*4     # a0此时为4096
        csrr a1, mhartid  # a1为当前CPU的id号
        addi a1, a1, 1    # a1加1
        mul a0, a0, a1    # id号加1再乘以4096
        add sp, sp, a0    # sp原本为基地址,加偏移之后为堆栈指针初始值
        # jump to start() in start.c
        call start        # 调用start函数
spin:
        j spin

其中, s t a c k 0 stack0 stack0 的定义位于文件kernel/start.c中,该变量地址按照 16 16 16 字节对齐,原因在于 64 64 64 位下栈按照 16 16 16 字节对齐。__attribute__ ((aligned (16))) char stack0[4096 * NCPU]; 该变量的作用就是为CPU的每一个核设置初始启动时的C语言栈帧空间。

由于堆栈指针SP需要指向栈的底部,以便向上增长,所以对于取出来的从0开始的核心号,需要加1处理之后,再乘以4096加到 s t a c k 0 stack0 stack0 的基地址之上作为 S P SP SP 的初始值。随后调用 s t a r t start start 函数。该函数位于文件 k e r n e l / s t a r t . c kernel/start.c kernel/start.c 中。如果调用失败或该函数返回,则一直执行死循环。

c a l l call call 指令调用 s t a r t start start 函数,此时 r a ra ra 寄存器中的内容为指令 j   s p i n \textcolor{blue}{j \ spin} j spin 的地址。

C P U 0 CPU0 CPU0 栈帧的角度来看,此时栈帧还未形成,但是 S P SP SP 的值为 s t a c k 0 + 4096 stack0+4096 stack0+4096

s t a r t . c start.c start.c

该文件负责设置时钟中断,并设置内核陷阱入口,随后通过 m r e t mret mret 指令将内核从 M − M o d e M-Mode MMode 引入 S u p e r v i s o r − M o d e Supervisor-Mode SupervisorMode 并执行内核的 m a i n main main 函数,正式在 S − M o d e S-Mode SMode 下开始初始化页表,文件系统,用户第一个进程等内容。

s t a r t . c start.c start.c 文件内容如下所示:

void main();
void timerinit();

// 每个CPU的一个初始栈
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];

// 为每个CPU时钟中断准备的临时区域
// a scratch area per CPU for machine-mode timer interrupts.
uint64 timer_scratch[NCPU][5];

// M-Mode下时钟中断响应程序
extern void timervec();

// entry.S jumps here in machine mode on stack0.
void
start()
{
  // 设置旧模式为Supervisor Mode,方便mret返回
  unsigned long x = r_mstatus();
  x &= ~MSTATUS_MPP_MASK;
  x |= MSTATUS_MPP_S;
  w_mstatus(x);

  // 为mret指令设置 M Exception Program Counter 寄存器值为 main函数地址
  // requires gcc -mcmodel=medany
  w_mepc((uint64)main);

  // 禁用分页机制
  w_satp(0);

  // 把中断和异常委派到Supervisor Mode
  w_medeleg(0xffff);
  w_mideleg(0xffff);
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

  // 物理内存保护机制Physical Memory Protection,见知识扩展部分介绍
  // configure Physical Memory Protection to give supervisor mode
  // access to all of physical memory.
  w_pmpaddr0(0x3fffffffffffffull);
  w_pmpcfg0(0xf);

  // 时钟中断初始化
  timerinit();

  // CPU的hartid存入tp寄存器,方便S-Mode读取
  int id = r_mhartid();
  w_tp(id);

  // mret返回并切换模式到Supervisor Mode,执行main.c:main函数
  asm volatile("mret");
}

s t a r t start start 函数的核心功能是要初始化好时钟中断的响应工作,主要原因在于时钟中断和软件中断无法委派到 S − M o d e S-Mode SMode,只能在 M − M o d e M-Mode MMode 下处理好。

物理内存保护部分在文末知识扩展中会详细解释。简单理解为此处 x v 6 xv6 xv6 为了实现上简单易懂,把物理内存设置了读写执行的权限。

其次需要委派好中断和异常到 S − M o d e S-Mode SMode 并使 S − M o d e S-Mode SMode 中断和异常使能。构造好一个供 m r e t mret mret 返回的 S u p e r v i s o r − M o d e Supervisor-Mode SupervisorMode 去执行 k e r n e l / m a i n . c : m a i n kernel/main.c:main kernel/main.c:main 函数的 S − M o d e S-Mode SMode 现场。函数最后通过内嵌汇编 m r e t mret mret 返回,切换当前特权级到 S − M o d e S-Mode SMode ,执行 m a i n main main 函数。

m r e t mret mret 指令执行时几步操作如下:
0:把 s e p c sepc sepc 的内容放入 P C PC PC ,其为 m a i n main main 函数的地址。
1:切换特权级到 S − M o d e S-Mode SMode ,这一步主要是把 m s t a t u s mstatus mstatus 寄存器中 M P P MPP MPP 位设置为当前模式。代码中已经设置了 m s t a t u s mstatus mstatus 寄存器 M P P MPP MPP S − M o d e S-Mode SMode
2:把 m s t a t u s mstatus mstatus 寄存器中 M I E MIE MIE 位设置为 M P I E MPIE MPIE M P P MPP MPP 置0, M P I E MPIE MPIE 置1。( M P I E MPIE MPIE 位用于在陷入 M − M o d e M-Mode MMode 时记录进入之前的中断使能与否)

C P U 0 CPU0 CPU0 栈帧的角度来看,在 s t a r t start start 函数中栈帧一层,如下所示:
在这里插入图片描述

t i m e r i n i t timerinit timerinit函数

该函数主要用于时钟中断初始化工作,确保时钟中断在M-Mode发生时能够正确处理。
函数内容如下所示:

void
timerinit()
{
  // 每个CPU有一个单独的时钟中断源,CLINT单独控制
  int id = r_mhartid();

  // 设置CLINT,控制时钟中断发生频率
  int interval = 1000000; // 设置时钟中断发生的间隔,以中断控制器的周期为单位,时间约为0.1s
  *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

  // 在scratch数组中设置时钟中断所需要的信息,把地址放入该CPU的mscratch寄存器中
  // scratch[0..2] : 前三个位置再timervec时钟中断响应程序中用于作为辅助空间存储a1-a3
  // scratch[3] : 记录当前CPU的MTIMECMP地址
  // scratch[4] : 记录所需时钟中断之间的间隔
  uint64 *scratch = &timer_scratch[id][0];
  scratch[3] = CLINT_MTIMECMP(id);        //第4个位置存储当前CPU所对应的CLINT中MTIMECMP寄存器映射地址
  scratch[4] = interval;                  //第5个位置存储时钟中断之间间隔周期数
  w_mscratch((uint64)scratch);            //把scratch数组该CPU所对应的位置地址写入mscratch寄存器

  // 设置M-Mode时钟中断处理函数位timervec
  w_mtvec((uint64)timervec);

  // M-Mode下中断使能
  w_mstatus(r_mstatus() | MSTATUS_MIE);

  // M-Mode下时钟中断使能
  w_mie(r_mie() | MIE_MTIE);
}

M − M o d e M-Mode MMode 下也有类似 S − M o d e S-Mode SMode 下的 s c r a t c h scratch scratch 寄存器,暂存寄存器,但在特定模式下专用。 x v 6 xv6 xv6 在把 m s c r a t c h mscratch mscratch 寄存器用于存储和时钟中断配置相关的内容,在 M − M o d e M-Mode MMode 下时钟中断响应时方便找到位置并更新配置,响应中断。用法和 S − M o d e S-Mode SMode 中一样, s c r a t c h scratch scratch 寄存器和 a 0 a0 a0 交换,腾出空间,以便后续操作。

具体使用 m s c r a t c h mscratch mscratch 寄存器的位置在 M − M o d e M-Mode MMode 下时钟中断响应程序 t i m e r v e c timervec timervec 中。

t i m e r v e c timervec timervec函数

M − M o d e M-Mode MMode 时钟中断响应程序如下所示:

.globl timervec
.align 4
timervec:
        # start中已经设置了M-Mode下寄存器mscratch寄存器指向当前CPU所属scratch内存地址
        # scratch[0,8,16] : 为寄存器临时存放的保留地址
        # scratch[24] : 当前CPU在CLINT映射空间中MTIMECMP 寄存器的地址.
        # scratch[32] : 两次时钟中断之间的间隔数
        # mscratch首先和a0交换,用scratch的前三个空间存储a1-a3寄存器返回时恢复
        
        csrrw a0, mscratch, a0
        sd a1, 0(a0)           # 存储a1到scratch[0]
        sd a2, 8(a0)           # 存储a2到scratch[1]
        sd a3, 16(a0)          # 存储a3到scratch[2]

        # 在mtimecmp中在原有数的基础上再加intervel以便触发下一次时钟中断
        ld a1, 24(a0) # CLINT_MTIMECMP(hart)
        ld a2, 32(a0) # interval
        ld a3, 0(a1)
        add a3, a3, a2
        sd a3, 0(a1)

        # arrange for a supervisor software interrupt
        # after this handler returns.
        li a1, 2
        csrw sip, a1       # 设置S-Mode下sip.SSIP位,触发S-Mode下的软件中断

        ld a3, 16(a0)     # 恢复a3-a1
        ld a2, 8(a0)
        ld a1, 0(a0)
        csrrw a0, mscratch, a0 # 交换回a0的值,中断返回

        mret

由于 s t v e c stvec stvec 寄存器的 M o d e Mode Mode 域的存在,所以设置 s t v e c stvec stvec 基地址都需要为4的整数倍,故而有 . a l i g n   4 .align \ 4 .align 4 的限制。

m a i n . c main.c main.c

m r e t mret mret 返回 m a i n main main 函数执行之后, m a i n main main 函数启动剩余初始化工作,包括 U A R T UART UART ,页表和文件系统等等。具体源码如下,此时调用的各初始化函数不再详细展开:

volatile static int started = 0;

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator,初始化物理页分配器
    kvminit();       // create kernel page table,初始化内核页表,映射内核栈
    kvminithart();   // turn on paging,内核使用页表机制
    procinit();      // process table,给每一个进程分配内核栈
    trapinit();      // trap vectors,时钟中断锁
    trapinithart();  // install kernel trap vector,设置内核中断向量
    plicinit();      // set up interrupt controller,设置外中断VIRTIO0和UART0
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache层
    iinit();         // inode table层
    fileinit();      // file table层
    virtio_disk_init(); // emulated hard disk,虚拟磁盘初始化
    userinit();      // first user process,初始化第一个用户程序,打开终端
    __sync_synchronize();
    started = 1;
  } else {
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();        //开始调度
}

最后需要注意:每一个 C P U CPU CPU 最终都会有一个调度器 s c h e d u l e r scheduler scheduler 进程,他们不在 s c h e d u l e r scheduler scheduler 调度器调度的目标进程之中,他们使用的内核栈就是我们上文提及的 s t a c k 0 stack0 stack0 变量为每一个 C P U CPU CPU 核心提供的栈。这个进程就是每一个 C P U CPU CPU 核心都要执行一遍的从 e n t r y . S entry.S entry.S s t a r t . c : s t a r t start.c:start start.c:start 再到 m a i n main main 函数而后转到调度器函数 s c h e d u l e r scheduler scheduler 函数的初始进程。这过程中用到的内核栈都是 s t a c k 0 stack0 stack0 为每个 C P U CPU CPU 单独提供的空间。

C P U 0 CPU0 CPU0 栈帧的角度来看,在 m a i n main main 函数中栈帧两层,由于 m r e t mret mret 指令不设置 r a ra ra 寄存器, r a ra ra 寄存器为 s t a r t start start 函数在 m r e t mret mret 之前最新的 r a ra ra 值,由于 s t a r t start start 函数调用过 t i m e r i n i t timerinit timerinit 函数,故 r a ra ra 寄存器最新的内容为 t i m e r i n i t timerinit timerinit 函数调用下面语句的地址。栈帧内容如下所示:
在这里插入图片描述
返回地址验证:修改 m a i n main main 函数内容为一个 r e t u r n return return 语句,以单 C P U CPU CPU 启动 q e m u − g d b qemu-gdb qemugdb 并设置 m a i n main main 函数断点,单步执行指令,观察返回位置。启动内核。结果如下所示:

在这里插入图片描述结合 s t a r t start start 函数源码, a u i p c + j a l r auipc+jalr auipc+jalr 配合调用函数 t i m e r i n i t timerinit timerinit,后方指令为代码 int id = r_mhartid(); 的汇编表示,结合后续两条指令我们知道是要给 t p tp tp 寄存器赋当前 C P U CPU CPU i d id id 值。

验证完毕。读者可自行实验验证正确性。

知识扩展 a u i p c + j a l r auipc+jalr auipc+jalr

由于 A d d r e s s   l a y o u t   r a n d o m i z a t i o n ( A S L R ) Address \ layout \ randomization (ASLR) Address layout randomization(ASLR) 地址空间随机的存在,系统每次加载可执行文件时该文件在内存中的位置都随机,这也是为什么链接时需要重定位的机制。同时,在编译文件为可重定位目标程序 ( . o / . o b j ) (.o/.obj) (.o/.obj) 文件时会使用对于文件起始的相对地址来方便重定位程序的操作。就像 R I S C V RISCV RISCV 标准中写的:The unconditional jump instructions all use PC-relative addressing to help support position-independent code. 无条件跳转指令都使用 p c pc pc 相对寻址来帮助支持与位置无关的代码。

a u i p c auipc auipc 指令编码方式(指令长度为4):
在这里插入图片描述指令作用: A U I P C AUIPC AUIPC 指令用于建立 p c pc pc 相对地址。 A U I P C AUIPC AUIPC 把无符号数 U − i m m e d i a t e U -immediate Uimmediate 左移 12 12 12位形成一个 32 32 32位偏移量,用 0 0 0填充最低的 12 12 12位,符号扩展结果为 64 64 64位,将其与 A U I P C AUIPC AUIPC 指令的地址相加之后将结果放入寄存器 r d rd rd
例如指令: a u i p c   r d , 0 x 0 auipc \ rd,0x0 auipc rd,0x0 多用于获取当前 P C PC PC 的值并存入寄存器 r d rd rd 中。

j a l r jalr jalr 指令编码方式(指令长度为4):
在这里插入图片描述指令作用:实现间接跳转,跳转目标地址为 r s 1 rs1 rs1 寄存器的值加12位立即数符号扩展到字长之后相加,跟随在 j a l r jalr jalr 指令后的指令地址: ( P C + 4 ) (PC+4) (PC+4) 会被放入 r d rd rd 寄存器中,当不指定 r d rd rd 时默认为寄存器 x 1 ( r a   r e t u r n   a d d r e s s ) x1(ra \ return \ address) x1(ra return address) 。当不需要返回地址时,可以把 X 0 X0 X0 寄存器作为 r d rd rd 寄存器使用。
例如指令: j a l r   x 0 , 0 ( x 1 ) jalr \ x0, 0(x1) jalr x0,0(x1) 跳转到寄存器 x 1 x1 x1 的地址执行,不在意返回地址值的设置。 j a l r   t 1 jalr \ t1 jalr t1 会扩展为 j a l r   r a , 0 ( t 1 ) jalr \ ra, 0(t1) jalr ra,0(t1)

c a l l call call 指令多用于在一个程序中调用另一个程序,其一般格式为 c a l l   o f f s e t call \ offset call offset o f f s e t offset offset 为待调用程序相对当前指令的偏移。
R I S C V RISCV RISCV 架构中编译器在遇到函数调用时会使用 a u i p c + j a l r auipc+jalr auipc+jalr 指令代替 c a l l call call 指令达到函数调用的效果。另外, j a l r jalr jalr 指令中 r s 1 rs1 rs1 r d rd rd 两个寄存器需要相同,一般都为 x 1 ( r a   r e t u r n   a d d r e s s ) x1(ra \ return \ address) x1(ra return address) ,这么做的好处在于可以实现 m a c r o   o p   f u s i o n macro \ op \ fusion macro op fusion,简单来说这是一种硬件机制,用于加速相邻两条指令的执行,具体机制请读者自行了解,(MOP Fusion)参考链接

我们可打开使用 o b j d u m p objdump objdump 反编译后的文件,定位到函数调用部分,看到 a u i p c + j a l r auipc+jalr auipc+jalr 的配合调用。例如下文代码中调用 o p e n open open 系统调用和 c a t cat cat 自定义函数的部分。

 28     if((fd = open(argv[i], 0)) < 0){
 27   b4:   4581                    li      a1,0
 26   b6:   00093503                ld      a0,0(s2)
 25   ba:   00000097                auipc   ra,0x0
 24   be:   310080e7                jalr    784(ra) # 3ca <open>
 23   c2:   84aa                    mv      s1,a0
 22   c4:   02054d63                bltz    a0,fe <main+0x74>
 21       fprintf(2, "cat: cannot open %s\n", argv[i]);
 20       exit(1);
 19     }
 18     cat(fd);
 17   c8:   00000097                auipc   ra,0x0
 16   cc:   f38080e7                jalr    -200(ra) # 0 <cat>

假设待调用程序相对于 a u i p c auipc auipc 指令所在地址的偏移地址为 o f f s e t offset offset ,要配合实现 c a l l call call 指令的效果,编译器对于 j a l r jalr jalr 指令和 a u i p c auipc auipc 指令分别需要一个由 o f f s e t offset offset 计算而来的参数作为指令中的立即数部分。

以上文中函数调用 c a t ( f d ) cat(fd) cat(fd) 为例, c a t cat cat 函数位于文件开头,其指令地址在未重定向之前位于 0 0 0位置。 a u i p c auipc auipc 指令地址为 0 x c 8 0xc8 0xc8 十进制为 200 200 200 。结合下文 j a l r jalr jalr 指令,其跳转目标地址为 r a − 200 = 0 ra-200=0 ra200=0。也就是 c a t cat cat 函数的地址。另外会把下条指令地址记录到返回地址寄存器,即 0 x c c + 4   − > r a 0xcc+4 \ ->ra 0xcc+4 >ra

下面的C语言程序代表了编译器一种可能的计算两个指令所需要的立即数参数的计算过程。其中, j a l r jalr jalr 的参数和 a u i p c auipc auipc 的参数分别由程序中变量 j a l r _ a r g   &   0 x f f f f f jalr\_arg \ \& \ 0xfffff jalr_arg & 0xfffff a u i p c _ a r g   &   0 x f f f auipc\_arg \ \& \ 0xfff auipc_arg & 0xfff 分别表示。

核心: o f f s e t offset offset 是待调用程序相对于指令 a u i p c auipc auipc 指令地址 P C PC PC 的偏移

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv){
    if (argc != 2) return 1;    
    int offset = atoi(argv[1]);

    int jalr_arg = (offset << 20) >> 20;
    int auipc_arg = (offset - jalr_arg) >> 12;
    
    printf("Offset = %d 0x%08x\n", offset, offset);
    printf("auipc ra, %d # 0x%05x\n", auipc_arg, auipc_arg & 0xfffff);
    printf("jalr ra, %d(ra) # 0x%03x\n", jalr_arg,  jalr_arg & 0xfff);
    return 0;
}

大致分两种情况:
当后12bit中最高有效位为0时,高20位数据右移12位作为 a u i p c auipc auipc 的参数,低12位符号扩展(形式和0扩展一样)作为 j a l r jalr jalr 的参数。
当后12bit中最高有效位为1时,高20位数据右移12位并加1作为 a u i p c auipc auipc 的参数,低12位符号扩展为负数作为 j a l r jalr jalr 的参数。

这里原理主要在于:
当后 12 b i t 12bit 12bit 中最高有效位为0时, j a l r _ a r g jalr\_arg jalr_arg 在计算之后为低12bit表示的非负数。随后的减法使得低 12 b i t 12bit 12bit 直接置0。以 o f f s e t offset offset 0 x 12345678 0x12345678 0x12345678 为例:
o f f s e t j a l r _ a r g a u i p c _ a r g a u i p c _ a r g & 0 x f f f f f j a l r _ a r g & 0 x f f f 0 x 12345678 0 x 678 0 x 12345 0 x 12345 0 x 678 \def\arraystretch{1.5} \begin{array}{c:c:c:c:c} offset& jalr\_arg & auipc\_arg & auipc\_arg \& 0xfffff & jalr\_arg \& 0xfff\\ \hline 0x12345678 & 0x678 & 0x12345 & 0x12345 & 0x678\\ \end{array} offset0x12345678jalr_arg0x678auipc_arg0x12345auipc_arg&0xfffff0x12345jalr_arg&0xfff0x678

当后 12 b i t 12bit 12bit 中最高有效位为 1 1 1 时,假设 j a l r _ a r g jalr\_arg jalr_arg所表示的数字值为 a a a 十六进制表示 0 x f f f f f x y z 0xfffffxyz 0xfffffxyz − a -a a 的计算则需要对 0 x f f f f f x y z 0xfffffxyz 0xfffffxyz 取反加1。取反操作对于高位的 f f f 取反后变为全0,但低 12 b i t 12bit 12bit o f f s e t offset offset 的低 12 b i t 12bit 12bit 刚好相反,相加之后使得结果后 12 b i t 12bit 12bit 为全1。加1之后向第 12 b i t 12bit 12bit (从0开始)位进 1 1 1。以 o f f s e t offset offset 0 x 12345876 0x12345876 0x12345876 为例:
o f f s e t j a l r _ a r g o f f s e t − j a l r _ a r g a u i p c _ a r g a u i p c _ a r g & 0 x f f f f f j a l r _ a r g & 0 x f f f 0 x 12345876 0 x f f f f f 876 0 x 12346000 0 x 12346 0 x 12346 0 x 876 \def\arraystretch{1.5} \begin{array}{c:c:c:c:c:c} offset& jalr\_arg & offset - jalr\_arg&auipc\_arg & auipc\_arg \& 0xfffff & jalr\_arg \& 0xfff\\ \hline 0x12345876 & 0xfffff876 &0x12346000 & 0x12346 & 0x12346 & 0x876\\ \end{array} offset0x12345876jalr_arg0xfffff876offsetjalr_arg0x12346000auipc_arg0x12346auipc_arg&0xfffff0x12346jalr_arg&0xfff0x876

0 x 12345876 0x12345876 0x12345876 十进制值为 305420406 305420406 305420406 ,所以待跳转地址为 P C + o f f s e t = P C + 305420406 PC+offset=PC+305420406 PC+offset=PC+305420406 0 x 876 0x876 0x876 按照 12 b i t 12bit 12bit 数字表示为负数,符号扩展为 32 b i t 32bit 32bit 表示为 0 x f f f f f 876 0xfffff876 0xfffff876 其十进制值为 − 1930 -1930 1930
指令 a u i p c   r a , 0 x 12346 auipc \ ra,0x12346 auipc ra,0x12346 执行之后, r a ra ra 的值为 P C + 0 x 12346000 PC+0x12346000 PC+0x12346000
指令 j a l r   r a , − 1930 ( r a ) jalr \ ra, -1930(ra) jalr ra,1930(ra) 之后,跳转位置为: P C + 0 x 12346000 − 1930 = P C + 305420406 = P C + o f f s e t PC+0x12346000-1930=PC+305420406=PC+offset PC+0x123460001930=PC+305420406=PC+offset

知识扩展 物理内存保护 P M P PMP PMP

R I S C V RISCV RISCV 提供物理内存保护机制,用于限制处于 S − M o d e S-Mode SMode U − M o d e U-Mode UMode 下的物理内存访问必须符合访问该物理内存所必须具备的权限,否则就会引发异常并执行异常处理函数。
物理内存保护由一个 P M P PMP PMP 项来定义一个访问控制区域。一个 P M P PMP PMP 项包括一个 8 位控制寄存器和一个地址寄存器,仅可在 M − M o d e M-Mode MMode 下访问,处理器支持最多 64 个 P M P PMP PMP 项,其中控制寄存器命名为 ( p m p 0 c f g − p m p 63 c f g ) (pmp0cfg-pmp63cfg) (pmp0cfgpmp63cfg),和控制寄存器对应的地址寄存器命名为 ( p m p a d d r 0 − p m p a d d r 63 ) (pmpaddr0-pmpaddr63) (pmpaddr0pmpaddr63) P M P PMP PMP 访问控制区域的大小是可设置的,最小可支持 4 字节大小的区域。

P M P PMP PMP控制寄存器和地址寄存器

一个 P M P PMP PMP 控制寄存器包含8位,共分为 L 、 A 、 X 、 W 、 R L、A、X、W、R LAXWR 五个字段。各字段区域如下所示:
在这里插入图片描述 W W W 控制该区域的写。 R R R 控制该区域的读。 X X X 控制该区域内容的执行。 A A A 字段控制区域计算规则。具体包括以下四种:
在这里插入图片描述
p m p c f g . A pmpcfg.A pmpcfg.A 字段为0,则禁用物理内存保护。
p m p c f g . A pmpcfg.A pmpcfg.A 字段为1,则表示和上一 P M P PMP PMP项的结束地址相连续。若当前控制寄存器为pmp1cfg,对应地址寄存器表示地址为PMP1ADDR, p m p 0 c f g pmp0cfg pmp0cfg 对应地址寄存器表示地址为 P M P 0 A D D R PMP0ADDR PMP0ADDR 时,那么当前区域地址范围是 [ P M P 0 A D D R , P M P 1 A D D R ) [PMP0ADDR , PMP1ADDR) [PMP0ADDR,PMP1ADDR)。特别的,当前 p m p pmp pmp控制寄存器为 p m p 0 c f g pmp0cfg pmp0cfg 时,范围表示为 [ 0 , P M P 0 A D D R ) [0 , PMP0ADDR) [0,PMP0ADDR)
p m p c f g . A pmpcfg.A pmpcfg.A 字段为2,表示地址按照4字节区域控制,每个区域大小为4字节。
p m p c f g . A pmpcfg.A pmpcfg.A 字段为3,表示按照2的次幂大小字节数来控制区域。具体按照2的多少次幂作为区域大小需要根据控制寄存器对应的地址寄存器的内容来确定。规则如下表所示:
在这里插入图片描述
p m p c f g pmpcfg pmpcfg 控制寄存器A字段为2即代表 N A 4 NA4 NA4 时,区域大小为4字节。
p m p c f g pmpcfg pmpcfg 控制寄存器A字段为3,对应地址寄存器最后一位为0,则按照8字节对齐。
p m p c f g pmpcfg pmpcfg 控制寄存器A字段为3,对应地址寄存器为全1,则按照 2 X L E N + 3 2^{XLEN+3} 2XLEN+3 字节对齐。

32位下地址寄存器格式,一共32位, R V 32 RV32 RV32 支持 34 b i t 34bit 34bit 物理地址空间,地址寄存器存储34位地址中从第2到33位,从0-1两位不记录:
在这里插入图片描述64位下地址寄存器格式, R V 64 RV64 RV64 支持 S V 39 SV39 SV39 S V 48 SV48 SV48 等模式,共 56 b i t 56bit 56bit 物理地址空间,地址寄存器不记录0-1两位,记录2-55共54位,地址寄存器的高10位直接置零:
在这里插入图片描述根据地址寄存器格式大家可以发现,在 R I S C V RISCV RISCV 内存保护机制中,地址默认按照 w o r d   32 b i t word \ 32bit word 32bit 对齐。

p m p c f g . L pmpcfg.L pmpcfg.L 位用于设置 l o c k lock lock 是否打开。当 l o c k lock lock 打开时, M − M o d e M-Mode MMode 下的访问也会强制按照权限访问,权限不匹配则触发异常。 l o c k lock lock 关闭时, M − M o d e M-Mode MMode 下的访问都会成功。

控制寄存器的分组

为了方便上下文切换, R I S C V RISCV RISCV p m p pmp pmp 控制寄存器放入 C S R CSR CSR 状态控制寄存器来设置。

32 b i t 32bit 32bit 模式下, C S R CSR CSR状态控制寄存器长度为 32 b i t 32bit 32bit,可存储4个 p m p pmp pmp 控制寄存器。pmp控制寄存器的命名方式为 ( p m p 0 c f g − p m p 63 c f g ) (pmp0cfg-pmp63cfg) (pmp0cfgpmp63cfg)。一个 C S R CSR CSR可存储4个 p m p pmp pmp控制寄存器, R I S C V RISCV RISCV 规定最多支持64个pmp控制寄存器,那么在 32 b i t 32bit 32bit 下, C S R CSR CSR 用于 p m p pmp pmp 控制的寄存器一共有16个,命名为 ( p m p c f g 0 − p m p c f g 15 ) (pmpcfg0-pmpcfg15) (pmpcfg0pmpcfg15)。如下所示:
在这里插入图片描述
64 b i t 64bit 64bit 模式下, C S R CSR CSR 状态控制寄存器长度为 64 b i t 64bit 64bit ,可存储8个 p m p pmp pmp 控制寄存器。 p m p pmp pmp 控制寄存器的命名方式为 ( p m p 0 c f g − p m p 63 c f g ) (pmp0cfg-pmp63cfg) (pmp0cfgpmp63cfg) 。一个 C S R CSR CSR可存储8个 p m p pmp pmp控制寄存器, R I S C V RISCV RISCV 规定最多支持64个 p m p pmp pmp 控制寄存器,在 64 b i t 64bit 64bit 下为了降低软件成本与 32 b i t 32bit 32bit 保持一致, C S R CSR CSR 用于 p m p pmp pmp控制的寄存器一共有8个,命名为 ( p m p c f g 0 − p m p c f g 14 ) (pmpcfg0-pmpcfg14) (pmpcfg0pmpcfg14) 但只采用偶数计数,没有单数表示。如下所示:
在这里插入图片描述

x v 6 xv6 xv6物理地址保护设置

  w_pmpaddr0(0x3fffffffffffffull);
  w_pmpcfg0(0xf);

首先设置了 p m p a d d r 0 pmpaddr0 pmpaddr0 地址寄存器中内容为全1,同时设置状态控制寄存器 C S R CSR CSR p m p c f g 0 pmpcfg0 pmpcfg0 中的内容为 0 x f 0xf 0xf,即设置 p m p 0 c f g pmp0cfg pmp0cfg 即与地址寄存器 p m p a d d r 0 pmpaddr0 pmpaddr0 相对应的控制寄存器内容为 0 x 0 f 0x0f 0x0f,设置读写执行并设置地址匹配模式为 T O R TOR TOR。那么此次设置受保护的物理内存地址范围为 [ 0 , 0 x 3 f f f f f f f f f f f f f 00 ) [0, 0x3fffffffffffff00) [0,0x3fffffffffffff00)
同时设置的权限为可读可写可执行。 x v 6 xv6 xv6 的设置方式非常直观,也没使用 N A P O T NAPOT NAPOT 模式等内容。

总结

简单记录了 R I S C V RISCV RISCV 的栈帧结构以及 x v 6 xv6 xv6 的启动前 M − M o d e M-Mode MMode 下的准备流程和跳转机制。最后部分研究了 a u i p c + j a l r auipc+jalr auipc+jalr 的实现 c a l l call call 指令效果机制以及 R I S C V RISCV RISCV 的物理内存保护机制。

参考

关于 R I S C V RISCV RISCV 中断的处理响应委派机制参考了大佬的博客,收益良多。博客详细地址

R I S C V RISCV RISCV 指令集手册 priv-isa-asciidoc.pdf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值