《The embedonomicon》学习笔记

和原教程的一些差别

  • 重写了第五章,因为!asm!global_asm曾经进入stable,原文那种形式切实太繁琐。
  • Rust Edition原书采纳2018,本文采纳2021。依照官网的说法,Rust Edition和Rust的版本应该是独立的,基于任何一个Rust Edition的代码应该都能够在最新版本的Rust编译器上编译。(All Rust code, regardless of edition, is ultimately compiled to the same internal representation within the compiler.
  • 原文中是通过cargo子命令的形式去调用cargo-binutils,例如应用cargo size而不是rust-size。集体感觉这样尽管简洁然而不够直观,因而我将应用间接调用的形式调用cargo-binutils,将编译和查看二进制两个过程分离出来。
  • 所有命令都是在powershell下运行的

【0】前言

《The embedonomicon》蕴含了以下内容:

  • 率领你从头开始构建#![no_std]应用程序(裸机编程)
  • 开发基于特定架构的Cortex-M微控制器的应用程序,并一直迭代这个过程

目标

通过本文,你会学到:

  • 如何构建一个#![no_std]程序(裸机编程,不依赖OS)。
  • 如何管制Rust程序的内存布局(在链接阶段确定),这须要接触链接器、链接脚本和一些跟ABI相干的Rust feature
  • 如何实现能够被动态笼罩(statically overridden)的函数,动态笼罩意味着没有运行时耗费

前置条件

因为本文是基于win10的,所以只介绍win10下相干工具的装置过程

  • Rust相干

    • Rust编译器:原书采纳1.31,本文采纳1.60.0
    • cargo-binutils:间接应用最新
  • QEMU模拟器

    采纳7.0.0版本(qemu-w64-setup-20220419.exe)

  • gdb工具

    应用gcc-arm-none-eabi-10.3-2021.10-win32或arm-gnu-toolchain-11.3.rel1-mingw-w64-i686-arm-none-eabi。

    留神不要应用gcc-arm-11.2-2022.02-mingw-w64-i686-arm-none-eabi,这个版本依赖过期的32位python2.7,在Win10上很难筹备相干的运行环境给它,相干探讨见这里

【1】最小的#![no_std]程序

这个章节次要是写一个#![no_std]版的hello world程序,残缺代码见:https://github.com/youth7/the-embedonomicon-note/tree/01-the-smallest-nostd-program

std和core

std

std意味着程序运行在通用OS上(通用OS通常提供线程,过程,文件系统,socket等API),它封装了OS形象层的拜访形式。除此还提供其它性能,包含:

  • 栈溢出爱护
  • 命令行参数解决
  • main函数的调用前生成主线程

core

#![no_std]意味着程序将依赖core而不是stdcorestd的子集,只提供语言的根本类型(如float、string、slice)以及裸露处理器的个性(如一些原子操作和SIMD指令)。core不提供规范的运行时,没有堆调配。因而,#![no_std]通常是零碎中的第一或惟一的程序,它能做一般Rust程序所不能的事件,例如:

  • 内核或者OS
  • 固件
  • bootloader

例子

当初咱们来看看这个程序

#![no_main]//通知编译器不要应用main函数作为程序的入口,因为main对运行时有要求#![no_std]//下面曾经解释过use core::panic::PanicInfo;#[panic_handler]//自定义程序奔溃时的行为,因为不足运行时的起因这个必须本人定义fn panic(_panic: &PanicInfo<'_>) -> ! {    loop {}}

顺次应用以下2个命令将源码编译为指标文件,并查看其中的符号

# 编译代码,--target是cargo命令的参数,指定了编译产物的指标平台,详情参考cargo book# --emit指定了编译产物的文件类型,详情参考rustc bookcargo rustc --target thumbv7m-none-eabi -- --emit=obj# 列出指标文件中的符号rust-nm  target/thumbv7m-none-eabi/debug/deps/app-*.o

此时的输入为

00000000 T rust_begin_unwind

如果你装置了wsl2的话,能够用其它命令来查看符号,例如应用readelf来查看

readelf -s ./target/thumbv7m-none-eabi/debug/deps/*.o

控制台输入:

Symbol table '.symtab' contains 10 entries:   Num:    Value  Size Type    Bind   Vis      Ndx Name     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS 49d4frxgydk7ies3     2: 00000000     0 SECTION LOCAL  DEFAULT    3     3: 00000000     0 NOTYPE  LOCAL  DEFAULT    3 $t.0     4: 00000000     0 SECTION LOCAL  DEFAULT    6     5: 00000000     0 SECTION LOCAL  DEFAULT    7     6: 00000000     0 SECTION LOCAL  DEFAULT   11     7: 00000000     0 SECTION LOCAL  DEFAULT   18     8: 00000000     0 SECTION LOCAL  DEFAULT   20     9: 00000001     8 FUNC    GLOBAL HIDDEN     3 rust_begin_unwind

或者应用nm来查看

nm ./target/thumbv7m-none-eabi/debug/deps/*.o

控制台输入:

00000000 t $t.000000001 T rust_begin_unwind

#[panic_handler]eh_personality

刚开始的时候感觉两者都是程序解体时调用的,它们有什么区别?文档是这样解释的:

Unwinding the Stack or Aborting in Response to a Panic

By default, when a panic occurs, the program starts unwinding, which means Rust walks back up the stack and cleans up the data from each function it encounters. However, this walking back and cleanup is a lot of work. Rust, therefore, allows you to choose the alternative of immediately aborting, which ends the program without cleaning up. Memory that the program was using will then need to be cleaned up by the operating system. If in your project you need to make the resulting binary as small as possible, you can switch from unwinding to aborting upon a panic by adding panic = 'abort' to the appropriate [profile] sections in your Cargo.toml file. For example, if you want to abort on panic in release mode, add this:

[profile.release]panic = 'abort'

可见前者是程序panic时候调用的,而运行panic处理程序时候,能够抉择是unwind stack或者间接abort。我集体的疑难是这两个设定是互相独立的吗,还是说#[panic_handler]指向的实现外面蕴含了eh_personality

但无论如何,在本文的运行环境下并不需要对eh_personality做任何批改

另:eh应该是exception handling的缩写,见这里的探讨

【2】内存布局

这个章节次要是介绍如何通过各种工具来调整内存布局,使得生成的二进制程序可能在裸机上运行,并通过gdb来验证生成的程序是否正确,残缺代码见:https://github.com/youth7/the-embedonomicon-note/tree/02-memory-layout

这一章次要是讲如何生成正确构造的二进制文件,使其可能在特定架构的CPU上运行。要实现这个指标就必须:

  • 理解CPU对二进制文件构造的要求
  • 编写Rust代码
  • 通过链接器调整二进制文件构造

理解CPU对二进制文件构造的要求

教程是基于Cortex-M3微控制器LM3S6965编写的,对于它的技术细节能够查阅文档,目前对咱们来说最重要的是:

初始化vector table 前两个指针的值

vector_table是一个指针数组,外面每个元素(vector)都指向了某个内存地址(大部分是异样处理函数的起始地址),对于它的具体构造能够看这里。对本教程来说最重要的是前2个指针:

  • 第1个:(ISP:Initial SP value)栈顶指针,用于初始化栈
  • 第2个:(Reset)指向了reset handler,它是一个函数,会在零碎被重置或者加电时运行(同时也是程序栈帧外面的第一帧)。

所以咱们须要做的事件就是:

  1. 在Rust代码中编写reset handler函数,并将其裸露进去以供链接脚本应用
  2. 联合步骤1,通过链接脚本将vector table前两个元素的值设置好

vector_table属于异样模型的一部分,外面每个vector指向的对象都跟异样解决相干。Initial SP valueReset其实也能够了解为当零碎因异样重启时须要如何初始化零碎零碎。

零碎重置的时候,vector_table的默认地址是0x00000000,能够通过批改VTOR(Vector Table Offset Register)来调整vector_table的默认地址。

编写Rust代码

后续会用到一些跟编译相干的attribute,这里先对立介绍

  • #[export_name = "foo"] 指定源码中的某个变量、函数编译后的符号名为 foo.
  • #[no_mangle] 应用变量、函数在源码中的名称作为符号名
  • #[link_section = ".bar"] 将符号搁置到名为 .bar的节中

首先编写reset handler函数,它是零碎栈帧中的第一个帧,从第一个帧返回是一种未定义行为,因而这个函数永远不能退出,即它必须是一个发散函数

#[no_mangle]pub unsafe extern "C" fn Reset() -> ! {    let _x = 42;    //永不退出的发散函数    loop {}}//阐明这个函数须要编译到名称为.vector_table.reset_vector的这个节中,这个节在前面会被援用到#[link_section = ".vector_table.reset_vector"]//通知编译器不要用Rust的命名规定为Reset重命名,保留原来的名称就好#[no_mangle]//RESET_VECTOR就是vector table中的第二个元素,指向了异样处理函数Reset//其实这里不太明确为何要多用一个变量RESET_VECTOR而不是间接应用Reset函数pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

通过链接器调整二进制文件构造

在根目录下创立一个名为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.*);  }}

这个脚本初始化了vector table的前两项并将它搁置到正确的中央,对于链接脚本的残缺语法请参考https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_toc.html#TOC16,这里咱们只简要阐明一下各个局部的作用。

MEMORY

定义了可用的存储空间的地址和大小。此处定义了两个可用的存储空间:

  • FLASH:定义了微控制器上的闪存起始和大小
  • RAM:定义为微控制器上的内存起始和大小

具体的值是依据LM3S6965的技术文档

对于FLASH 、RAM、ROM的区别请看这里

ENTRY

定义了程序的入口为Rust代码中定义的函数Reset。链接器会摈弃未应用的节,如果脚本中没有这一行则链接器会认为Reset未被应用从而摈弃之。

Reset就是在零碎重置时运行的第一个函数,因而指定它为入口也是正当的。

EXTERN

链接器会从entry命令指定的函数开始,从指标文件中递归搜寻所有用到的符号,一旦所有合乎解析实现了就进行,即便此时还有指标文件未被搜寻。EXTERN的作用是强制链接器去持续解析被EXTERN作为参数的符号,例如本节中的RESET_VECTOR

其实不太明确为何要多用一个变量RESET_VECTOR而不是间接应用Reset这个符号,Reset曾经蕴含了足够的信息用来填充vector table( 在下一小结咱们会通过查看符号表来印证这个论断),惟一的可能性是应用RESET_VECTOR会使得链接脚本更加容易编写?

留神:文中对于EXTERN的目标和文档中的记录在字面上不是完全一致,这里先记录几个关键点:

  • ELF中undefined symbol的相干常识,能够参考这里。
  • 文档说EXTERN-u等价,-u中有这么一句:If this option is being used to force additional modules to be pulled into the link,这可能就是文章中应用EXTERN的目标

SECTIONS

定义了如何将input指标文件中的节(section)输入到output指标文件中。此处在output指标文件中定义了3个节:.vector_table.text/DISCARD/

  • .vector_table被输入到FLASHFLASHMEMORY中定义)中,它的前两个元素的值别离是LONG(ORIGIN(RAM) + LENGTH(RAM))KEEP(*(.vector_table.reset_vector)),前者就是栈顶(SP)的值(栈的增长方向是从高地址往低地址),后者将所有名为.vector_table.reset_vector的节都搁置到SP后,因为Rust代码中只有一个.vector_table.reset_vector节且这个节中只有RESET_VECTOR这个符号,因而这条语句的作用相当于将RESET_VECTOR作为vector table的第2个元素。留神KEEP关键字是必须的,对于KEEP的具体阐明看这里。
  • .text也是被输入到FLASH,它紧跟着.vector_table
  • /DISCARD/:这是一个特地的节名称,所有被搁置到这个节的内容都会被摈弃

查看可执行文件

因为采纳自定义的链接脚本,所以必须通知编译器应用这个脚本,这能够通过批改.cargo/config文件来实现,新建.cargo文件夹并创立文件config文件,在外面增加以下内容

# 针对这个target应用链接脚本[target.thumbv7m-none-eabi]rustflags = ["-C", "link-arg=-Tlink.x"]# 指定编译的target,批改这里之后就无需在命令行传递--target参数了[build]target = "thumbv7m-none-eabi"

先应用cargo build --bin app编译我的项目。胜利后应用以下命令来查看后果是否合乎预期

# 应用rust-objdump去查看最终生成的可执行文件中的汇编代码rust-objdump -d --no-show-raw-insn .\target\thumbv7m-none-eabi\debug\app

此时输入为:

.\target\thumbv7m-none-eabi\debug\app:  file format elf32-littlearmDisassembly of section .text:00000008 <Reset>:       8:       sub     sp, #4       a:       movs    r0, #42       c:       str     r0, [sp]       e:       b       0x10 <Reset+0x8>        @ imm = #-2      10:       b       0x10 <Reset+0x8>        @ imm = #-4

能够看到Reset位于0x00000008,这和预期是一样的。因为.vector_table的大小是8个字节,它前面就是.text节,而函数Reset又是.text节里惟一的内容。

保险起见咱们还须要检查一下vector table,应用以下命令:

# 应用rust-objdump去查看最终生成的可执行文件中指定节的具体内容rust-objdump -s --section .vector_table .\target\thumbv7m-none-eabi\debug\app

此时的输入为

.\target\thumbv7m-none-eabi\debug\app:  file format elf32-littlearmContents of section .vector_table: 0000 00000120 09000000                    ... ....

vector table第1个元素是的值由LONG(ORIGIN(RAM) + LENGTH(RAM))决定,这个表达式的值为0x20000000 + 64*1024 = 0x20010000,二进制示意为00100000_00000001_00000000_00000000。将这个二进制依照小端法读取进去就是0x00000120(这是objump的行为)。

第2个元素的值是0x09000000,然而从下面咱们能够晓得Reset函数其实位于0x00000008。其实这是ARM cpu的标准,用函数地址的最低位的奇偶性来示意以后处于哪种模式,具体能够看这里(记住这个探讨,上面会持续用到)

查看RESET_VECTORReset

再用命令rust-readobj.exe .\target\thumbv7m-none-eabi\debug\app -s -S --elf-output-style=GNU检查一下符号表和section header:

There are 17 section headers, starting at offset 0x111fc:Section Headers:  [Nr] Name              Type            Address  Off    Size   ES Flg Lk Inf Al  [ 0]                   NULL            00000000 000000 000000 00      0   0  0  [ 1] .vector_table     PROGBITS        00000000 010000 000008 00   A  0   0  4  [ 2] .text             PROGBITS        00000008 010008 00000a 00  AX  0   0  2  [ 3] .debug_abbrev     PROGBITS        00000000 010012 00013c 00      0   0  1  [ 4] .debug_info       PROGBITS        00000000 01014e 00058e 00      0   0  1  [ 5] .debug_aranges    PROGBITS        00000000 0106dc 000030 00      0   0  1  [ 6] .debug_ranges     PROGBITS        00000000 01070c 000018 00      0   0  1  [ 7] .debug_str        PROGBITS        00000000 010724 00048c 01  MS  0   0  1  [ 8] .debug_pubnames   PROGBITS        00000000 010bb0 0000c9 00      0   0  1  [ 9] .debug_pubtypes   PROGBITS        00000000 010c79 00036b 00      0   0  1  [10] .ARM.attributes   ARM_ATTRIBUTES  00000000 010fe4 000032 00      0   0  1  [11] .debug_frame      PROGBITS        00000000 011018 00003c 00      0   0  4  [12] .debug_line       PROGBITS        00000000 011054 000054 00      0   0  1  [13] .comment          PROGBITS        00000000 0110a8 000013 01  MS  0   0  1  [14] .symtab           SYMTAB          00000000 0110bc 000050 10     16   3  4  [15] .shstrtab         STRTAB          00000000 01110c 0000c3 00      0   0  1  [16] .strtab           STRTAB          00000000 0111cf 00002a 00      0   0  1Key to Flags:  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),  L (link order), O (extra OS processing required), G (group), T (TLS),  C (compressed), x (unknown), o (OS specific), E (exclude),  R (retain), y (purecode), p (processor specific)Symbol table '.symtab' contains 5 entries:   Num:    Value  Size Type    Bind   Vis       Ndx Name     0: 00000000     0 NOTYPE  LOCAL  DEFAULT   UND     1: 00000000     0 FILE    LOCAL  DEFAULT   ABS 11xi6ura3lrymj6b     2: 00000008     0 NOTYPE  LOCAL  DEFAULT     2 $t.1     3: 00000004     4 OBJECT  GLOBAL DEFAULT     1 RESET_VECTOR     4: 00000009    10 FUNC    GLOBAL DEFAULT     2 Reset

从上可知:

  • Reset位于.text,其value和size别离是9和10,示意了这个函数在.text中的偏移量和大小(留神偏移量9和咱们后面探讨的地址0x09000000是统一的)
  • RESET_VECTOR位于.vector_table,其value和size别离是4和4,示意了这个函数在.text中的偏移量和大小,具体的值还须要进一步读取。

测试一下

用以下命令启动一个Qemu模拟器,关上一个gdb服务器并监听端口3333

qemu-system-arm `      -cpu cortex-m3 `      -machine lm3s6965evb `      -gdb tcp::3333 `      -S `      -nographic `      -kernel target/thumbv7m-none-eabi/debug/app

而后在新窗口启动GDB并进行近程调试

arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/appReading symbols from target/thumbv7m-none-eabi/debug/app...(gdb) target remote :3333     #连贯近程调试服务器,程序会主动进行在第1行期待调试Remote debugging using :3333app::Reset () at src/main.rs:1212      pub unsafe extern "C" fn Reset() -> ! {(gdb) print/x $sp            #打印栈顶的值$1 = 0x20010000(gdb) s                        #运行下一行代码13          let _x = 42;(gdb) s                        #运行下一行代码15          loop {}(gdb) print _x                #打印变量_x的值$2 = 42(gdb) print &_x                #打印变量_x的地址$3 = (*mut i32) 0x2000fffc(gdb) quit                    #退出

【3】main接口

本章节次要介绍了如何将上一章节的成绩从binary package转化为lib package,以便其余开发者能够应用它来开发本人的应用程序。这样相当于建设了一个形象层,屏蔽了裸机相干的内容,开发者只需编写本人的main程序即可。本章难点在于了解为何须要初始化内存,以及如何初始化内存,残缺代码见:https://github.com/youth7/the-embedonomicon-note/tree/03-main-interface

为了达到这个指标,咱们须要将之前的我的项目改为lib package(名为rt,即runtime的意思),而后再新建一个binary package(名为app),而后在app中援用rt

对于package和crate的比照能够看这里

将原我的项目革新名为rt的lib package

首先将之前类型为binary的package改为lib类型,这须要:

  • main.rs重命名为lib.rs
  • Cargo.toml[package]name改为rt,同时将我的项目根目录重命名为rt

    [package]name = "rt" #批改这里version = "0.1.0"edition = "2021"
  • 改写Reset函数,让它去调用用户编写的main函数,留神这里只列出了改写的局部

    #![no_std]use core::panic::PanicInfo;// CHANGED!#[no_mangle]pub unsafe extern "C" fn Reset() -> ! {    extern "Rust" {        fn main() -> !;//将控制权交给用户的main函数,因而main必须是发散的    }    main()}
  • 在根目录创立build.rs,这是十分要害的一步,原理是通过Rust提供的构建脚本来调整一些编译时的行为,请看考代码中的正文

    use std::{env, error::Error, fs::File, io::Write, path::PathBuf};fn main() -> Result<(), Box<dyn Error>> {    //从环境变量OUT_DIR中读取一个门路,用于寄存构建过程的一些两头产物    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());    //这是最重要的一步了,通过特定的指令通知编译器从哪个门路搜寻链接脚本    println!("cargo:rustc-link-search={}", out_dir.display());    // 将链接脚本复制到上一步指定的门路    // 如果在上一步中将链接脚本的搜寻门路设置为库的根目录,则这一步能够省略    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;    Ok(())}

注:当在rt下批改build.rs时会触发主动编译,此时OUT_DIR指向rt/target下的子目录;而编译appOUT_DIR则指向app/target下的子目录,这意味着link.x会被复制到我的项目app中。

应用rt编写应用程序

创立一个binary package

cargo new --bin app

批改cargo.toml,引入rt作为依赖

[dependencies]rt = { path = "../rt" }

rt.cargo文件夹复制到app的根目录,应用和rt一样的cargo配置编译。

最初编写本人的main.rs

#![no_std]#![no_main]extern crate rt;#[no_mangle]pub fn main() -> ! {    let _x = 42;    loop {}}

而后编译并查看二进制文件

cargo build --bin app # 编译rust-objdump   -d --no-show-raw-insn .\target\thumbv7m-none-eabi\debug\app # 显示汇编代码

会有如下输入,能够看到Reset曾经被链接进来并调用了main函数

.\target\thumbv7m-none-eabi\debug\app:  file format elf32-littlearmDisassembly of section .text:00000008 <main>:       8:       sub     sp, #4       a:       movs    r0, #42       c:       str     r0, [sp]       e:       b       0x10 <main+0x8>         @ imm = #-2      10:       b       0x10 <main+0x8>         @ imm = #-400000012 <Reset>:      12:       push    {r7, lr}      14:       mov     r7, sp      16:       bl      0x8 <main>              @ imm = #-18      1a:       trap

注:其实app.cargo/config外面的rustflags配置项申明了须要了link.x,阐明apprt还是有些显式依赖的,不晓得后续有无方法可能移除这个配置项,使得app无需晓得rt中的任何内容。

rt革新为类型平安

用户尽管能够编写本人的main函数,但如果main不合乎rt的要求,例如用户不小心把main编写为一个非发散的函数,此时编译不会报错但运行时会有不可预知的谬误。

集体认为尽管rt中申明了main的类型是发散函数,但给rt提供main是链接器行为而不是编译器行为,链接器不会去管语言级别的一些细节,因而不会有链接谬误。而编译时rtapp`之间没有代码层面的依赖,因而也不会觉察类型不匹配的问题。

能够在rtlib.rs中定义并裸露一个宏给app调用,通过宏来做类型查看以打消上述隐患。

#[macro_export]macro_rules! entry {    ($path:path) => {        #[export_name = "main"]//__main导出为main,则rt中最终链接的是以后文件内的main函数,而不是用户的main函数        pub unsafe fn __main() -> ! {            // $path就是用户传入的函数,对它进行类型测验后调用,此时用户写的函数的名称能够自定义了,不肯定就是要用main            let f: fn() -> ! = $path;            f()        }    }}

而后批改app中的main.rs

#![no_std]#![no_main]use rt::entry;//应用rt中裸露进去的宏来调用用户编写的函数,此时用户编写的函数能够用其它名称//其实这样做减少了一些复杂性,之前的办法用户只须要编写一个main函数就能够了,其它什么不必管//而当初则须要理解entry宏entry!(main2);fn main2() -> ! {    let _x = 42;    loop {}}

main运行之前初始化内存(难点)

rt当初曾经比拟残缺了,然而性能上还是有一些缺失,比方用户不能在本人的代码中应用动态变量和字符串,因为编译器会将这些内容生成到.bss.data.rodata节中,但咱们的链接脚本中摈弃了这些节。为此咱们须要批改一下链接脚本:

  /DISCARD/ :  {    *(.ARM.exidx .ARM.exidx.*);  }  /* 新增三个用于保留数据的section */  .rodata :  {    *(.rodata .rodata.*);  } > FLASH  .bss :  {    *(.bss .bss.*);  } > RAM  .data :  {    *(.data .data.*);  } > RAM

这样就能在代码中应用这些变量了

#![no_std]#![no_main]use rt::entry;//应用rt中裸露进去的宏来调用用户编写的函数,此时用户编写的函数能够用其它名称,例如这里就用了main2//其实这样做减少了一些复杂性,之前的办法用户只须要编写一个main函数就能够了,其它什么不必管//而当初则须要理解entry宏entry!(main2);static RODATA: &[u8] = b"Hello, world!";static mut BSS: u8 = 0;static mut DATA: u16 = 1;fn main2() -> ! {    let _x = RODATA;    let _y = unsafe { &BSS };    let _z = unsafe { &DATA };    loop {}}

如果在真机上调试这段代码,你会发现BSSDATA的值并非预期中的0和1,这是因为真机启动后内存中的值是随机导致的。但在Qemu上你无奈重现这个问题,因为Qemu曾经帮你初始化了。

对于真机内存中的值是随机这个问题须要这样意识:

  • 编译和链接时会确定一些信息并将它们记录到ELF中,这些信息包含:

    • 变量BSSDATA的值(它们别离属于节.bss.data
    • .bss.data的LMA、VMA值(了解LMA和VMA十分重要,能够参考这里和这里)
  • ELF会被加载(烧录)到ROM外面,但运行时却在RAM外面。(例如链接脚本外面就把.bss.data调配到RAM中,因而相干变量的地址是指向RAM的,即编译时候就认为这些数据/代码是在RAM的地址空间内运行,而此时RAM外面的内容尚未被初始化,间接读取的话会读到脏数据)
  • 因为第2条的起因,须要在运行前将ROM外面的相干数据复制到RAM中,营造一个与预期统一的运行环境

上述的核心思想是,编译和链接器为了放弃程序可能失常执行,对程序和运行环境作出了一些约定(这里次要是指地址空间),这些约定被记录在ELF文件中。加载器或者OS必须保障程序运行时这些约定都得以满足。而对于本教程,Qemu负责将ELF加载到ROM,lib.rs中的代码负责初始化RAM并(将ROM内的局部数据加载到RAM)。

(以上阐述仅针对裸机编程,对于古代罕用的通用操作系统来说,因为OS曾经帮你将虚拟内存设置好了,程序面向的是一个现实的环境:VMA和LMA相等,因而无需关注上述的一些细节了。)

为此咱们在应用内存先须要先初始化,首先批改link.x,这相当于将一些约定信息写入到ELF中

/* 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.*);  }  /* 新增三个用于保留数据的section */  .rodata :  {    *(.rodata .rodata.*);  } > FLASH  .bss :  {    _sbss = .; /* 将.bss的起始地址保留到_sbss中 */    *(.bss .bss.*);    _ebss = .;/* 将.bss的完结地址保留到_ebss中 */  } > RAM/*用AT命令指定.data的LMA,让这个段紧贴着.rodata段,但这里并没有指定VMA这里十分重要,了解AT命令请看这里的例子:https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_chapter/ld_3.html#SEC16务必了解这里通过什么样的形式指定了.data的VMA和LMA(链接脚本并没有指定VMA)*/  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))    {    _sdata = .;/* 将.data的起始地址保留到_sdata中,留神:这里相当于保留了该节的VMA! */    *(.data .data.*);    _edata = .;/* 将.data的完结地址保留到_edata中 */  } > RAM  _sidata = LOADADDR(.data);/*将.data的LMA与某个符号关联起来*/}

而后批改lib.rs,减少对RAM进行初始化的代码

    // 为何这里须要extern块润饰呢?因为这些符号都是由链接脚本间接定义的,须要间接从ELF文件中读取。    extern "C" {        static mut _sbss: u8;        static mut _ebss: u8;        static mut _sdata: u8;        static mut _edata: u8;        static _sidata: u8;    }    //初始化.bss只须要将对应区域全副置为0即可    let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;    ptr::write_bytes(&mut _sbss as *mut u8, 0, count);    //初始化.data则须要从ROM复制    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);

最初编译并检查一下后果是否合乎预期

cargo build --bin apprust-readobj .\target\thumbv7m-none-eabi\debug\app --program-headers --elf-output-style=GNU

输入如下:

Elf file type is EXEC (Executable file)Entry point 0x51There are 6 program headers, starting at offset 52Program Headers:  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align  LOAD           0x010000 0x00000000 0x00000000 0x00008 0x00008 R   0x10000  LOAD           0x010008 0x00000008 0x00000008 0x0038c 0x0038c R E 0x10000  LOAD           0x0103a0 0x000003a0 0x000003a0 0x00094 0x00094 R   0x10000  LOAD           0x020000 0x20000000 0x20000000 0x00000 0x00001 RW  0x10000  LOAD           0x020002 0x20000002 0x00000434 0x00002 0x00002 RW  0x10000  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x0 Section to Segment mapping:  Segment Sections...   00     .vector_table   01     .text   02     .rodata   03     .bss   04     .data   05   None   .debug_abbrev .debug_info .debug_aranges .debug_ranges .debug_str .debug_pubnames .debug_pubtypes .ARM.attributes .debug_frame .debug_line .comment .symtab .shstrtab .strtab
留神下面输入中的VMA(VirtAddr)和LMA(PhysAddr)都是从程序运行的角度登程的,和它们在ELF中的具体偏移量齐全没有关系,不要混同

能够看到.data的LMA和VMA是不一样。并且,.rodata的LMA=0x000003a0,长度是0x00094, 0x000003a0+0x00094 =0x00000434,这正好就是 .data的LMA。阐明咱们在link.x中通过AT命令对.data的LMA进行了调整的确失效了。

【4】异样解决

这一章是通过欠缺vector table,为rt减少更多的异样处理程序,同时实际编译期重写(compile time overridable behavior)这个性能。这节残缺代码见这里。

本章次要实现两个性能:

  • rt中vector table中的其它项提供一个默认值(在此之前只提供了前两项)
  • 用户在应用rt的时候,可能用自定义的函数去笼罩vector table中的默认函数

对于中断、异样(trap)、陷入的概念请参考这里

调整Rust代码

为了演示不便,只批改vector table的前16个函数,因为它们与设施无关,且实用于所有Cortex-M系列的微控制器。先批改lib.rs

pub union Vector {    // 一个Vector就是vector table中的一项,依据arm的文档,每一项要么是一个异样处理函数,要么是预留(值为0)    reserved: u32,    handler: unsafe extern "C" fn(),}extern "C" {    //申明会用到的内部函数,因为有可能是用户提供的所以必须用extern,不明确为何是C标准而不是Rust标准,    //留神这里只是申明并没有提供具体实现,实现有两种,一种是应用默认的DefaultExceptionHandler;一种是用户提供    fn NMI();    fn HardFault();    fn MemManage();    fn BusFault();    fn UsageFault();    fn SVCall();    fn PendSV();    fn SysTick();}#[link_section = ".vector_table.exceptions"]// 将异样处理函数保留到节.vector_table.exceptions中#[no_mangle]pub static EXCEPTIONS: [Vector; 14] = [//定义vector table中残余的14项    Vector { handler: NMI },    Vector { handler: HardFault },    Vector { handler: MemManage },    Vector { handler: BusFault },    Vector { handler: UsageFault},    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { handler: SVCall },    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { handler: PendSV },    Vector { handler: SysTick },];#[no_mangle]pub extern "C" fn DefaultExceptionHandler() {// 定义一个默认的异样处理函数    loop {}}

调整链接脚本

EXTERN(EXCEPTIONS); SECTIONS{  .vector_table ORIGIN(FLASH) :  {    /* vector table第一项:ISP */    LONG(ORIGIN(RAM) + LENGTH(RAM));    /* vector table第二项 */    KEEP(*(.vector_table.reset_vector));    KEEP(*(.vector_table.exceptions)); /* 将残余的14个异样处理函数保留到flash中,加上下面已有的两项刚好16项 */  } > FLASH  /* 为符号提供默认值,只有当用户未提供自定义的异样处理程序时候才会失效,留神被提供默认值的项都是在lib.rs中申明过的内部函数 */  PROVIDE(NMI = DefaultExceptionHandler);  PROVIDE(HardFault = DefaultExceptionHandler);  PROVIDE(MemManage = DefaultExceptionHandler);  PROVIDE(BusFault = DefaultExceptionHandler);  PROVIDE(UsageFault = DefaultExceptionHandler);  PROVIDE(SVCall = DefaultExceptionHandler);  PROVIDE(PendSV = DefaultExceptionHandler);  PROVIDE(SysTick = DefaultExceptionHandler);...

测试一下

先批改main.rs,在main2()中触发一个异样

#![no_std]#![no_main]#![feature(core_intrinsics)]// 因为应用了core_intrinsics的起因,必须切换到nightly来构建use core::intrinsics;use rt::entry;//应用rt中裸露进去的宏来调用用户编写的函数,此时用户编写的函数能够用其它名称,例如这里就用了main2//其实这样做减少了一些复杂性,之前的办法用户只须要编写一个main函数就能够了,其它什么不必管//而当初则须要理解entry宏entry!(main2);fn main2() -> ! {    //触发 HardFault exception    intrinsics::abort()}

因为用到了core_intrinsics,因而须要切换到nightly中编译

rustup default nightly # 切换为nightlyrustup target add thumbv7m-none-eabi #在nightly下须要先重新安装target和其它相干工具cargo  build --bin app# 编译我的项目

而后依照第二章中的办法启动QEMU和GDB进行调试(留神须要在app我的项目的根目录下进行)

(gdb)  target remote :3333Remote debugging using :3333rt::Reset () at src/lib.rs:1212      pub unsafe extern "C" fn Reset() -> ! {(gdb) b DefaultExceptionHandler # 在默认的异样处理函数DefaultExceptionHandler设置一个断点Breakpoint 1 at 0x100: file src/lib.rs, line 103.(gdb) c # 持续运行Continuing.# intrinsics::abort()触发异样,使得执行流程切换到DefaultExceptionHandler中Breakpoint 1, rt::DefaultExceptionHandler () at src/lib.rs:103 103         loop {}(gdb) list # 列出断点左近的源码98          Vector { handler: SysTick },99      ];100101     #[no_mangle]102     pub extern "C" fn DefaultExceptionHandler() {// 103         loop {}104     }(gdb)

平安起见查看vector table是否合乎咱们的预期,通过以下命令编译并查看

cargo build --bin app --release # 应用release模式编译.\target\thumbv7m-none-eabi\release\app -s -j .vector_table #查看.vector_table内容 

输入如下:

.\target\thumbv7m-none-eabi\release\app:        file format elf32-littlearmContents of section .vector_table: 0000 00000120 45000000 83000000 83000000  ... E........... 0010 83000000 83000000 83000000 00000000  ................ 0020 00000000 00000000 00000000 83000000  ................ 0030 00000000 00000000 83000000 83000000  ................

可见.vector_table中有16项,比照一下各项的值可知它的确和lib.rs中的EXCEPTIONS数组是统一的(0x83000000就是默认的异样函数的值)。此外须要注意第4项,它是异样处理函数HardFault()的地址,下一节咱们会在app中笼罩这个函数,笼罩后它的地址就不再是0x83000000。

用户自定义异样处理函数

因为在lib.rs中申明了各个异样处理函数为extern,所以用户能够在内部自定义异样处理函数来代替rt中的DefaultExceptionHandler(),这只须要在rtmain.rs中定义合乎签名的函数即可。

#[no_mangle]pub extern "C" fn HardFault() -> ! {    //自定义异样处理函数,用QEMU调试时候应该停留在这里    loop {}}

而后像下面那样编译debug,能够发现代码的确停留在用户自定义的 HardFault()

(gdb) target remote :3333Remote debugging using :3333rt::Reset () at src/lib.rs:1212      pub unsafe extern "C" fn Reset() -> ! {(gdb) b HardFaultBreakpoint 1 at 0x44: file src/main.rs, line 19.(gdb) cContinuing.Breakpoint 1, app::HardFault () at src/main.rs:1919          loop {}(gdb) list14      }1516      #[no_mangle]17      pub extern "C" fn HardFault() -> ! {18          //QEMU19          loop {}20      }

再像上一大节那样编译并查看.vector_table

.\target\thumbv7m-none-eabi\release\app:        file format elf32-littlearmContents of section .vector_table: 0000 00000120 47000000 85000000 41000000  ... G.......A... 0010 85000000 85000000 85000000 00000000  ................ 0020 00000000 00000000 00000000 85000000  ................ 0030 00000000 00000000 85000000 85000000  ................

此时DefaultExceptionHandler()的地址变成了0x85000000,然而第4项HardFault()的值和DefaultExceptionHandler()不一样,这是因为用户在app中定义了本人的异样处理函数。

【5】应用新办法写汇编

这一章次要是在代码中嵌入汇编来批改寄存器的值,从而实现为HardFault()传递参数。原文编写时候asm!global_asm!尚未稳固,因而是应用旧形式嵌入汇编。这种形式十分繁琐,因而我将它改为用asm!来实现。残缺代码见这里

在上一章中,咱们将HardFault()搁置到vector table的特定地位,当对应的异样产生时候HardFault()就会被调用。而本章不再将HardFault()间接搁置到vector table,而是创立一个辅助函数HardFaultTrampoline()并将它搁置到vector table,而后通过它来调用HardFault()。而HardFaultTrampoline()在调用HardFault()前会批改特定寄存器的值,从而实现为HardFault()传参。

批改rt

首先批改lib.rs如下:

extern "C" {    fn NMI();    // fn HardFault();删除对HardFault的申明,因为不须要在rust代码中调用它    fn MemManage();    fn BusFault();    fn UsageFault();    fn SVCall();    fn PendSV();    fn SysTick();}#[link_section = ".vector_table.exceptions"]// 将异样处理函数保留到节.vector_table.exceptions中#[no_mangle]pub static EXCEPTIONS: [Vector; 14] = [//定义vector table中残余的14项    Vector { handler: NMI },    Vector { handler: HardFaultTrampoline },// 改为应用辅助函数,通过它去调用HardFault    Vector { handler: MemManage },    Vector { handler: BusFault },    Vector { handler: UsageFault},    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { handler: SVCall },    Vector { reserved: 0 },    Vector { reserved: 0 },    Vector { handler: PendSV },    Vector { handler: SysTick },];#[allow(non_snake_case)]#[no_mangle]pub fn DefaultExceptionHandler(_ef: *const u32) -> ! {//因为HardFaultTrampoline会传递参数,因而函数签名也要同步批改    loop {}}

HardFaultTrampoline()搁置到vertor table,取代原来的HardFault()。它的性能是读取以后栈指针的值,而后将它作为参数传递给HardFault(),这样HardFault()就能够读取异样产生时候栈外面的内容了。为何不间接在HardFault()中读取栈指针呢?因为HardFault()是用Rust写的,无奈间接拜访寄存器的值。

而后在lib.rs中应用汇编实现HardFaultTrampoline()

#[no_mangle]extern "C" fn HardFaultTrampoline() {    unsafe{        asm!(          "mrs r0, MSP",          "b HardFault"        )    }}

批改app

因为HardFaultTrampoline()会给HardFault()传参,所以须要批改main.rsHardFault()的签名

#[no_mangle]#[allow(non_snake_case)]pub fn HardFault(_ef: *const u32) -> ! {//因为HardFaultTrampoline会传递参数,因而函数签名也要同步批改    loop {}}

测试一下

而后用以下命令编译并查看汇编代码

 cargo build --bin app --release #编译 rust-objdump -d --print-imm-hex --no-show-raw-insn .\target\thumbv7m-none-eabi\release\app #显示汇编

会有以下输入

.\target\thumbv7m-none-eabi\release\app:        file format elf32-littlearmDisassembly of section .text:00000040 <HardFault>:      40:       b       0x40 <HardFault>        @ imm = #-0x400000042 <main>:      42:       trap      44:       trap00000046 <Reset>:      46:       push    {r7, lr}      48:       mov     r7, sp      4a:       bl      0x42 <main>             @ imm = #-0xc      4e:       trap00000050 <UsageFault>:      50:       b       0x50 <UsageFault>       @ imm = #-0x400000052 <HardFaultTrampoline>:      52:       push    {r7, lr}# 比咱们的汇编多了两行,可能是因为函数调用导致生成了保留寄存器的代码      54:       mov     r7, sp      56:       mrs     r0, msp      5a:       b.w     0x40 <HardFault>        @ imm = #-0x1e      5e:       pop     {r7, pc}

可知HardFaultTrampoline()的地址是0x00000052,再检查一下vector table的内容

rust-objdump -s -j .vector_table  .\target\thumbv7m-none-eabi\release\app

输入如下:

.\target\thumbv7m-none-eabi\release\app:        file format elf32-littlearmContents of section .vector_table: 0000 00000120 47000000 51000000 53000000  ... G...Q...S... 0010 51000000 51000000 51000000 00000000  Q...Q...Q....... 0020 00000000 00000000 00000000 51000000  ............Q... 0030 00000000 00000000 51000000 51000000  ........Q...Q...

注意第4项的值为0x53000000(Thumb mode),此时证实HardFaultTrampoline()的确曾经失效。

【6】利用符号进行日志输入

在嵌入式零碎中,常见的日志输出/输入形式有以下几种:

  • 串口:将日志信息写到串口,串口的接收端连贯着显示设施
  • 内存:将日志信息写到RAM中,而后再从RAM中读取
  • 文件:将日志信息写到文件,这要求设施必须有sd卡或者片外flash(呈现文件概念的话意味着须要OS反对?)
  • 嵌入式设施自带的显示模块、网络网口等

本章次要是2和3的混合体,不过略有区别:不是间接输入日志内容(字符串),而是输入日志内容的地址,而后再依据地址去ELF文件中查找日志的具体内容。(为何不间接输入字符串?是因为在嵌入式中不容易实现?)

具体步骤如下:

  1. 在Rust代码中定义若干变量,
  2. 批改变量在ELF中的符号名(symbol name),将日志内容嵌入到符号名中,则通过编译后日志的内容便存储到ELF中
  3. 在Rust代码中输入变量的地址,在符号表中查问这些地址便能失去日志的内容

残缺代码请看这里

批改变量导出的符号名称的例子

创立一个名为foo的lib package,而后批改lib.rs文件如下:

#![allow(unused)]fn main() {    #[export_name = "Hello, world!"] //批改变量A的符号名    #[used]// 要求编译器不要抛弃动态变量A,即便它没有被应用    static A: u8 = 0;    #[export_name = "你好,这是一个中文句子"]    #[used]// 要求编译器不要抛弃动态变量B,即便它没有被应用    static B: u8 = 0;//批改变量B的符号名}

接着编译并查看符号表

cargo build --lib;rust-nm .\target\debug\libfoo.rlib

会有以下输入:

   Compiling foo v0.1.0 (D:\workspace\rust\app\foo)    Finished dev [unoptimized + debuginfo] target(s) in 0.09sfoo-71f85fa4234f96d2.3bevs2kbnkn4yeof.rcgu.o:00000000 a @feat.0000000000 R Hello, world!00000000 D __imp_Hello, world!00000000 D __imp_你好,这是一个中文句子00000000 R 你好,这是一个中文句子lib.rmeta:.\target\debug\libfoo.rlib:lib.rmeta: no symbols

可见咱们胜利将一些自定义信息当做符号名写入了ELF中。

将日志信息编码到符号名中

通过以下步骤将日志信息编码

  1. 为每条日志创立一个对应的static类型变量,但变量自身不存储日志的内容,日志的内容是通过attribute编码到符号名中
  2. 将步骤1中的变量的地址输入到控制台

先批改app中的main.rs

#![no_main]#![no_std]use core::fmt::Write;//应用semihosting技术进行输入,因为QEMU间接反对semihosting。而在真机环境则可能须要用到串口等技术use cortex_m_semihosting::{debug, hio};use rt::entry;entry!(main);fn main() -> ! {    let mut hstdout = hio::hstdout().unwrap();    #[export_name = "Hello, world!"]// 将日志信息编码到动态变量A的符号名中,    static A: u8 = 0;    // 将地址的值作为usize输入    let _ = writeln!(hstdout, "{:#x}", &A as *const u8 as usize);    #[export_name = "Goodbye"]    static B: u8 = 0;    let _ = writeln!(hstdout, "{:#x}", &B as *const u8 as usize);    debug::exit(debug::EXIT_SUCCESS);    loop {}}

而后引入相干依赖

[dependencies]cortex-m-semihosting = "0.5.0"rt = { path = "../rt" }

最初构建并运行

cargo build #构建#开启semihosting运行qemu-system-arm `    -cpu cortex-m3 `    -machine lm3s6965evb `    -nographic `    -semihosting-config enable=on,target=native `    -kernel target/thumbv7m-none-eabi/debug/app

会有如下输入:

Timer with period zero, disabling0x1c4c0x1c4d

每次都要构建后再运行一大串QEMU命令是很繁琐的,能够通过批改.cargo/config来简化:

[target.thumbv7m-none-eabi]# 减少runner的相干配置项runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"

这样构建和运行能够简化为一行命令:cargo run --release

PS D:\workspace\rust\app\app> cargo run --release    Finished release [optimized] target(s) in 0.03s     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target\thumbv7m-none-eabi\release\app`Timer with period zero, disabling0x11ec0x11ed

从地址中解码出日志信息

在上一步中咱们失去了变量的地址,当初须要在符号表中查找这些地址,从而取得这些地址对应的符号名,这能够通过以下命令实现

rust-objdump .\target\thumbv7m-none-eabi\debug\app -t | findstr "00000001"

而后有以下输入:

#从左到右各列的意义是:符号地址 | 符号的flag | 跟符号相干的节的序号 | 所属的节 | 符号长度 | 符号名00001c4c g     O .rodata        00000001 Hello, world!00001c4d g     O .rodata        00000001 Goodbye

findstr去查找objdump的输入中含有字符串00000001的行,如无意外应该只会找到咱们定义的两个符号,因为动态变量AB的类型都是u8,只占一个字节,所以第五列(符号长度)的值必然是00000001

一些改良

在之前的实现中,动态变量被编译后会存储到.rodata节,这意味着把ELF烧录到flash中后这些动态仍然占据空间,即便它们的值并没有被应用(只用到了它们的地址)。因而要想一个方法让程序不要加载这些节从而节俭空间,这须要应用链接脚本。

app中创立一个新的链接脚本log.x并增加以下内容:

SECTIONS{  .log 0 (INFO) : {    *(.log);  }}

这段代码的意义如下:

  • 收集所有input指标文件中的名为.log的节,而后在output指标文件中生成名为.log的节
  • INFO示意该节在运行时不须要在内存中调配空间
  • 0示意该节的的加载地址(设为0是有它的非凡意义的,等下再说)

而后批改app中的main.rs为:

#![no_main]#![no_std]//应用semihosting技术进行输入,因为QEMU间接反对semihosting。而在真机环境则可能须要用到串口等技术use cortex_m_semihosting::{debug, hio};use rt::entry;entry!(main);fn main() -> ! {    let mut hstdout = hio::hstdout().unwrap();    #[export_name = "Hello, world!"]// 将日志信息编码到动态变量A的符号名中,    #[link_section = ".log"]// 指定该动态变量输入到.log这个节    static A: u8 = 0;    // 将地址的值作为usize输入    let address = &A as *const u8 as usize as u8;    hstdout.write_all(&[address]).unwrap(); // 不应用core中的格式化I/O,改为应用第三方依赖的二进制I/O    #[export_name = "Goodbye"]    #[link_section = ".log"]// 指定该动态变量输入到.log这个节    static B: u8 = 0;    let address = &B as *const u8 as usize as u8;    hstdout.write_all(&[address]).unwrap(); // 不应用core中的格式化I/O,改为应用第三方依赖的二进制I/O    debug::exit(debug::EXIT_SUCCESS);    loop {}}

代码中最值得注意的中央有两点:

  • 将变量的地址类型强转为u8
  • 不应用core中的格式化I/O,改为应用第三方依赖的二进制I/O

因为改为了二进制I/O,因而会波及到多字节的序列化问题,为了防止这个问题罗唆将地址改为单个字节。但地址原本是4字节的,这样可能会导致地址的值被截断。为了防止截断的问题咱们将.log的加载地址设为0,同时将地址的取值范畴限度为[0, 255],这样就能保障地址可能用单个字节来准确示意,避开了被截断的问题。但弊病就是最多只能应用255条日志。

最初在.cargo/config中指定新的链接脚本

# 针对这个target应用链接脚本[target.thumbv7m-none-eabi]rustflags = [    "-C", "link-arg=-Tlink.x",    "-C", "link-arg=-Tlog.x"]runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"# 指定编译的target,批改这里之后就无需在命令行传递--target参数了[build]target = "thumbv7m-none-eabi"

而后运行cargo run --release | Format-Hex,会有以下输入(留神,原文中是用linux中的xxd来显示二进制输入,这里用Win11中powershell自带的的Format-Hex来代替):

    Finished dev [unoptimized + debuginfo] target(s) in 0.03s     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target\thumbv7m-none-eabi\debug\app`      Timer with period zero, disabling           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F00000000   00 01 

可见输入的地址是0x00和0x01,在.log节中查找这两个地址:

rust-objdump .\target\thumbv7m-none-eabi\release\app -t | findstr log

会有以下输入

00000000 g     O .log   00000001 Hello, world!00000001 g     O .log   00000001 Goodbye

所有都合乎预期。

形象为一个库

下面尽管曾经实现了输入日志,然而这个过程还是相当繁琐用且不直观的,用户的冀望的输入日志应该像应用std::println!那么简略,为实现这个目标咱们须要将下面的逻辑封装为一个lib crate。

先用命令cargo new --lib log创立一个名为lib的lib package,而后批改lib.rs的内容为:

#![no_std]pub trait Log {    type Error;    fn log(&mut self, address: u8) -> Result<(), Self::Error>;}#[macro_export]macro_rules! log {    ($logger:expr, $string:expr) => {{//用户调用宏的时候参数包含2个:一个log Trait实例;一个日志字符串        #[export_name = $string]        #[link_section = ".log"]        static SYMBOL: u8 = 0;// 每条日志字符串都有一个对应的动态变量        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)        // 由用户提供具体的输入实现,然而对于本教程来说,集体认为应该由库提供实现才对,这样用户就无需关注这方面的细节    }};}

和之前的rt一样,须要提供build.rs用于构建时复制log.x

use std::{env, error::Error, fs::File, io::Write, path::PathBuf};fn main() -> Result<(), Box<dyn Error>> {    // Put the linker script somewhere the linker can find it    let out = PathBuf::from(env::var("OUT_DIR")?);    File::create(out.join("log.x"))?.write_all(include_bytes!("log.x"))?;    println!("cargo:rustc-link-search={}", out.display());    Ok(())}

最初批改app中的main.rs,让它调用log中的宏来输入日志

#![no_main]#![no_std]use cortex_m_semihosting::{    debug,    hio::{self, HostStream}//0.5.0之后改为应用HostStream构造体,原文中是应用HStdout};use log::{log, Log};use rt::entry;struct Logger {    hstdout: HostStream,}impl Log for Logger {    type Error = ();    fn log(&mut self, address: u8) -> Result<(), ()> {        self.hstdout.write_all(&[address])    }}entry!(main);fn main() -> ! {    let hstdout = hio::hstdout().unwrap();    let mut logger = Logger { hstdout };    let _ = log!(logger, "Hello, world!");    let _ = log!(logger, "Goodbye");    debug::exit(debug::EXIT_SUCCESS);    loop {}}

此时打印日志所用的宏曾经十分靠近println!,比之前那种艰涩的办法好多了!同时不要遗记批改 Cargo.toml ,引入log作为依赖

[dependencies]rt = {path ="../rt"}log = {path ="../log"}cortex-m-semihosting  = "0.5.0"

最初运行一下cargo run --release | Format-Hex会有以下输入:

    Finished release [optimized] target(s) in 0.71s     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target\thumbv7m-none-eabi\release\app`Timer with period zero, disabling           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F00000000   00 01   

依照上一大节的办法用rust-objdump去查看ELF文件,会发现相干地位就是咱们日志中输入的字符串,这里不再反复

日志分级

实现日志分级的思维比较简单,如上所述咱们地址的无效范畴是[0, 255],咱们在这个区间之中取一点n,让[0, n]这个地址区间放error级别的日志地址,[n, 255]放warn级别的日志地址,则实现了日志分级。此时想专门看某种类型的日志的话,只须要拿特定范畴的地址值去搜寻ELF文件即可。

首先咱们须要批改log中的lib.rs,提供别离用于输入warn和error级别日志的宏:

#![no_std]pub trait Log {    type Error;    fn log(&mut self, address: u8) -> Result<(), Self::Error>;}// 输入error等级的日志#[macro_export]macro_rules! error {    ($logger:expr, $string:expr) => {{        #[export_name = $string]        #[link_section = ".log.error"] // 搁置到.log.error这个节        static SYMBOL: u8 = 0;        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)//最终都是调用log函数,只是搁置的中央不一样    }};}// 输入warn等级的日志#[macro_export]macro_rules! warn {    ($logger:expr, $string:expr) => {{        #[export_name = $string]        #[link_section = ".log.warning"] // 搁置到.log.warning这个节        static SYMBOL: u8 = 0;        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)//最终都是调用log函数,只是搁置的中央不一样    }};}

而后调整链接脚本,让不同级别的日志依照咱们下面形容的形式搁置,留神__log_warning_start__就是咱们下面说的n,它是不同级别日志的分界点

SECTIONS{  .log 0 (INFO) : {    *(.log.error);              /*后面局部搁置error级别日志*/    __log_warning_start__ = .;  /*将以后地址值与符号__log_warning_start__关联起来,意味着剩下地址存的都是正告级别的日志*/    *(.log.warning);            /*剩下局部搁置warning级别日志*/  }}

最初批改app中的main.rs,应用新的宏来输入不同级别的日志

#![no_main]#![no_std]use cortex_m_semihosting::{    debug,    hio::{self, HostStream}//0.5.0之后改为应用HostStream构造体,原文中是应用HStdout};use log::{error, warn, Log};use rt::entry;struct Logger {    hstdout: HostStream,}impl Log for Logger {    type Error = ();    fn log(&mut self, address: u8) -> Result<(), ()> {        self.hstdout.write_all(&[address])    }}entry!(main);fn main() -> ! {    let hstdout = hio::hstdout().unwrap();    let mut logger = Logger { hstdout };    let _ = warn!(logger, "Hello, world!");    let _ = error!(logger, "Goodbye");    let _ = error!(logger, "你好呀");    let _ = warn!(logger, "是的师父!");    debug::exit(debug::EXIT_SUCCESS);    loop {}}

cargo run --release | Format-Hex运行程序,会有以下输入:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F00000000   02 00 01 03 

尽管rust代码里交织输入warn和error日志,然而这并不会扭转它们的关系:error日志的地址永远小于warn日志。这是由链接脚本决定的,能够从输入中察看到这个法则。同时咱们检查一下生成的二进制文件

# 进入wsl而后执行以下命令:objdump ./target/thumbv7m-none-eabi/release/app -t | grep log# 而后会有以下输入00000002 g     O .log   00000001 Hello, world!00000000 g     O .log   00000001 Goodbye00000001 g     O .log   00000001 你好呀00000003 g     O .log   00000001 是的师父!00000002 g       .log   00000000 __log_warning_start__# 在wsl执行的起因是程序中用了中文字符串,在powershell下用rust-objdump去解析的话会乱码,成果不好。如果没有装置wsl的话也能够像之前那样应用命令:# rust-objdump .\target\thumbv7m-none-eabi\release\app -t | findstr log

相比之前多了符号__log_warning_start__,地址值属于[0, __log_warning_start__]的符号都代表error级别日志;地址值属于[__log_warning_start__, 255]的符号都代表warn级别日志。

【7】全局的单例对象

本章次要是对上一章基进行改良,使得log!的应用更加像里的std::println!,即应用相似log!(日志信息)这样的API来输入日志。这种调用形式和之前相比少了一个logger对象,实现这个个性的原理是,注册一个全局的logger对象,从而使得用户在调用log!时不须要再提供logger。残缺代码请见这里

首先要批改loglib.rs,它对外提供这些性能:

  • 让用户注册全局的单例对象logger
  • 提供log!宏,该宏能够应用全局或自定义的logger进行日志输入

具体代码如下,一些重要的细节曾经正文:

#![no_std]//对于Sync、Send的语义参考:https://www.zhihu.com/question/303273488/answer/2345814358pub trait GlobalLog: Sync {    /**    申明一个trait,全局的单例日志对象必须实现它,须要留神以下几点:    1,log办法只须要&self,不耗费所有权,因为它用的是单例的全局共享对象。    2,这里并没有像上面的Log trait那样定义一个关联的谬误类型,这是为了简化细节。       将错误处理委托给用户,而不是由库指定错误处理规定并强制用户实现。    **/    fn log(&self, address: u8);}pub trait Log {    type Error;    fn log(&mut self, address: u8) -> Result<(), Self::Error>;}#[macro_export]macro_rules! log {    //该宏承受两种传参,第一种传参不须要提供日志对象,第二种传参须要提供日志对象    //第一种传参形式,此时会应用一个名为"LOGGER"的全局对象进行日志输入,它是一个定义在某处的全局对象    ($string:expr) => {        unsafe {            extern "Rust" {                //对于$crate请见:                //https://zjp-cn.github.io/tlborm/decl-macros/minutiae/hygiene.html?highlight=%24crate#unhygientic                //咱们并不知道LOGGER的具体类型,但要求它必须实现了这里必须实现了GlobalLog,所以必须用trait object                static LOGGER: &'static dyn $crate::GlobalLog;            }            #[export_name = $string]            #[link_section = ".log"]            static SYMBOL: u8 = 0;            $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)        }    };    //第二种传参形式,须要用户本人提供日志对象进行输入,这是上一章的形式    ($logger:expr, $string:expr) => {{        #[export_name = $string]        #[link_section = ".log"]        static SYMBOL: u8 = 0;        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)    }};}//提供一个宏,让用户注册单例的全局日志对象,并将符号名称定为"LOGGER",这样正好和下面对应#[macro_export]macro_rules! global_logger {    ($logger:expr) => {        #[no_mangle]        pub static LOGGER: &dyn $crate::GlobalLog = &$logger;    };}

而后咱们批改app/main.rs来应用上述的新个性:

#![no_main]#![no_std]use cortex_m::interrupt;use cortex_m_semihosting::{    debug,    hio::{self, HostStream}//0.5.0之后改为应用HostStream构造体,原文中是应用HStdout};use log::{global_logger, log, GlobalLog};use rt::entry;struct Logger;global_logger!(Logger);//将Logger注册为全局logger,这样在应用log!宏的时候就不再须要提供logger对象entry!(main);fn main() -> ! {    log!("Hello, world!");//更为简洁的日志API,不须要被动提供logger对象    log!("Goodbye");    debug::exit(debug::EXIT_SUCCESS);    loop {}}//全局logger的实现impl GlobalLog for Logger {    fn log(&self, address: u8) {        //interrupt::free作用是在一个无中断的上下文环境中执行函数,这是拜访static mut类型变量的要求        //因为HSTDOUT是动态变量,只有做到这样能力保障内存平安。        //这种机制就是所谓的临界区(critical section)        interrupt::free(|_| unsafe {            static mut HSTDOUT: Option<HostStream> = None;            // 提早初始化            if HSTDOUT.is_none() {                HSTDOUT = Some(hio::hstdout()?);            }            let hstdout = HSTDOUT.as_mut().unwrap();            hstdout.write_all(&[address])        })        .ok(); // 调用ok()意味着疏忽谬误并返回Option    }}

不要遗记在Cargo.toml中引入新的依赖

[dependencies]rt = {path ="../rt"}log = {path ="../log"}cortex-m-semihosting  = "0.5.0"cortex-m = "0.7.6"

cargo run --release | Format-Hex运行程序,会有以下输入:

           00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F00000000   00 01 

应用全局单例对象来简化log!宏的指标曾经达成。

因为应用了trait object,因而可能会因为引入vtable(实现动静分派)而造成性能损耗。但LLVM仿佛相当智能地做了优化,比照上面的命令和输入能够发现,在release模式下LOGGER都找不到了

cargo objdump --bin app --release -- -t| findstr LOGGER    Finished release [optimized] target(s) in 0.03s #release模式下曾经找不到符号LOGGERcargo objdump --bin app -- -t| findstr LOGGER    Finished dev [unoptimized + debuginfo] target(s) in 0.03s00000730 g     O .rodata        00000008 LOGGER

【8】DMA

第八章对实现DMA做了介绍并给出一些代码,但没有可供理论运行的例子,因而这个章节临时先放一放,日后如果有能力在此基础上实现一个能够运行的DMA时候再回来补充笔记