Include Assembly Source Files In Rust Project (and Build with Cargo)

Published by Philipp Schuster on

Update: Since Rust 1.59, the macros global_asm! and asm! are finally stable! I also updated the blog post a little but it is mostly in the state from back then.

In this blog post, I show how you can create assembly files next to Rust files and compile everything by Cargo.

What are .S files?

Recently I stumbled upon a Rust project, where *.S– and *.rs-files were side by side in the same Cargo source directory, and I was like: “What the heck, I always wanted this!” At the time when I created this article, there was no documentation about this, or it was well hidden. If you’ve programmed in C/C++ before, you probably have seen that gcc compiles *.c and *.S files to *.o-files and the linker combines all of them together into a library or an executable. These *.S-files (the assembly) are usually written in GAS (GNU Assembler) syntax style/flavor.

Why are they important?

When writing firmware or operating systems, it’s important to be able to define some functions in assembly language. For example, you can construct an aligned multiboot2-Header using assembly or initialize the stack and several other registers before you jump into the code of a higher level language, such as C or Rust. For some symbol you might want to define the section, like .data, .text, .bss, or .foobar. One needs assembly language and an assembler tool for that.

How to include them in Rust?

Unlike in CMake/Make-based projects, .S files are not specified as input files directly. At first, in a Cargo project, assembly files can have an arbitrary name and must be included via the core::arch::global_asm macro. Definitions of symbols/functions written in assembly must live in the global_asm! -macro, as inline assembly (macro core::arch::asm) in Rust is limited to instructions. Rust uses a GAS-like syntax with intel syntax flavor by default for all assembly [GitHub PR, early language reference preview]. It forwards all the assembly source code to LLVM’s internal assembler, which implements the GAS-like flavor. Therefore, the assembly format stays the same for different architectures, but of course, not all architectures support all instructions.

Quote from the language reference:

Currently, all supported targets follow the assembly code syntax used by LLVM’s internal assembler which usually corresponds to that of the GNU assembler (GAS). On x86, the .intel_syntax noprefix mode of GAS is used by default. On ARM, the .syntax unified mode is used. These targets impose an additional restriction on the assembly code: any assembler state (e.g. the current section which can be changed with .section) must be restored to its original value at the end of the asm string. Assembly code that does not conform to the GAS syntax will result in assembler-specific behavior.

Down below, you can find an example.

// `global_asm!` and `asm!` are stable features since Rust nightly 1.59.
// Before that version, you need `#![feature(global_asm)] and the import 
// of `core::arch::global_asm` is not required.

use core::arch::global_asm;

      mov rax, 1337
// or with include_str!: relative to src directory
// Cargo will rebuild the file if "foo.S" changes.

extern "C" {
    fn foo() -> u64;

fn main() {
    unsafe {
        println!("foo : {}", foo());

This way $ cargo build will also take care of the assembly files. Furthermore, cargo notices if one of these files changes and recompile the necessary parts. I didn’t trace the internal process, but I guess it creates a separate object file and links it together with the object file from the Rust code.

I made a project on GitHub that uses this feature to include global functions written in assembly. The official documentation in the language reference is available in the official Rust language reference.

Philipp Schuster

Hi, I'm Philipp and interested in Computer Science. I especially like low level development, making ugly things nice, and de-mystify "low level magic".


Leave a Reply

Your email address will not be published. Required fields are marked *