Binutils / Linker - Resolve external symbol references statically

Viewed 106

Background

I am working on some projects target old embedded devices with latest GNU toolchain.

Since the memory is very limited, the code is separated into a main part and some overlays, in which the overlays are compressed to save space and only loaded from ROM image as needed.

Both the main binary and overlays are static objects, which means they are always loaded to some fixed address.

Difficulty

In order to process main binary and overlays to ROM image, I need the outputs to be separate files.

However, in the intermediate stage of building process, main binary and overlays are shared objects with external references to each other. To the best of my knowledge, I can either:

  1. Link all of them in to single output with custom linker script to resolve these references, and dump individual sections back into separate files via other utilities like objcopy.
  2. Write some scripts to perform such relocations on these objects statically, just like what modern operating systems do upon loading dynamic libraries.

But both approaches are quite complicated, and I would like to make the building script as simple as possible.

Question

  1. Can I resolve external symbol references only, without actually links these objects to output file?
  2. Is there any other way to create such "static" libraries (overlays)?
1 Answers

Short Answer

This is possible, which is exactly what ld's -R options does.

Example

This small C example shows how to craft such static overlay binaries using modern GNU toolchains.

In the example, we use GCC 11.1.0 and GNU Binutils 2.36.1, which targets to PowerPC.

This should also work on other targets with some modifications.

Sources

1.c:

extern int boot();

void loop() {
    while (1);
}

void start() {
    boot();
    __builtin_unreachable();
}

2.c:

extern void loop();

int boot() {
    loop();
    return 0;
}

Linker Scripts

1.ld:

OUTPUT_FORMAT("elf32-powerpc", "elf32-powerpc",
              "elf32-powerpc")
SECTIONS
{
  . = 0x10000;
  .text BLOCK(0x10) : {
    *(.text*)
  }
  .data BLOCK(0x10) : {
    *(.rodata*)
    *(.data*)
  }
  .bss BLOCK(0x10) : {
    *(.bss*)
  }
  /DISCARD/ : {
    *(.eh_frame*)
  }
}

2.ld:

OUTPUT_FORMAT("elf32-powerpc", "elf32-powerpc",
              "elf32-powerpc")
SECTIONS
{
  . = 0x20000;
  .text BLOCK(0x10) : {
    *(.text*)
  }
  .data BLOCK(0x10) : {
    *(.rodata*)
    *(.data*)
  }
  .bss BLOCK(0x10) : {
    *(.bss*)
  }
  /DISCARD/ : {
    *(.eh_frame*)
  }
}

Shell Input

cc1 -O3 -mno-eabi -fomit-frame-pointer -fno-stack-protector -fno-exceptions -ffunction-sections -fdata-sections -ffreestanding -fno-ident 1.c -o 1.s
cc1 -O3 -mno-eabi -fomit-frame-pointer -fno-stack-protector -fno-exceptions -ffunction-sections -fdata-sections -ffreestanding -fno-ident 2.c -o 2.s
as -o 1.o 1.s
as -o 2.o 2.s
ld -r -nostdlib --no-eh-frame-hdr -T 1.ld -Map 1.plf.map -o 1.plf 1.o
ld -r -nostdlib --no-eh-frame-hdr -T 2.ld -Map 2.plf.map -o 2.plf 2.o
ld -s -e start -nostdlib --no-eh-frame-hdr -T 1.ld -R 2.plf -Map 1.elf.map -o 1.elf 1.plf
ld -s -nostdlib --no-eh-frame-hdr -T 2.ld -R 1.plf -Map 2.elf.map -o 2.elf 2.plf

Outputs

1.elf (loaded at 0x10000)

0x10000: ; loop
b         0x10000 ; loop

0x10004: ; start
stwu      r1, -8(r1)
mflr      r0
stw       r0, 12(r1)
bl        0x20000 ; boot

2.elf (loaded 0x20000)

0x20000: ; boot
stwu      r1, -8(r1)
mflr      r0
stw       r0, 12(r1)
bl        0x10000 ; loop
lwz       r0, 12(r1)
li        r3, 0
addi      r1, r1, 8
mtlr      r0
blr

The 1.elf and 2.elf are what we want.

Notes

  1. From binutils 2.25, one may use objcopy --extract-symbol to build an object file with only symbols and address. For example, objcopy --extract-symbol 1.plf 1.sym and link 2.elf with 1.sym. This would significantly reduce size of input files of ld in case there are a lot of overlays.
  2. Jumps may out of range depend on base address of binaries and architecture's addressing mode. For instance, jal in MIPS cannot alter the top 4 bits of program counter, and thus one may need to add something like __attribute__((far)) for such long calls to make the compiler to use other ways (e.g., jr) instead.
  3. Some sections should always stand inside the main binary to work correctly, which may requires extra works during linking process. This includes exceptions (unwinding) in standard C++, and global constant tables (i.e., .sdata, .sbss, .lit4, ...) on some targets like PowerPC (eabi) or MIPS (-G n) for efficient data access with constant register(s).
  4. Things could be more complicated with MinGW since it's ld seems behave oddly. One may want to employ GNU toolchains that target corresponding unix-like platforms to workaround it at the time of writing.
Related