2团
Published on 2024-08-29 / 62 Visits
0
0

rCore学习——建立跳板页面

1.jr和call指令的区别

1.1 pc指令

在计算机体系结构中,程序计数器(Program Counter,简称PC)寄存器用于存储当前正在执行的指令的地址。PC寄存器的工作机制如下:

1.1.1 指令执行周期

  1. 取指(Fetch):

    • 从PC寄存器中读取当前指令的地址。

    • 将该地址发送到内存或指令缓存中,以获取指令。

    • 将指令加载到指令寄存器中。

  2. 译码(Decode):

    • 对取回的指令进行译码,以确定操作码和操作数。

    • 根据指令类型,确定需要执行的操作。

  3. 执行(Execute):

    • 执行指令所指定的操作。

    • 这可能涉及算术运算、逻辑运算、内存访问或其他操作。

  4. 写回(Write-back):

    • 将执行结果写回到目标寄存器或内存中。

  5. 更新PC(Update PC):

    • 根据指令类型更新PC寄存器的值。

    • 对于顺序执行的指令,PC通常增加指令长度。

    • 对于跳转或分支指令,PC将更新为目标地址。

1.1.2 示例

以下是一个简单的RISC-V汇编代码示例,展示了PC寄存器的工作原理:

.text

.globl _start

_start:

    # 初始化寄存器

    li t0, 10       # t0 = 10

    li t1, 20       # t1 = 20

    # 算术运算

    add t2, t0, t1  # t2 = t0 + t1

    # 条件分支

    beq t2, t1, label1  # 如果 t2 == t1,跳转到 label1

    # 顺序执行

    li t3, 30       # t3 = 30

label1:

    li t4, 40       # t4 = 40

    # 无限循环

    j label1

解释:

  1. 初始化寄存器:

    • li t0, 10li t1, 20 指令将立即数加载到寄存器 t0t1 中。

    • PC寄存器在每条指令执行后增加4字节。

  2. 算术运算:

    • add t2, t0, t1 指令将 t0t1 的值相加,并将结果存储在 t2 中。

    • PC寄存器增加4字节。

  3. 条件分支:

    • beq t2, t1, label1 指令检查 t2t1 是否相等。如果相等,PC寄存器将更新为 label1 的地址。

    • 如果不相等,PC寄存器增加4字节。

  4. 顺序执行:

    • li t3, 30 指令将立即数加载到寄存器 t3 中。

    • PC寄存器增加4字节。

  5. 无限循环:

    • j label1 指令将PC寄存器更新为 label1 的地址,形成无限循环。

1.2 区别

在RISC-V架构中,jr(Jump Register)和call(Call)指令用于控制程序的执行流,但它们有不同的用途和行为。以下是它们的主要区别:

1.2.1 jr 指令

  • 全称: Jump Register

  • 功能: 无条件跳转到寄存器中存储的地址。

  • 语法: jr rs

    • rs 是包含目标地址的寄存器。

  • 用途: 通常用于实现函数返回或间接跳转。

  • 行为: 直接将程序计数器(PC)设置为寄存器 rs 中的值,不保存返回地址。

1.2.2 call 指令

  • 全称: Call

  • 功能: 调用子程序,并保存返回地址。

  • 语法: call offset

    • offset 是相对于当前PC的偏移量,用于计算目标地址。

  • 用途: 用于调用子程序,并在调用结束后返回到调用点。

  • 行为: 将返回地址(即下一条指令的地址)保存到链接寄存器(ra),然后跳转到目标地址。

2. 创建跳板页面操作

2.1 创建跳板函数

rCore中跳板函数定义于trap.S文件中,节选部分代码如下:

    .section .text.trampoline
    .globl __alltraps
    .globl __restore
    .align 2

这段汇编代码是将 __alltraps 函数放置在 .text.trampoline 段中,并且对齐到 4 字节边界(因为 .align 2 表示 2^2 = 4 字节对齐),具体解释如下:

  1. .section .text.trampoline:

    • 这行代码将接下来的代码放置在名为 .text.trampoline 的段中。这个段名是用户自定义的,通常用于特定用途,比如放置特定的汇编代码。

  2. .globl __alltraps:

    • 这行代码声明 __alltraps 符号为全局符号,使其可以被其他文件引用。它并没有具体说明 __alltraps 的位置,只是声明它是全局的。

  3. .globl __restore:

    • 类似于 __alltraps,这行代码声明 __restore 符号为全局符号,使其可以被其他文件引用。

  4. .align 2:

    • 这行代码将接下来的代码对齐到 4 字节边界(2^2 = 4)。这通常是为了满足指令对齐的要求,以提高性能或满足架构要求。

需要注意,__alltraps放置于.text.trampoline 段中,且因为其是段中声明的第一个符号,因此其位于段的起始位置。

2.2 保存跳板页面至.text段

    stext = .;
    .text : {
        *(.text.entry)
        . = ALIGN(4K);
        strampoline = .;
        *(.text.trampoline);
        . = ALIGN(4K);
        *(.text .text.*)
    }

此段代码将 trap.S 中的整段汇编代码放置在 .text.trampoline 段,并在调整内存布局的时候将它对齐到代码段的一个页面中,,且 __alltraps 恰好位于这个物理页帧的开头,其物理地址被外部符号 strampoline 标记。

2.3 放置跳板页面至内核以及应用地址空间顶层

image-lvvz.png

rCore教材中有如上一段,后续将解释如何将跳板页面置于内核空间和应用空间的最高虚拟页面上。

2.3.1 创建跳板页面映射函数

kernel-as-high.png

参照上图所示,将跳板页面置于最高虚拟页面(Rust中,usize::MAX数值为2的2^64 - 1)。

    pub const TRAMPOLINE: usize = usize::MAX - PAGE_SIZE + 1;
    /// Mention that trampoline is not collected by areas.
    fn map_trampoline(&mut self) {
        self.page_table.map(
            VirtAddr::from(TRAMPOLINE).into(),
            PhysAddr::from(strampoline as usize).into(),
            PTEFlags::R | PTEFlags::X,
        );
    }

创建跳板页面,并将TRAMPOLINE的虚拟地址与.text.trampoline段的实际物理地址进行映射。

2.3.2 跳板页面放置于内核空间

    /// Without kernel stacks.
    pub fn new_kernel() -> Self {
        let mut memory_set = Self::new_bare();
        // 手动加载跳板页面地址(建立虚拟地址最高页面到跳板页面物理地址的映射关系)
        memory_set.map_trampoline();

        // map kernel sections
        println!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
        println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
        println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
        println!(
            ".bss [{:#x}, {:#x})",
            sbss_with_stack as usize, ebss as usize
        );
        println!("mapping .text section");
        memory_set.push(
            MapArea::new(
                (stext as usize).into(),
                (etext as usize).into(),
                MapType::Identical,
                MapPermission::R | MapPermission::X,
            ),
            None,
        );

这里需要注意,在完成跳板页面的映射(虚拟地址最高页面到跳板页面物理地址的映射)之后,又陆续加载了.text(需要注意注意,.text段中的首个页面是跳板页面的物理地址,这是由编译器/汇编器/链接器进行后端代码生成和链接形成最终机器码时设置此指令的地址)、.rodata、.data等段。

2.3.3 跳板页面放置于应用空间

    /// Include sections in elf and trampoline and TrapContext and user stack,
    /// also returns user_sp and entry point.
    pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize) {
        // 映射跳板页面
        let mut memory_set = Self::new_bare();
        // map trampoline
        memory_set.map_trampoline();

        // 省略后续代码

2.4 指令跳转

#[repr(C)]
/// trap context structure containing sstatus, sepc and registers
pub struct TrapContext {
    /// general regs[0..31]
    pub x: [usize; 32],
    /// CSR sstatus      
    pub sstatus: Sstatus,
    /// CSR sepc
    pub sepc: usize,
    /// Addr of Page Table
    pub kernel_satp: usize,
    /// kernel stack
    pub kernel_sp: usize,
    /// Addr of trap_handler function
    pub trap_handler: usize,
}

rCore在初始化应用的时候,会将kernel_satpkernel_sp以及trap_handler数值赋予应用的Trap上下文中,以便调用的时候使用。下文看跳转的实现:

__alltraps:
    csrrw sp, sscratch, sp
    # now sp->*TrapContext in user space, sscratch->user stack
    # save other general purpose registers
    sd x1, 1*8(sp)
    # skip sp(x2), we will save it later
    sd x3, 3*8(sp)
    # skip tp(x4), application does not use it
    # save x5~x31
    .set n, 5
    .rept 27
        SAVE_GP %n
        .set n, n+1
    .endr
    # we can use t0/t1/t2 freely, because they have been saved in TrapContext
    csrr t0, sstatus
    csrr t1, sepc
    sd t0, 32*8(sp)
    sd t1, 33*8(sp)
    # read user stack from sscratch and save it in TrapContext
    csrr t2, sscratch
    sd t2, 2*8(sp)
    # load kernel_satp into t0
    ld t0, 34*8(sp)
    # load trap_handler into t1
    ld t1, 36*8(sp)
    # move to kernel_sp
    ld sp, 35*8(sp)
    # switch to kernel space
    csrw satp, t0
    sfence.vma
    # jump to trap_handler
    jr t1

结合TrapContext的实现,可知trap_handler的地址被存放于t1寄存器中,函数最后直接使用jr跳转到trap_handler入口。至于为何不使用call指令直接调用trap_handler,rCore的解释如下:

即如果根据linker-qemu/k210.ld的地址布局描述,计算出来的指令偏移量是不正确的(具体原因可以见2.3.2章节,跳板页面的真实物理地址位于.text段中,但是我们又手动创建了虚拟空间最高页面到跳板页面的映射,这两个地址是不一致的)。

因此若使用call指令执行(call offset,offset为trap_handler与pc指令当前数值的偏移量,但是这个偏移量是错误的)会出现错误。基于此原因,rCore中直接使用jr指令跳转到trap_handler


Comment