Viewing assembly for Rust function

George Shuklin
journey to rust
Published in
4 min readFeb 26, 2021

I got a funky task: to prove that my benchmark doing the right thing. (turned out I used benchmark without proper black_box’ing of constants and my opponent was right). Nevertheless, as a separate instrumentation task, ‘view assembly of the function’ was a reasonable request.

I want to see the assembly for my function in a release mode, with all optimizations enabled. Preferably, without need to disassemble and read through the whole binary (which is hellish task for any normal program with extensive dependency list).

So, the task: for a given project to be able to see assembly for a given function in the project. Let’s say our project is foobar, and there is foo.rs code somewhere in src or (as in my case) in bench directory. We want to see foo.s with assembly, with annotations to which function this assembly it intended.

Generating assembly

That part is easy:

RUSTFLAGS="--emit asm" cargo build --release

or

RUSTFLAGS="--emit asm" cargo bench --bench foo

And you have pile of ‘.s’ files in target. You can say ‘jobs’ done, but I have hard time to read the result. There is a nice site https://godbolt.org/, which gives very much readable output. I want to reproduce this result on my machine.

It leaves no junk and it’s easy to read. I want the same!

cargo-asm

Turns out, there is an extension for cargo:

cargo install cargo-asm

which adds cargo asm sub-command.

To work with it one needs to provide a proper path to the object. In my case it’s quadtree module for my equart application where I want to see subarea method for Boundry structure.

cargo asm --asm-style=intel \
--rust equart::quadtree::Boundry::subarea

With it it’s much nicer output.

Benchmark code disassembly

It’s a bummer. I wasn’t able to find a way to force asm sub-command to look for benchmark code. May be I miss something, or cargo asm just don’t support it.

I though about a way around: stop using benches/ directory and move all code to src/main.rs. The benchmark itself looked broken, and the assembly wasn’t there too.

I’ve thrown away all benchmark related code, but wasn’t able to see my function in assembly. I’ve looked for the next function (not the one you see above), and it was really hard.

Turned out, it wasn’t cargo asm fault…

Optimize, inline, eliminate!

After I removed bulky criterion and assembly generation become fast, I was able to pinpoint issue to inlining.

This is a source code:

fn foo(){
println!("Hello, world!");
}
fn main() {
foo();
}

and here is an assembly for the main function:

in-lining at its glory!

Yep, foo was inlined. There is no foo function to disassemble.

The solution was simple: disable inlining:

#[inline(never)]
fn foo(){
println!("Hello, world!");
}

And you have it:

Inlining disabled, function present

Side effects and optimization

If only this worked for all normal functions. My original goal was to see the code for the very simple pure function, for example, fn foo42()->i32 {42}

Turned out, if function has no side effects, its much, much, much harder to force the compiler to keep it as a function. Even debug mode does not work. Function just disappears. Compiler is too smart.

You can see this thread with many suggestions on how to preserve simple function from been washed away. It includes using mod (modules), extern mark on the function, moving code into separate source file, adding #[inline(never)], marking function pub (public)…

None of it worked. If the compiler see an easy target, it switches into hunt mode.

The single way was to make it into a true library.

Make it library

Turned out, if you declare your rs file (any, except of main.rs) as a separate library with pub function, it won’t inline that function, even without additional hints.

This is my ‘fdiv’ project, with simple foo42 function I want to disassemble:

├── Cargo.lock
├── Cargo.toml
├── src
├── foo.rs
└── main.rs

Content of my Cargo.toml:

[dependencies]
...
[lib]
name = "mylib"
path = "src/foo.rs"
[[bin]]
name = "fdiv"
path = "src/main.rs"

Content of src/foo.rs:

pub mod view{
pub fn foo42() -> i32{42}
}

Content of my main.rs (which is not strictly necessary to see assembly):

mod foo;fn main(){
println!("{}",foo::view::foo42());
}

Now, it’s assembly part (don’t forget to install cargo-asm crate, cargo install cargo-asm):

$ cargo asm --lib
mylib::view::foo42

It shows our function (foo42 in list of available objects to disassemble). Now, fanfare:

The disassembly of the function in Rust

Hurray! I got a shiny new tool I love to use. It’s not like I’m going to do something useful with assembly, but it’s a great debug tool for some rather complicated questions on performance. Sometimes peeking into code may be easier than reasoning about benchmark numbers.

--

--

George Shuklin
journey to rust

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.