存储布局
下一步是确保程序有正确的存储布局,目标平台才可以执行它。在我们的例子里,将使用一个虚拟的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
链接器脚本的这个部分描述了目标中存储区块的大小和位置。有两个存储区块被定义了: FLASH
和 RAM
;它们都与目标中可用的物理存储有关。这里用的值与LM3S6965微控制器有关。
ENTRY
这里我们指给链接器,符号名为 Reset
的重置处理函数是程序的 entry point 。链接器会主动丢弃未使用的部分。链接器认为entry point和从它处被调用的函数是被使用了的,所以链接器将不会丢弃它们。没有这一行,链接器将会丢弃 Reset
函数和所有它接下来会调用的函数。
EXTERN
链接器是懒惰的;一旦它们找到了从entry point处递归引用的所有的符号,它们将停止查看输入目标文件。EXTERN
强迫链接器去寻找 EXTERN
的参数,即使其它所有的被引用的符号都被找到后。因为thumb的一个规则,如果你想要一个符号,其没有被entry point调用,总是出现在输出的二进制项中,你应该使用结合了 KEEP
的 EXTERN
。
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