Cannot write to variable defined in LD linker script when compiling with -Os option

Viewed 68

I am writing a program to run bare metal. I am trying to write to a variable from a custom linker script. The code runs perfectly when compiled with -O0 options but not as expected when compiled with -Os option.

The code I used is as follows.

main.c:

#define TTB_BASE (&Image$$TTB)
extern unsigned int Image$$TTB;

int main ()
{
    *TTB_BASE = 56326;

    unsigned int *ttb=TTB_BASE+16;

    for (int i = 0; i < 8; i++ )
    {
        *ttb++ = 1;
    }
    *(volatile unsigned int*)(0x00100000) = *TTB_BASE;
}

When compiled with -Os option, the code *TTB_BASE = 56326; is seems to be optimized out and the value is not stored to the address TTB_BASE.

The linker script used:

ARMCA7.ld

ENTRY(main)
SECTIONS
{
    . = 0x00160000;
    .text : { *(.text*) }
    .data : { *(.data .data.* .gnu.linkonce.d*) }
    .bss (NOLOAD) : {
        . = ALIGN(16);
        *(.bss .bss.*)
        *(COMMON)
    }
    . = 0x00170000;
    .ttb :
    {
        Image$$TTB = .;
    }
}

Compiling statement:

arm-none-eabi-gcc -mcpu=cortex-a7 -Os -std=gnu99 -c -o main.o main.c
arm-none-eabi-gcc -mcpu=cortex-a7 -Os -T ARMCA7.ld -nostartfiles -Xlinker --gc-sections -Wl,-Map,"main.map" -o main.elf main.o

Disassembly got:

Address : Opcode         Statement
-------   ------         ---------
 8                        unsigned int *ttb=TTB_BASE+16;
                      main:
00160000: 24 30 9f e5   ldr     r3, [pc, #36]   ; 0x16002c <main+44>
12                            *ttb++ = 1;
00160004: 01 10 a0 e3   mov     r1, #1
00160008: 20 20 83 e2   add     r2, r3, #32
0016000c: 04 10 83 e4   str     r1, [r3], #4
10                        for (int i = 0; i < 8; i++ )
00160010: 02 00 53 e1   cmp     r3, r2
00160014: fc ff ff 1a   bne     0x16000c <main+12>
14                        *(volatile unsigned int*)(0x00100000) = *TTB_BASE;
00160018: 60 20 13 e5   ldr     r2, [r3, #-96]  ; 0xffffffa0
0016001c: 01 36 a0 e3   mov     r3, #1048576    ; 0x100000
15                    }
00160020: 00 00 a0 e3   mov     r0, #0
14                        *(volatile unsigned int*)(0x00100000) = *TTB_BASE;
00160024: 00 20 83 e5   str     r2, [r3]
15                    }
00160028: 1e ff 2f e1   bx      lr
0016002c: 40 00 17 00   andseq  r0, r7, r0, asr #32

It can be seen that nothing is written to the address TTB_BASE which is 0x00170000 defined in the linker script, and the value written to address 0x00100000 is wrong.

Is there some bug in the code or linker script?

Note:

If I change the first two lines of main.c to the following code,

#define TTB_BASE (Image$$TTB)
extern unsigned int Image$$TTB[];

The code can be correctly compiled and the address TTB_BASE is written correctly. However, as I seen from other questions, both approach should be correct.

gcc version used:

arm-none-eabi-gcc --version
arm-none-eabi-gcc.exe (Arm GNU Toolchain 11.3.Rel1) 11.3.1 20220712
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

I also test the code with arm-none-eabi-gcc.exe 10.3.1 20210621 (release) and got the same problem.

2 Answers

Your external variable has no side effects visible to the compiler, and as such it is allowed to optimize the assignment out.

The standard compliant way to avoid this is to add the qualifier volatile. It tells the compiler that this variable has side effects. Therefore it cannot optimize the assignment out.

extern volatile unsigned int Image$$TTB;

You need this qualifier also, if you have multiple threads like interrupt service routines, which share variables with each other or the main thread.


Addition:

As others pointed out in comments, there is an error in the linker script. You don't reserve space for the variable.

I found that the question posted is related to variables references other than linker script. Please refer to this new post for further discussion.

For a conslusion, the real reason why the code does not work is as follows. &Image$$TTB+16 is Undefined Behavior in C.

That is, the following declaration will be needed if you want to change the address regions defined in linker script file.

extern unsigned int Image$$TTB[];

The following is another example code that has the same bug. Note that it should be compiled with -O1 or above. You can try it at Compiler Explorer.

#include <stdio.h>
unsigned int var;
volatile unsigned int valid_address[1024];
int main ()
{
    var = 56326;
    unsigned int *ttb=&var;
    ttb += 16;
    for (int i = 0; i < 8; i++ )
    {
        *ttb++ = 1;
    }
    valid_address[0] = var;
    printf("Value is: %d", valid_address[0]);
}

If you execute this code, you might get the following output:

Value is: 0
Related