Hacker News new | past | comments | ask | show | jobs | submit login
From Zero to Main(): Bare Metal Rust (memfault.com)
293 points by fra on Dec 17, 2019 | hide | past | favorite | 84 comments



A fun alternative way to play with Rust on bare-metal, is https://github.com/rust-console/gba, which is a Rust crate for writing (unavoidably bare-metal) homebrew for the Gameboy Advance.

See the second half of https://rust-console.github.io/gba/bitmap-video.html for an example of what code using the gba crate looks like.

Tangent: IMHO, the GBA is a nearly-perfect platform to get started with bare-metal programming on. It's not painfully-constrained like the 8-bit micros of old, but it doesn't have any extra layers of abstraction like virtual memory, either. Plus, multimedia IO (drawing to the screen, playing sounds, reading buttons) are all just banging bits, similar to using POKEs in BASIC on an Apple II, which makes for immediate gratification, important for younger learners. And you can run GBA software through emulation on pretty much any device you own; with there being some excellent visual debuggers available as well for Windows/Linux.


> are all just banging bits

At the end of the day, isn't playing the latest and greatest game on a new nVIDIA graphics card at 5k across 2 monitors also just "banging bits" too? :)


Bitbanging traditionally refers to doing what comms hardware can do in software, for instance to create a serial port using just software and a single bit of digital output and input.

https://en.wikipedia.org/wiki/Bit_banging


No, because you aren't writing directly to the frame buffer, you have to go through drivers, kernels and PCIe bus.


What do the drivers in the kernel talking to the device do over the PCI-e bus? Just send data, right?


In the previous scenario the cpu isn't running using virtual memory and doesn't need to go through the kernel to do something outside of writing to its isolated memory space. It can write to certain memory addresses that make up the frame buffer that the screen is being drawn from.

This is not how a modern PC works. There are system calls (specific assembly instructions) that a user space program can use to communicate with global resources. These go through the kernel and thus are an escape hatch to isolated processes.

These are two very different scenarios with the fundamental difference between virtual memory and multiple isolated processes. There isn't much semantic wiggle room.


Great blog post. It's been wonderful to see rust take off in the space and the approaches the folks in the embedded wg and ecosystem have come up with. The cortex-m starter projects, embedded-hal tying the ecosystem together, and various intro books like "discovery". I never would have thought I would explore anything this "low level" but rust has opened those doors for me.


I hated using C/C++ in college because of all the arcane runtime errors and how hard they could be to debug, as well as the finicky tooling/library landscape, the mental overhead of object lifetime management, etc. Coming back around, Rust has given me my first opportunity to really appreciate and enjoy the more gratifying aspects of lower-level programming.


Well it's exhausting to keep hearing the same old C/C++/Rust/Zig anecdotal testimonials of the day which might easily sway the decision for some but may bring misery for others as we are now starting to get credible alternatives in low-level programming.

Hence this, from the perspective of a typical embedded developer shop across the street with a medium sized team of 25+ engineers, would you rather adopt a accepted bad standard known and used across the industry or a new non-standard alternative which will require 'getting used to' by all engineers but comes with a risk of it being either suitable or unsuitable in the future?

The choice is yours.


As the CTO of a IoT sensor manufacturer doing a new project recently, I chose C for firmware over C++ and Rust (and Nim and Zig). C++, Rust etc would be technically more suitable languages. But it is just much easier to get a hold of competent people for firmware development in C than C++, and certainly than Rust. Both from microcontroller vendors, third-party vendors, development houses, independent consultants and new hires. If someone know microcontroller firmware development, they generally know C. Sometimes something else in addition, but always C.


If such a large number of JavaScript and Ruby devs could learn Rust relatively quickly[0], I'm hoping that a firmware dev could as well. The only semi-hard concept about Rust is lifetimes, and that's getting much easier now.

I mean, if you're not allocating to the heap, Rust memory management is not as useful, I concede. But once you learn to properly model data using algebraic data types (in Rust: enums and tuples), it's a huge, huge gamechanger in how you write programs. You can literally make it impossible to represent illegal states, thus saving massive amounts of error handling[1]. Functional devs have been enjoying this for decades, and now it's available to embedded world via Rust.

0. Seriously, a huge number of active Rust devs come from those communities. Even a significant number of Rust compiler devs come from Ruby and JavaScript backgrounds.

1. Yes, with some clever techniques, this is also possible - to a certain extent - in languages without ADTs, but only to a very limited extent. It becomes simple and natural with ADTs


For me Rust keeps the perfect balance between having all these awesome functional concepts but still be a language that works on real world state problems. Don’t get me wrong. I like Haskells Monads etc. But they try to hide the fact that we have state and all it’s wonderful side effects ;). Rust embraces state but with a clever system that feels always one step ahead of you. I also think a paradigm shift will happen when it comes to c. Yes it is used everywhere. Yes you get more developers now. Yes it has more libraries etc. But this was also true for the languages that where used before c. And I think Rust also has the chance to topple over JS in the web world. Lets see. I enjoy Rust at the moment very much and play around with it in all kinds of fields. It is already my number one language for command line utilities :)


It's hard to even convince most embedded C developers to try even a small subset of C++. A language like Rust is waaaaaay out of their comfort zone, and doesn't address any of their perceived problems (it shouldn't be, but this is how a large fraction will approach it: they would likely not even give it a fair chance due to the mode in which they operate).


I think one can both be excited about Rust and acknowledge that for most projects C is still the way to go.

The libraries, communities, compilers, vendor support, ...etc. is much more mature on the C side of things and will remain that way for a while.

At the same time, Rust is getting to the point where early adopters can start using it for their projects. Those are the people who will make Rust a full class option in the future.


Rust can interoperate with C libraries, it's a pretty substantial part of its value proposition. At the same time, it's true that C has way better compiler support for weird embedded targets, and it's also a comparatively mature language that's not going to change much over the years, with a vendor-independent standard definition. For some projects, these things might be important.


> Rust can interoperate with C libraries

True, but speaking as an embedded developer and Rust fan: embedded C libraries just love their #defines in public APIs. This makes interfacing with them from anything but C or C++ more cumbersome, because you can't just declare "extern" functions, you also have to translate the #define macros, which are sometimes non-trivial.


I take the standard of tomorrow any day. Or at least would dare to experiment with it more with putting a small team to make a module with it. Rust has enough steam now, it would be irresponsible to bet against it.


This comment reminds me of what Java and PHP devs were telling me about Merb / Rails in 2008. I'm not saying it's going to go the same way, but I'd bet rust continues to gain traction to the point of escape velocity in embedded systems.


Embedded Systems is considerably more conservative than web development. For example, many greenfield projects are still C over C++. And most new C projects are probably C99 instead of C11.

But yes, I believe Rust will continue to make progress and pick up pace. But it will be a slow process. And C is not going away anytime soon.


C11 didn't change much for uniprocessors iirc.


I think you overestimate the difficulty in "getting used to" something new when it comes to a PL (and rust, even though it has a bit of a learning curve for OOP folks).

I don't want to use C/C++ for embedded projects in the same way that I don't want to use a hand drill when I have a nice corded Milwaukee power drill on the workbench.


> would you rather adopt a accepted bad standard known and used across the industry or a new non-standard alternative which will require 'getting used to' by all engineers but comes with a risk of it being either suitable or unsuitable in the future?

Whichever one works with the hardware? >.> I don't think embedded folks are too used to having choices, usually we just take whatever toolchain the chip manufacturer gives us, so when there's a second option it's scary and strange.


That's the same kind of choice than the one to move from asm to C a few decades ago. Of course, there's many situations where switching to the new tech isn't worth it, but in the long run it will become a more and more obvious choice (assuming the language takes off, the ecosystem will mature and the number of developers will rise).


And maybe the parent was talking about their experience working in a really small team, or on small embedded components of a larger system, or on hobby projects, and has entirely different considerations. Someone saying "I enjoy using Rust for this" does not mean "everyone has to use Rust now".


Like the article is exhausting? Or the anecdotes about Rust in the comments on an article... about Rust?


>C/C++/Rust/Zig

You can now add Zen to that list, seems to be gaining from traction in the Japanese embedded programming community.


Zen is just a copy-paste of Zig with cringy people and marketing on top, it's not a real language.


Zen is sponsored by a Japanese company which also happens to be sponsoring the top contributors in Zig.

As with all open source project you cant please everyone, and if you dont like it you can always fork it and make your own version. Which is exactly what they did.


Zen appears to be a fork of Zig.


This is great and I love Rust, but

> However, when working directly with the hardware, which has no knowledge of Rust’s guarantees, it is necessary to work in Rust’s unsafe mode, which allows some additional behaviors, but requires the developer to uphold certain correctness guarantees manually.

If the whole code will be wrapped in unsafe blocks, then what is the point of using Rust?


This example is a bit of an outlier since it's so small, but a larger program is going to need proportionally smaller amounts of unsafe code. Remember, just because it's marked unsafe, doesn't mean it's not rust. It's just that for doing low level micro-controller type things, you're explicitly addressing parts of RAM (or memory mapped I/O), and that means creating your own pointer.


So is it the case that you only need 'unsafe' for some of your codebase where you're eg bit-banging, or using peripherals, or whatever - but the majority of your logic is running in normal 'safe' mode?


Some amount of unsafe is impossible to avoid for doing certain things. Rust unsafe allows developers to focus their memory auditing efforts, rather than existing languages where _everywhere_ must be checked.

Here's an example. The Rust standard library has unsafe code to do things like allocate memory and read from files. However, it provides a safe abstraction that my code can use "safely" and be confident I won't encounter any memory errors.

Or if you're writing some kind of concurrent data structure, you want to be doing pointer and memory shenanigans for efficiency, but you'd make a safe API for consumers of the data structure.

I think a lot of people are scared initially seeing something like "unsafe". It's kind of equivalent to just regular C/C++ land... It doesn't automatically mean "The program will now eat your memory".


That's right.

For instance, in the redox-os kernel, there is less than 100 places where unsafe is needed : https://doc.redox-os.org/book/introduction/unsafes.html.


And to be more specific you'd ideally write an interface implementation that does that bit banging for you such that you call those functions from your safe code.


Ecosystem, build tools, ergonomics, familiarity, etc. Rust is more than just handrails for C code.


Why not? There are people who are more familiar with Rust than C/C++, why shouldn't they use a more familiar language?


at that point you're getting the worst of both worlds: no safety guarantees and a very complex language.


Unsafe Rust has significantly more safety guarantees than C or C++. It doesn't disable the type system, bounds checking, ownership tracking or borrow checking.


What does Unsafe Rust disable then?


It does not disable, it enables. Unsafe doesn't turn anything off, it adds new capabilities instead. https://doc.rust-lang.org/nomicon/what-unsafe-does.html


You don't have to do everything in unsafe blocks.


  xxd target/thumbv7em-none-eabihf/release/from-scratch.bin | head -n 5
  00000000: 0000 0120 dd00 0000 0000 0000 0000 0000  ... ............
  00000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
  00000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
  00000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
  00000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
> Reading this, our initial stack pointer is 0x20100000, and our start address pointer is 0x000000dd.

I see the pointers in the top line, but is this a standard feature of objcopy's binary format? Are the pointers the stack and entrypoint always at the beginning?


This is part of the spec for ARM MCUs: the vector table starts at 0x0, and the first two words in it are the initial stack pointer and the entrypoint. We go into this in details (with references) in our earlier post: https://interrupt.memfault.com/blog/zero-to-main-1


Ah, I see. Thanks!


Also, they've misread the pointer -- the initial SP is 0x2001_0000 (SRAM + 64K), not 0x2010_0000 (SRAM + 1MB).


Ah, you're right! fixed.


No, "bin" explicitly has no format, but it's interpreted by the bootloader/whatever on the board - you can see the linker script setting this up manually here:

https://github.com/ferrous-systems/zero-to-main/blob/master/...


FWIW, this is a pretty unusual way of setting up vectors. A more typical approach in C is to declare the vector table entirely in C or assembly, and only use the linker script to position it:

https://github.com/duskwuff/stm32f103-example/blob/master/ld...


...and what you're seeing is after the linker has positioned it. that is the defined format of the ARM vector table as it is laid out in memory.


I'm referring to the linker script in kimixa's comment. Explicitly placing each vector table entry in the linker script, as this one does, is unusual.


> objcopy's binary format

There is no file format - it’s just whatever stream of data and instructions the user wrote. It’s a flat file.


That's what I thought, hence the surprise that they managed to summon these two things just by looking at the beginning of the file.


I implemented firmware for very specialized AC motor speed and torque control that also does whole bunch of other things on a very low end microcontroller. Was implemented in C. Out of curiosity I just dug into source code and tried to imagine how it could benefit from Rust. Frankly the benefits would be none. As for safety: this firmware something like 6 years in production with just a single update. And the update was not made because of the bug. Somewhere along the way bunch of parts was ordered for new devices and wrong letter code was used. So instead of receiving 2% tolerance on part it was now 10%. As the order was expensive and not returnable I modified firmware to account for this.


Nobody is saying that every application of C is one where Rust would provide a clear benefit. But there are many examples where writing code C++ or C would result in many more memory safety bugs than if the project was written in Rust (even partially).

In the firmware world, arguably if the ESP8266 firmware was written in Rust it's less likely that some of the remote exploits from September would've been possible.


> In the firmware world, arguably if the ESP8266 firmware was written in Rust it's less likely that some of the remote exploits from September would've been possible.

Less likely, but not guaranteed. Rust is still very new, which means if people are migrating from writing firmware in C to Rust, chances are they are going to bring across C habbits, which will frankly lead to writing C in unsafe Rust.

The security of Rust is only as strong as the author who wrote it. We currently don't have anyone who started their firmware career on Rust.


Just because one project doesn't benefit much doesn't mean others don't. Microcontrollers are pretty powerful these days - Cortex M7s might run at 200+MHz and have 2MB of flash. You could easily end up with a project with a small webserver, wifi and ethernet, a GUI and more. When you have 10s of thousands of lines of code having the kind of guarantees Rust gives becomes more important.


I was specifically referring to my example doing strictly hard real time stuff.

What you are describing looks more like a regular software and can be more cost effective implemented in any higher level language then C.

On a side note while I do not know very much about Rust beyond some main concepts I did couple of experiments with decent size example and compile speed made me turn away from it. When/if significantly improved I might be more interested working with it.

EDIT: added "higher level language then C" instead of "higher level language" to make more clear


Depending on when you tried, the speed might be much better; it's hard to measure exactly, but it's gotten roughly 3x faster since 2015. It's still not where we'd like it to be, and are continuing to invest in improving this, especially in the incremental case.


Semi-related: are there any good recommendations for Rust-based RTOSs. Bare metal is great, but for a lot of projects you'll want some sort of scheduler.


Check out Tock: https://www.tockos.org/. As far as I know it's the most complete effort out there.


Tock definitely looks interesting, but reading the walkthrough[0] leaves a bad taste in my mouth. Mixing C and Rust without even calling that out, let alone explaining the why. Their init() function (serves same purpose as the reset_handler in OP’s article) is also a gnarly mess in comparison to OP’s version.

That said, judging a project based solely on its howto will mean you’ll miss out on a lot of otherwise good stuff. As such, will definitely keep digging into it as I have a project that could leverage it.

[0] https://www.tockos.org/documentation/walkthrough/


There is also Real Time For The Masses https://rtfm.rs/0.5/book/en/


Is Tock a Real Time OS though ? They mention a preemptive scheduling, but not priority inversion for example. And they do not claim anything regarding real time in their doc.


There's also Drone: https://www.drone-os.com/

I've not used either though, yet. I need to play with them.


I mean it's not that hard to write your own scheduler. You can easily set up a simple event poll loop that's not much more than a raw loop. See cooperative multitasking which is completely sufficient for this case if you want something a little more complex.


> Rust does have one additional requirement for a bare metal program: You must define the panic handler.

What would panic? Who is doing (and where) the panic checking? Since no additional libraries are being used, is the panic checking in the core library?

What is the overhead of panic checking?


There's no "panic checking". AFAIK in a no_std environment panic simply jumps to the panic handler, which is more or less an ordinary function.

As for what can panic, anything that calls the `panic!` macro. For example trying to read an out-of-bounds element from an array will cause a panic.


> trying to read an out-of-bounds element

So I understand there are runtime checks for out-of-bound reading/writing, correct? This has a cost (overhead) and for embedded systems this is valuable information that should be considered when choosing a language.


You have to be aware of this but most of the time this is not a problem. Most of the useless checks are optimized out, and if you suffer bound check performance somewhere in the program, and you know what you are doing, you can do unchecked indexing using an unsafe block with `get_unchecked()` .


The indexing operator [] does bound checks, the method `get_unchecked()` does not. If you need the extra speed at the expense of safety, you can do it, but you have to consciously choose this trade-off. Unlike in C, in Rust safety is opt-out.


But in this case the overhead may come from calling get_unchecked() for every array access, is this correct? Unless the function it's inlined and does a quantifiable amount of work (for a given arch), but this may also have an impact on code size. In a way or another, there should be a trade-off for accessing array elements (a very common operation).

I don't know how all of this can be coupled with the embedded ecosystem we are used to work with.


So, here's a fun example of how this works out in practice: https://godbolt.org/z/DXo25P

Here, rustc is able to see that we always have a first element, and will actually completely remove the unchecked one, and replace the body with the "checked" one, which has no checks.

For some reason, I can't get it to show the assembly for just the two functions; it always optimzies everything out. Putting it on the rust playground says this:

  playground::access_first_element: # @playground::access_first_element
  # %bb.0:
 pushq %rax
 cmpq $2, %rsi
 jb .LBB5_2
  # %bb.1:
 movq %rdi, %rax
 addq $4, %rax
 popq %rcx
 retq

  .LBB5_2:
 movq %rsi, %rdx
 leaq .L__unnamed_2(%rip), %rdi
 movl $1, %esi
 callq *core::panicking::panic_bounds_check@GOTPCREL(%rip)
 ud2
                                        # -- End function

  playground::access_first_unchecked: # @playground::access_first_unchecked
  # %bb.0:
 leaq 4(%rdi), %rax
 retq
                                        # -- End function
as you can see, it gets super inlined, and does no work, compared to the bound checked version.


Thank you and the other users for the patience.

I guess in the case of your code, the checks are removed/optimized because the compiler knows the size of the array (since it's static, as in non-dynamically-allocated)?

Probably the out-of-bounds runtime checks will (or should be) enforced when it's about dynamically allocated arrays.


Yep!

Rust can do a lot, even for dynamically allocated ones. Here's a fun example: https://godbolt.org/z/n4ubZS

You can see that add has to do the bounds checks, because it doesn't know how long the slice is. But, foo gets compiled down to a single lea, because even though we create a dynamic length vector, the compiler can tell that it has two elements, and get rid of all of it: the malloc, the bounds checks, everything.

Or take these two versions of a summation function: https://godbolt.org/z/LzGURB

The first uses a manual loop, but rustc eliminates all of the bounds checks, because it knows that it can't possibly go out of bounds. For completeness, I included the iterator version too; you can see the ASM is (I think) identical. (I only looked at the line count and glanced, I didn't compare all 89 lines exactly.)

If rustc can't analyze something, but you can, you can also help hint it with asserts. I don't have an easy example handy, but for example, if rustc does a bounds check in the body of the loop, you could assert! outside of the loop and it has the effect of hoisting the check manually. It's pretty good at doing this on its own, though.


get_unchecked() is marked as an #[inline] function (like most other low level primitives), and through the magic of compiler optimization it should have exactly 0 overhead. The function should most likely compile down to a single CPU instruction.


There are some issues I have with using Rust for bare metal work:

- “zero cost” abstractions end up compiling to lots of code/code that does weird things that can cause cashe thrashing and/or pipeline misprediction. In C the cost of everything is very explicit, in Rust it’s very easy to end up with an inefficient binary. The low correspondence between Rust/C++ and machine code is unattractive when doing bare metal work.

- unsafe blocks seem to defeat the purpose of Rust. It’s like building a proof on top of lemmas that are just random guesses.

- Clean builds of my test project are very slow.


> In C the cost of everything is very explicit, in Rust it’s very easy to end up with an inefficient binary.

I will admit when I first picked up Rust, I struggled with feeling as though the code I was writing was too abstract and I had no idea what the compiled code would look like. But if you actually look at the performance characteristics of things such as Rust's default hashmap you find it's more efficient than most hashmaps in C code (because AVL trees are easier to write correctly in C, but B trees are faster and in Rust users can be sure they're using them safely).

> unsafe blocks seem to defeat the purpose of Rust. It’s like building a proof on top of lemmas that are just random guesses.

Not really, (using your analogy) unsafe blocks are more like human-checked proofs while everything else is machine-checked proofs. Obviously the humans doing the checking of the unsafe blocks can make mistakes (hence why you should minimise it), but unsafe blocks still must obey Rust's memory model.

> Clean builds of my test project are very slow.

Yeah that is still a noticeable problem. But to their credit, they have been slowly improving compile times for several years now.


For the first point it's more that a single instance of panic! in your code will balloon your binary size


Am I missing something -- shouldn't "panic = <some_c_func>" (in Cargo.toml) solve that problem?


1. There are no zero cost abstractions. But Rust ones are as close to zero as possible. You may not want to pay for these on the smallest Cortex M devices but I would sure take them over the pain that is debugging C code on stuff like TM4C123G.

2. That's not correct. Board support packages have some unsafe code in them, sure, but it's a misconception that this defeats the purpose of Rust. Even unsafe code has to uphold borrow checker semantics. Unsafe doesn't turn them off, you can't take two mutable references to the same variable in unsafe block, it won't compile. But you can use raw pointers and it's developer's job to make sure that pointer math is correct. This way your application code calling these low level bits of unsafe code doesn't have to do anything to benefit from the regular Rust guarantees. It's a "make things correct once, benefit always" sort of a deal.

3. In general Rust build times aren't great but I can't say they are worse than working with TI's CCS. Extended compile times on desktop are noticeable but I can't say this is true to the same extent on bare metal.


> you can't take two mutable references to the same variable in unsafe block,

Huh? Creating multiple, unchecked mutable references to the same object is one of the few reasons to even use unsafe. If you get this wrong, Rust's type safety won't save you--any bug effectively undermines the integrity of the remainder of the program. Of course, that doesn't negate the benefits of Rust's type safety, or mean that any program using unsafe is no better than a C or C++ project.


Usually, "mutable reference" refers to &mut T. "mutable pointer" refers to *mut T. Creating aliasing &mut Ts is UB, even in an unsafe block.

Sometimes people are not precise about these words, but that's how I read the parent, given they drew the distinction between "mutable reference"s and "raw pointers".


Thank you, this is exactly what I meant.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: