In the last lesson, we compiled C and startup assembly together. The nm output showed that Reset_Handler exists, but symbols such as _estack and _sdata still have no addresses. Compilation creates the pieces; the linker places them in memory and resolves their references. In this lesson, we’ll give it a memory layout and inspect the resulting ELF file.
Do not flash the ELF from this lesson. It has only the first two vector-table entries; a usable firmware image needs a complete exception and interrupt table. This is a linking exercise, and no board is required.
Continue the project from lesson 3
Stay in the stm32-cmake-lesson workspace from lessons 2 and 3. If you’re in its parent directory, enter it now:
cd stm32-cmake-lesson
Keep main.c, startup_stm32l433.s, and cmake/gcc-arm-none-eabi.cmake as they are. We’ll add a linker script and update CMakeLists.txt so the existing object files become an executable. The workspace will then have these files (along with the existing build/ directory):
stm32-cmake-lesson/
├── CMakeLists.txt
├── STM32L433xx_FLASH.ld
├── build/
├── main.c
├── startup_stm32l433.s
└── cmake/
└── gcc-arm-none-eabi.cmake
The two variables in main.c still give us initialized and zero-filled data to place, while the startup file refers to the memory addresses we’ll define next. You don’t need to rewrite either file.
Describe Flash and RAM
The linker needs a linker script to describe the target’s memory regions and decide where each section belongs. For this STM32L433RCT6P example, Flash starts at 0x08000000 and has 256 KiB; main RAM starts at 0x20000000 and has 48 KiB; RAM2 starts at 0x10000000 and has 16 KiB. Other STM32 devices need a layout that matches their memory. Create STM32L433xx_FLASH.ld in the project directory:
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 48K
RAM2 (rwx) : ORIGIN = 0x10000000, LENGTH = 16K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector))
} > FLASH
.text :
{
*(.text*)
*(.rodata*)
. = ALIGN(4);
} > FLASH
.ARM.extab : { *(.ARM.extab*) } > FLASH
.ARM.exidx : { *(.ARM.exidx*) } > FLASH
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
_sidata = LOADADDR(.data);
.bss (NOLOAD) :
{
. = ALIGN(4);
_sbss = .;
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
}
/* Leave at least 1 KiB of RAM for the descending stack. */
ASSERT(_ebss + 0x400 <= _estack, "RAM overflow: no room for stack")
The script fills in the addresses that were undefined in lesson 3:
- Memory regions:
MEMORYnames Flash and the two RAM regions._estackis the top of main RAM (0x2000C000), where the downward-growing stack begins. RAM2 is listed but unused.ENTRYrecords the ELF entry point; at reset, the Cortex-M instead reads the stack pointer and reset address from the vector table. - Code and vectors:
SECTIONSplaces the vector table first, followed by code and read-only data in Flash.KEEPprevents the vector table from being discarded..ARM.extaband.ARM.exidxhold Arm exception metadata if any is present. - Initialized data:
.datahas a load address in Flash for its initial bytes and a run address in RAM for the writable variables.AT > FLASHselects the load region;_sidata = LOADADDR(.data)gives startup code the Flash source, while_sdataand_edatamark the RAM destination. - Zero-initialized data and stack:
.bss (NOLOAD)occupies RAM without storing initial bytes in Flash;_sbssand_ebssmark the range to clear. The finalASSERTreserves at least 1 KiB of main RAM above the data for the stack.
The linker will now resolve the memory symbols that showed up as U in lesson 3’s nm output. It will also resolve main from the C object file.
Ask CMake to link an ELF
Keep the Arm toolchain file from lesson 3. Replace CMakeLists.txt so firmware changes from an OBJECT library into an executable that CMake links:
cmake_minimum_required(VERSION 3.22)
project(stm32-cmake-lesson LANGUAGES C ASM)
set(LINKER_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/STM32L433xx_FLASH.ld")
add_executable(firmware main.c startup_stm32l433.s)
set_target_properties(firmware PROPERTIES
SUFFIX ".elf"
C_STANDARD 11
C_STANDARD_REQUIRED ON
LINK_DEPENDS "${LINKER_SCRIPT}"
)
target_compile_options(firmware PRIVATE $<$<COMPILE_LANGUAGE:C>:-Wall;-Wextra>)
target_link_options(firmware PRIVATE
-nostdlib
"-T${LINKER_SCRIPT}"
"-Wl,-Map=$<TARGET_FILE_DIR:firmware>/firmware.map"
-Wl,--gc-sections
)
These CMake settings turn the object-only build into a linked ELF:
add_executableinvokes the linker to resolve references between the C and assembly objects.-Tselects our linker script.-Wl,-Map=...writes a map showing where the linker placed each part of the program.--gc-sectionsdiscards unused sections.-nostdlibavoids pulling in a C runtime this example does not use.LINK_DEPENDStriggers a relink when the script changes.
Build and inspect the ELF
From stm32-cmake-lesson, reconfigure the existing build/ directory and build the linked ELF. CMake keeps the toolchain and Ninja generator selected in lesson 2:
cmake -S . -B build
cmake --build build
The output is build/firmware.elf. Inspect its size, sections, and resolved symbols:
arm-none-eabi-size build/firmware.elf
arm-none-eabi-objdump -h build/firmware.elf
arm-none-eabi-nm -n build/firmware.elf
Here is the output from building the example with STM32CubeCLT; sizes and some addresses may vary with the toolchain. First, arm-none-eabi-size shows the totals for code, initialized data, and zero-initialized data:
text data bss dec hex filename
96 4 4 104 68 build/firmware.elf
text— 96 bytes: Code and read-only contents in Flash, including the vector table.data— 4 bytes: Initialized data that runs in RAM; its initial value is also stored in Flash.bss— 4 bytes: Zero-initialized RAM, with no initial bytes stored in Flash.dec/hex— 104 /0x68: The sum oftext,data, andbss, shown in decimal and hexadecimal. It is not the size of the ELF file on disk.filename: The ELF that was measured.
arm-none-eabi-objdump -h shows where the linked sections belong:
build/firmware.elf: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .isr_vector 00000008 08000000 08000000 00001000 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .text 00000058 08000008 08000008 00001008 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
2 .data 00000004 20000000 08000060 00002000 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000004 20000004 08000064 00002004 2**2
ALLOC
4 .comment 00000043 00000000 00000000 00002004 2**0
CONTENTS, READONLY
5 .ARM.attributes 00000032 00000000 00000000 00002047 2**0
CONTENTS, READONLY
- Vector table and code:
.isr_vectorbegins at0x08000000, followed by.textin Flash. - Initialized data:
.datahas a VMA (run address) of0x20000000in RAM and an LMA (load address) of0x08000060in Flash. Startup copies its initial bytes from Flash to RAM. - Zero-initialized data:
.bssoccupies RAM but hasALLOCwithoutCONTENTSorLOAD, so there are no stored initial bytes to copy.
Finally, arm-none-eabi-nm -n shows that the symbols left unresolved in lesson 3 now have addresses:
08000008 T main
08000020 T Reset_Handler
08000060 A _sidata
20000000 D _sdata
20000000 D initialized_counter
20000004 D _edata
20000004 B _sbss
20000004 B zero_counter
20000008 B _ebss
2000c000 R _estack
_sidata points into Flash, _sdata points into RAM, and _estack is at the top of main RAM. Open build/firmware.map in a text editor if you want to see which input sections contributed to these addresses.
What’s next
We now have a linked ELF and can see how startup connects Flash and RAM. Do not flash it: linking the file does not make its two-entry vector table complete or safe to run on a board. In the next lesson, we’ll replace this teaching example’s startup with ST’s complete device startup and add CMSIS and the HAL.