This is the first post in my Cortex‑M7 without hardware series, which aims to build an Executable and Linkable Format executable (ELF) and run it without hardware (using emulated / simulated STM32F746 microcontroller unit. 1).

In this part we focus on the linker script. The linker script is read by the linker ( ld ) during the link phase step of a build (which happens after compilation phase, where sources files are compiled to object files (objects)). The linker script describes how the content of the objects are mapped to output. 2

Linker script is crucial because all of the other parts (startup code, debugger symbols, whether the executable even runs) depends on having a correct memory model. It also brings us “closer to hardware” as we need to make sure that memory is defined correctly for the target hardware, however for this example finer details of RAM are ignored (such as Instruction RAM and Tightly Coupled Memory interface).

Below is the minimal (no symbol exports for accessing RAM during runtime, no alignment etc.) linker script for STM32F746 microcontroller unit.

MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 320K
}

ENTRY(Reset_Handler)

SECTIONS
{
    .text : {
        KEEP(*(.isr_vector))    /* Interrupt vector table */
        *(.text*)               /* Code */
        *(.rodata*)             /* Read-only data */
    } > FLASH

    .data : {
        *(.data*)
    } > RAM AT > FLASH

    .bss : {
        *(.bss*)
        *(COMMON)
    } > RAM
}

1) MEMORY { ... } 3

Linker’s MEMORY command complements the SECTIONS command by describing the available memory regions. The block in declares:

  1. the memory regions the linker may place sections into
  2. their attributes (r for read-only, x for executable and w for read/write region)
  3. length which is memory region’s size in bytes.

In cases where MEMORY is omitted, the linker assumes enough contiguous memory exists, which is not suitable for embedded environment where RAM is very limited and placing SECTIONS to unsuitable memory locations might cause numerous cases of undefined behavior.

The addresses and sizes match the STM32F746 hardware description, which states that FLASH section starts at 0x0800_0000 (size of 1024KB readable and executable) and RAM section at 0x2000_0000 (size of 320KB, readable, executable and writable). 4

2) ENTRY(Reset_Handler) 5

ENTRY() sets the entry symbol which happens by linker placing the value of this symbol into the e_entry field which is located at the ELF header.

Note that Cortex‑M still boots from the vector table’s reset entry, but ENTRY() is still the advisable thing to do for example, tooling purposes (i.e. build systems, debugger).

3) SECTIONS { ... } 6

This block declares output sections which are regions that appear in the ELF and determines how they are laid out in memory. Each output section ends with > MEMORY_REGION syntax (i.e. > FLASH or > RAM) which tells linker to place declared output section to selected memory region. Again this is very crucial, as if omitted, input sections are placed in order of appearance to a identical output section, which causes undefined behavior in embedded environment.

.text output section

.text : {
  KEEP(*(.isr_vector))
  *(.text*)
  *(.rodata*)
} > FLASH

In short .text is the conventional name of the input section that holds all compiled executable instructions (or code). The linker script creates a output section of the same that instructs where each of the compiled instructions are, for example, placed. So each of the input sections are placed in this order to the ELF.

  1. KEEP(*(.isr_vector)) collects the vector table and forces it to be retained
  2. *(.text*) collects compiled functions (such as main)
  3. *(.rodata*) collects compiled constant data

This whole output section is placed to physical region FLASH.

Note that the vector table is used by the microcontroller unit via address, not by normal symbol references, so it can look “unused” to the linker and may be subject of garbage collection making it not appear in the ELF. KEEP() is the intended solution for this exact case. 7

Also note that by default, the microcontroller unit reads Main Stack Pointer (MSP) and Reset_Handler from the vector table base (often flash base). It can be relocated via Vector Table Offset Register (VTOR), but in this minimal setup it is kept at flash start.

.data output section — runs in RAM, loads from Flash

.data : { *(.data*) } > RAM AT > FLASH

Here we have a new keyword AT/AT> which is used to control the load memory address (LMA) and virtual memory address (VMA). 8

  • .data input section contains initialized globals/statics.
  • At runtime they must be in RAM in writable state, so the section’s VMA is in RAM.
  • However their initial bytes (as they have some initial value) must be stored in Flash, so the LMA is in Flash.

This one is the key concept for bare‑metal C/C++, as values used in initialization do not appear from thin air automatically as statup code must copy .data from flash to RAM before main function.

.bss output section — zeroed RAM + *(COMMON)

.bss : { *(.bss*) *(COMMON) } > RAM
  • .bss contains zero‑initialized globals/statics.
  • As with .data, they are in writable state in RAM, but as they do not need any special values, LMA is not needed
    • Startup code also zero‑initialize .bss before main function
  • Finally *(COMMON) collects “common symbols” (legacy/common allocations) and places them into .bss. 9

Next

In Part 2 I’ll explain the matching startup.c and how the microcontroller unit uses the vector table.

References