Zach Mitchell Physicist, programmer, breaker of working things.

Interacting with assembly in Rust

3 min read 1087

Interacting With Assembly in Rust

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.

Viewing 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.

Godbolt Compiler Explorer

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.

We made a custom demo for .
No really. Click here to check it out.

Color-Coded Assembly Code

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.

Reveal Linked Code

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:

cargo-asm Output

Rust developers learning assembly may find the ability to compare unfamiliar assembly to the corresponding (familiar) Rust code particularly useful.

Including assembly

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 the asm! macro, and it required Rust nightly. This macro was essentially a wrapper around LLVM’s inline assembler directives. This macro has been renamed to llvm_asm! while a new asm! macro is worked on in Rust nightly, but a nightly compiler is still required to use llvm_asm!.

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 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.

Conclusion

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.

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    : Full visibility into your web apps

    LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

    In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

    .
    Zach Mitchell Physicist, programmer, breaker of working things.

    Leave a Reply