Dependency Free Rust Binary
09 Dec 2024Introduction
In some situations, you may need to build yourself a bare machine binary file. Some embedded applications can require this, as well as systems programming where you might be building for scenarios where you don’t have libraries available to you.
In today’s post, we’ll go through building one of these binaries.
Getting Started
Let’s create a standard binary project to start with.
This will produce a project that will have the following structure:
Your application should have no dependencies:
and, you shouldn’t have much in the way of code:
We build and run this, we should see the very familiar message:
This is already a pretty minimal program. Now our job starts!
Standard Library
When you build an application, by default all Rust crates will link to the standard library.
We can get rid of this by using the no_std
attribute like so:
After a quick re-build, we quickly run into some issues.
Clearly, println
is no longer available to us, so we’ll ditch that line.
We also need to do some extra work around handling our own panics.
Handling Panics
Without the no_std
attribute, Rust will setup a panic handler for you. When you have no_std
specified, this
implementation no longer exists. We can use the panic_handler
attribute to nominate a function that will handle our
panics.
Now we’ve defined a panic handler (called panic
) that will do nothing more than just spin-loop forever. The
return type of !
means that the function won’t ever return.
We’re also being told that unwinding panics are not supported when we’re not using the standard library. To simplify
this, we can just force panics to abort. We can control this in our Cargo.toml
:
We’ve just disabled unwinding panics in our programs.
If we give this another rebuild now, we get the following:
This is progress, but it looks like we can’t hold onto our main
function anymore.
Entry Point
We need to define a new entry point. By using the no_main
attribute, we are free to no longer define a main
function in our program:
We really have no entry point now. Building this will give you a big horrible error and basically boils down to a linker error:
Fair enough. Our linker is taking exception to the fact that we don’t have a _start
function which is what the
underlying runtime is going to want to call to start up. The linker will look for this function by default.
So, we can fix that by defining a _start
function.
The no_mangle
attribute makes sure that the _start
function maintains its name, otherwise the compiler will
use its own creativity and generate a name for you. When it does this, it mangles the name so bad that the linker
can no longer find it.
The extern "C"
is as you’d expect, giving this function C calling conventions.
The C Runtime
After defining our own _start
entrypoint, we can give this another build.
You should see a horrific linker error.
The program that the compiler and linker is trying to produce (for my system here at least) is trying to do so using the C runtime. As we’re trying to get dependency-free, we need to tell the build chain that we don’t want to use this.
In order to do that, we need to build our program for a bare metal target. It’s worth understanding what a “target triple” is and what one is made up of that you can start using. The rust lang book has a great section on this.
These take the structure of cpu_family-vendor-operating_system
. A target triple encodes information about the target of a
compilation session.
You can see all of the targets available for you to install with the following:
You need to find one of those many targets that doesn’t have any underlying dependencies.
In this example, I’ve found x86_64-unknown-none
. A 64-bit target produced by unknown
for not particular operating
system: none
. Install this runtime:
Let’s build!
We’ve got a build!
Output
Now we can inspect the binary that we’ve just produced. objdump
tells us that we’ve at least made an elf64:
Taking a look at our _start
entrypoint:
There’s our infinite loop.
Running, and more
Did you try running that thing?
As expected, the application just stares at you doing nothing. Excellent. It’s working.
Let’s add some stuff back in. We can start writing a little inline assembly language easy enough to start to do some things.
We can import asm
from the core::arch
crate:
The syscall at 60 is sys_exit
. In 64-bit style, we load it up in rax
and
put the exit code in rdi
.
We can relax in _start
point now that it’s unsafe:
We can now build this one:
We can crack this one open now, and take a look at the underlying implementation.
Unsurprisingly, we’re calling our exit implementation which has been mangled - you’ll notice.
Let’s give it a run.
Conclusion
Success - we’ve made some very bare-bones software using Rust and are ready to move onto other embedded and/or operating system style applications.