一个 main
接口
我们现在有了一个可以工作的最简的程序了,但是我们需要用一种方式将它打包起来,让终端用户可以在其上搭建安全的程序。在这部分,我们将实现一个 main
接口,它用起来就像我们在一个标准的Rust程序中所用的那样。
首先,我们将把我们的binary crate转换成一个library crate:
$ mv src/main.rs src/lib.rs
然后把它重命名为 rt
,其表示"runtime" 。
$ sed -i s/app/rt/ Cargo.toml
$ head -n4 Cargo.toml
[package]
edition = "2018"
name = "rt" # <-
version = "0.1.0"
第一个改变是让重置处理函数调用一个外部的 main
函数:
$ head -n13 src/lib.rs
#![no_std] use core::panic::PanicInfo; // CHANGED! #[no_mangle] pub unsafe extern "C" fn Reset() -> ! { extern "Rust" { fn main() -> !; } main() }
我们也去掉了 #![no_main]
attribute,因为它对library crates没有影响。
到了这里出现了一个正交问题:
rt
库应该提供一个标准的恐慌时行为吗, 或者它不应该提供一个#[panic_handler]
函数而让终端用户去选 择恐慌时行为吗?这个文档将不会深入这个问题,而且为了方便,在rt
crate 中留了一个空的#[panic_handler]
函数。然而,我们想告诉读者还存在其它选择。
第二个改变涉及到给应用crate提供我们之前写的链接器脚本。链接器将会在库搜索路径(-L
)和调用它的文件夹中寻找链接器脚本。应用crate不应该需要将link.x
的副本挪来挪去所以我们将使用一个build script让 rt
crate 将链接器脚本放到库搜索路径中。
$ # 在`rt`的根目录中生成一个带有这些内容的 build.rs 文件
$ cat build.rs
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 `link.x` in the build directory File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?; Ok(()) }
现在用户可以写一个暴露了main
符号的应用了,且将它链接到rt
crate上。rt
将负责给予程序正确的存储布局。
$ cd ..
$ cargo new --edition 2018 --bin app
$ cd app
$ # 修改Cargo.toml将`rt` crate包含进来作为一个依赖
$ tail -n2 Cargo.toml
[dependencies]
rt = { path = "../rt" }
$ # 拷贝整个config文件,它设置了一个默认的目标并修改了链接器命令
$ cp -r ../rt/.cargo .
$ # 把 `main.rs` 的内容改成
$ cat src/main.rs
#![no_std] #![no_main] extern crate rt; #[no_mangle] pub fn main() -> ! { let _x = 42; loop {} }
反汇编的结果将是相似的,除了现在包含了用户的main
函数。
$ cargo objdump --bin app -- -d --no-show-raw-insn
app: file format elf32-littlearm
Disassembly of section .text:
<main>:
sub sp, #0x4
movs r0, #0x2a
str r0, [sp]
b 0x10 <main+0x8> @ imm = #-0x2
b 0x10 <main+0x8> @ imm = #-0x4
<Reset>:
push {r7, lr}
mov r7, sp
bl 0x8 <main> @ imm = #-0x12
trap
把它变成类型安全的
main
接口工作了,但是它容易出错。比如,用户可以把main
写成一个non-divergent function,它们将不会带来编译时错误,但会带来未定义的行为(编译器将会错误优化这个程序)。
我们通过暴露一个宏给用户而不是符号接口可以添加类型安全性。在 rt
crate 中,我们可以写这个宏:
$ tail -n12 ../rt/src/lib.rs
#![allow(unused)] fn main() { #[macro_export] macro_rules! entry { ($path:path) => { #[export_name = "main"] pub unsafe fn __main() -> ! { // type check the given path let f: fn() -> ! = $path; f() } } } }
然后应用的作者可以像这样调用它:
$ cat src/main.rs
#![no_std] #![no_main] use rt::entry; entry!(main); fn main() -> ! { let _x = 42; loop {} }
如果作者把main
的签名改成non divergent function,比如 fn
,将会出现一个错误。
main之前的工作
rt
看起来不错了,但是它的功能不够完整!用它编写的应用不能使用 static
变量或者字符串字面值,因为 rt
的链接器脚本没有定义标准的.bss
,.data
和 .rodata
sections 。让我们修复它!
第一步是在链接器脚本中定义这些sections:
$ # 只展示文件的一小块
$ sed -n 25,46p ../rt/link.x
.text :
{
*(.text .text.*);
} > FLASH
/* NEW! */
.rodata :
{
*(.rodata .rodata.*);
} > FLASH
.bss :
{
*(.bss .bss.*);
} > RAM
.data :
{
*(.data .data.*);
} > RAM
/DISCARD/ :
它们只是重新导出input sections并指定每个output section将会进入哪个内存区域。
有了这些改变,下面的程序可以编译:
#![no_std] #![no_main] use rt::entry; entry!(main); static RODATA: &[u8] = b"Hello, world!"; static mut BSS: u8 = 0; static mut DATA: u16 = 1; fn main() -> ! { let _x = RODATA; let _y = unsafe { &BSS }; let _z = unsafe { &DATA }; loop {} }
然而如果你在真正的硬件上运行这个程序并调试它,你将发现到达main
时,static
变量 BSS
和 DATA
没有 0
和 1
值。反而,这些变量将是垃圾值。问题是在设备上电之后,RAM的内容是随机的。如果你在QEMU中运行这个程序,你将看不到这个影响。
在目前的情况下,如果你的程序在对static
变量执行一个写入之前,读取任何 static
变量,那么你的程序会出现未定义的行为。让我们通过在调用main
之前初始化所有的static
变量来修复它。
我们需要修改下链接器脚本去进行RAM初始化:
$ # 只展示文件的一块
$ sed -n 25,52p ../rt/link.x
.text :
{
*(.text .text.*);
} > FLASH
/* CHANGED! */
.rodata :
{
*(.rodata .rodata.*);
} > FLASH
.bss :
{
_sbss = .;
*(.bss .bss.*);
_ebss = .;
} > RAM
.data : AT(ADDR(.rodata) + SIZEOF(.rodata))
{
_sdata = .;
*(.data .data.*);
_edata = .;
} > RAM
_sidata = LOADADDR(.data);
/DISCARD/ :
让我们深入下细节:
_sbss = .;
_ebss = .;
_sdata = .;
_edata = .;
我们将符号关联到.bss
和.data
sections的开始和末尾地址,我之后将会从Rust代码中使用。
.data : AT(ADDR(.rodata) + SIZEOF(.rodata))
我们将.data
section的加载内存地址(LMA)设置成.rodata
section的末尾处。.data
包含非零值的static
变量;.data
section的虚拟内存地址(VMA)在RAM中的某处 -- 这是static
变量所在的地方。这些static
变量的初始值,然而,必须被分配在非易失存储中(Flash);LMA是Flash中存放这些初始值的地方。
_sidata = LOADADDR(.data);
最后,我们将一个符号和.data
的LMA关联起来。我们可以从Rust代码引用我们在链接器脚本中生成的符号。这些符号的地址1指向 .bss
和 .data
sections的边界处。
下面展示的是更新了的重置处理函数:
$ head -n32 ../rt/src/lib.rs
#![no_std] use core::panic::PanicInfo; use core::ptr; #[no_mangle] pub unsafe extern "C" fn Reset() -> ! { // NEW! // Initialize RAM extern "C" { static mut _sbss: u8; static mut _ebss: u8; static mut _sdata: u8; static mut _edata: u8; static _sidata: u8; } let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize; ptr::write_bytes(&mut _sbss as *mut u8, 0, count); let count = &_edata as *const u8 as usize - &_sdata as *const u8 as usize; ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count); // Call user entry point extern "Rust" { fn main() -> !; } main() }
现在终端用户可以直接地和间接地使用static
变量而不会导致未定义的行为了!
我们上面展示的代码中,内存的初始化按照一种逐字节的方式进行。可以强迫
.bss
和.data
section对齐,比如,四个字节。然后Rust代码可以利用这个事实去执行逐字的初始化而不需要对齐检查。如果你想知道这是如何做到的,看下cortex-m-rt
crate 。
必须在这使用链接器脚本符号这件事会让人疑惑且反直觉。可以在这里找到与这个怪现象有关的详尽解释。