MSP430-GCC-OPENSOURCE: Possible compiler bug: caller-saved register is not saved across indirect call

Part Number: MSP430-GCC-OPENSOURCE
Other Parts Discussed in Thread: MSP430F249

I iterate over a table of function-pointers, placed inside the flash-memory using the linker-script. GCC keeps the loop/iteration pointer in r12 across the indirect call. I understand r12 to be a caller-saved register, so the called function if free to clobber the register and the caller should preserve it across the call.

Here is my example application:

typedef void(*sys_init_t)(void);

static void init_fn_0(void) {
    /* trivial, does not disturb r12 */
}

static void init_fn_1(void) {
    volatile int a = 1, b = 2, c = 3, d = 4, e = 5;
    (void)a; (void)b; (void)c; (void)d; (void)e;
    /* enough live locals at -Os to force r12 use as scratch */
}

__attribute__((section(".sys_init"), used))
static const sys_init_t init0 = init_fn_0;

__attribute__((section(".sys_init"), used))
static const sys_init_t init1 = init_fn_1;

extern const sys_init_t __sys_init_start;
extern const sys_init_t __sys_init_end;

void sys_init(void) {
    for(const sys_init_t* fn = &__sys_init_start; fn < &__sys_init_end; fn++) {
        (*fn)();
    }
}

int main() {
    sys_init();
    return 0;
}

I use the original linker-script for the MSP430F249 with the following addition to the SECTIONS part:

.sys_init :
{
__sys_init_start = .;
KEEP(*(.sys_init));
__sys_init_end = .;
} > ROM


I'm building the application like this:


msp430-elf-gcc -mcpu=msp430 -mmcu=msp430f249 -g0 -Os -D__MSP430F249__=1 -c main.c -o main.o
msp430-elf-gcc -mcpu=msp430 -mmcu=msp430f249 -g0 -Os -D__MSP430F249__=1 -T linker.ld main.o -o example.out
 
 
The generated assembly code for the sys_init function is this:
00001148 <sys_init>:
    1148:    3c 40 00 11     mov    #4352,    r12    ;#0x1100

0000114c <.L4>:
    114c:    3c 90 04 11     cmp    #4356,    r12    ;#0x1104
    1150:    01 28           jnc    $+4          ;abs 0x1154
    1152:    30 41           ret            

00001154 <.L5>:
    1154:    3d 4c           mov    @r12+,    r13    ;
    1156:    8d 12           call    r13        ;
    1158:    f9 3f           jmp    $-12         ;abs 0x114c

As one can see, after the call r13 we jump right back to 0x114c, where r12 is evaluated again, even though the called function could have clobbered the register. In that case, the code will crash.

msp430-elf-gcc --version:

msp430-elf-gcc (Mitto Systems Limited - msp430-gcc 9.3.1.11) 9.3.1

  • Hi Christian,

    This is GCC's IPA-RA optimization (-fipa-ra, enabled at -Os and above). GCC sees both init_fn_0 and init_fn_1 as static in the same TU, determines neither clobbers r12, and skips saving it across the indirect call. That reasoning is valid for a closed call graph but wrong for linker-section dispatch, where other TUs can contribute entries GCC never sees.

    Fix: add -fno-ipa-ra to disable the optimization, or move sys_init() to its own translation unit away from the init_fn_* definitions — GCC then treats the indirect call as opaque and conservatively saves r12.

    Best Regards,
    Brian