long_calls between RAM and ROM sections on bare metal ARM with gcc

Viewed 5930

I'm working on an ARM7TDMI project using GCC 4.3 and I'm having some difficulity telling the compiler to use long calls in certain cases but not others.

The build process runs arm-eabi-gcc to generate relocatable ELF object files for each .c source file (most relevant CFLAGS include -Os -ffunction-sections -fdata-sections -mthumb -mthumb-interwork), then links them all into an ELF executable (most relevant LDFLAGS include -Wl,--gc-sections -Wl,-static -Wl,-n -nostdlib, and a custom linker script). Then that ELF file is converted to a raw executable with arm-eabi-objcopy -O binary, and a custom bootloader copies it from ROM to RAM (a single SRAM with both code and data) at startup. So everything then executes from RAM, .rodata is present in RAM, and everything proceeds quickly, completely ignoring ROM after boot.

I'm now trying to change that, so that certain select pieces of RO data and the text of select functions can live only in ROM and be accessed during runtime as necessary. I've modified the linker script to know about two new sections ".flashdata" and ".flashtext", both of which should be placed at a fixed address in ROM. I've also sprinkled __attribute__((__section__(".flashdata"))) and __attribute__((__section__(".flashtext"),__long_call__)) throughout the C code as appropriate, and I've rejiggered the build process so that the old objcopy now adds -R .flashdata -R .flashtext, and I do a second objcopy with -j for each of those sections, then I combine the two output files so that the bootloader can do the right thing and the ROM sections appear at the expected memory-mapped location.

This all works fine - I can printf strings tagged into the .flashdata section, and I can call a .flashtext function from code running out of RAM (which knows to use a long call because of the __long_call__ attribute next to the __section__(".flashtext") attribute). That ROM-based function can happily short-call other ROM-based functions, and it can return back to its RAM-based caller.

The problem comes in trying to call from a ROM-based function into a RAM-based one, which must also be a long call. I do not want to use long calls everywhere, so I do not want -mlong_calls in my CFLAGS. If I group all of the functions to live in ROM into a single rom.c, I can build that one file with -mlong-calls and everything works. However, I strongly prefer to avoid that, and keep functions grouped generally by purpose, simply tagging a few here and there as appropriate to run from ROM.

Incidentally, this was not sufficient under gcc 3.4. Using -mlong-calls got the compiler thinking the right thing, but it couldn't follow through because it was only willing to perform long jumps with its helpers _call_via_rX...which all lived in RAM and could only be accessed through a long call. This was fixed in the linker in gcc 4.0, but not backported to anything in the 3.x tree.

So it is wonderful that I can now call back into RAM at all since I'm using gcc 4.3. It would be even better if I could somehow tag the code in the ROM-based functions to force it to use long calls. There is a #pragma long_calls, but it only affects declarations, so I could use it instead of __attribute__((__long_call__)). It sadly does not magically force the compiler to use long calls for all function calls encountered while it is in effect.

Organizationally, it is simply not the right thing to do to group all of the slow-running code into a single file, out of context and separate from other code in its general category. Please tell me there is an option I haven't yet considered. Why isn't -ffunction-sections or just the fact that the code is in different sections (.text versus .flashtext) automatically fixing my problem?

By the way, the error out of the linker when it figures out that the compiler used a short call which didn't leave it enough space to manage the relocation is: relocation truncated to fit: R_ARM_THM_CALL against symbolfoo' defined in .text.foo section in objs/foo.o(and the section.text.foois used instead of.textbecause of-ffunction-sections` in CFLAGS).

3 Answers

Just in case anyone else stumbles across this problem like me here's the solution I came up with.

Like Eric Angell said ideally you'd be able to specify that you would like to invoke a function via long calls. Because there is no way to do this directly in C/C++ with GCC I turned to the assembler and wrote some macros that intercept the bl and blx assembly GCC generates and replaces them with long calls.

//longCall.h
#pragma once

#define INLINE inline __attribute__((always_inline))

template<typename T1, typename T2>
struct IsType {
    constexpr operator bool() { return false; }
};

template<typename T>
struct IsType<T, T> {
    constexpr operator bool() { return true; }
};

INLINE void EnableLongCallBranches() {
    asm volatile(R"(
        interceptBranchAndConditions bl
        interceptBranchAndConditions blx
    )");
}

INLINE void DisableLongCallBranches() {
    asm volatile(R"(
        disableInterceptBranchAndConditions bl
        disableInterceptBranchAndConditions blx
    )");
}

template<auto func, typename... ArgsT>
INLINE auto LongCall(ArgsT... args) {

    EnableLongCallBranches();

    //Check if function call returns void
    if constexpr(IsType<decltype(func(args...)), void>()) {
    
        //just evalute function
        func(args...);
        DisableLongCallBranches();
    
    } else {

        //return result of function
        //Note: cannot simply use 'return func(args...)' or else compiler will treat 
        //      'DisableLongCallBranches()' as dead code and remove it 
        auto val = func(args...);
        DisableLongCallBranches();
        return val;
    }
}

//Static assembly macros to intercept and replace relative branches with long jumps
asm(R"(
    
    @ macro for intercepting a branch call and replacing them with calls to longCallIntercept
    .macro interceptBranch instruction, condition
        .macro \instruction\condition label
            longCallIntercept \instruction, \condition, \label
        .endm
    .endm
    
    @ macro for intercepting a branch call and all of its conditnal calls with longCallIntercept
    .macro interceptBranchAndConditions instruction
        interceptBranch \instruction, "eq" @ Equal. Z==1
        interceptBranch \instruction, "ne" @ Not equal. Z==0
        interceptBranch \instruction, "cs" @ Unsigned higher or same (or carry set). C==1
        interceptBranch \instruction, "hs" @ Unsigned higher or same (or carry set). C==1
        interceptBranch \instruction, "cc" @ Unsigned lower (or carry clear). C==0
        interceptBranch \instruction, "lo" @ Unsigned lower (or carry clear). C==0
        interceptBranch \instruction, "mi" @ Negative. The mnemonic stands for "minus". N==1
        interceptBranch \instruction, "pl" @ Positive or zero. The mnemonic stands for "plus". N==0
        interceptBranch \instruction, "vs" @ Signed overflow. The mnemonic stands for "V set". V==1
        interceptBranch \instruction, "vc" @ No signed overflow. The mnemonic stands for "V clear". V==0
        interceptBranch \instruction, "hi" @ Unsigned higher. (C==1) && (Z==0)
        interceptBranch \instruction, "ls" @ Unsigned lower or same. (C==0) || (Z==1)
        interceptBranch \instruction, "ge" @ Signed greater than or equal. N==V
        interceptBranch \instruction, "lt" @ Signed less than. N!=V
        interceptBranch \instruction, "gt" @ Signed greater than. (Z==0) && (N==V)
        interceptBranch \instruction, "le" @ Signed less than or equal. (Z==1) || (N!=V)
        interceptBranch \instruction, "al" @ Always executed.
        interceptBranch \instruction, ""   @ Always executed.
    .endm
    
    @ macro for deleting all branch call and condition call interception macros
    .macro disableInterceptBranchAndConditions instruction
        .purgem \instruction\()eq @ Equal.  Z==1
        .purgem \instruction\()ne @ Not equal.  Z==0
        .purgem \instruction\()cs @ Unsigned higher or same (or carry set). C==1
        .purgem \instruction\()hs @ Unsigned higher or same (or carry set). C==1
        .purgem \instruction\()cc @ Unsigned lower (or carry clear). C==0
        .purgem \instruction\()lo @ Unsigned lower (or carry clear). C==0
        .purgem \instruction\()mi @ Negative. The mnemonic stands for "minus".  N==1
        .purgem \instruction\()pl @ Positive or zero. The mnemonic stands for "plus". N==0
        .purgem \instruction\()vs @ Signed overflow. The mnemonic stands for "V set". V==1
        .purgem \instruction\()vc @ No signed overflow. The mnemonic stands for "V clear".  V==0
        .purgem \instruction\()hi @ Unsigned higher. (C==1) && (Z==0)
        .purgem \instruction\()ls @ Unsigned lower or same. (C==0) || (Z==1)
        .purgem \instruction\()ge @ Signed greater than or equal. N==V
        .purgem \instruction\()lt @ Signed less than. N!=V
        .purgem \instruction\()gt @ Signed greater than. (Z==0) && (N==V)
        .purgem \instruction\()le @ Signed less than or equal.  (Z==1) || (N!=V)
        .purgem \instruction\()al @ Always executed.
        .purgem \instruction      @ Always executed.
    .endm
    
    @ macro for replacing relative jumps with long jumps
    .macro longCallIntercept instruction, condition, label
        
        @ check if label is register
        .set _longCallIntercept_LabelIsReg, 0
        .irp register r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, ip, lr, pc
            .ifeqs "\label", "\register"
                .set _longCallIntercept_LabelIsReg, 1
            .endif
        .endr

        @ delete instruction macro so 'instruction' is treated as opcode
        .purgem blx\condition
    
        .if _longCallIntercept_LabelIsReg
    
            .warning "Ignoring: [\instruction \label] long call"
    
            @ keep existing branch to register instruction
            \instruction\condition \label
    
        .else

            .print "Replacing: [\instruction \label] with long call"
        
            @ invoke long branch
            @ Note: could use 'ldr ip, =\label' to favor size over speed
            movw ip, #:lower16:\label
            movt ip, #:upper16:\label
            blx\condition ip
    
        .endif
 
        @ Redefine instruction macro
        interceptBranch blx, \condition
    .endm
)");

And now anywhere in your code you explicitly specify which calls you would like to be long.

//example.cpp
#include "longCall.h"

//Note: Only marked noinline to prevent optimize from evaluating
//      function at compiletime
__attribute__((noinline)) int foo(int x) { 
    return x;
}

int main() {
    
    int v1 = LongCall<foo>(1);
    int v2 = foo(2);   

    return v1 + v2;
}

Outputs the Assembly:

foo(int):
    bx      lr

main:
    push    {r4, lr}
    
    @ long call to foo(1) 
    movs    r0, #1
    movw    ip, #445    ; 0x1bd
    movt    ip, #1
    blx     ip
    
    mov     r4, r0
    
    @ short call to foo(2)
    movs    r0, #2
    bl      101bc       ; <foo(int)>
    
    add     r0, r4
    pop     {r4, pc}

Here's a Compiler Explorer of the above example.

Related