我們的最小程式現在能進到 reset_handler 了,但重要的是能進到使用者寫的 main 函式,不然這個 kernel 也沒用。 我們先把我們的 main.rs 改成 lib.rs,rt 編成 library 之後,類似 FreeRTOS 的感覺,再搭配使用者寫的 main.rs 編成完整的執行檔; 使用者寫的 main.rs 可以呼叫 kernel 提供的服務函式。

binary to library

mv src/main.rs src/lib.rs

新的 lib.rs 內容如下,移除開頭的 #![no_main],library 也不管有沒有 main 函式:

#[no_mangle]
pub unsafe extern "C" fn reset_handler() -> ! {
  extern "Rust" {
    fn main() -> !;
  }

  main()
}

pub static RESET_VECTOR: unsafe fn() -> ! = reset_handler;

我們宣告了 extern 的 main 函式並在 reset_handler 呼叫它,因為我們外部的 main 函式會被判定為 unsafe 函式, 因此 reset_handler 跟 RESET_VECTOR 的型別都要加上 unsafe。

build script

因為 linker 會搜尋 linker script 的位址,這個 linker script 是位在 kernel 而不是應用程式端, 這裡作者是使用 build script,把 linker.ld 加到編譯之中。

use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box<dyn Error>> {
  // build directory for this crate
  let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

  // extend the library search path
  println!("cargo:rustc-link-search={}", out_dir.display());

  // put `linker.ld` in the build directory
  File::create(out_dir.join("linker.ld"))?.write_all(include_bytes!("linker.ld"))?;

  Ok(())
}

現在使用者可以另外開一個 project app,並提供 main 函式的實作,Cargo.toml dependancy 新增 rt:

[dependencies]
rt = { path = "../rt" }

把我們寫好的 cargo config 複製過來:

cp -r ../rt/.cargo .

本來 main.rs 的實作移到 app 的 main.rs

#![no_std]
#![no_main]

extern crate rt;

#[no_mangle]
pub fn main() -> ! {
    let _x = 42;

    loop {}
}

編譯測試

一樣在 app 內呼叫 cargo build 就可以編譯完成,用 objdump 觀看編譯結果:

arm-none-eabi-objdump -d target/thumbv7m-none-eabi/debug/app

target/thumbv7m-none-eabi/debug/app:     file format elf32-littlearm

Disassembly of section .text:

00000008 <main>:
   8:    b081          sub    sp, #4
   a:    202a          movs    r0, #42    ; 0x2a
   c:    9000          str    r0, [sp, #0]
   e:    2000          movs    r0, #0
  10:    b001          add    sp, #4
  12:    4770          bx    lr

00000014 <reset_handler>:
  14:    b580          push    {r7, lr}
  16:    466f          mov    r7, sp
  18:    f7ff fff6     bl    8 <main>
  1c:    defe          udf    #254    ; 0xfe

type safety

原書裡有一個小章節是在講 main 函式的 type safety issue, 現在的寫法 app main 如果型別不是 fn() -> !,編譯仍然會通過,這會造成未定義行為。
雖然為什麼編譯會過這件事實在很謎,但原書提供了一個 macro 的解法,讓 main 函式在定義時透過 macro 進行一次型別檢查。 不過它的做法會讓 reset_handler 到 main 之間多一次函式呼叫,變成 reset_handler -> main -> main mangled, 讓 rust 沒辦法像 C 這樣的簡潔,因此我這裡先略過這個實作。

exception handler

在 Rust 上寫 Reset Handler,把 main 分離之後,這篇來處理其他的 handler 的部分,其實大同小異,不知道為什麼作者把這章放第四章了。

來看一下 Cortex M3 的圖,不知道為什麼官網的圖解析度超低, 這張是從其他網站取得的:

cortexM3_resetvector

從 Reset 之後一路往上,就是 NMI(non-maskable interrupt)、Hard Fault、Memory Management Fault 等等, 也就是當這些 interrupt 發生時,處理器會終止目前執行的行程,從 reset vector 拿出處理函式的位址並跳過去執行,結束後再回到原本的行程。

我們希望 rt library 提供預設的實作,使用者若提供自己的實作則用使用者寫的:

extern "Rust" {
  fn nmi();
  fn hard_fault();
  fn mem_manage();
  fn bus_fault();
  fn usage_fault();
  fn svcall();
  fn pendsv();
  fn systick();
}

pub union Vector {
  reserved: u32,
  handler: unsafe fn(),
}

#[link_section = ".vector_table.exceptions"]
#[no_mangle]
pub static EXCEPTIONS: [Vector; 14] = [
  Vector { handler: nmi },
  Vector { handler: hard_fault },
  Vector { handler: mem_manage },
  Vector { handler: bus_fault },
  Vector { handler: usage_fault, },
  Vector { reserved: 0 },
  Vector { reserved: 0 },
  Vector { reserved: 0 },
  Vector { reserved: 0 },
  Vector { handler: svcall },
  Vector { reserved: 0 },
  Vector { reserved: 0 },
  Vector { handler: pendsv },
  Vector { handler: systick },
];

#[no_mangle]
pub fn default_exception_handler() {
  loop {}
}

先定義好 extern 的函式,並用 union Vector 讓函式與佔位的 u32 共用空間,以實作 reserved 的 interrupt;書裡的 extern 函式是用 C ABI 但目前應該沒差。 exception hander 就是一個長度 14,從 NMI 到 systick 的陣列,分段到區段 .vector_table.exceptions。 最後是預設的 exception handler,會進到無窮迴圈裡面。

linker script

在 linker script ,reset_vector 之後就接著 exception handler,把 .vector_table.exceptions 區段放在這裡。

 .vector_table ORIGIN(FLASH) :
{
  LONG(ORIGIN(RAM) + LENGTH(RAM));
  KEEP(*(.vector_table.reset_vector));

  KEEP(*(.vector_table.exceptions));
} > FLASH

另外我們用 PROVIDE 把各 exception handler 指定給 default_exception_handler,只要使用者沒有定義,就會由 linker 來提供符號定義。

PROVIDE(nmi = default_exception_handler);
PROVIDE(hard_fault = default_exception_handler);
PROVIDE(mem_manage = default_exception_handler);
PROVIDE(bus_fault = default_exception_handler);
PROVIDE(usage_fault = default_exception_handler);
PROVIDE(svcall = default_exception_handler);
PROVIDE(pendsv = default_exception_handler);
PROVIDE(systick = default_exception_handler);

測試

因為 QEMU 不能模擬 HardFault 或是 Memory access fault,我們只能切去 nightly version,並用 core::intrinsics 來產生一個 trap。

#![feature(core_intrinsics)]
#![no_main]
#![no_std]

extern crate rt;

use core::intrinsics;

#[no_mangle]
pub fn main() -> u32 {  !
  intrinsics::abort();
}

結果與除錯

使用 objdump 觀察反組譯的結果,main 會由 intrinsics::abort 發動一個 udf (permanently undefined) 指令,形成一個 hardfault exception。

arm-none-eabi-objdump -d --no-show-raw-insn target/thumbv7m-none-eabi/debug/app
00000040 <main>:
  40:    udf    #254    ; 0xfe
  42:    udf    #254    ; 0xfe

00000044 <reset_handler>:
  44:    push    {r7, lr}
  46:    mov    r7, sp
  48:    bl    40 <main>
  4c:    udf    #254    ; 0xfe

0000004e <default_exception_handler>:
  4e:    b.n    50 <default_exception_handler+0x2>
  50:    b.n    50 <default_exception_handler+0x2>

除錯的話也會看到程式走入 default_exception_handler:

debug_exception_handler

使用 objdump 觀察 .vector_table section 的部分,除了 reset handler 是 0x44 之外,其他都指向 0x4e 的 default_exception_handler;reserved 的部分就留下 0x0 的 reserved word。

arm-none-eabi-objdump -s --section .vector_table target/thumbv7m-none-eabi/debug/app

target/thumbv7m-none-eabi/debug/app:     file format elf32-littlearm

Contents of section .vector_table:
 0000 00000120 45000000 4f000000 4f000000  ... E...O...O...
 0010 4f000000 4f000000 4f000000 00000000  O...O...O.......
 0020 00000000 00000000 00000000 4f000000  ............O...
 0030 00000000 00000000 4f000000 4f000000  ........O...O...

若使用者自行定義函式 hard_fault,就會看到 Hard Fault 的 exception handler 被設定給使用者定義的 hard_fault 函式了。