Chapter 9: memory.x and the Linker Script
September 17, 2026 · View on GitHub
Introduction
A microcontroller does not load firmware into a blank process. Code must land at
specific addresses: the bootrom looks for specific headers in flash, the vector
table must be at the flash base, and the stack pointer must be initialized to
the top of RAM before the first Rust instruction runs. The linker script
expresses those facts. In embedded Rust it is a file called memory.x, and it
is the same for all three of our drivers.
The RP2350 Memory Regions
The linker script starts by naming the chip's physical memory regions and their sizes:
0x10000000 FLASH 4096K (XIP, firmware)
0x20000000 RAM 512K (stack + data)
0x20080000 SRAM8 4K (bootrom scratch)
0x20081000 SRAM9 4K (bootrom scratch)
In linker script syntax this becomes the MEMORY block:
MEMORY {
FLASH : ORIGIN = 0x10000000, LENGTH = 4096K
RAM : ORIGIN = 0x20000000, LENGTH = 512K
SRAM8 : ORIGIN = 0x20080000, LENGTH = 4K
SRAM9 : ORIGIN = 0x20081000, LENGTH = 4K
}
SRAM8 and SRAM9 are the bootrom's private scratch regions. They appear in
the script because the RP2350 bootrom implements part of its secure-boot
handshake there, but our firmware itself runs from FLASH and RAM.
Complete Linker Script
Here is the full memory.x shared by all three drivers:
MEMORY {
FLASH : ORIGIN = 0x10000000, LENGTH = 4096K
RAM : ORIGIN = 0x20000000, LENGTH = 512K
SRAM8 : ORIGIN = 0x20080000, LENGTH = 4K
SRAM9 : ORIGIN = 0x20081000, LENGTH = 4K
}
_stack_start = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS {
.start_block : ALIGN(4)
{
__start_block_addr = .;
KEEP(*(.start_block));
KEEP(*(.boot_info));
} > FLASH
} INSERT AFTER .vector_table;
_stext = ADDR(.start_block) + SIZEOF(.start_block);
SECTIONS {
.bi_entries : ALIGN(4)
{
__bi_entries_start = .;
KEEP(*(.bi_entries));
. = ALIGN(4);
__bi_entries_end = .;
} > FLASH
} INSERT AFTER .text;
SECTIONS {
.end_block : ALIGN(4)
{
__end_block_addr = .;
KEEP(*(.end_block));
} > FLASH
} INSERT AFTER .uninit;
PROVIDE(start_to_end = __end_block_addr - __start_block_addr);
PROVIDE(end_to_start = __start_block_addr - __end_block_addr);
The Start Block
The RP2350 bootrom does not boot a raw executable. It boots an image
definition — a preamble plus a list of "boot info" entries that describe the
image type, the entry point, and signatures. Embassy generates this preamble for
us when we enable imagedef-secure-exe (Chapter 8); the linker must reserve
space for it at a known offset.
0x10000000 +---------------------+
| .vector_table | (cortex-m-rt)
| |
+---------------------+
| .start_block | INSERT AFTER .vector_table
| __start_block_addr|
| KEEP .start_block |
| KEEP .boot_info |
+---------------------+
| .text ... |
+---------------------+
.start_block collects every .start_block and .boot_info input section and
keeps them even if the linker would otherwise garbage-collect them (KEEP).
INSERT AFTER .vector_table places this block immediately after the vector
table, which is where the RP2350 bootrom expects the image definition.
Boot Info Entries
The bootrom also needs an array of boot-info entries — the tag list describing
size, flags, and image type. The .bi_entries section collects them and
publishes their bounds so firmware can locate them:
SECTIONS {
.bi_entries : ALIGN(4)
{
__bi_entries_start = .;
KEEP(*(.bi_entries));
. = ALIGN(4);
__bi_entries_end = .;
} > FLASH
} INSERT AFTER .text;
The . symbols are location counters — read them as "memory address right
now." __bi_entries_start is the address of the first byte of the array;
__bi_entries_end is one past the last byte, aligned up to four. Any code that
must walk the phrase table uses these two symbols to know its extents.
The End Block
The end marker closes the image:
SECTIONS {
.end_block : ALIGN(4)
{
__end_block_addr = .;
KEEP(*(.end_block));
} > FLASH
} INSERT AFTER .uninit;
INSERT AFTER .uninit places it at the very end of the image, after all code,
data, and uninitialized sections. The bootrom uses these blocks plus the size
symbols to validate the image.
Provided Symbols
The final statements expose computed addresses to the firmware:
PROVIDE(start_to_end = __end_block_addr - __start_block_addr);
PROVIDE(end_to_start = __start_block_addr - __end_block_addr);
PROVIDE defines a symbol only if the firmware has not already defined it.
start_to_end is the image size in bytes (distance from the start-block address
to the end-block address); end_to_start is its negative. The Embassy RP2350
support code reads these to help the bootrom verify the image.
Why This Matters
Every one of these addresses is a number in the language of Chapter 2:
0x10000000 firmware base (BOOT2 image start)
0x20000000 stack base; top of RAM is the first SP value
0x20080000 SRAM8 — secure-boot scratch
0x20081000 SRAM9 — secure-boot scratch
When your board flashes and boots, the hardware is literally reading these
regions: the vector table, then the image definition, then code. If the linker
places the start block in the wrong spot, the bootrom radios nothing but
silence. memory.x is how we keep the promise that code lands where the
hardware expects it.
Summary
MEMORYdeclaresFLASHat0x10000000,RAMat0x20000000, and the two SRAM8/9 scratch regions._stack_startis the top of RAM — the initial stack pointer..start_block+.boot_infoform the RP2350 image definition after the vector table..bi_entriesholds the boot-info entry array with published bounds..end_blockcloses the image;start_to_end/end_to_startexpose sizes.- The side effects (
imagedef-secure-exe) and this script together make the .uf2 bootable.
Next, build.rs, the Makefile, and getting firmware onto the chip.