For many Rust developers, the process of producing a binary from their Rust code is a straightforward process that doesn’t require much thought. However, modern compilers are complicated programs in and of themselves and may yield binaries that perform very differently in response to a minor change in the source code.
In diagnosing performance issues like this, inspecting the output of the compiler can be helpful. The Rust compiler emits various types of output, one of which is assembly. Rust also has facilities for embedding assembly. In this guide, we’ll explore what the Rust community has to offer for extracting and embedding assembly.
To view the assembly output of each tool, we’ll use the following example program.
const NAMES: [&'static str; 10] = [ "Kaladin", "Teft", "Drehy", "Skar", "Rock", "Sigzil", "Moash", "Leyten", "Lopen", "Hobber", ]; fn main() { roll_call(); } pub fn roll_call() { println!("SOUND OFF"); for name in NAMES.iter() { println!("{}: HERE!", name); } let num_present = NAMES.len(); println!("All {} accounted for!", num_present); }
rustc
The quickest and easiest way to generate assembly is with the compiler itself. This method doesn’t require installing any additional tools, but the output can be difficult to navigate. rustc
can emit assembly with the --emit asm
option.
To format the output with Intel syntax (instead of the default AT&T syntax), you can also pass the -C llvm-args=-x86-asm-syntax=intel
option to rustc
. However, it’s more common to interact with cargo
than with rustc
directly.
You can pass this option to rustc
in one of two ways:
$ cargo rustc -- --emit asm -C llvm-args=-x86-asm-syntax=intel $ RUSTFLAGS="--emit asm -C llvm-args=-x86-asm-syntax=intel" cargo build
The assembly will be placed in target/debug/deps/<crate name>-<hash>.s
. If compiled in release mode, it will be under target/release
. The assembly file contains all the assembly for the crate and can be hard to navigate.
A simple way to examine short snippets of code is to run it through the Godbolt Compiler Explorer. This tool is a web application and, as such, doesn’t require you to install any additional tools.
Code entered in the left pane is compiled to assembly and displayed in the right pane. The code entered in the left pane acts as if it’s inside of the main
function, so you don’t need to enter your own main
function.
Sections of the code in the left pane are color-coded so that the assembly in the right pane can be easily identified. For example, entering the roll_call
function and NAMES
array into the left pane displays the following view of the roll_call
function.
You can identify the assembly corresponding to the println!("SOUND OFF")
macro by right-clicking that line and selecting “Reveal linked code” or by searching for the assembly that’s highlighted in the same color.
cargo-asm
cargo-asm
is a Cargo subcommand that displays the assembly for a single function at a time. The beauty of this tool is its ability to resolve symbol names and display the source code interleaved with the corresponding assembly.
However, that cargo-asm
appears to only work with library crates. Put the NAMES
array and roll_call
function into a library crate called asm_rust_lib
, then call cargo-asm
as follows (note: the --rust
option interleaves the source code as this is not the default).
$ cargo asm --rust asm_rust_lib::roll_call
The first few lines of the output should look like this:
Rust developers learning assembly may find the ability to compare unfamiliar assembly to the corresponding (familiar) Rust code particularly useful.
We could always compile assembly into an object file and link that into our binary, but that adds more complexity than we’d like, especially if we only need to include a few lines of assembly. Luckily, Rust provides some facilities to make this process easy, especially in simple cases.
llvm_asm!
Until recently, the official method for including inline assembly into Rust code was an initial asm!
macro, and it required Rust nightly. This macro was essentially a wrapper around LLVM’s inline assembler directives.
The initial asm!
macro was renamed to llvm_asm!
while the new, stable asm!
macro was worked on in Rust nightly — more on that below. Since rustc-nightly-2022-01-17
, the llvm_asm!
macro was removed in favor of the new asm!
macro.
The syntax for the macro is as follows:
llvm_asm!(assembly template : output operands : input operands : clobbers : options );
The assembly template
section is a template string that contains the assembly. The input and output operands handle how values should cross the Rust/assembly boundary.
The clobbers
section lists which registers the assembly may modify to indicate that the compiler shouldn’t rely on values in those registers remaining constant. The options
section, as you can imagine, contains options, notably the option to use Intel syntax. Each section of the macro requires a specific syntax, so I highly recommend reading the documentation for more information.
Note that using the llvm_asm!
macro requires an unsafe
block since assembly bypasses all of the safety checks normally provided by the compiler.
asm!
The new asm!
macro provides a much nicer syntax for using inline assembly than the deprecated llvm_asm!
macro. An understanding of LLVM inline assembler directives is no longer necessary, and the documentation is extensive compared to that of llvm_asm!
.
The new syntax is closer to the normal format string syntax used with the println!
and format!
macros while still allowing the Rust/assembly boundary to be crossed with precision. Consider the small program shown below.
let mut x: u64 = 3; unsafe { asm!("add {0}, {number}", inout(reg) x, number = const 5); }
The inout(reg) x
statement indicates that the compiler should find a suitable general-purpose register, prepare that register with the current value of x
, store the output of the add
instruction in the same general-purpose register, then store the value of that general-purpose register in x
. The syntax is nice and compact given the complexity of crossing the Rust/assembly boundary.
Assembly is a language that many developers don’t use on a daily basis, but it can still be fun and educational to see how code manipulates the CPU directly. A debugger wasn’t mentioned above, but modern debuggers (GDB, LLDB) also allow you to disassemble code and step through it instruction by instruction.
Armed with the tools above and a debugger, you should be able to explore the assembly that your code is translated into in a multitude of ways.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
3 Replies to "Interacting with assembly in Rust"
This might be a tad off-topic, but… I love seeing names from the Stormlight Archive used in the wild, including in examples like the ones given here!
The 3 links in the `llvm_asm!` and `asm!` section all point to the same, wrong `cargo-asm` url.
Thanks for letting us know, we’ve fixed the links. We also made some updates to those sections to indicate that `llvm_asm!` has been deprecated in favor of the new `asm!` macro.