01-QEMU
QEMU
译文 · 基于 The Embedded Rust Book
QEMU
原文链接: https://doc.rust-lang.org/stable/embedded-book/start/qemu.html
我们将开始为 LM3S6965(一颗 Cortex-M3 微控制器)编写程序。
我们选择它作为初始目标,是因为它可用 QEMU 模拟,
因此本节无需折腾硬件,我们可以专注于工具链与开发流程。
重要
本教程中我们将用 “app” 作为项目名。
每当你看到 “app” 这个词,都应替换为你为自己项目选择的名称。或者,你也可以把项目命名为 “app”,从而避免替换。
创建非标准 Rust 程序
我们将使用 cortex-m-quickstart 项目模板从中生成新项目。生成的项目会包含一个极简应用:适合作为新嵌入式 Rust 应用的起点。此外,项目还会包含一个 examples 目录,内有若干独立应用,展示嵌入式 Rust 的一些关键功能。
使用 cargo-generate
先安装 cargo-generate
1
| cargo install cargo-generate
|
然后生成新项目
1
| cargo generate --git https://github.com/knurling-rs/app-template
|
1
2
3
| Project Name: app
Creating project called `app`...
Done! New project created /tmp/app
|
使用 git
克隆仓库
1
2
| git clone https://github.com/rust-embedded/cortex-m-quickstart app
cd app
|
然后填写 Cargo.toml 文件中的占位符
1
2
3
4
5
6
7
8
9
10
11
12
| [package]
authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
edition = "2018"
name = "{{project-name}}" # "{{project-name}}" -> "app"
version = "0.1.0"
# ..
[[bin]]
name = "{{project-name}}" # "{{project-name}}" -> "app"
test = false
bench = false
|
两者都不用
获取 cortex-m-quickstart 模板的最新快照并解压。
1
2
3
4
| curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
unzip master.zip
mv cortex-m-quickstart-master app
cd app
|
或者你可以浏览到 cortex-m-quickstart,点击绿色的 “Clone or download” 按钮,再点击 “Download ZIP”。
然后像 “使用 git” 版本的第二部分那样填写 Cargo.toml 中的占位符。
程序概览
为方便起见,以下是 src/main.rs 中最重要的部分:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {
// 你的代码写在这里
}
}
这个程序与标准 Rust 程序有些不同,让我们仔细看看。
#![no_std] 表示该程序不会链接到标准 crate std。相反,它会链接到其子集:core crate。
#![no_main] 表示该程序不会使用大多数 Rust 程序使用的标准 main 接口。选择 no_main 的主要原因是:在 no_std 语境下使用 main 接口需要 nightly。
use panic_halt as _;。该 crate 提供定义程序 panic 行为的 panic_handler。我们会在本书的 Panic 处理 一章中更详细地介绍。
#[entry] 是 cortex-m-rt crate 提供的属性,用于标记程序的入口点。由于我们不使用标准 main 接口,需要另一种方式标明程序入口,那就是 #[entry]。
fn main() -> !。我们的程序将是目标硬件上运行的唯一进程,因此我们不希望它结束!我们使用发散函数(divergent function)(函数签名中的 -> !)在编译期确保这一点。
交叉编译
首先我们需要目标微控制器(这里是 LM3S6965)的内存布局,否则构建会在链接镜像时失败。在项目根目录创建名为 memory.x 的文件,并粘贴以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| MEMORY
{
/* 注意 1 K = 1 KiBi = 1024 字节 */
/* TODO 调整这些内存区域以匹配你的设备内存布局 */
/* 这些值对应 LM3S6965,QEMU 能模拟的少数设备之一 */
FLASH : ORIGIN = 0x00000000, LENGTH = 256K
RAM : ORIGIN = 0x20000000, LENGTH = 64K
}
/* 调用栈将在此分配。 */
/* 栈为 full descending 类型。 */
/* 你可能想用此变量把调用栈与静态变量放在不同内存区域。下面是默认值 */
/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */
/* 可用此符号自定义 .text 段的位置 */
/* 若省略,.text 段会紧接在 .vector_table 段之后 */
/* 仅在向量表之后还存放某些配置的微控制器上需要 */
/* _stext = ORIGIN(FLASH) + 0x400; */
/* 将未初始化变量放入自定义 RAM 位置的示例。 */
/* 这假设你已在上方定义了 RAM2 区域,并在 Rust
源码中给要放在那里的数据加了属性 `#[link_section = ".ram2bss"]`。 */
/* 注意:该段不会被运行时零初始化! */
/* SECTIONS {
.ram2bss (NOLOAD) : ALIGN(4) {
*(.ram2bss);
. = ALIGN(4);
} > RAM2
} INSERT AFTER .bss;
*/
|
下一步是为 Cortex-M3 架构交叉编译程序。
若你知道编译目标($TRIPLE)应是什么,只需运行 cargo build --target $TRIPLE。幸运的是,模板中的 .cargo/config.toml 已给出答案:
1
| tail -n6 .cargo/config.toml
|
1
2
3
4
5
6
| [build]
# 从下列编译目标中任选其一
# target = "thumbv6m-none-eabi" # Cortex-M0 与 Cortex-M0+
target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 与 Cortex-M7(无 FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4F 与 Cortex-M7F(有 FPU)
|
要为 Cortex-M3 架构交叉编译,我们必须使用 thumbv7m-none-eabi。安装 Rust 工具链时不会自动安装该目标;若尚未添加,现在是很好的时机:
1
| rustup target add thumbv7m-none-eabi
|
由于 thumbv7m-none-eabi 编译目标已在 .cargo/config.toml 中设为默认,下面两条命令效果相同:
1
2
| cargo build --target thumbv7m-none-eabi
cargo build
|
检查
现在我们在 target/thumbv7m-none-eabi/debug/app 有了一个非原生 ELF 二进制。可用 cargo-binutils 检查它。
用 cargo-readobj 可以打印 ELF 头,以确认这是 ARM 二进制。
1
| cargo readobj --bin app -- --file-headers
|
注意:
--bin app 是检查 target/$TRIPLE/debug/app 处二进制的简写--bin app 也会在必要时(重新)编译该二进制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0x0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x405
Start of program headers: 52 (bytes into file)
Start of section headers: 153204 (bytes into file)
Flags: 0x5000200
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 19
Section header string table index: 18
|
cargo-size 可以打印二进制各链接器段的大小。
1
| cargo size --bin app --release -- -A
|
我们使用 --release 来检查优化后的版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| app :
section size addr
.vector_table 1024 0x0
.text 92 0x400
.rodata 0 0x45c
.data 0 0x20000000
.bss 0 0x20000000
.debug_str 2958 0x0
.debug_loc 19 0x0
.debug_abbrev 567 0x0
.debug_info 4929 0x0
.debug_ranges 40 0x0
.debug_macinfo 1 0x0
.debug_pubnames 2035 0x0
.debug_pubtypes 1892 0x0
.ARM.attributes 46 0x0
.debug_frame 100 0x0
.debug_line 867 0x0
Total 14570
|
ELF 链接器段速记
.text 包含程序指令.rodata 包含字符串等常量值.data 包含初始值不为零的静态分配变量.bss 也包含初始值为零的静态分配变量.vector_table 是我们用来存放向量(中断)表的非标准段.ARM.attributes 与 .debug_* 段包含元数据,烧录二进制时不会加载到目标上。
重要:ELF 文件包含调试信息等元数据,因此其磁盘大小并不能准确反映程序烧录到设备后所占空间。务必用 cargo-size 检查二进制的真实大小。
cargo-objdump 可用于反汇编二进制。
1
| cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hex
|
注意 若上述命令抱怨 Unknown command line argument,请参阅此 bug 报告:https://github.com/rust-embedded/book/issues/269
注意 该输出在你的系统上可能不同。新版本的 rustc、LLVM 与库可能生成不同的汇编。我们截断了部分指令以保持片段简短。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| app: file format ELF32-arm-little
Disassembly of section .text:
main:
400: bl #0x256
404: b #-0x4 <main+0x4>
Reset:
406: bl #0x24e
40a: movw r0, #0x0
< .. truncated any more instructions .. >
DefaultHandler_:
656: b #-0x4 <DefaultHandler_>
UsageFault:
657: strb r7, [r4, #0x3]
DefaultPreInit:
658: bx lr
__pre_init:
659: strb r7, [r0, #0x1]
__nop:
65a: bx lr
HardFaultTrampoline:
65c: mrs r0, msp
660: b #-0x2 <HardFault_>
HardFault_:
662: b #-0x4 <HardFault_>
HardFault:
663: <unknown>
|
运行
接下来,看看如何在 QEMU 上运行嵌入式程序!这次我们使用实际上会做些事情的 hello 示例。默认情况下,该示例使用 [defmt] 与 RTT 打印文本。
注意 defmt 是嵌入式 Rust 生态中广泛使用的第三方依赖(即非 core)。
为了在主机上读取并解码 defmt 产生的消息,我们需要把 RTT 传输输出切换为半主机(semihosting)。在真实硬件上这需要调试会话,但在使用 QEMU 时可以直接工作。
让我们切换依赖:
1
2
| cargo remove defmt-rtt
cargo add defmt-semihosting
|
打开 src/lib.rs,将 use defmt_rtt as _; 替换为 use defmt_semihosting as _;
现在可以构建该示例:
1
| cargo build --bin hello
|
输出二进制将位于 target/thumbv7m-none-eabi/debug/hello。
要在 QEMU 上运行该二进制,通常以下命令就够了:
1
2
3
4
5
6
| qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-kernel target/thumbv7m-none-eabi/debug/hello
|
在我们的情况下,由于使用了 defmt,主机将无法解码输出。我们需要 Ferrous Systems 的工具 qemu-run:
1
2
3
| git clone git@github.com:knurling-rs/defmt.git
cd defmt/qemu-run/
cargo run -- --machine lm3s6965evb ../qemu-rs/target/thumbv7m-none-eabi/debug/hello
|
该命令在打印文本后应成功退出(退出码 = 0)。在 *nix 上可用以下命令检查:
让我们拆解这条 QEMU 命令:
qemu-system-arm。这是 QEMU 模拟器。这类 QEMU 二进制有几个变体;这个做完整的 ARM 机器系统模拟,因而得名。
-cpu cortex-m3。告诉 QEMU 模拟 Cortex-M3 CPU。指定 CPU 型号可帮助捕获一些错误编译:例如,运行为带硬件 FPU 的 Cortex-M4F 编译的程序,会让 QEMU 在执行时出错。
-machine lm3s6965evb。告诉 QEMU 模拟 LM3S6965EVB——一块包含 LM3S6965 微控制器的评估板。
-nographic。告诉 QEMU 不要启动其 GUI。
-semihosting-config (..)。告诉 QEMU 启用半主机。半主机让被模拟设备除其它功能外,还能使用主机的 stdout、stderr 与 stdin,并在主机上创建文件。
-kernel $file。告诉 QEMU 在被模拟机器上加载并运行哪个二进制。
每次敲那么长的 QEMU 命令太费事了!我们可以设置自定义 runner 来简化流程。.cargo/config.toml 中有一段被注释掉、用于调用 QEMU 的 runner;让我们取消注释:
1
| head -n3 .cargo/config.toml
|
1
2
3
| [target.thumbv7m-none-eabi]
# 取消此行注释,使 `cargo run` 在 QEMU 上执行程序
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
|
该 runner 仅适用于我们的默认编译目标 thumbv7m-none-eabi。现在 cargo run 会编译程序并在 QEMU 上运行:
1
| cargo run --example hello --release
|
1
2
3
4
| Compiling app v0.1.0 (file:///tmp/app)
Finished release [optimized + debuginfo] target(s) in 0.26s
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/examples/hello`
Hello, world!
|
调试
调试对嵌入式开发至关重要。让我们看看如何进行。
调试嵌入式设备涉及远程调试,因为我们要调试的程序不会运行在运行调试器程序(GDB 或 LLDB)的机器上。
远程调试涉及客户端与服务器。在 QEMU 设置中,客户端是 GDB(或 LLDB)进程,服务器是同时运行嵌入式程序的 QEMU 进程。
本节我们将使用已编译好的 hello 示例。
调试的第一步是以调试模式启动 QEMU:
1
2
3
4
5
6
7
8
| qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-gdb tcp::3333 \
-S \
-kernel target/thumbv7m-none-eabi/debug/examples/hello
|
该命令不会向控制台打印任何内容,并会阻塞终端。这次我们多传了两个标志:
接下来在另一个终端启动 GDB,并告诉它加载该示例的调试符号:
1
| gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/hello
|
注意:根据你在安装章节安装的版本,你可能需要 gdb-multiarch 之外的其它 gdb,也可能是 arm-none-eabi-gdb 或就是 gdb。
然后在 GDB shell 中连接到在 TCP 端口 3333 上等待连接的 QEMU。
1
2
3
| Remote debugging using :3333
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
473 pub unsafe extern "C" fn Reset() -> ! {
|
你会看到进程已停住,程序计数器指向名为 Reset 的函数。那是复位处理函数:Cortex-M 内核启动时执行的内容。
注意:在某些环境下,gdb 可能不显示上面那样的 Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473,而打印类似如下警告:
core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254
src/libcore/num/bignum.rs: No such file or directory.
这是已知小问题。你可以安全地忽略这些警告,你很可能已经停在 Reset()。
该复位处理函数最终会调用我们的 main 函数。让我们用断点与 continue 命令直接跳到那里。要设置断点,先用 list 命令看看我们想在代码的哪里停下。
这将显示源码,来自文件 examples/hello.rs。
1
2
3
4
5
6
7
8
9
10
| 6 use panic_halt as _;
7
8 use cortex_m_rt::entry;
9 use cortex_m_semihosting::{debug, hprintln};
10
11 #[entry]
12 fn main() -> ! {
13 hprintln!("Hello, world!").unwrap();
14
15 // 退出 QEMU
|
我们想在 “Hello, world!” 之前加断点,也就是第 13 行。用 break 命令完成:
现在可以指示 gdb 运行到我们的 main 函数,使用 continue 命令:
1
2
3
4
| Continuing.
Breakpoint 1, hello::__cortex_m_rt_main () at examples\hello.rs:13
13 hprintln!("Hello, world!").unwrap();
|
我们现在接近打印 “Hello, world!” 的代码了。用 next 命令向前迈一步。
1
| 16 debug::exit(debug::EXIT_SUCCESS);
|
此时你应在运行 qemu-system-arm 的终端上看到 “Hello, world!”。
1
2
| $ qemu-system-arm (..)
Hello, world!
|
再次调用 next 将终止 QEMU 进程。
1
| [Inferior 1 (Remote target) exited normally]
|
现在可以退出 GDB 会话。