Custom CPU instructions with `asm!`

I need to emit some custom CPU instructions (I'm developing for a modified RISC-V architecture). I naively tried using .insn, which was rejected with "unknown directive" compile error. I would even be fine with hand-writing some inline machine code.

I tried a lot of search terms but didn't find anything useful. As a work-around, I could do it from C and link it (or directly link the assembler file). But it would be cool to be able to do this from Rust as well, even though this is a quite niche use case.

asm doesn't verify its input (that is, it doesn't parse the assembly language you use), so you can use any instruction you want. Example:

asm!("cease", options(readonly, nostack, nomem, pure, noreturn)); // halt SiFive RISC-V processor execution

Don't write raw machine code. Otherwise you'd have to patch your binary. You have one other option: write a function in a separate .S file and then use global_asm! to include it. Then you can call the function you wrote in assembly language in the .S file.

global_asm! looks more like what I need, however it gives me the same compile error.

I tried building a separate assembler file and linking it:

.global assembler_test
assembler_test:
	nop
	.insn r CUSTOM_0, 0x6, 0x7f, x0, x0, x0
    cc::Build::new()
        .file("test.S")
        .compiler("riscv32-none-elf-gcc")
        .compile("my-asm-lib");

The interesting bit is that I had to set compiler (or TARGET_CC env variable) for it to work, otherwise I would get the same error too. I thus imply that cargo is somehow not choosing the correct compiler for the platform for inline assembly.

How does the global_asm! macro work at all internally? I ran strace cargo build and could not find any assembler/compiler calls related to it. I think that the "unknown directive" error comes from the wrong compiler target being used (I'm cross compiling).

Writing an assembler file and linking against it is a good stop-gap solution, but the functions cannot be inlined so there is a hard performance hit (three instructions prelude vs one or two instructions of work).

By default, rustc does not run an external assembler. There is an integrated assembler inside LLVM, which rustc uses. So if your custom instruction is only known to your custom assembler and not known to LLVM's integrated assembler, it will not work.

This can be inconvenient, so Rust used to have external assembler mode. In this mode, Rust instructs LLVM to generate assembly instead of binary, and passes generated assembly to external assembler. But it was a maintenance burden used by few, so external assembler mode got removed in 2020.

https://github.com/rust-lang/rust/pull/70345

2 Likes

That's unfortunate, but thank you for the link. A small clarification though: although I considered it as a solution, I don't require a custom assembler. The .insn directive was purposefully built so that one can write arbitrary non-existing assembler commands without having to modify the assembler itself.

In another note: would it be possible to get some feature like this back out of tree, e.g. using procedural macros?

Is .insn a RISC-V thing? If so, I think the best solution is to get LLVM's integrated assembler for RISC-V target to implement .insn. LLVM generally wants to be compatible with standard external assembler.

1 Like

The general concept of the .insn directive (treat some data as code, at the place of declaration) doesn't seem to be architecture specific per se, but I could only find mentions of for RISC-V or MIPS. Also, both architectures seem to have different syntax for the directive.

As of today's nightly, the .insn directive is supported by Rust's LLVM.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.