3

Problem

I want to execute the exit system call in ARM using inline assembly on a Linux Android device, and I want the exit value to be read from a location in memory.

Example

Without giving this extra argument, a macro for the call looks like:

#define ASM_EXIT() __asm__("mov     %r0, #1\n\t" \
                           "mov     %r7, #1\n\t" \
                           "swi     #0")

This works well. To accept an argument, I adjust it to:

#define ASM_EXIT(var) __asm__("mov     %r0, %0\n\t" \
                              "mov     %r7, #1\n\t" \
                              "swi     #0"          \
                              :                     \
                              : "r"(var))

and I call it using:

#define GET_STATUS() (*(int*)(some_address)) //gets an integer from an address

ASM_EXIT(GET_STATUS());

Error

invalid 'asm': operand number out of range

I can't explain why I get this error, as I use one input variable in the above snippet (%0/var). Also, I have tried with a regular variable, and still got the same error.

Community
  • 1
  • 1
Paschalis
  • 11,929
  • 9
  • 52
  • 82
  • Does `GET_STATUS` really have a `//` on it or did you add that to clarify the question? What happens if you remove it? Also, how about trying ASM_EXIT(1)? Lastly, it might be informative to try compiling this with `-E`. – David Wohlferd May 21 '16 at 06:00
  • 1
    `%r7` - well, you don't have 8 operands to the extended asm, do you? In the case of the simple asm (where the compiler doesn't try to parse the contents), it just happens that the assembler apparently ignores the bogus syntax and does assemble what you _think_ you mean. – Notlikethat May 21 '16 at 08:53
  • 1
    For GCC ARM asm you don't need the % prefixed to register names. – Jeremy May 21 '16 at 08:56
  • @DavidWohlferd no it does not. For clarification. – Paschalis May 21 '16 at 17:48
  • Here is a minimal working example with register variables: https://stackoverflow.com/questions/3929442/how-to-specify-an-individual-register-as-constraint-in-arm-gcc-inline-assembly/54845046#54845046 – Ciro Santilli Feb 25 '19 at 11:38

1 Answers1

5

Extended-asm syntax requires writing %% to get a single % in the asm output. e.g. for x86:

asm("inc %eax")                // bad: undeclared clobber
asm("inc %%eax" ::: "eax");    // safe but still useless :P

%r7 is treating r7 as an operand number. As commenters have pointed out, just omit the %s, because you don't need them for ARM, even with GNU as.


There aren't specific-register constraints for ARM to request operands in specific registers like there are for x86. (e.g. x86's "a" constraint only lets the compiler pick (the bottom part of of) rax).

You can use register int var asm ("r7") to force a var to use a specific register, and then use an "r" constraint and assume it will be in that register. (This is in fact the only supported use of register ... asm("regname"): https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html)

This generated efficient code which avoids wasting an instruction on a reg-reg move.
See it on the Godbolt Compiler Explorer:

__attribute__((noreturn)) static inline void ASM_EXIT(int status)
{
  register int status_r0 asm ("r0") = status;
  register int callno_r7 asm ("r7") = 1;
  asm volatile("swi  #0\n"
      :
      : "r" (status_r0), "r" (callno_r7)
      : "memory"    // any side-effects on shared memory need to be done before this, not delayed until after
  );
  // __builtin_unreachable();  // optionally let GCC know the inline asm doesn't "return"
}

#define GET_STATUS() (*(int*)(some_address)) //gets an integer from an address

void foo(void) { ASM_EXIT(12); }
    push    {r7}    @            # gcc is still saving r7 before use, even though it sees the "noreturn" and doesn't generate a return
    movs    r0, #12 @ stat_r0,
    movs    r7, #1  @ callno,
    swi  #0
     # yes, it literally ends here, after the inlined noreturn

void bar(int status) { ASM_EXIT(status); }
    push    {r7}    @
    movs    r7, #1  @ callno,
    swi  #0                  # doesn't touch r0: already there as bar()'s first arg.

Since you always want the value read from memory, you could use an "m" constraint and include a ldr in your inline asm. Then you wouldn't need the register int var asm("r0") trick to avoid a wasted mov for that operand.

The mov r7, #1 might not always be needed either, which is why I used the register asm() syntax for it, too. If gcc wants a 1 constant in a register somewhere else in a function, it can do it in r7 so it's already there for the ASM_EXIT.


Any time the first or last instructions of a GNU C inline asm statement are mov instructions, there's probably a way to remove them with better constraints.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 1
    "You can use register int var asm ("r7") to force a var to use a specific register" - as far as I know its the only way of doing it (without the extra mov) on ARM and was recommended to me by the GCC team. I don't know why you cant just give the register name like you can on x86. – Jeremy May 21 '16 at 16:44
  • 1
    A `"memory"`clobber may be needed ensure memory writes are not reordered past the exit system call by the compiler if there are writeable memory mapped files or the process is multithreaded. – Timothy Baldwin Feb 08 '22 at 17:16
  • @TimothyBaldwin: Thanks, great point. Fixed. – Peter Cordes Feb 08 '22 at 17:27