9 ms·
From Zero to Main(): Bare Metal Rust
- saagarjha 7y agoxxd 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?
- kimixa 7y agoNo, "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/from-scratch/linker.ld#L31 https://github.com/ferrous-systems/zero-to-main/blob/master/...
- duskwuff 7y agoFWIW, 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/ldscript.ld#L26 https://github.com/duskwuff/stm32f103-example/blob/master/ld...
- TickleSteve 7y ago...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.
- duskwuff 7y agoI'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.
- fra 7y agoThis 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 https://interrupt.memfault.com/blog/zero-to-main-1
- saagarjha 7y agoAh, I see. Thanks!
- chrisseaton 7y ago> 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.
- saagarjha 7y agoThat's what I thought, hence the surprise that they managed to summon these two things just by looking at the beginning of the file.
- duskwuff 7y agoAlso, they've misread the pointer -- the initial SP is 0x2001_0000 (SRAM + 64K), not 0x2010_0000 (SRAM + 1MB).
- fra 7y agoAh, you're right! fixed.
- leshow 7y agoGreat 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.
- _bxg1 7y agoI 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.
- rvz 7y agoWell 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.
- 3pt14159 7y agoThis 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.
- jononor 7y agoEmbedded 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.
- wiremine 7y agoSemi-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.
- deleted 7y ago[deleted]
- fra 7y agoCheck out Tock: https://www.tockos.org/ https://www.tockos.org/. As far as I know it's the most complete effort out there.
- nicoburns 7y agoThere is also Real Time For The Masses https://rtfm.rs/0.5/book/en/ https://rtfm.rs/0.5/book/en/
- jsjohnst 7y agoTock 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/ https://www.tockos.org/documentation/walkthrough/
- seren 7y agoIs 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.
- SAI_Peregrinus 7y agoThere's also Drone: https://www.drone-os.com/ https://www.drone-os.com/ I've not used either though, yet. I need to play with them.
- privethedge 7y agoThis 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?
- hackcasual 7y agoThis 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.
- chubs 7y agoSo 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?
- littlestymaar 7y agoThat'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 https://doc.redox-os.org/book/introduction/unsafes.html.
- mlindner 7y agoAnd 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.
- PudgePacket 7y agoSome 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".
- ilovecaching 7y agoThere 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.
- DominikD 7y ago1. 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.
- wahern 7y ago> 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.
- derefr 7y agoA fun alternative way to play with Rust on bare-metal, is https://github.com/rust-console/gba 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 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.
- MuffinFlavored 7y ago> 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? :)
- BubRoss 7y agoNo, because you aren't writing directly to the frame buffer, you have to go through drivers, kernels and PCIe bus.
- MuffinFlavored 7y agoWhat do the drivers in the kernel talking to the device do over the PCI-e bus? Just send data, right?
- BubRoss 7y agoIn 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.
- FpUser 7y agoI 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.
- cyphar 7y agoNobody 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.
- jonny383 7y ago> 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.
- leoedin 7y agoJust 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.
- scoutt 7y ago> 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?
- paavohtl 7y agoThere'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.
- scoutt 7y ago> 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.
- Uther 7y agoYou 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()` .
- sjustinas 7y agoThe 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.
- scoutt 7y agoBut 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.