cgccinline-assemblygotoavr32

Jump out of inline assembly goes to the wrong target on AVR32


We are developing an application for the Atmel AVR32 / UC3C0512C using AtmelStudio 7.0.1645. While doing some basic tests, I noticed something very weird.

Please consider the following code (I know that it is bad style and uncommon, but that's not the point here):

float GetAtan2f(float p_f_y,
                float p_f_x)
{
  unsigned int l_ui_x,
               l_ui_y,
               l_ui_Sign_x,
               l_ui_Sign_y,
               l_ui_Result;

  float l_f_Add,
        l_f_Result;

  asm volatile(
    "RJMP GETATAN2_EXIT \n"
    :
    : /* 0 */ "m" (p_f_y),
      /* 1 */ "m" (p_f_x)
    : "cc", "memory", "r0", "r1", "r2", "r3", "r5"
  );

  GETATAN2_EXIT:
  return (l_f_Result);
}

When looking into the disassembly of that code (after it has been compiled / linked), I find the following:

Disassembly of section .text.GetAtan2f:

00078696 <GetAtan2f>:
   78696:   eb cd 40 af     pushm   r0-r3,r5,r7,lr
   7869a:   1a 97           mov r7,sp
   7869c:   20 9d           sub sp,36
   7869e:   ef 4c ff e0     st.w    r7[-32],r12
   786a2:   ef 4b ff dc     st.w    r7[-36],r11
   786a6:   e0 8f 00 00     bral    786a6 <GetAtan2f+0x10>
   786aa:   ee f8 ff fc     ld.w    r8,r7[-4]
   786ae:   10 9c           mov r12,r8
   786b0:   2f 7d           sub sp,-36
   786b2:   e3 cd 80 af     ldm sp++,r0-r3,r5,r7,pc

We notice that rjmp has become bral - perfectly acceptable, just another mnemonic for the same thing.

But when looking at the branch target in that line, we also notice that this will produce an endless loop, which it clearly shouldn't. It should branch to 786aa (which is the begin of the function return) instead of 786a6.

If I change the code so that it reads

float GetAtan2f(float p_f_y,
                float p_f_x)
{
  unsigned int l_ui_x,
               l_ui_y,
               l_ui_Sign_x,
               l_ui_Sign_y,
               l_ui_Result;

  float l_f_Add,
        l_f_Result;

  asm volatile(
    "RJMP GETATAN2_EXIT \n"
    :
    : /* 0 */ "m" (p_f_y),
      /* 1 */ "m" (p_f_x)
    : "cc", "memory", "r0", "r1", "r2", "r3", "r5"
  );

  asm volatile(
    "GETATAN2_EXIT: \n"
    :
    :
    : "cc", "memory"
  );

  return (l_f_Result);
}

it works as expected, i.e. the disassembly now reads

Disassembly of section .text.GetAtan2f:

00078696 <GETATAN2_EXIT-0x12>:
   78696:   eb cd 40 af     pushm   r0-r3,r5,r7,lr
   7869a:   1a 97           mov r7,sp
   7869c:   20 9d           sub sp,36
   7869e:   ef 4c ff e0     st.w    r7[-32],r12
   786a2:   ef 4b ff dc     st.w    r7[-36],r11
   786a6:   c0 18           rjmp    786a8 <GETATAN2_EXIT>

000786a8 <GETATAN2_EXIT>:
   786a8:   ee f8 ff fc     ld.w    r8,r7[-4]
   786ac:   10 9c           mov r12,r8
   786ae:   2f 7d           sub sp,-36
   786b0:   e3 cd 80 af     ldm sp++,r0-r3,r5,r7,pc

We notice that the branch target now is correct.

So the inline assembler obviously does not know about C labels (i.e. labels which are not in inline assembly), which per se would be O.K. - lesson learned.

But in addition it does not warn or throw errors when it encounters an unknown (undefined) label, but instead produces endless loops by just using an offset of 0 when branching / jumping to such labels.

I am considering the latter a catastrophic bug. It probably means that (without any warning) I'll get an endless loop in my software whenever I use an undefined label in inline assembly code (e.g. because of a typo).

Is there anything I can do about it?


Solution

  • If you don't tell the compiler that execution might not come out the other side of your asm statement, the compiler assumes that to be the case.

    So both your examples are unsafe, and it's just luck that the 2nd one doesn't break anything because the function is too simple.


    I'm not sure how your code compiled at all; C local labels don't normally appear as asm labels with the same name. If they're used at all by compiler-generated code, gcc uses names like .L1 the same as for branch targets it invents for if() and for/while loops. @Kampi reports a linker error for your source with AtmelStudios 7.0.1931.

    Perhaps you're actually looking at a non-linked .o, where the branch target was just a placeholder to be filled in by the linker. (And the reference to the undefined symbol is a linker error waiting to happen). The encoding of e0 8f 00 00 certainly fits that: the assembler didn't find the branch target label in the .s the compiler gave it, so it treated it as an external symbol and used a branch with more displacement bytes. Apparently on AVR32, relative branch displacements are relative to the start of the branch instruction, unlike many ISAs where it's relative to the end of the branch. (i.e. PC while an instruction is decoded/executed has already been incremented.)

    So that would explain your lack of linker errors (because you never ran the linker), and seeing a bogus branch target. Update: this was being linked, but into a library. So the library itself still had an unresolved symbol.

    The target defined in another inline asm statement is present in the compiler's asm output, so the assembler finds it and can use a short rjmp.

    (Some assemblers help you catch mistakes like this by requiring extern foo declarations. GAS does not; it simply assumes that any undefined symbol is extern. GAS syntax comes from traditional Unix assemblers that are designed to assemble compiler output, where ancient compilers that only compiled one C function at a time (not whole-file optimization) wouldn't know whether a definition for a function would appear in this .c file or a separate .c file. So this syntax enables one-pass compiling of C on machines without enough memory to go back and add extern declarations for symbols that aren't defined later in the asm output.)


    GNU C asm goto makes this safe

    GNU C inline asm does have syntax for jumping out of inline-asm statements (to C labels). https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#GotoLabels. And see an example on SO: Labels in GCC inline assembly. (Using x86 instructions, but the contents of the asm template are irrelevant to how you use the asm goto syntax.)

    On targets without GCC6 syntax for condition-code / flag outputs, it can be a handy way to use a conditional branch in inline asm to jump to a some_label: return true; or fall through to a return false;. (Using condition flags as GNU C inline asm outputs)

    But according to the commit message stating reasons for the Linux kernel dropping AVR32 support, AVR32 gcc is stuck at gcc4.2. asm goto only appeared in gcc4.5.

    Unless the AtmelStudio compiler is (based on?) a more recent gcc, you simply can't safe do this safely.