存储布局

下一步是确保程序有正确的存储布局,目标平台才可以执行它。在我们的例子里,将使用一个虚拟的Cortex-M3微控制器:LM3S6965 。我们的程序是运行在设备上的唯一一个进程,因此它必须要负责初始化设备。

背景信息

Cortex-M 设备需要把向量表放在代码区的开始处。 向量表是一组指针;启动设备需要前两个指针,剩下来的指针都与异常有关。我们现在忽略它们。

链接器决定程序的最终存储布局,但是我们可以用链接器脚本对它进行一些控制。链接器提供的控制布局的精细度在sections层级。一个section是存储在连续的内存地址中的符号的集合。符号,依次,可以是数据(一个静态变量),或者指令(一个Rust函数)。

每一个符号有一个由编译器分配的名字。自Rust 1.28以来,Rust编译器分配给符号的名字都是这样的格式: _ZN5krate6module8function17he1dfc17c86fe16daE,其展开是 krate::module::function::he1dfc17c86fe16da 其中 krate::module::function 是函数或者变量的路径,he1dfc17c86fe16da 是某个哈希值。Rust编译器将把每个符号放进它自己的独有的section中;比如之前提到的符号将被放进一个名为 .text._ZN5krate6module8function17he1dfc17c86fe16daE 的section中。

这些编译器产生的符号和section名在不同的Rust编译器发布版中不保证是不变的。然而,语言让我们可以使用这些attributes控制符号名和放置的section:

  • #[export_name = "foo"] 把符号名设置成 foo
  • #[no_mangle] 意思是: 使用函数或者变量名(不是它的全路径)作为它的符号名。 #[no_mangle] fn bar() 将产生一个名为 bar 的符号。
  • #[link_section = ".bar"] 把符号放进一个名为 .bar 的section中。

有了这些attributes,我们可以稳定一个程序的ABI,并在链接器脚本中使用它。

Rust 部分

像上面提到的,对于Cortex-M设备,我们需要修改向量表的前两项。第一个,栈指针的初始值,只能使用链接器脚本修改。第二个,重置向量,需要在Rust代码中生成并使用链接器脚本放置到正确的地方。

重置向量是一个指向重置处理函数的指针。重置处理函数是在一个系统重启后,或者第一次上电后,设备将会执行的函数。重置处理函数总是硬件调用栈中的第一个栈帧;从它返回是未定义的行为,因为没有其它栈帧可以给它返回。通过让它变成一个divergent function,我们可以强调这个重置处理函数从来不会返回,divergent function的签名是 fn(/* .. */) -> !

#![allow(unused)]
fn main() {
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // can't return so we go into an infinite loop here
    loop {}
}

// The reset vector, a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

}

我们通过使用 extern "C" 告诉编译器下面的函数使用C的ABI,而不是Rust的ABI,后者不稳定,将函数变成硬件期望的格式。

为了从链接器脚本中指出重置处理函数和重置向量,我们需要它们有一个稳定的符号名因此我们使用 #[no_mangle] 。我们需要稍微控制下 RESET_VECTOR 的位置,因此我们将它放进一个已知的section中,.vector_table.reset_vector 。重置处理函数,Reset ,的确切位置不重要。我们只使用编译器默认生成的section 。

当遍历输入目标文件的列表时,这个链接器将忽略是内部链接的符号(也叫内部符号),因此我们需要让我们的符号变成外部链接。在Rust中让一个符号变成外部的方法,只有使它的相关项变成公共的(pub)和可到达的(在项和crate的根路径间没有私有模块)。

链接器脚本部分

一个最小的,将向量表放进正确的位置的链接器脚本如下所示。让我们仔细看看。

$ cat link.x
/* Memory layout of the LM3S6965 microcontroller */
/* 1K = 1 KiBi = 1024 bytes */
MEMORY
{
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

/* The entry point is the reset handler */
ENTRY(Reset);

EXTERN(RESET_VECTOR);

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));
  } > FLASH

  .text :
  {
    *(.text .text.*);
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx .ARM.exidx.*);
  }
}

MEMORY

链接器脚本的这个部分描述了目标中存储区块的大小和位置。有两个存储区块被定义了: FLASHRAM ;它们都与目标中可用的物理存储有关。这里用的值与LM3S6965微控制器有关。

ENTRY

这里我们指给链接器,符号名为 Reset 的重置处理函数是程序的 entry point 。链接器会主动丢弃未使用的部分。链接器认为entry point和从它处被调用的函数是被使用了的,所以链接器将不会丢弃它们。没有这一行,链接器将会丢弃 Reset 函数和所有它接下来会调用的函数。

EXTERN

链接器是懒惰的;一旦它们找到了从entry point处递归引用的所有的符号,它们将停止查看输入目标文件。EXTERN 强迫链接器去寻找 EXTERN 的参数,即使其它所有的被引用的符号都被找到后。因为thumb的一个规则,如果你想要一个符号,其没有被entry point调用,总是出现在输出的二进制项中,你应该使用结合了 KEEPEXTERN

SECTIONS

这部分描述了输入目标文件中的sections(也被称为input sections)要如何被安排进输出目标文件的sections(也被称为output sections)中的或者它们是否应该被丢弃。这里,我们定义两个输出sections:

  .vector_table ORIGIN(FLASH) : { /* .. */ } > FLASH

.vector_table 包含向量表且坐落于 FLASH 存储的开始处。

  .text : { /* .. */ } > FLASH

.text 包含程序的子程序且坐落于 FLASH 的某些位置。它的开始地址没有指定,但是链接器将把它放在先前的输出section,.vector_table 之后。

输出 .vector_table section包含:

    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

我们将把(调用)栈放在RAM的末尾(栈是完全递减的;它向着更小的地址增长)因此RAM的末尾地址将被用作栈指针(SP)值。链接器使用我们输入的RAM存储区块的信息,可以算出那个地址。

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));

接下来,我们使用 KEEP 去强迫链接器在初始的SP值之后插入所有的名为 .vector_table.reset_vector 的input sections 。位于那个section中的唯一一个符号是 RESET_VECTOR,所以这将可以把 RESEET_VECTOR 放在向量表的第二个位置。

输出的 .text section 包含:

    *(.text .text.*);

这包括所有的名为 .text.text.* 的input sections 。注意我们这里没有使用 KEEP 去让链接器丢弃不使用的部分。

最后,我们使用特殊的 /DISCARD/ section 去丢弃

    *(.ARM.exidx .ARM.exidx.*);

名为 .ARM.exidx.* 的input sections 。这些sections与异常处理有关,但是我们在恐慌时不进行栈展开,它们占用Flash的存储空间,因此我们就丢弃它们。

全放到一起去

现在我们能链接应用了。作为参考,这里是完整的Rust程序:

#![allow(unused)]
#![no_main]
#![no_std]

fn main() {
use core::panic::PanicInfo;

// The reset handler
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // can't return so we go into an infinite loop here
    loop {}
}

// The reset vector, a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}
}

我们不得不修改链接的过程让它使用我们的链接器脚本。通过传递 -C link-arg 标志给 rustc 来完成它。使用 cargo-rustc 或者 cargo-build 就可以完成了。

重要: 在运行这个命令之前确保你有 .cargo/config 文件,其在上个章节末尾处被添加了。

使用 cargo-rustc 子命令:

$ cargo rustc -- -C link-arg=-Tlink.x

或者你可以在 .cargo/config 中设置rustflags,并继续使用 cargo-build 子命令。我们将会使用后者因为它与cargo-binutils集成的更好。

# 将 .cargo/config 修改成这些内容
$ cat .cargo/config
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi] 部分告知这些标志只会被用于当交叉编译的目标是thumbv7m-none-eabi时。

检查它

现在让我们检查下输出的二进制项,确保存储布局跟我们想要的一样 (这需要 cargo-binutils):

$ cargo objdump --bin app -- -d --no-show-raw-insn

app:	file format elf32-littlearm

Disassembly of section .text:

<Reset>:
               	sub	sp, #0x4
               	movs	r0, #0x2a
               	str	r0, [sp]
               	b	0x10 <Reset+0x8>        @ imm = #-0x2
               	b	0x10 <Reset+0x8>        @ imm = #-0x4

这是 .text section的反汇编。我们看到重置处理函数,名为 Reset,位于 0x8 地址。

$ cargo objdump --bin app -- -s --section .vector_table

app:	file format elf32-littlearm
Contents of section .vector_table:
 0000 00000120 09000000                    ... ....

这是 .vector_table section 的内容。我们可以看到section开始于地址 0x0 且 section 的第一个字是 0x2001_0000 (objdump 的输出是小端模式)。这是初始的SP值,它与RAM的末尾地址匹配。第二个字是 0x9;这是重置处理函数的 thumb mode 的地址。当一个函数运行在thumb mode下,它的地址的第一位被设置成1 。

测试它

这个程序是一个有效的LM3S6965程序;我们可以在一个虚拟微控制器(QEMU)中执行它去测试。

$ # 这个程序将会阻塞住
$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -gdb tcp::3333 \
      -S \
      -nographic \
      -kernel target/thumbv7m-none-eabi/debug/app
$ # 在一个不同的终端上
$ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/app
Reading symbols from target/thumbv7m-none-eabi/debug/app...done.

(gdb) target remote :3333
Remote debugging using :3333
Reset () at src/main.rs:8
8       pub unsafe extern "C" fn Reset() -> ! {

(gdb) # the SP has the initial value we programmed in the vector table
(gdb) print/x $sp
$1 = 0x20010000

(gdb) step
9           let _x = 42;

(gdb) step
12          loop {}

(gdb) # next we inspect the stack variable `_x`
(gdb) print _x
$2 = 42

(gdb) print &_x
$3 = (i32 *) 0x2000fffc

(gdb) quit