2016-06-07

Small Fixes II

From time to time, I'm making small fixes here or there that neither fit into an existing article, nor do they warrant their own. Today, I'm going to show you a few of those fixes that I've made over the past few months.

Let's start with program initialization. There are a few things that need to happen before a program can start. When using an operating system, all of this stuff is usually taken care for you, so you don't see it. When writing an embedded program without relying on operating systems or external libraries, like we do, you have to take care of it yourself.[1]

One of those initialization steps is copying global variables from the flash memory into RAM. The initial values of global variables need to be available to the program, so they're compiled into the binary. When you change those variables, you don't want to change them where they are located in the binary, however. That would mean global variables retained their value between different runs of the program, which makes no sense. That's why we need to copy the variables.

There's some linker trickery going on to make that possible: The compiler puts these initial values into a special segment of the binary, called .data, so the linker knows about them. The linker also knows at which address the RAM starts, which means it knows where the initial values are supposed to end up. It makes this information available to the program via global variables that we can refer to.[2] Here's how to do that:

extern {
    static mut _etext    : u32;
    static mut _srelocate: u32;
    static mut _erelocate: u32;
}
            

Code is located in the .text segment, global variables come after the code. _etext is where the .text segment ends, i.e. where the global variables are located. _srelocate and _erelocate mark the beginning and end of the area in RAM where we're going to place those global variables.

With that information, we can copy the global variables to RAM:

unsafe {
    let mut src = &mut _etext     as *const u32;
    let mut dst = &mut _srelocate as *mut u32;

    while dst < &mut _erelocate as *mut u32 {
        ptr::write_volatile(
            dst,
            ptr::read_volatile(src),
        );

        src = src.offset(1);
        dst = dst.offset(1);
    }
}
            

That takes care of all variables with initial values. What about uninitialized variables though? Those are located in the .bss segment. Rust doesn't have uninitialized global variables, so I don't believe the .bss segment is used in Rust (this might be totally wrong). Uninitialized global variables exist in C though, and there they are specified to actually have an initial value of zero.

All of this is probably not very relevant to our Rust code, but we might use C libraries later that rely on that behavior. Instead of opening ourselves up to problems that are going to be near-impossible to debug, let's just make sure now that it works. Again, the linker gives us the start and end address of the area where those uninitialized global variables live:

extern {
    static mut _szero: u32;
    static mut _ezero: u32;
}
            

All we need to do is to initialize that area by zeroeing it:

unsafe {
    let mut dst = &mut _szero as *mut u32;

    while dst < &mut _ezero as *mut u32 {
        ptr::write_volatile(dst, 0);
        dst = dst.offset(1);
    }
}
            

That takes care of initialization, but there's another thing I want to get to in this article: Unwinding. Unwinding is something that happens when a Rust program panics. I'm not going to explain what exactly unwinding is, so please refer to the official documentation.

What's relevant to us, is that the capability to unwind comes with complexity and a runtime cost. We're not going spin up threads and restart them when they panic, so we really don't need unwinding at all. To us, a panic is a catastrophic failure, and we want the program to just restart in that case. Fortunately, Rust has recently grown a feature to make this possible: Panic as abort. To remove unwinding completely, we can disable it in Cargo.toml

[profile.release]
panic = "abort"
            

This already shaves off a few bytes from our program size, but there's more. There's a language item that we needed to provide to support unwinding. Here's what it looked like:

#[lang = "eh_personality"]
pub extern fn eh_personality() {
    panic!("eh_personality function has been called")
}
            

With unwinding disabled, the function can just be removed completely.

And that wraps us up for today! As always, the full code is available on GitHub. See you next time!