我們的目標先訂在 arm cortex m3 的處理器, m3 處理器參考 arm 官方的開發文件 , 在 arm 處理器一上電的時候,會從記憶體位址 0x0 的地方讀取兩個值:
- 0x0 是初始的 stack pointer value。
- 0x4 是 reset exception handler。
處理器會拿了 stack pointer value 寫入 SP register,然後跳到 reset exception handler 的位址開始執行;其他種類的 exceptions handler,則是依序排列在後。
在裸機程式上,我們必須用 linker script
來分配各區塊要放在哪裡,包括編譯出來的 code、程式要用的 data,靜態變數等等。
除了 linker script,在 rust 原始碼,我們也需要用 attribute 的方式,指定某些特定的函式編譯過後的 symbol 名稱以及目標的區段,包括:
- #[no_mangle] 關閉函式、變數的名稱修飾 name mangling ,函式名字是什麼,編譯完就是什麼,而不是 rust 修飾過那一長串看不懂的文字。
- #[export_name = “foo”] 指定編譯完的符號名就是 foo。
- #[link_section = “.bar”] 指定編譯完的符號會放在 .bar 區段。
Rust reset_handler
stack pointer 初始化我們交給 linker script,先在 rust 加上 reset handler:
// src/main.rs
#[no_mangle]
pub fn reset_handler() -> ! {
let _x = 42;
loop {}
}
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: fn() -> ! = reset_handler;
RESET_VECTOR 就是指向 reset_handler 的 pointer,MCU 上電之後第一個執行的函式,從這個函式回傳會是未定義行為,
因為 stack 裡面也沒有上一個函式可以回傳。我們把 reset_handler 的回傳設為 !
表示不會回傳的函式
。
書裡的 reset_handler 跟 RESET_VECTOR 的型態是用 unsafe extern "C" fn() -> !
,指定是 unsafe 函式並用 C ABI,
但我試過用 Rust ABI 也沒問題,unsafe 目前還不需要,未來視函式實作有 unsafe behavior 的時候要加上去。
這裡我們實作了 reset_handler 函式,RESET_VECTOR 則是它的 pointer,指定這個 pointer 的區段為 .vector_table.reset_vector, 為了讓 linker 可以看到這個符號,函式與指標都要加上 pub。
linker script
先增檔案 linker.ld,並新增以下內容(可惡大概 linker script 太冷門 hugo 沒有上色):
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
/* The entry point is the reset handler */
ENTRY(reset_handler);
EXTERN(RESET_VECTOR);
SECTIONS
{
.vector_table ORIGIN(FLASH) :
{
/* initial Stack Pointer value */
LONG(ORIGIN(RAM) + LENGTH(RAM));
/* reset vector */
KEEP(*(.vector_table.reset_vector));
} > FLASH
.text :
{
*(.text .text.*);
} > FLASH
/DISCARD/ :
{
*(.ARM.exidx .ARM.exidx.*);
}
}
MEMORY
在 memory 指定硬體平台的 FLASH 和 RAM 分別的起始位置和長度,這會對應到實際的 FLASH 和記憶體位址。
ENTRY
指定 reset_handler 函式是我們程式的進入點,因為 linker 會激進地把沒用的東西都丟掉,沒加這行連 reset_handler 都會被 linker 丟掉,
linker:當我瘋起來連我自己都會怕用 ENTRY 保留 reset_handler 以及所有它呼叫到的函式。
EXTERN
linker 的運作是這樣,讀入所有輸入的 .o 檔之後,從 ENTRY 函式開始搜尋所有可見的符號,其餘沒被找到的就丟棄掉; EXTERN 告訴 linker 還有一個外部來的符號,讓 linker 除了 ENTRY 會也要保留 EXTERN 指定的符號。
sections
這部分切成三個區塊:.vector_table、.text 及被丟棄的其他。
.vector_table 我們指定必須從 ORIGIN(FLASH) 開始放起,內含兩個條目:
- 因為 ARM stack 的成長方向是往下長,由上面 MEMORY 區段指定的 RAM ,算出 RAM 的頂端,作為 initial stack pointer。
- 我們保留區段 .vector_table.reset_vector 的內容,也就是 RESET_VECTOR 的所在。
.text 是剩餘的程式碼,reset_handler 的實作會在這裡。
/DISCARD/ 就是剩餘不需要的部分,這是跟 ARM unwinding stack 相關的部分,把它們丟了。
config
編譯時可以由 cargo 指定 linker script,但跟 target 一樣太麻煩了,我們直接加在 .cargo/config
裡:
[build]
target = "thumbv7m-none-eabi"
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlinker.ld"]
指定在編譯 thumbv7m-none-eabi 的時候,使用 linker.ld 作為 linker script,這樣就能編譯成功啦。
結果
讓我們來檢視一下編譯結果,使用 objdump 反組譯程式,可以看到 reset_handler ,移動 sp 空出 _x 的空間,在裡面存入 42,然後開始無窮迴圈; 其實我個人比較好奇編譯器怎麼沒有把 _x = 42 這段給最佳化掉:
$ arm-none-eabi-objdump -d target/thumbv7m-none-eabi/debug/rt
target/thumbv7m-none-eabi/debug/rt: file format elf32-littlearm
Disassembly of section .text:
00000008 <reset_handler>:
8: b081 sub sp, #4
a: 202a movs r0, #42 ; 0x2a
c: 9000 str r0, [sp, #0]
e: e7ff b.n 10 <reset_handler+0x8>
10: e7fe b.n 10 <reset_handler+0x8>
另外是 section 的部分:
$ arm-none-eabi-objdump -s --section .vector_table target/thumbv7m-none-eabi/debug/rt
target/thumbv7m-none-eabi/debug/rt: file format elf32-littlearm
Contents of section .vector_table:
0000 00000120 09000000 ... ....
因為是 little endian,第一個 4 bytes 是 0x20010000,第二個是 0x00000009。
- 0x2001_0000 正是我們指定的 0x20000000 + 64K (2^16) 的位址。
- 0x0000_0009 則對應上面 reset_handler 的位址 0x00000008,LSB 的 1 表示跳過去之後會執行 arm thumb mode。
實際測試
可以使用 qemu 進行實際測試(用 Ctrl+A, X 來關掉 qemu):
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-gdb tcp::3333 \
-S -nographic \
-kernel target/thumbv7m-none-eabi/debug/rt
並使用另一個終端機進行除錯:
arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/rt
這個畫面怎麼文字還會溢出畫面啦 Orz。
漂亮。