Inline assembly noreturn clobber

I'm writing a bootloader for cortex-m4 and I think I want something like this:

    let x = addr;
    unsafe{
        asm!(
            "ldr {tmp}, [{x}, #4]",
            "ldr sp, [{x}]",
            "blx {tmp}",
            x = in(reg) x,
            tmp = out(reg) _,
            options(noreturn),
        );
    }

However that is not possible:

error: asm outputs are not allowed with the `noreturn` option
  --> src/main.rs:19:13
   |
19 |             tmp = out(reg) _,
   |             ^^^^^^^^^^^^^^^^

error: could not compile `bootloader` (bin "bootloader") due to 1 previous error

What would be the correct way to do this?

My current workaround is to add core::hint::unreachable_unchecked() after the asm block. Is that reasonable?

Given that the reference mentions this use case, it would make sense to allow it even for noreturn IMO. If nobody else comes up with a better way, I think this is worth a feature request.

An underscore (_) may be specified instead of an expression, which will cause the contents of the register to be discarded at the end of the assembly code (effectively acting as a clobber).

1 Like

this error makes sense from the perspective of data flow analysis. [1]

the workaround is very simple: just split the assembly instructions into two parts, one with normal data flow of out(reg), and the other for noreturn without out(reg):

    let x = addr;
    unsafe{
        let tmp: isize;
        asm!(
            "ldr {tmp}, [{x}, #4]",
            "ldr sp, [{x}]",
            x = in(reg) x,
            tmp = out(reg) tmp,
        );
        asm!(
            "blx {tmp}",
            tmp = in(reg) tmp,
            options(noreturn),
        );
    }

  1. sidenote: I always see it a (clever) hack of (ab-)using out(reg) _ to emulate clobbers, and this is another counter example. â†Šī¸Ž

Nice, I'll try that. I guess the compiler will not reorder those blocks?

That is UB in this case. The stack pointer needs to be restored before the end of the asm!() block. A codegen backend is allowed to outline asm!() blocks or for other reasons allowed to insert stack accesses between both asm!() blocks.

1 Like

I went with the following, which gave me slightly larger code size. (I guess that is because the constant propagation is pretty early so it "inlines" the addition of 4 instead of using a load with offset.)

    let next_pc: u32 = unsafe { *((addr + 4) as *const u32) };
    unsafe {
        asm!(
            "ldr sp, [{sp}]",
            "blx {next_pc}",
            sp = in(reg) addr,
            next_pc = in(reg) next_pc,
            options(noreturn)
        )
    }

I wonder why couldn't blocks be split like this, instead:

    let x = addr;
    let tmp;
    unsafe{
        asm!(
            "ldr {tmp}, [{x}, #4]",
            x = in(reg) x,
            tmp = out(reg) tmp,
        );
        asm!(
            "ldr sp, [{x}]",
            "blx {tmp}",
            x = in(reg) x,
            tmp = in(reg) tmp,
            options(noreturn),
        );
    }

This means you no longer have clobber registers and compiler knows that you've got something in tmp, but it described things that are happening cleanly and doesn't involve any UB.

Yeah, I could, but at that point the first block is just "read this memory location", which I think is more readable if it is implemented in rust.

The docs appear to say you cannot assume instructions won't be inserted between, but this doesn't seem to indicate that they could actually be reordered:

You cannot assume that two asm! blocks adjacent in source code, even without any other code between them, will end up in successive addresses in the binary without any other instructions between them.

I'm guessing that language is just to allow padding nops for alignment?

It also allows the asm blocks to be separately compiled and called as functions.