< 返回版块

洛佳 发表于 2024-09-23 15:32

Tags:rustsbi, risc-v

背景

RISC-V是一个快速演进的指令集。和x86、ARM等历史厚重的指令集相比,它在快速演进的过程中,会毫不犹豫地舍弃一些陈旧的设计。至今,RISC-V特权指令集的设计已经大体稳定,更多RISC-V芯片也在今天的历史机遇下不断推出和发展。然而,K210芯片并非诞生于目前RISC-V繁花遍野的历史时期;它是勇于接受新指令集的弄潮儿,诞生在RISC-V鸿蒙初开的日子里。

1.9.1版RISC-V特权指令集诞生于2016年11月4日。随着RISC-V的历史发展,今天的1.12版特权指令集已和K210推出时的RISC-V 1.9.1版特权指令集有较大的不同。举个例子:

11.1.11 特权地址转换和保护 (satp) 寄存器

satp CSR 是一个 SXLEN 位读/写寄存器,其格式如图 56 所示(SXLEN=32)和图 57 所示(SXLEN=64),用于控制特权模式地址转换和保护。该寄存器保存根页表的物理页号 (PPN),即其特权物理地址除以 4 KiB;地址空间标识符 (ASID),用于在每个地址空间基础上实现地址转换隔离;以及 MODE 字段,用于选择当前的地址转换方案。有关访问该寄存器的更多详细信息,请参见第 3.1.6.6 节。

而1.9.1版特权指令集中,相应的定义如下:

3.1.8 mstatus 寄存器中的虚拟化管理字段

虚拟内存管理字段 VM[4:0] 表示当前活动的虚拟内存方案,包括虚拟内存转换和保护。表 3.3 显示了当前定义的虚拟化方案。……

VM 是一个 WARL 字段,因此可以通过将值写入 VM,然后从 VM 读回该值以查看是否返回相同的值来确定实现是否支持 VM 设置。

4.1.10 特权页表基址寄存器 (sptbr)

sptbr 寄存器是一个 XLEN 位读/写寄存器,其格式如图 4.9(RV32)和图 4.10 所示。sptbr 寄存器仅存在于支持分页虚拟内存系统的系统中。该寄存器保存根页表的物理页号 (PPN),即其特权物理地址除以 4 KiB,以及地址空间标识符 (ASID),这有助于在每个地址空间基础上进行地址转换隔离。

我们发现,同样是虚拟地址翻译功能,1.12版本中由satp寄存器的MODE字段控制,而1.9.1版本由mstatus寄存器的VM字段控制。1.12版本的设计更合理,因为此时satp寄存器可以由特权态即S态访问,允许操作系统配置用于内核本身和用户程序的虚拟内存空间。

为了运行基于1.12版本开发的操作系统内核,我们需要借助高于内核特权级的方法,妥善处理与版本有关的虚拟地址翻译设置功能。例如,当新版本虚拟地址刷新指令sfence.vma执行时,我们读取位于satp寄存器的物理页号(PPN)和地址空间标识符(ASID),分别写入旧版本的sptbr寄存器和mstatus.VM位中,完成虚拟地址翻译模式和基地址的设置操作。

具体来说,K210具有的1.9.1版本和目前的1.12版本存在以下的差异:

  • 虚拟地址翻译设置时,页表设置并非统一位于satp寄存器,而是将页表基地址和地址空间标识符位于sptbr中,但页表翻译模式位于mstatus.VM中;
  • 页表刷新指令是sfence.vm [vaddr],而不是sfence.vma [vaddr][, asid]
  • 不存在单独的页异常,即页表读取异常(Load page fault)和页表保存/原子操作异常(Store/AMO page fault),而是合并到读取权限错误(Load access fault)和保存/原子操作权限错误(Store/AMO access fault)中。

此外,针对操作系统开发,两版本的差异还有以下内容:

  • 为了允许访问页表设置中U=1的页,应将sstatusPUM位应设为1,而不是将sstatusSUM位设为0。

最后,仍然一些差异是K210硬件上独有的,而与1.9.1版本特权指令集关联不紧:

  • K210上不存在特权态(S态)外部中断。

原来有这么多地方不一样!在不更改K210硬件的前提下,为了尽可能无缝地运行1.12版本的操作系统内核,我们可以设计一款权限高于内核特权态的软件,为内核处理版本间的兼容性问题。让我们开始吧!

从MMU到MMU的兼容性

正如背景章节所言,K210硬件和最新版本RISC-V的页表设置位于不同名称的寄存器中。当RISC-V执行页翻译,它除了将访问satp寄存器,还将访问各级页表项的内容和必要的安全检查寄存器位(请参阅附录)。那么新旧版本之间,页表项的定义是否相同呢?以Sv39为例,我们查阅1.12版本的RISC-V手册:

图片

而1.9.1版本的RISC-V手册给了我们这样的定义:

图片

我们发现,除了1.12版本分别于SvnapotSvpbmt扩展引入的位于高位的配置位(NPBMT)之外,它在物理页号(PPN)和页属性位(XWR等)的定义都是相同的!这说明,我们可以直接复用1.9.1版本硬件中的页表项,组成1.12版本页表翻译所需的各级页表。为了实现的严格性,我们考虑1.12内核使用了SvnapotSvpbmt扩展的情况。1.9.1版本对保留的位域(图中Reserved)有如下的定义:

4.6.1 寻址和内存保护

……

Sv39 的 PTE 格式如图 4.16 所示。位 9–0 的含义与 Sv32 相同。位 63–48 保留供将来使用,必须由软件清零以实现向前兼容。

而1.12版本手册中的定义是:

11.4.1. 寻址和内存保护

……

Sv39 的 PTE 格式如图 63 所示。位 9-0 的含义与 Sv32 的相同。位 63 保留供第 12 章中的 Svnapot 扩展使用。如果未实现 Svnapot,则位 63 保持保留,并且必须由软件将其清零以实现前向兼容,否则将引发页面错误异常。位 62-61 保留供第 13 章中的 Svpbmt 扩展使用。如果未实现 Svpbmt,则位 62-61 保持保留,并且必须由软件将其清零以实现前向兼容,否则将引发页面错误异常。位 60-54 保留供将来的标准使用,并且在某些标准扩展定义其用途之前,必须由软件将其清零以实现前向兼容。如果设置了这些位中的任何一个,则会引发页面错误异常。

1.12版本手册中,规定不使用这两个扩展时,操作系统软件应当将63、62-61位清零,而所有情况下60-54位都应当清零,此时63-48位全零,良好地符合了1.9.1版本地硬件定义。因此,在SvnapotSvpbmt未实现时,以上1.9.1版本模拟1.12版本Sv39页表的操作是严格的。

事实上,1.9.1手册并未对63-48位(包含新版的NPBMT)有非零配置时给出任何行为定义。由于我们的兼容性实现仅为K210芯片使用,而不是为所有的1.9.1版本硬件(事实上,只有K210是1.9.1时代吃螃蟹的先驱者!),我们可以在K210硬件上测试63-48位非零时的硬件表现,来判断是否可以复用它来表示SvnapotSvpbmt扩展有实现的情况。

RISC-V操作系统中,在页表配置完毕后,还需要使用页表刷新指令,将修改好的页表应用到RISC-V硬件。1.12版本和1.9.1版本的RISC-V手册对页表刷新指令的定义是不同的。翻阅1.12版本手册,我们能找到以下内容:

11.2.1 特权内存管理隔离指令

特权内存管理隔离指令 SFENCE.VMA 用于将内存中的内存管理数据结构的更新与当前执行同步。指令执行会导致对这些数据结构的隐式读取和写入;但是,这些隐式引用通常不是相对于显式加载和存储排序的。执行 SFENCE.VMA 指令可确保当前 RISC-V hart 已经可见的任何先前存储都排在该 hart 中后续指令对内存管理数据结构的某些隐式引用之前。SFENCE.VMA 排序的特定操作集由 rs1rs2 确定,如下所述。SFENCE.VMA 还用于使与 hart 关联的地址转换缓存中的条目无效(参见第 11.3.2 节)。有关该指令行为的更多详细信息,请参见第 3.1.6.6 节和第 3.7.2 节。

对于转换数据结构仅针对单个地址映射(即一个页面或超级页面)进行修改的常见情况,rs1 可以指定该映射内的虚拟地址,以仅对该映射产生转换隔离。此外,对于转换数据结构仅针对单个地址空间标识符进行修改的常见情况,rs2 可以指定地址空间。SFENCE.VMA 的行为取决于 rs1rs2,如下所示:

……

相应地,1.9.1版本手册给出了以下内容:

4.2.1 特权内存管理隔离指令

特权内存管理隔离指令 SFENCE.VM 用于将内存中的内存管理数据结构的更新与当前执行同步。指令执行会导致对这些数据结构的隐式读取和写入;但是,这些隐式引用通常不按指令流中的加载和存储顺序排序。执行 SFENCE.VM 指令可确保在 SFENCE.VM 之前的指令流中的任何存储都排在 SFENCE.VM 之后的所有隐式引用之前。此外,执行 SFENCE.VM 可确保在 SFENCE.VM 之前的指令引起的任何隐式写入都排在 SFENCE.VM 之后的所有加载和存储之前。

SFENCE.VM 的行为取决于 sptbr 寄存器中 ASID 字段的当前值。如果 ASID 非零,SFENCE.VM 仅对当前地址空间中的地址转换有效。如果 ASID 为零,SFENCE.VM 会影响所有地址空间的地址转换。在这种情况下,它还会影响全局映射,如第 4.5.1 节所述。

寄存器操作数 rs1 包含一个可选的虚拟地址参数。如果 rs1=x0,则栅栏会影响所有虚拟地址转换和对任何级别的页表进行的存储。

我们发现,特权内存隔离指令从旧版本SFENCE.VM升级为了新版本的SFENCE.VMA。相比于旧版本的SFENCE.VM指令,新版本SFENCE.VMA不再从sptbr寄存器中决定隔离指令影响的地址空间,而是增加了rs2参数,用于决定地址空间;rs1决定的虚拟地址参数仍然是相同的。

当我们希望从机器态(M态)软件模拟新版本指令集时,我们通常仅能模拟二进制编码上不存在的指令,而不是更改已有指令的执行逻辑。那么,新旧版本的内存管理隔离指令在二进制编码上相同吗?我们继续查阅手册,可以得出以下的对比:

指令 31-25 24-20 19-16 15-12 11-7 6-0
SFENCE.VMA 0001001 rs2 rs1 000 00000 1110011
SFENCE.VM 0001000 00100 rs1 000 00000 1110011

我们发现,新旧两条指令的二进制表示在31-25位上是不同的,他们代表着不同的操作数常量。这意味着,无论rs1rs2是哪个寄存器,我们总能通过31-25位的区别区分这两条指令。事实上,当我们尝试在K210硬件上运行SFENCE.VMA指令的二进制码时,我们会得到一个非法指令异常,这意味着K210实现的1.9.1版本RISC-V硬件并不存在SFENCE.VMA指令。

如果我们处理新版指令执行返回的异常,然后在异常处理代码中,运行旧版本的指令,能达到刷新页表的效果吗?

为了完成我们的设想,RustSBI-K210项目编写了以下的代码

fn emulate_illegal_instruction(ctx: &mut SupervisorContext, ins: usize) -> bool {
    /* 省略 feature::emulate_rdtime(ctx, ins) …… */
    if feature::emulate_sfence_vma(ctx, ins) {
        return true;
    }
    false
}

代码段中,emulate_sfence_vma函数的实现如下:

// There is no `sfence.vma` in 1.9.1 privileged spec; however there is a `sfence.vm`.
// For backward compability, here we emulate the first instruction using the second one.
// sfence.vma: | 31..25 funct7=SFENCE.VMA(0001001) | 24..20 rs2/asid | 19..15 rs1/vaddr |
//               14..12 funct3=PRIV(000) | 11..7 rd, =0 | 6..0 opcode=SYSTEM(1110011) |
// sfence.vm(1.9):  | 31..=20 SFENCE.VM(000100000100) | 19..15 rs1/vaddr |
//               14..12 funct3=PRIV(000) | 11..7 rd, =0 | 6..0 opcode=SYSTEM(1110011) |

#[inline]
pub fn emulate_sfence_vma(ctx: &mut SupervisorContext, ins: usize) -> bool {
    if ins & 0xFE007FFF == 0x12000073 {
        // sfence.vma instruction
        // discard rs2 // let _rs2_asid = ((ins >> 20) & 0b1_1111) as u8;
        // let rs1_vaddr = ((ins >> 15) & 0b1_1111) as u8;
        // read paging mode from satp (sptbr)
        let satp_bits = satp::read().bits();
        // bit 63..20 is not readable and writeable on K210, so we cannot
        // decide paging type from the 'satp' register.
        // that also means that the asid function is not usable on this chip.
        // we have to fix it to be Sv39.
        let ppn = satp_bits & 0xFFF_FFFF_FFFF; // 43..0 PPN WARL
                                               // write to sptbr
        let sptbr_bits = ppn & 0x3F_FFFF_FFFF;
        unsafe { asm!("csrw 0x180, {}", in(reg) sptbr_bits) }; // write to sptbr
                                                               // enable paging (in v1.9.1, mstatus: | 28..24 VM[4:0] WARL | ... )
        let mut mstatus_bits: usize;
        unsafe { asm!("csrr {}, mstatus", out(reg) mstatus_bits) };
        mstatus_bits &= !0x1F00_0000;
        mstatus_bits |= 9 << 24;
        unsafe { asm!("csrw mstatus, {}", in(reg) mstatus_bits) };
        ctx.mstatus = mstatus::read();
        // emulate with sfence.vm (declared in privileged spec v1.9)
        unsafe { asm!(".word 0x10400073") }; // sfence.vm x0
                                             // ::"r"(rs1_vaddr)
        ctx.mepc = ctx.mepc.wrapping_add(4); // skip current instruction
        return true;
    } else {
        return false; // is not a sfence.vma instruction
    }
}

在判断是否为新版SFENCE.VMA指令后,这段代码完成了两部分功能。

首先,它取出新版satp寄存器中的物理页号PPN,保存到旧版本的sptbr寄存器中。由于两个寄存器的CSR编号相同(均为0x180),新版本系统写入satp寄存器时,事实上将sptbr看作是一个临时的数据保存位置,在SFENCE.VM指令执行前暂时保存着以satp格式编码的数据。我们以1.12版本satp的二进制编码取出保存于其中的PPNMODE,分别保存到1.9.1版本的sptbrmstatus.VM寄存器中。

在这之后,我们执行SFENCE.VM指令,真正完成硬件上的页表刷新功能。虽然SFENCE.VM给出了vaddr参数,且根据1.9.1版本手册,sptbr寄存器中应当保存ASID参数,但是查阅K210使用的Rocket RISC-V核源码,这两个参数并未被使用。根据1.12版本手册,我们阅读到以下的内容:

11.2.1 特权内存管理隔离指令

……

过度隔离始终是合法的,例如,仅基于 rs1 和/或 rs2 中的位子集进行隔离,和/或简单地将所有 SFENCE.VMA 指令视为具有 rs1=x0 和/或 rs2=x0。例如,更简单的实现可以忽略 rs1 中的虚拟地址和 rs2 中的 ASID 值并始终执行全局隔离。当 rs1 中保存无效的虚拟地址时不引发异常的选择有助于实现这种类型的简化。

出于K210的处理器核未使用vaddrASID参数,我们将执行一条rs1=x0为参数的SFENCE.VM指令,以表示隔离了所有地址空间、所有虚拟地址有关的页表缓存;它属于1.12版本手册中提到的“过度隔离”,因此符合RISC-V 1.12特权指令集的标准。

此外,异常处理结束时,我们应当增加mepc寄存器的值,表示模拟的SFENCE.VMA指令执行成功,RISC-V硬件应当跳过而不是再次执行此条指令。到这里,我们已经完整地在K210芯片上,运用1.9.1版本硬件实现了RISC-V 1.12特权指令集的页表翻译部分。恭喜您!

特权态外部中断

外部中断是RISC-V架构中重要的中断来源,内核使用外部中断处理外设中产生的外部事件。虽然RISC-V 1.9.1版本中定义了特权态(S态)外部中断,但是出于未知的原因,K210芯片上无法触发特权态外部中断。为了满足内核使用外部中断的需求,RustSBI-K210增加了额外的SBI调用,为S态注册外部中断的入口地址,从而允许内核处理外部中断。

RustSBI-K210的机器态(M态)软件并不使用中断。因此,当机器态中断信号产生时,RustSBI-K210全部转发到特权态。我们编写的代码如下:

Some(MachineTrap::ExternalInterrupt()) => unsafe {
    let ctx = rt.context_mut();
    feature::call_supervisor_interrupt(ctx)
},

这里,call_supervisor_interrupt的实现如下

pub unsafe fn call_supervisor_interrupt(ctx: &mut SupervisorContext) {
    let mut mstatus: usize;
    asm!("csrr {}, mstatus", out(reg) mstatus);
    // set mstatus.mprv
    mstatus |= 1 << 17;
    // it may trap from U/S Mode
    // save mpp and set mstatus.mpp to S Mode
    let mpp = (mstatus >> 11) & 3;
    mstatus = mstatus & !(3 << 11);
    mstatus |= 1 << 11;
    // drop mstatus.mprv protection
    asm!("csrw mstatus, {}", in(reg) mstatus);
    // compiler helps us save/restore caller-saved registers
    devintr();
    // restore mstatus
    mstatus = mstatus & !(3 << 11);
    mstatus |= mpp << 11;
    mstatus -= 1 << 17;
    asm!("csrw mstatus, {}", in(reg) mstatus);
    ctx.mstatus = mstatus::read();
}

在这段代码中,我们保存mstatus.MPP的值,重新设置其值为1,说明将这个中断转发到S态。随后,我们调用devintr函数,进入S态调用中断处理函数。当函数返回后,我们恢复先前mstatus.MPP的值,以继续运行随后的处理代码。

中断处理函数是如何给定的呢?我们设置了特殊的SBI调用sbi_rustsbi_k210_sext,来设置S态外部中断的入口函数。这个SBI调用专属于RustSBI-K210,它的扩展编号和函数编号分别为0x0A0000040x210。当SBI调用进入M态时,RustSBI-K210判断它是否为此设置函数,代码如下:

// We use implementation specific sbi_rustsbi_k210_sext function (extension
// id: 0x0A000004, function id: 0x210) to register S-level interrupt handler
// for K210 chip only. This chip uses 1.9.1 version of privileged spec,
// which did not declare any S-level external interrupts.
#[inline]
pub fn emulate_sbi_rustsbi_k210_sext(ctx: &mut SupervisorContext) -> bool {
    if ctx.a7 == 0x0A000004 && ctx.a6 == 0x210 {
        unsafe {
            DEVINTRENTRY = ctx.a0;
        }
        // enable mext
        unsafe {
            mie::set_mext();
        }
        // return values
        ctx.a0 = 0; // SbiRet::error = SBI_SUCCESS
        ctx.a1 = 0; // SbiRet::value = 0
        ctx.mepc = ctx.mepc.wrapping_add(4); // PC += 4
        return true;
    } else {
        return false;
    }
}

这个专有的SBI调用将设置全局变量DEVINTRENTRY,并打开M态的外部中断开关,以供K210硬件在外部中断时通过devintr函数调用S态内核软件设置的中断入口。至此,K210上的特权态外部中断已经被良好处理,K210上的内核通过调用RustSBI-K210,可以正常地注册和监听外部中断。

页异常

页的交换是虚拟内存操作系统的重要功能,它的软件设计依赖于硬件提供的页异常功能。我们阅读1.9.1版本的RISC-V特权级手册,能发现以下的内容:

图片

而1.12版本的手册中这样描述:

图片

我们发现,相比RISC-V 1.12版本,RISC-V 1.9.1版本不存在独立的页异常。事实上,1.9.1版本的页异常合并到访存异常中,当页异常发生时,硬件将调用访存异常的软件逻辑,这为操作系统的缺页处理功能带来不便。

为了解决这个问题,操作系统可以在内核态的访存异常逻辑中,判断异常是否属于页异常。事实上,我们也可以通过机器态程序判断异常类型;机器态程序在访存异常产生时,模拟页表的访问过程,判断硬件抛出的访存异常是否属于页异常。RustSBI-K210中此部分的处理逻辑如下:

// This function will lookup virtual memory module and page table system
// if memory fault from address `addr` is a page fault, return true
// otherwise when not a page fault, or when paging is disabled, return false
pub fn is_page_fault(addr: usize) -> bool {
    if !is_s1p9_mstatus_sv39_mode() {
        return false;
    }
    if !check_sext_sv39(addr) {
        return true;
    }
    let base_ppn = read_sptbr_ppn();
    let level_2_ppn = unsafe {
        let vpn2 = (addr >> 30) & 0x1FF;
        let ptr = ((base_ppn << 12) as *const usize).add(vpn2);
        let level_2_pte = if let Ok(ans) = try_read_address(ptr) {
            ans
        } else {
            // level 2 ppn read failed
            return true;
        };
        if (level_2_pte & 0b1) == 0 {
            // level 2 pte is not valid
            return true;
        }
        if (level_2_pte & 0b1110) != 0b0000 && (level_2_pte >> 10) & 0x3FFFF != 0 {
            // 大页对齐出错,返回页异常
            // level 2 huge page align not satisfied
            return true;
        }
        (level_2_pte >> 10) & 0x3F_FFFF_FFFF
    };
    let level_1_ppn = unsafe {
        let vpn1 = (addr >> 21) & 0x1FF;
        let ptr = ((level_2_ppn << 12) as *const usize).add(vpn1);
        let level_1_pte = if let Ok(ans) = try_read_address(ptr) {
            ans
        } else {
            // level 1 ppn read failed
            return true;
        };
        if (level_1_pte & 0b1) == 0 {
            // level 1 pte is not valid
            return true;
        }
        if (level_1_pte & 0b1110) != 0b0000 && (level_1_pte >> 10) & 0x1FF != 0 {
            // 大页对齐出错,返回页异常
            // level 1 huge page align not satisfied
            return true;
        }
        (level_1_pte >> 10) & 0x3F_FFFF_FFFF
    };
    let _ppn = unsafe {
        let vpn0 = (addr >> 12) & 0x1FF;
        let ptr = ((level_1_ppn << 12) as *const usize).add(vpn0);
        let final_pte = if let Ok(ans) = try_read_address(ptr) {
            ans
        } else {
            // level 0 ppn read failed
            return true;
        };
        if (final_pte & 0b1) == 0 {
            // level 0 pte is not valid
            return true;
        }
        if (final_pte & 0b1110) == 0b0000 {
            // level 0 page cannot have leaves
            return true;
        }
        (final_pte >> 10) & 0x3F_FFFF_FFFF
    };
    // 到这一步都没有错误,说明查找是成功的,并非页异常
    false
}

此处,is_page_fault函数能够判断1.9.1版本访存异常的类型。在异常处理入口,RustSBI-K210编写以下的代码:

Some(MachineTrap::LoadFault(addr)) => {
    let ctx = rt.context_mut();
    if feature::is_page_fault(addr) {
        unsafe { feature::do_transfer_trap(ctx, Trap::Exception(Exception::LoadPageFault)) }
    } else {
        unsafe { feature::do_transfer_trap(ctx, Trap::Exception(Exception::LoadFault)) }
    }
}
Some(MachineTrap::StoreFault(addr)) => {
    let ctx = rt.context_mut();
    if feature::is_page_fault(addr) {
        unsafe { feature::do_transfer_trap(ctx, Trap::Exception(Exception::StorePageFault)) }
    } else {
        unsafe { feature::do_transfer_trap(ctx, Trap::Exception(Exception::StoreFault)) }
    }
}

这里,在LoadFaultStoreFault产生时,判断是否属于页异常;如果属于,就转发到特权态,供内核识别。使用这种方法,RustSBI-K210可以辅助内核完成1.9.1版本异常类型的判断,从而可以增强与1.12版本内核的兼容性。

结语

通过以上的方法,我们能够在遵守1.9.1版RISC-V标准的K210芯片上,运行RISC-V特权级架构1.12版本的各类内核,从而扩展了K210芯片的生命周期。由此可见,SBI固件是一种提升软硬件兼容性的解决方案,通过在SBI固件上的各类兼容性设计,芯片产品的生命周期可被延长,维护成本也可一并降低。文章中为K210芯片设计的兼容性方法也可为未来的芯片软硬件设计提供参考。

阅读到这里的朋友中,既有像K210团队这样的先驱者,也有那些逐步将新思想与技术推广开来的实践者。无论站在创新前沿,还是传播和应用已有的知识,我们都为推动技术进步做出了独特的贡献。RISC-V架构正式在这样的群体下,从生根发芽到枝繁叶茂、独当一面。我们终将在计算机历史的漫卷中,见证RISC-V浓墨重彩的一页!

参考文献

  1. Waterman, Andrew, et al. "The RISC-V instruction set manual volume II: Privileged architecture version 1.9.1." EECS Department, University of California, Berkeley. (2016)
  2. The RISC-V Community. "The RISC-V Instruction Set Manual: Volume II", version 1.12. (2024)
  3. Yifan Wu, et al. "为 rCore-Tutorial 提供 K210 支持". URL: https://github.com/rcore-os/rCore-Tutorial/issues/80 . (2020)

附录:RISC-V 1.12版特权指令虚拟地址翻译过程

根据RISC-V特权指令集手册(1.12版)翻译。

11.3.2 虚拟地址转换过程

虚拟地址 va 转换为物理地址 pa 的过程如下:

  1. asatp.ppn×PAGESIZE,令 i=LEVELS-1。(对于 Sv32,PAGESIZE=212LEVELS=2。)satp 寄存器必须处于活动状态,即有效特权模式必须是 S 模式或 U 模式。
  2. pte 为地址 a+va.vpn[i]×PTESIZE 处的 PTE 值。(对于 Sv32,PTESIZE=4。)如果访问 pte 违反了 PMA 或 PMP 检查,则引发与原始访问类型相对应的访问错误异常。
  3. 如果 pte.v=0,或者 pte.r=0pte.w=1,或者 pte 中设置了任何为将来的标准使用而保留的位或编码,则停止并引发与原始访问类型相对应的页面错误异常。
  4. 否则,PTE 有效。如果 pte.r=1pte.x=1,则转到步骤 5。否则,此 PTE 是指向页表下一级的指针。令 i=i-1。如果 i<0,则停止并引发与原始访问类型相对应的页面错误异常。否则,令 a=pte.ppn×PAGESIZE 并转到步骤 2。
  5. 已找到叶 PTE。根据当前特权模式和 mstatus 寄存器的 SUMMXR 字段的值,确定 pte.rpte.wpte.xpte.u 位是否允许请求的内存访问。如果不是,则停止并引发与原始访问类型相对应的页面错误异常。
  6. 如果 i>0pte.ppn[i-1:0] ≠ 0,则这是一个未对齐的超级页面;停止并引发与原始访问类型相对应的页面错误异常。
  7. 如果 pte.a=0,或者原始内存访问是存储且 pte.d=0
  • 如果实现了 Svade 扩展,则停止并引发与原始访问类型相对应的页面错误异常。
  • 如果对 pte 的存储违反了 PMA 或 PMP 检查,则引发与原始访问类型相对应的访问错误异常。
  • 以原子方式执行以下步骤:
    • pte 与地址 a+va.vpn[i]×PTESIZE 处的 PTE 值进行比较。
    • 如果值匹配,则将 pte.a 设置为 1,如果原始内存访问是存储,则也设置 pte.d 为 1。
    • 如果比较失败,则返回步骤 2。
  1. 转换成功。转换后的物理地址如下:
  • pa.pgoff = va.pgoff
  • 如果 i>0,则这是一个超级页面转换,pa.ppn[i-1:0] = va.vpn[i-1:0]
  • pa.ppn[LE​​VELS-1:i] = pte.ppn[LE​​VELS-1:i]

本算法中对地址转换数据结构的所有隐式访问均使用宽度 PTESIZE 执行。

作者

我是洛佳,来自华中科技大学网络空间安全学院,RustSBI项目维护者。目前致力于向产业、研究界推广Rust语言、RISC-V及其系统软件应用。

评论区

写评论
aozima 2024-09-23 16:21

学习了,给大佬点赞!!

1 共 1 条评论, 1 页