c++linker-errorsavravr-gcc

avr-g++ - Undefined reference to register Y upon linking


I have a weird problem when doing inline asm and compiling/linking with avr-g++ (version 16, fresh from the Git). I think this might be a bug of the tool chain, but I wanted second opinions in case I am just not seeing something.

Here is the minimal piece of code I managed to come up with that does not work:

int main() {
    unsigned char value = 10;
    asm volatile(
        "lds r12, %0"
        :: "m" (value)
        : "r12");
    return 0;
}

Upon compilation, nothing particular happens; but upon linking, I get this weird error message (these are the exact command I use to build my elf):

> avr-g++ -std=gnu++20 -DF_CPU=20000000L -g -Wall -Wextra -fdata-sections -mmcu=avr128da28 -Os -c mwe.cpp -o mwe.o
> avr-g++ -mrelax -Wl,--gc-sections -mmcu=avr128da28 -Os mwe.o -o mwe.elf
/usr/lib/gcc/avr/16.0.0/../../../../avr/bin/ld: mwe.o: in function `.Loc.3':
.../tests/mwe.cpp:26:(.text.startup+0xc): undefined reference to `Y'
collect2: error: ld returned 1 exit status

(Edit: for reference, l. 26 corresponds to lds r12, %0 exactly; the file contains also some commented code, irrelevant here)

No elf is produced; I tried to objdump mwe.o:

> avr-objdump -s -d mwe.o
...
Disassembly of section .text.startup:

00000000 <main>:
   0:   0f 92           push    r0
   2:   cd b7           in  r28, 0x3d   ; 61
   4:   de b7           in  r29, 0x3e   ; 62

00000006 <.Loc.1>:
   6:   8a e0           ldi r24, 0x0A   ; 10
   8:   89 83           std Y+1, r24    ; 0x01

0000000a <.Loc.3>:
   a:   c0 90 00 00     lds r12, 0x0000 ; 0x800000 <__SREG__+0x7fffc1>

0000000e <.Loc.4>:
   e:   80 e0           ldi r24, 0x00   ; 0
  10:   90 e0           ldi r25, 0x00   ; 0
  12:   0f 90           pop r0
  14:   08 95           ret

I see nothing particularly strange... The linker says the problem is at .text.startup+0xc, but this would be in the middle of the lds instruction, I do not understand why...

What is causing the error?

Edit: a colleague asked me to generate the assembler with -S, and I think we see the culprit here:

...
.L__stack_usage = 1
    .loc 1 23 5 view .LVU1
    .loc 1 23 19 is_stmt 0 view .LVU2
    ldi r24,lo8(10)
    std Y+1,r24
    .loc 1 24 5 is_stmt 1 view .LVU3
/* #APP */
 ;  24 "mwe.cpp" 1
    lds r12, Y+1
 ;  0 "" 2
    .loc 1 28 5 view .LVU4
    .loc 1 29 1 is_stmt 0 view .LVU5
/* #NOAPP */
    ldi r24,0
    ldi r25,0
...

We have Y appearing as second operand of lds, which is indeed incorrect (I think). It seems like the linker takes it as being a symbol. This is confirmed by running nm:

> nm mwe.o
00000034 a __CCP__
00000000 t L0
00000016 t L0
00000016 t L0
00000000 N L0
00000000 N .Ldebug_abbrev0
00000000 N .Ldebug_info0
00000000 N .Ldebug_line0
00000000 t .LFB0
00000016 t .LFE0
0000000c N .LLRL0
00000000 t .Loc.0
00000006 t .Loc.1
00000006 t .Loc.2
0000000a t .Loc.3
0000000e t .Loc.4
0000000e t .Loc.5
00000000 T main
0000003b a __RAMPZ__
0000003e a __SP_H__
0000003d a __SP_L__
0000003f a __SREG__
00000000 a __tmp_reg__
         U Y
00000001 a __zero_reg__

The question now is why would it use the Y register here...


Solution

  • unsigned char value = 10;
    asm volatile ("lds r12, %0" :: "m" (value) : "r12");
    

    The problem with the "m" constraint is that you don't really know what kind of address expression the compiler will use. What you know is that LDS won't fit since that requires an address known at link time, but the address of value is not known at link time since it is in storage class auto. See for example AVR-LibC's Inline Asm Cookbook that mentions constraints like LDS r,i, i.e. reg and immediate.

    What you can do is to take the address of the variable, and supply that to a pointer register X, Y or Z which have constraint e:

        uint8_t value = 10;
        uint8_t result;
        asm ("ld %0, %a1" : "=r" (result) : "e" (&value));
        // Use result.
    

    The problem is that taking the address of an auto variable will force a frame, and you don't want the overhead of a frame when you don't absolutely need it. So you can let the compiler load the variable for you; and when value lives in a register, no load is needed to begin with:

        uint8_t value = 10;
        uint8_t result;
        asm ("mov %0, %1" : "=r" (result) : "r" (value));
        // Use result.
    

    Now the mov may be superfluous when result and value live in the same register, so you can request the same reg from the start:

        uint8_t value = 10;
        uint8_t result;
        asm ("" : "=r" (result) : "0" (value));
        // Use result.
    

    While this is a working example, it's likely not the use case you are after, so the question / use case should be improved.


    How the error works

    FYI, the error works like this: The inline asm produces something like

    LDS r12, Y+1
    

    which is legal assembly. As Y is never defined, the assembler takes it as a global symbol and produces according object code. Then you disassemble the object file and

    [you] see nothing particularly strange

    This is because the code has not been linked yet, and therefore you see

    a:   c0 90 00 00     lds r12, 0x0000
    

    since ordinary disassembly doesn't show RELOCs. In order to see the RELOC, disassemble with, say, -dr to get:

    a:   c0 90 00 00     lds r12, 0x0000
                         2: R_AVR_16    Y+0x1
    

    This is all fine and is passed to the linker, which doesn't know about a symbol Y and hence throws that error.