ld.lld and linker files

LLVM’s linker was a real pain…

LLVM linker (ld.lld-5.0)

To get my STM32 to run I have to tell the linker where to store the executable data.
This is usually done with a linker file. But unfortunately ld.lld doesn’t support the same gcc-linker syntax.

Memory definition

We first start with the memory definition file

STM32-linker/STM32F3/STM32F303VC.ld

MEMORY{
	RAM    (xrw)  : ORIGIN = 0x20000000, LENGTH = 40K
	FLASH  (rx)   : ORIGIN = 0x08000000, LENGTH = 256K
	CCMRAM (xrw)  : ORIGIN = 0x10000000, LENGTH = 8K
}

The STM32F303VC6 which is used in the STM32F3Discovery board is defined by this (mem) linker file. We see that this Chip has a RAM region in 0x20000000 which is 40KB long and used for saving variables and the stack. Also it has a FLASH region for the program code at the adress 0x0800000 which is 256KB long.
And we just ignore the CCMRAM.

(simple) linker file

Now we want to define a simple linker file. We tell meson in the meson.build file to use the memory layout file and append the simple linker file together. So we have a uC agnostic linker file and for every uC an independent memory layout file.

OUTPUT_FORMAT ("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")

/* define entry point */
ENTRY(Reset_Handler)

first we have some configuration shenanigans. Without any further explanation.
Then starts the black magic

/* calculate the Last RAM address*/
 _estack = ORIGIN(RAM) + LENGTH(RAM) - 1;

we first define a variable with the the start-address of the stack pointer. It will be on the top of the RAM memory.

SECTIONS
{
	/*--------------------------------------------------------------------------*/
  /* interrupt vector goes at the beginning of FLASH */
	.isr_vector : {
			. = ALIGN(4);					/* align */
			_sisr_vector = .;     /* define start symbol */
			KEEP(*(.isr_vector))  /* Interrupt vectors */
			. = ALIGN(4);					/* align */
			_eisr_vector = .;			/* define end symbol */
  } > FLASH

then we define the first memory entry in the FLASH memory. It’s the interrupt vector table.

define a section

this syntax defines a block with the name .<region> which going to be saved in the FLASH memory

 .<region> : {
   ...
 } > FLASH

The command KEEP(*(.isr_vector)) will copy the section defined in the startupfile with the name .isr_vector in the surrounding <region>.

Sadly I used the same name for region and the section defined in the startupfile.

next we copy the program code and constant values to the FLASH memory

  /*--------------------------------------------------------------------------*/
  /* program data goes into FLASH */
  .text : {
      . = ALIGN(4);					/* align */
      _stext = .;           /* define start symbol */
      *(.text)						  /* insert program code .text */
      *(.text*)             /* .text* sections */

      *(.glue_7)            /* glue arm to thumb code */
      *(.glue_7t)           /* glue thumb to arm code */
      *(.eh_frame)

      . = ALIGN(4);				  /* align */
      _etext = .;           /* define end symbol */
  } > FLASH
  /*--------------------------------------------------------------------------*/
  /* constant data goes into FLASH */
  .rodata : {
     *(.rodata)            /* .rodata sections (constants, strings, etc.) */
     *(.rodata*)           /* .rodata* sections (constants, strings, etc.) */
     . = ALIGN(4);         /* align */
      _erodata = .;
  } > FLASH

As you can read in the comments : *(.text) is the program code and the glue sections are some special arm / thumb magic.

then follows some more arm magic

/* ARM stack unwinding section (GDB uses this) */
.ARM.extab : {
     __extab_start__ = .;/* define start symbol */
     *(.ARM.extab* .gnu.linkonce.armextab.*)
     __extab_end__ = .;  /* define end symbol */
} > FLASH

.ARM : {
     __exidx_start__ = .;/* define start symbol */
     *(.ARM.exidx* .gnu.linkonce.armexidx.*)
     __exidx_end__ = .;  /* define end symbol */
} > FLASH

Initialized globals

Then we want to save the global variables which are initialized with a defined value.
We have to link the variable addresses in the RAM memory, but we have to save the initial values in the FLASH memory.
In gcc you would do this like this:

 .<region> : {
   ...
 } >RAM AT >FLASH

But this failes in llvm’s linker.

I had some time to figure this out…

  • define all the FLASH regions
  • set the current address locator to the RAM address space . = ORIGIN(RAM);

And then define the RAM region which values are stored in FLASH memory

. = ORIGIN(RAM);

.<region> : AT(<Flash-location>) {
    ...
 } > RAM

 <marker> = LOADADRESS(.<region>)

Any other way will fail!
So in the end I defined it that way:

. = ORIGIN(RAM);

.data : AT(__exidx_end__) {
    _sdata = .;            /* create a global symbol at data start */
    *(.data)              /* .data sections */
    *(.data*)             /* .data* sections */

    . = ALIGN(4);         /* align */
    _edata = .;           /* define a global symbol at data end */
 } > RAM

 _sidata = LOADADDR(.data); /* get the start address of the .data section */

As you might expect *(.data) are the values. We store them appending to the __exidx_end__ address in FLASH. The linker will reserve the same space in RAM. Also we define a location variable _sidata and copy the address of the .data section to it

We will need to copy the values from FLASH to RAM by Hand in the startup file.

and then follows the so called .bss section with uninitialized variables and a special arm attribute .ARM.attributes 0 : { *(.ARM.attributes) }

conclusion

After hours of trial and error I finally got a minimalistic linker file to work with my microcontrollers.
ld.lld might not be in a very mature state at the moment.
But maybe this will be better with LLVM-6.0

Written on January 8, 2018