一个 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 scriptrt 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 变量 BSSDATA 没有 01 值。反而,这些变量将是垃圾值。问题是在设备上电之后,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 。

1

必须在这使用链接器脚本符号这件事会让人疑惑且反直觉。可以在这里找到与这个怪现象有关的详尽解释。