2

I'm messing around with the asm! macro on an embedded ARM (Thumb) target. I have an interrupt service routine that is designed to get the number that the svc instruction was called with:

#[cortex_m_rt::exception]
unsafe fn SVCall() {
    let mut svc_num: u8;

    asm!(
        "ldr {0}, [sp, #40]",   // read the PC that was saved before this interrupt happened
        "movs {1}, #2",         // store 2 in a reg
        "subs {0}, {1}",        // subtract 2 from that PC we recovered
        "ldrb {2}, [{0}]",      // read the byte at that position
        out (reg) _,
        out (reg) _,
        lateout (reg) svc_num
    );

    defmt::info!("svcall #{}", svc_num);
}

When I disassemble the resulting code compiled with opt-level = 2 (this is important, I get completely different results with opt-level = 0), I get the following:

08000502 <SVCall>:
 8000502:       b580            push    {r7, lr}
 8000504:       466f            mov     r7, sp
 8000506:       b082            sub     sp, #8
 8000508:       980a            ldr     r0, [sp, #40]   ; 0x28
 800050a:       2102            movs    r1, #2
 800050c:       1a40            subs    r0, r0, r1
 800050e:       7802            ldrb    r2, [r0, #0]
 8000510:       f807 2c05       strb.w  r2, [r7, #-5]
 8000514:       f000 fb04       bl      8000b20 <_defmt_acquire>
 8000518:       f240 000e       movw    r0, #14
 800051c:       f2c0 0000       movt    r0, #0
 8000520:       f000 fb70       bl      8000c04 <_ZN5defmt6export9make_istr17h6ffa41eb00995773E>
 8000524:       f8ad 0004       strh.w  r0, [sp, #4]
 8000528:       a801            add     r0, sp, #4
 800052a:       f000 fba4       bl      8000c76 <_ZN5defmt6export6header17h9dd906a13f87833fE>
 800052e:       f240 0002       movw    r0, #2
 8000532:       f2c0 0000       movt    r0, #0
 8000536:       f000 fb65       bl      8000c04 <_ZN5defmt6export9make_istr17h6ffa41eb00995773E>
 800053a:       f827 0c02       strh.w  r0, [r7, #-2]
 800053e:       1eb8            subs    r0, r7, #2
 8000540:       f000 fb61       bl      8000c06 <_ZN5defmt6export4istr17hddd45161235dee63E>
 8000544:       1f78            subs    r0, r7, #5
 8000546:       f000 fbb3       bl      8000cb0 <_ZN5defmt6export8integers2i817h6232ecd7ea5eb90dE>
 800054a:       f000 faeb       bl      8000b24 <_defmt_release>
 800054e:       b002            add     sp, #8
 8000550:       bd80            pop     {r7, pc}

My calculations indicate that I should only have to use an offset of 32 in my ldr instruction, but I am having to compensate for the sub sp, #8 instruction that is being inserted before my code.

My two questions are:

  • Is there a way to indicate to Rust that I do not want this instruction to be inserted (or at least, that it must come after my asm! instructions), because I need to read the value of the sp register?
  • Can I assume that sp will always be copied to r7 and use r7 as a stack frame base pointer?
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
laptou
  • 6,389
  • 2
  • 28
  • 59
  • 2
    That assumption about r7 is definitely not safe. If this handler is only ever invoked manually via `svc`, it might not be totally broken to have the compiler treat registers like it's a function, stepping on the call-clobbered registers instead of saving/restoring them the way system calls work on most OSes. But normally if you need something like this, it makes a lot more sense to write the entry-point in asm and set up args to call a normal function with, using the standard calling convention for that. – Peter Cordes Nov 28 '21 at 07:23

2 Answers2

2

... that I do not want this instruction to be inserted ...

This would not help you at all:

The next compiler version might use push {r6, r7, lr} instead of push {r7, lr} and the information you are interested in is located at sp+44 instead of sp+40.

... Can I assume that sp will always be copied to r7 ...

Even if this was the case, the problem with the push {r6, r7, lr} could not be solved with this assumption.

// read the PC that was saved before this interrupt happened

To do such things, there is no other way but writing the whole function in assembly and calling the high-level language part (e.g. Rust, C, C++ ...) from the assembly code.

There is no way of doing this using inline assembly within a high-level language function. At least none that is guaranteed to be working with a newer compiler version.

Martin Rosenau
  • 17,897
  • 3
  • 19
  • 38
  • 1
    Yup, if you want to write an interrupt handler in a high-level language, you need language support for it. Like GNU C `__attribute__((interrupt))`. The only way you could get *close* would be to write something that had a bunch of dummy args so you could have some stack args the compiler knows about to read stack memory above the incoming SP. (Like in this hacktastic UB-exploiting pure-C `_start` for x86-64 - [How Get arguments value using inline assembly in C without Glibc?](https://stackoverflow.com/a/50283880).) – Peter Cordes Nov 28 '21 at 07:55
2

You can use naked functions:

#![no_std]
#![feature(asm, naked_functions)]

#[naked]
#[export_name = "SVCall"]
pub unsafe extern "C" fn SVCall() {
    asm!(
        "ldr r0, [sp, #40]",       // read the PC that was saved before this interrupt happened
        "ldrb r0, [r0, #-2]",      // read the byte at PC - 2
        "b other_func",            // call other_func with that byte as first argument
        options(noreturn)
    );
}

pub extern "C" fn other_func(svc_num: u8) {
    // do something
}

Edit: Thanks to Peter Cordes for pointing out the many ways my previous answer did not work. Essentially naked functions should contain one inline-assembly block and you can only call functions with a defined ABI from it, in assembly.

laptou
  • 6,389
  • 2
  • 28
  • 59
HHK
  • 4,852
  • 1
  • 23
  • 40
  • 2
    Is it safe to use asm with output operands, and a non-asm function call, in a `naked` function? It isn't in GNU C or clang, so I'd guess it isn't in Rust either. e.g. with opt-level=0, it stores/reloads the `svc_num` local var to `[sp, #7]`, overwriting whatever was on the stack there. https://godbolt.org/z/3arqKGfo5 shows that, and the various warnings like "warning: Rust ABI is unsupported in naked functions" and for using output operands with your asm: "warning: only `const` and `sym` operands are supported in naked functions". – Peter Cordes Nov 28 '21 at 10:00
  • 1
    So yes you can use `naked` functions, but you should be writing that `b other_func` yourself. (And picking registers manually, and probably saving user-space's regs before you overwrite them.) – Peter Cordes Nov 28 '21 at 10:01
  • @PeterCordes Thanks, I have updated the answer accordingly. – HHK Nov 28 '21 at 10:50
  • Yup, now it should reliably build to the expected asm. Leaving the only potential problem being that if this is actually an interrupt entry point, the stack is probably not in a fit state to tailcall a function that expects to be able to return normally. And it destroys the incoming r0, which normal ABIs expect to hold a syscall arg. (Unless ARM banked registers avoid that?). But this is fine if called as a function by something with the saved PC high up the stack there. OTOH if this isn't an SVC entry-point directly, that already-hand-written asm might as well do this and pass an arg. – Peter Cordes Nov 28 '21 at 11:41
  • This worked perfectly. All I needed to do was add the `#[export_name]` attribute to get it to be recognized as the SVCall ISR (the `#[cortex_m_rt::exception]` attribute is not compatible with any other attributes, so I can't use it). Thanks! – laptou Nov 28 '21 at 20:09