Contents
STM32 Linker Script (From Scratch)
Foreword
When you hit "build" on something like STM32CubeIDE, something called a linker runs at some point in that process.
Its role is to take your code (more specifically, a generated object file, such as an .elf file) and map it onto the target device in a way that fits the specific hardware.
A Raspberry Pi Example
The clearest way I can think to explain its use is through an experience I had flashing the RP2040 with my firmware (bare-metal project, no SDK).
The way that MCU works is, it expects the first 256 bytes of code it reads from flash to be structured like this:
- 252 bytes of code (generally this should set up something called XIP with the external flash - but that's for another article)
- 4 bytes for CRC32 error checking (again, this needs another article at some point...)
Generally, there's no way to really guarantee that your compiled code will have the first 256 bytes set up like that. Luckily, as you can already guess, a linker script fixes that and it's exactly what it's used for.
Memory Layout (MEMORY())
Generally, on ARM Cortex-M chips, the memory is structured like this:
0x08000000 - 0x080X0000: flash memory*0x1FFFX000 - 0x1FFFX000: system memory (the bootloader)*0x20000000 - 0x3FFFFFFF: SRAM0x40000000 - 0x5FFFFFFF: peripherals *Xin these examples is hardware-specific, check your board's datasheet.
Because these are hardware-specific, and SRAM/peripherals aren't always at those addresses on every type of CPU architecture, you can specify where each begins, as well as its size, in the linker script:
MEMORY (
flash(rw) : ORIGIN = 0x08000000, LENGTH = 128K
sram(rwx) : ORIGIN = 0x20000000, LENGTH = 624K
);
Note that LENGTH doesn't have to be accurate - you can claim to have less than you actually do, the rest will just remain unutilised. Just don't claim to have more than you actually do, that'll lead to undefined behaviour.
You might also notice the (rw) and (rwx) parts of the code - that lets the linker know what it can do with that section of memory:
rmeans 'readable'wmeans 'writeable'xmeans 'executeable'
And they don't actually enforce any kind of memory protection, they're just there for the linker to warn you if you try to, for example, place a writeable section in a memory region marked as only readable.
Entry Point (ENTRY())
This is largely ceremonial on microcontrollers, or at least those powered by the Cortex-M like the STM32 family. It specifies where to start code execution from, and it looks like this:
ENTRY(_reset_handler);
The reason I say it's ceremonial is because Cortex-M chips always boot like this under normal conditions:
- Read
0x00000000for initial stack pointer value - Read
0x00000004for reset handler function's address - Jump to reset handler and begin code execution
So ENTRY() doesn't influence where the CPU starts code execution from.
What does get influenced, however, is the debugger. So for debugging purposes, the entry point is necessary.
Symbols
The part which reads
_reset_handlerinside ofENTRY(_reset_handler)is called a symbol. The linker script doesn't actually have a variable called_reset_handler- instead it is user-defined and ends up in the symbol table. More on this later
Symbols
This was the most confusing part of linker scripts for me because they look and behave like variables, but they are NOT variables.
Take this as an example:
_estack = ORIGIN(sram) + LENGTH(sram);
This creates a symbol (not variable) called _estack (meaning end-of-stack, it's a user-defined name so you can call it anything), and the value inside it becomes the origin of RAM (0x20000000) plus the length of RAM (624k), which is 0x20098580.
_estack can then be used inside of your source code:
// declare it first
extern uint32_t _estack;
// then use it like this
(uint32_t)&_estack,
And here's the confusing part: addressing &_estack will give you its address 0x20098580 which is what you want, NOT addressing it like _estack will give you the value stored at its address, which may or may not have something useful depending on your use case, in this case it will not.
This is exactly the same as a normal variale: uint32_t x = 5. Here _estack corresponds to x - and addressing &x will give you its address (e.g. 0x20001000), NOT addressing it will give you its value (in this case 5).
It's so similar to a variable, that it even gets added to the symbol table and looked up whenever it's needed.
The one distinction here is that a linker symbol is NOT a variable, so it reserves NO memory in order to exist. It's closer to a C macro #define in that sense, so it has no runtime overhead as it's resolved during the build process.
I'm not sure why it confused me so much... But hey, making things easy-to-understand is the whole point of this website!
Data Sections (SECTIONS{})
After making the linker aware of the hardware memory layout with MEMORY{}, we can now put data inside it. That's achieved with the SECTIONS{} keyword.
The way it works at a high level is as follows:
SECTIONS {
<section 1>
<section 2>
<section 3>
…
}
The general structure of each individual section is:
output_name : { input_pattern } > memory_region
You can define as many sections as you like.
Example
Here is a piece of example code (from STM32World - see sources). I'll go through it line-by-line.
SECTIONS {
.vectors : { KEEP(*(.vectors)) } > flash
.text : { *(.text*) } > flash
.rodata : { *(.rodata*) } > flash
.data : {
_sdata = .; /* .data section start */
*(.first_data)
*(.data SORT(.data.*))
_edata = .; /* .data section end */
} > sram AT > flash
_sidata = LOADADDR(.data);
.bss : {
_sbss = .; /* .bss section start */
*(.bss SORT(.bss.*) *(COMMON))
_ebss = .; /* .bss section end */
} > sram
. = ALIGN(8);
_end = .; /* for cmsis_gcc.h */
}
Breakdown
.vectors : { KEEP(*(.vectors)) } > flash:
- The first
.vectorscreates a section with that name in the output file - The wildcard
*()searches through all object files for inputs, and.vectorsinside the parenthesis specifies that these inputs are sections which exactly match that name KEEP()tells the linker to skip garbage-collection on this section*- Lastly,
> flashtells the linker to place this newly created.vectorssection into flash memory
* Garbage-collection is ran when the linker is invoked with the --gc-sections flag. It silently removes sections which appear to have no references to them. For optimisation that's great, but i the context of a startup file on Cortex-M chips, the vector table is never explicitly referenced in code; the CPU uses it automatically, so removing it will be catastrophic.
.text : { *(.text*) } > flash:
This is largely the same as the previous like, except:
- There is no
KEEP(), so garbage-collection can remove unused code from this section - The wildcard
*(.text*)once again searches through every object file, but in this case.text*means it will search for any section that begins with.text- that could be.text,.text-startup,.text-test-code-1, etc.
I will split the line starting with .data : { from here.
_sdata = .;:
- Creates a symbol called
_sdatain the symbol table (so, it can be used in the source code just like_estackabove) - Gives the symbol the value
., which in linker terms is the current memory address. Before this line,.vectors,.text, and.rodatawere placed in memory, so.will now return the address of the next free byte right after.rodataends - The symbol means "start data", and it can be used as a pointer to the beginning of the
.datasection
*(.first_data):
- Same as the lines above, it looks for sections exactly matching
.first_datain all object files
*(.data SORT(.data.*)):
- This includes 2 things - all sections exactly matching
.data, and all sections matching.data.*, where the wildcard*can be replaced with anything SORT()is an alias forSORT_BY_NAME(), which sorts the files/sections alphabetically (ascending) by name before placing it in the newly created section
_edata = .;:
- Exactly the same as the
_sdataline, but this now points to the end of the.datasection as it's created after the inputs have already been placed
> sram AT > flash:
Places the section in flash AND sram, however in different ways:
sramholds the VMA (Virtual Memory Address), which is where the runtime address of data is placed*flashholds the LMA (Load Memory Address), which is where the data is stored so it can be available and ready to be moved into its VMA at runtime
_sidata = LOADADDR(.data);:
- As you're (hopefully!) familiar by now, this creates a new symbol called
_sidata - The symbol has the value
LOADADDR(.data), which is the load address of the.datasection (its LMA) - This is necessary depending on your use case - despite having access to
_sdata, that only has the VMA,LOADADDR()returns the LMA
* LMA persists between power-offs as it's stored into persistent flash memory (in this case). VMA is placed in RAM (in this case yet again), which is non-persistent, so upon a power-off it will be lost. That's why I said VMA is 'placed' - there has to be code to copy it from its LMA to its VMA at startup, otherwise the CPU will try to access the VMA at runtime and (inevitably) access garbage data.
The code starting at .bss : { only has 2 noteworthy things to mention:
- The
COMMONkeyword makes the linker place all 'common' variables in this section - 'common' variables are non-static global variables that do not have an assigned value (they're only declared, not initialised). The reason they're in.bssis because C guarantees that uninitialised variables are automatically initialised to 0, that's a preview of what the.bsssection does as I'll explain soon > srammeans both the VMA and LMA lay in SRAM, so it takes no space in the final binary that will be flashed onto our STM32 - this is why_sbssand_ebssexist, so the startup code knows where the.bsssection lives and it can zero-out the relevant SRAM bytes at runtime
And the final noteworthy piece - . = ALIGN(8);:
.is still the 'pointer' to the current memory address, except this time we change its value through an assignmentALIGN(8)moves the pointer to the nearest address above the pointer that's an 8-byte multiple - e.g. it turns0x20003021to0x20003028
GNU-Style Attributes
While not part of the linker script, these work together with it to make everything function properly.
GNU-style attributes are a way you can:
- talk to the compiler and give it context so it can better understand your code
- tell the compiler about certain optimisations (e.g. always inline a function, function that doesn't return, and a lot more)
- label a variable/function with a specific section (the one we care about)
By doing this:
__attribute__((section(“.my_section”))) void my_function(void);
The compiler and assembler generate an additional section inside the object file, and call it “.my_section”.
Your linker can then access that section and whatever is inside it - that way you can place a function or variable anywhere in memory that you wish.
Final Words
This article barely scratches the surface of what's possible with the linker script. I mostly covered the basics that would be enough for the average basic project (as that's more-or-less what I know as of now), so I'd recommend looking into more linker script concepts. Check out the sources below - some of them (mostly the written sources) have a lot of extra information on more advanced concepts.
Sources
STM32World - STM32 Bare Metal Tutorial MCU on Eclipse - Accessing GNU Linker Script Symbols from C/C++ Memfault - Demistifying Firmware Linker Scripts