上一回我們看到我們用 rust 寫了一些底層的 code,當然,這個寫法並不好看,有沒有更漂亮的寫法呢? 這篇參考了 rust cortex-m 的寫法, 以及強者我同學在 Google 大殺四方的小新大大分享的文 A guide to better embedded C++

簡單來說,我們要把所有的 register 都封裝起來,然後把原本的位元操作虛擬化(abstraction),讓我們可以從更高層次看待硬體的行為、對硬體操作; 這個實作必須要是零成本(zero cost abstraction),也就是說有/沒有虛擬化產生的 code 應該要一樣好,不會有多餘的 cost。

Register Abstraction

以上一篇的 Mstatus 為例,這是本來的 code,讀出 mstatus CSR 的值、將 D12..D11 設成 0b01 再寫回 mstatus 中。

let mut x: u64;
unsafe { asm!("csrr {}, mstatus", out(reg) x); }
x &= !(3 << 11);
x |= (1 << 11);
unsafe { asm!("csrw mstatus, {}", in(reg) x); }

這段 code 有個問題,在於它很難懂也難以維護,過一段時間不看文件再看 code 就看不懂了,讓我們參考 rust cortex-m 的作法:

pub struct Mstatus {
  bits: u64,
}

這個 struct Mstatus 封裝了 mstatus 這個 register,讓我們先新增一些 method 跟 read/write 兩個實際讀寫 register 的介面:

impl Mstatus {
  #[inline]
  fn bits() -> u64 { self.bits }

  #[inline]
  fn from_bits(bits: u64) -> Self {
    Self { bits }
  }

  #[inline]
  fn read() -> Self {
    let bits: u64;
    unsafe {
      asm!("csrr {}, mstatus", out(reg) bits);
    }
    Self { bits }
  }

  #[inline]
  fn write(self) {
    let bits = self.bits();
    unsafe {
      asm!("csrw mstatus, {}", in(reg) bits);
    }
  }
}

下一步我們要把設定 bits 的行為封裝起來,來看一下 mstatus 的 bit field 的設定:

mstatus

要設定的是 MPP D12..D11,共有三種(視處理器實作而定)不同的模式:

  • Machine Mode: 0b11
  • Supervisor Mode: 0b01
  • User Mode: 0b00

三種模式可以輕鬆虛擬化為 enum

enum Mode {
  MachineMode,
  SupervisorMode,
  UserMode,
}

設定 MPP 這個單一的行為也封裝進函式裡:

impl Mstatus {
  #[inline]
  pub fn get_mpp(self) -> Mode {
    if (self.bits >> 11) & 3 == 3 {
      Mode::MachineMode
    } else if (self.bits >> 11) & 3 == 1 {
      Mode::SupervisedMode
    } else {
      Mode::UserMode
    }
  }

  #[inline]
  pub fn set_mpp(&mut self, mode: Mode) {
    self.bits &= !(3 << 11);
    self.bits |= match mode {
      Mode::MachineMode =>    (3 << 11),
      Mode::SupervisedMode => (1 << 11),
      Mode::UserMode =>       (0 << 11),
    }
  }
}

設定 mstatus 的 code 就能改寫為:

let mut ms = mstatus::read();
ms.set_mpp(mstatus::Mode::SupervisedMode);
mstatus::write(ms);

意圖明顯不少。

Zero Cost Abstraction

當然,如此實作是否會帶來對應的成本?答案是不會。
這個不能看 debug code ,我真的反組譯,完全沒最佳化的 Rust build 簡直比豬血糕香菜披薩還要悲劇,一堆 jump 還 jump 到它的下一個指令, 上面的 read/set_mpp/write 真的都生成 function call。
讓我們直接反組譯 release code:

riscv64-unknown-elf-objdump -d target/riscv64imac-unknown-none-elf/release/rrxv6

80000022: 30002573            csrr  a0,mstatus
80000026: 75f9                  lui a1,0xffffe
80000028: 7ff5859b            addiw a1,a1,2047
8000002c: 8d6d                  and a0,a0,a1
8000002e: 6585                  lui a1,0x1
80000030: 8005859b            addiw a1,a1,-2048
80000034: 8d4d                  or  a0,a0,a1
80000036: 30051073            csrw  mstatus,a0

這個結果跟 gcc 在 xv6-riscv 上面編譯出來的結果,是完全一模一樣的(register 不一樣),函式都 inline 了, 就算你不加 #[inline] rustc/LLVM 還是會很自動的幫你 inline:

  1. csrr 讀出 mstatus
  2. lui + addiw => !(3 « 11)
  3. lui + addiw => (1 « 11) 雖然我不知道為什麼準備一個 2048 要這麼麻煩
  4. csrw mstatus 寫入

其他的 register

同樣的概念可以類推到其他的 register,例如 supervisor interrupt enable (sie):

sie

同樣經過上面的改寫,我們在 start 裡面會這樣寫,設定 D9, D5, D1 為 0b1:

let mut sie = sie::Sie::read();
sie.set_supervisor_enable(Interrupt::SoftwareInterrupt);
sie.set_supervisor_enable(Interrupt::TimerInterrupt);
sie.set_supervisor_enable(Interrupt::ExternalInterrupt);
sie.write();

最佳化之後,直接 or 546 = 0x222 無負擔,真是太神奇啦傑克:

80000056: 10402573            csrr  a0,sie
8000005a: 22256513            ori a0,a0,546
8000005e: 10451073            csrw  sie,a0

換個角度來說,我們這個寫法讓 code 好理解一些,人類腦袋少動一點,代價就是操壞編譯器,不過反正 rust 編譯本來就夠慢了再慢一點也沒差

start

全部的 register 都這樣做過一輪之後,rust 寫的 start 函式會像這樣:

use core::panic::PanicInfo;

use crate::{csrr, csrw};

use crate::param;
use crate::riscv::register::mstatus;
use crate::riscv::register::mepc;
use crate::riscv::register::tp;
use crate::riscv::register::hartid;
use crate::riscv::register::delegate;
use crate::riscv::register::sie;
use crate::riscv::register::interrupt::Interrupt;
use crate::riscv::register::pmp::{PMPConfigMode,PMPConfigAddress,PMPAddress,PMPConfig};

#[no_mangle]
fn start() -> ! {
    extern "Rust" {
        fn main() -> !;
    }

    /* Set M Previous Privilege mode to SupervisedMode
     * so mret will switch to supervise mode
     */
    let mut ms = mstatus::read();
    ms.set_mpp(mstatus::Mode::SupervisedMode);
    mstatus::write(ms);

    // Setup M exception program counter for mret
    let m_mepc = mepc::Mepc::from_bits(main as u64);
    mepc::write(m_mepc);

    // Disable paging for now
    let x = 0;
    csrw!("satp", x);

    // Delegate all interrupts and exceptions to supervisor mode
    delegate::medeleg::write(0xffff);
    delegate::mideleg::write(0xffff);

    // Enable interrupt in supervisor mode
    let mut sie = sie::Sie::read();
    sie.set_supervisor_enable(Interrupt::SoftwareInterrupt);
    sie.set_supervisor_enable(Interrupt::TimerInterrupt);
    sie.set_supervisor_enable(Interrupt::ExternalInterrupt);
    sie.write();

    // Store hart id in tp register, for cpuid()
    let hartid = hartid::Mhartid::read().bits();
    tp::write(hartid);

    // Setup PMP so that supervisor mode can access memory
    PMPAddress::write(0, (!(0)) >> 10);

    let mut config = PMPConfig::from_bits(0);
    config.set_config(PMPConfigMode::Read);
    config.set_config(PMPConfigMode::Write);
    config.set_config(PMPConfigMode::Exec);
    config.set_config(PMPConfigMode::Address(PMPConfigAddress::TOR));
    PMPConfig::write(config);

    // Switch to supervisor mode and jump to main
    unsafe { asm!("mret"); }

    // mret will jump into kernel, should not execute to here
    loop {}
}

這樣設定過後,我們終於成功進到 main 函式了。

結語

為什麼我第一篇 會說:

感覺自己挖了一個深不見底的坑

因為 riscv 不知道為什麼 csr 超級多,按照 spec 來看可以多到 4096 個,根本不是 Cortex-m 那少少幾個可以比的, 要我重寫 xv6 幾乎就是要我做一套 riscv library,把全部的 csr 都虛擬化。
雖然有測試了一個動用 rust macro 的寫法,能自動產生上述 read/write 等固定的部分,如果成功的話當然是能省下非常多手刻的部分, 但我看個別 register 的細部行為,還是會需要大量人工,那我還不累死…。