Understanding the ESP8266 Linker Script
A Field Guide to Flash Layout, Memory Regions, and the LittleFS / SPIFFS Partition
1. Introduction
The ESP8266 Arduino core ships with a linker script that defines where firmware code, RAM variables, and the flash-resident filesystem are located within a 1 MB (1 MiB) flash chip. This guide walks through that script section by section, explains the two address spaces involved, and then covers how the flash layout can be safely customized to reclaim space for a larger application or a bigger filesystem. From here on, sizes are given in MiB/KiB (powers of two) rather than MB/KB, to match the actual byte counts involved.
Two Address Spaces
There are two different address spaces at play in the linker script:
● 0x402xxxxx — the ESP8266 SPI flash, memory-mapped into the CPU address space
● 0x3FFE... / 0x3FF... — the ESP8266 RAM and peripheral address space
Keeping these two ranges straight is the key to reading the rest of the script correctly — a flash address and a RAM address can look superficially similar but refer to completely different hardware.
|
Which Layout Is This? The official ESP8266 Arduino core ships many different 1 MiB linker script variants — eagle.flash.1m64.ld, 1m128.ld, 1m256.ld, 1m512.ld, a no-filesystem 1m.ld, and others — each splitting the same 1 MiB chip differently between sketch and filesystem. None of the current official variants use exactly the 600 KiB sketch / 400 KiB SPIFFS split shown throughout this guide. For example, eagle.flash.1m256.ld allocates roughly 743 KiB to the sketch and 256 KiB to the filesystem (with _FS_start at 0x402BB000), while eagle.flash.1m512.ld allocates roughly 487 KiB to the sketch and 512 KiB to the filesystem (with _FS_start at 0x4027B000). The irom0_0_seg length also varies to match — official scripts use lengths like 0xb9ff0 or 0x79ff0 rather than an exact 0x96000. The 600/400 split used here is a valid, self-consistent conceptual layout (and matches some older or custom scripts), which makes it a clean teaching example — but it should be treated as illustrative, not as “the” current default. Before touching a real project's flash layout, check the specific .ld file your board/core version actually selects, typically under the core's tools/sdk/ld/ directory, for the exact numbers in use. |
2. Flash Layout Comments
The script begins with a set of comments describing the 1 MiB flash partition:
|
/* Flash Split for 1M chips */ /* sketch @0x40200000 (~600KB) (614400B) */ /* empty @0x40296000 (~4KB) (4096B) */ /* spiffs @0x40297000 (~400KB) (409600B) */ /* eeprom @0x402FB000 (4KB) */ /* rfcal @0x402FC000 (4KB) */ /* wifi @0x402FD000 (12KB) */ |
Translated into physical flash offsets, the partition looks like this:
|
Region |
Flash Address |
Size |
|
Sketch |
0x000000 → 0x096000 |
600 KiB |
|
Empty |
0x096000 → 0x097000 |
4 KiB |
|
SPIFFS |
0x097000 → 0x0FB000 |
400 KiB |
|
EEPROM emulation |
0x0FB000 → 0x0FC000 |
4 KiB |
|
RF calibration |
0x0FC000 → 0x0FD000 |
4 KiB |
|
System / SDK params |
0x0FD000 → 0x100000 |
12 KiB |
The addresses written as 0x402xxxxx are memory-mapped CPU addresses, not raw SPI-flash offsets. For example, 0x40297000 corresponds to 0x97000 in physical flash, because:
|
0x40297000 - 0x40200000 = 0x97000 0x97000 = 618,496 bytes = 604 KiB |
0x97000 (618,496 bytes, 604 KiB) is not itself the sketch/SPIFFS boundary — it's 4 KiB further into flash than that boundary. The sketch occupies the first 600 KiB, ending at 0x96000 (614,400 bytes); the empty 4 KiB gap then runs from 0x96000 to 0x97000; and SPIFFS begins at 0x97000, immediately after that gap. So SPIFFS starts 4 KiB after the 600 KiB sketch boundary, not exactly at it.
3. The MEMORY Block
The script opens its region definitions with:
|
MEMORY { |
This tells the linker: “these are the memory areas available to my program.” Three regions are defined inside it.
3.1 dport0_0_seg
|
dport0_0_seg : org = 0x3FF00000, len = 0x10 |
A tiny 16-byte (0x10) region starting at 0x3FF00000, associated with the ESP8266 DPORT / peripheral area. It is not where ordinary Arduino variables live.
3.2 dram0_0_seg
|
dram0_0_seg : org = 0x3FFE8000, len = 0x14000 |
This is the data-RAM region available to the linker: starting at 0x3FFE8000, with a length of 0x14000 (81,920 bytes, roughly 80 KiB). Ordinary variables such as counters, buffers, and floats live here — but the ESP8266 runtime also uses part of this space, so the full 80 KiB is not all available to application code.
3.3 irom0_0_seg
|
irom0_0_seg : org = 0x40201010, len = 0x96000 |
IROM means instruction/data stored in flash and accessed through the instruction ROM mapping — this is where most compiled program code lives, including functions such as setup() and loop(). The ESP8266 executes this code directly from memory-mapped flash. The region starts at 0x40201010 and has a length of 0x96000 (614,400 bytes) — exactly the 600 KiB sketch area.
|
Why 0x40201010 and not 0x40200000? 0x40200000 is the nominal start of the memory-mapped flash image referenced in the partition comments, but it should not be read as simply “the first byte of application code after a 4 KiB bootloader.” 0x40201010 is the IROM region origin used specifically by the ESP8266 Arduino linker configuration, and it reflects the interaction of several pieces — the eboot bootloader/loader, the ESP8266 image header, and the linker's own section layout — rather than a single simple offset. The Arduino ESP8266 project's own discussion of this value treats it as a linker/build-chain detail, not a general-purpose ESP8266 fact. In practice: treat 0x40201010 as the value this specific linker configuration expects, and don't assume it generalizes to every ESP8266 image or bootloader combination without checking the corresponding core version. |
Not everything that runs actually lives in irom0_0_seg, though. Code marked with ICACHE_RAM_ATTR — interrupt handlers, flash-cache-sensitive routines, and anything that must keep running while the flash cache is temporarily unavailable — is deliberately placed in IRAM (Instruction RAM), mapped around 0x4010xxxx, rather than in IROM. That's a separate, much smaller region carved out of the ~80 KiB dram0_0_seg budget from Section 3.2, which is why liberal use of ICACHE_RAM_ATTR shrinks available RAM rather than flash: the function's code is copied into RAM at boot instead of being executed directly from the memory-mapped flash.
|
} |
This closing brace ends the MEMORY block.
4. Filesystem Symbols
After MEMORY, the script defines a set of linker symbols that tell the filesystem code where its partition lives.
4.1 _FS_start and _FS_end
|
PROVIDE ( _FS_start = 0x40297000 ); PROVIDE ( _FS_end = 0x402FB000 ); |
These mark where the filesystem begins and ends. Subtracting the two gives the filesystem size:
|
0x402FB000 - 0x40297000 = 0x64000 = 409,600 bytes = 400 KiB |
4.2 _FS_page and _FS_block
|
PROVIDE ( _FS_page = 0x100 ); PROVIDE ( _FS_block = 0x1000 ); |
These two symbols are easy to confuse but have very different jobs — neither one is a starting address.
|
Symbol |
Value |
Meaning |
|
_FS_start |
0x40297000 |
Where the filesystem begins |
|
_FS_end |
0x402FB000 |
Where the filesystem ends |
|
_FS_page |
0x100 (256 B) |
Filesystem logical page size |
|
_FS_block |
0x1000 (4096 B) |
Filesystem block size used by this configuration — matches the flash's 4 KiB erase sector for this particular layout, but is not a fixed ESP8266 constant |
ESP8266 SPI flash can be written a byte or a page at a time, but it can only be erased in 4 KiB sectors — that is a fixed hardware property. _FS_block, by contrast, is a filesystem configuration parameter, not a universal ESP8266 constant: it tells the filesystem implementation what block size to use for its own bookkeeping, and its valid range and relationship to the hardware erase-sector size depend on the specific filesystem implementation (LittleFS vs. SPIFFS) and core version in use — that relationship should be verified against the filesystem code being targeted, not assumed. In this particular 1 MiB layout, _FS_block is set to 0x1000 (4096 B), which happens to match the flash's 4 KiB erase sector exactly — a sensible and common choice, but not the only valid one. Larger layouts sometimes use a different value; for example, some published 4 MiB ESP8266 linker layouts set _FS_block to 0x2000 (8 KiB) instead.
With this layout's 400 KiB filesystem and 4 KiB block size, the partition works out to exactly 100 blocks:
|
409,600 ÷ 4,096 = 100 filesystem blocks |
4.3 EEPROM Start
|
PROVIDE ( _EEPROM_start = 0x402fb000 ); |
This tells the Arduino EEPROM emulation layer where its 4 KiB sector begins — immediately after the SPIFFS/LittleFS partition ends, so EEPROM gets the next 4 KiB sector in sequence.
5. Deprecated SPIFFS Symbols
The script also defines an older, SPIFFS-specific set of symbol names, kept for backward compatibility with code written before the core adopted the generic _FS_* naming:
|
PROVIDE ( _SPIFFS_start = 0x40297000 ); PROVIDE ( _SPIFFS_end = 0x402FB000 ); PROVIDE ( _SPIFFS_page = 0x100 ); PROVIDE ( _SPIFFS_block = 0x1000 ); |
Each of these mirrors its _FS_* counterpart exactly, so older code that still references _SPIFFS_start, for example, resolves to the same 0x40297000 address as _FS_start.
6. Pulling in the Common Linker Script
|
INCLUDE "local.eagle.app.v6.common.ld" |
Rather than defining every ESP8266 linker section itself, this file includes another script that supplies the common rules — sections such as .text, .data, .rodata, .bss, IRAM, IROM, exception handling, constructors, and initialization data. In short: this file defines the partition-specific memory layout, while the included file supplies the actual linking machinery.
7. The Whole Layout, Visually
Putting it all together, the 1 MiB flash chip breaks down as follows:
|
ESP8266 1 MiB FLASH (physical offsets) 0x000000 +--------------------------+ | Arduino Sketch | | ~600 KiB | 0x096000 +--------------------------+ | Empty 4 KiB | 0x097000 +--------------------------+ | SPIFFS | | 400 KiB | 0x0FB000 +--------------------------+ | EEPROM 4 KiB | 0x0FC000 +--------------------------+ | RF Calibration 4 KiB | 0x0FD000 +--------------------------+ | System / SDK params 12KB| 0x100000 +--------------------------+ |
And as the CPU sees it, mapped into its address space:
|
0x40200000 <- flash mapping begins |-- Sketch 0x40296000 |-- Empty 0x40297000 <- _FS_start |-- SPIFFS / LittleFS (400 KiB) 0x402FB000 <- _FS_end / _EEPROM_start |-- EEPROM 0x402FC000 |-- RF calibration 0x402FD000 |-- System / SDK params 0x40300000 |
Why This Matters for Filesystem Tooling
When a filesystem uploader (such as an mklittlefs tool) builds a filesystem image, it needs to know the start (0x97000) and size (0x64000) of the target region so it can create a matching 400 KiB image and write it to the correct flash offset. The firmware side then relies on _FS_start (0x40297000) and _FS_end (0x402FB000) so that calls such as LittleFS.begin() and LittleFS.open() know exactly which part of external SPI flash to use. In short, this linker script is the contract between the ESP8266 firmware, the Arduino core, the bootloader/flash layout, and the filesystem — and if you're modifying a custom Arduino core or filesystem tool, these addresses are exactly the values that must stay consistent.
8. Reclaiming Flash Space: What Can Actually Be Changed
A custom ESP8266 flash layout is possible, and in a typical 1 MiB layout there is genuinely some space to reclaim — but not all of it, and some regions should be left alone.
8.1 The Empty 4 KiB Gap — Reclaimable, With a Caveat
The “empty” 4 KiB area between the sketch and SPIFFS is only a comment in this script; it is not an actual linker-defined region in the MEMORY block, and nothing currently claims it. That makes it reclaimable: _FS_start can be moved back from 0x40297000 to 0x40296000, gaining 4 KiB for the filesystem, as shown below.
It's worth being precise about why this gap exists in the broader Arduino ESP8266 picture, though. The general ESP8266 Arduino flash architecture does reserve a separate area for an Over-The-Air (OTA) update image, and the documented partition order is sketch → OTA → filesystem → EEPROM → WiFi/SDK configuration. But this specific 4 KiB comment in this specific 1 MiB layout should not be read as “the OTA image” itself — a real OTA slot needs to be roughly as large as the sketch, far bigger than 4 KiB. Rather, this gap is simply unallocated padding within a layout that, as written, does not carve out full OTA space at all. Reclaiming it is safe precisely because nothing here currently depends on it, not because it was doing OTA's job and is now being freed from that job.
|
SKETCH 600 KiB SPIFFS 404 KiB (was 400 KiB) EEPROM 4 KiB RF_CAL 4 KiB SYSTEM 12 KiB ---------------------- TOTAL 1024 KiB |
The math checks out and lands on a clean 4 KiB sector boundary:
|
404 KiB = 413,696 bytes = 0x65000 0x40296000 + 0x65000 = 0x402FB000 |
8.2 The 12 KiB System / SDK Parameter Area — Do Not Remove
This region is better described as system/SDK parameter space than “WiFi code.” The official SDK partition definitions identify RF_CAL, PHY_DATA, and SYSTEM_PARAMETER as distinct system partitions here, not application or networking code. The core computes:
|
rf_cal = flash_size - 0x4000; system_parameter = flash_size - 0x3000; |
and registers PHY_DATA, RF_CAL, and SYSTEM_PARAMETER (3 × 4 KiB = 12 KiB) at the end of flash based on the physical chip size. Even an application that never calls the WiFi API still runs on an SDK/core that assumes these sectors exist — “WiFi disabled” does not mean “those sectors are unused.” They should be left in place.
8.3 EEPROM — Negotiable, But Not Casually
If a project never calls EEPROM.begin(), EEPROM.read(), EEPROM.write(), or EEPROM.commit(), the dedicated EEPROM sector is more negotiable than RF_CAL or the system parameters. However, this is the ESP8266 Arduino core's own SDK-3-specific behavior, not a general property of the raw Espressif SDK: on cores built against SDK 3.0 and later, the Arduino core explicitly computes phy_data as an offset from _EEPROM_start and overlays PHY_DATA there as part of its own initialization — so the sector participates in that core-specific mechanism whether or not the sketch uses Arduino's EEPROM emulation. Do not simply delete or repurpose the EEPROM sector on an SDK 3.x-based core without accounting for this; verify against the specific core version in use before changing it.
8.4 OTA — A Separate Mechanism, Not This 4 KiB Gap
It bears repeating: a genuine OTA-capable layout reserves an update slot roughly the size of the sketch itself, not 4 KiB. The ESP8266 Arduino core ships distinct flash-map configurations for OTA-enabled, maximum-filesystem, and no-filesystem builds precisely because these are different partitioning strategies, not variations achieved by nudging a single symbol. A project that doesn't need OTA has more freedom to grow the sketch or filesystem into that space — but doing so means deliberately choosing a non-OTA flash map, not merely reclaiming a leftover 4 KiB comment.
9. What Is Actually Mandatory?
|
Region |
Can Change? |
Recommendation |
|
Bootloader (first 4 KiB) |
No |
Keep |
|
Sketch |
Yes |
Resize as needed |
|
OTA slot |
Depends on flash map |
Reclaimable only when using a non-OTA layout |
|
LittleFS / SPIFFS |
Yes |
Can be resized, provided the filesystem image builder, linker symbols, flash layout, and uploader all agree on the same start address and size |
|
EEPROM |
Yes, if unused |
Can eliminate with a custom core/config |
|
RF_CAL |
No |
Keep |
|
PHY / system data |
No |
Keep |
|
Final system parameter sectors |
No |
Keep |
A note on “first 4 KiB”: the eboot bootloader itself does fit within that space, but the physical .bin file that esptool.py writes at offset 0x000000 also includes the ESP8266 image header alongside it. Treating the whole first 4 KiB sector as off-limits is the safe, conservative rule to follow — it's simpler than tracking the exact boundary between bootloader and header, and it guarantees esptool.py won't be pointed at a write that clobbers either one.
10. A Conceptual Custom 1 MiB Partition Map
|
Read This Before Using Any Layout Below What follows is a conceptual flash partition map, not a drop-in linker script. The irom0_0_seg origin, the eboot bootloader, the generated firmware image header, and esptool's flashing behavior are all interdependent — shifting where the sketch region physically begins is not simply a matter of editing one linker symbol. The ESP8266 Arduino core ships separate, tested flash-map configurations for OTA-enabled, maximum-filesystem, and no-filesystem builds precisely because these combinations need to be validated as a whole. Treat the addresses below as a planning aid, and validate any change by building, flashing, and booting successfully — not by arithmetic alone. |
With that caveat in place, here is the conceptual partitioning for a device that uses neither OTA nor EEPROM emulation, expressed as flash regions rather than as linker syntax:
|
Region |
Approx. Range |
Notes |
|
Bootloader / image header |
start of flash |
Reserved by eboot / the ESP8266 image format — not user-configurable |
|
Sketch (IROM) |
≈600 KiB |
Origin fixed by irom0_0_seg in the linker script actually used to build the image |
|
LittleFS |
≈400–404 KiB |
Size/location must match the filesystem image builder and uploader exactly |
|
EEPROM |
4 KiB |
Also used by PHY_DATA on SDK 3.x — see §8.3 |
|
RF_CAL |
4 KiB |
Do not relocate or remove |
|
System parameters |
12 KiB |
Do not relocate or remove |
The one change from this guide that is a straightforward, symbol-level edit — rather than a structural change to where the sketch begins — is reclaiming the 4 KiB empty gap described in §8.1, by moving _FS_start back by one sector:
|
PROVIDE ( _FS_start = 0x40296000 ); PROVIDE ( _FS_end = 0x402FB000 ); PROVIDE ( _FS_page = 0x100 ); PROVIDE ( _FS_block = 0x1000 );
PROVIDE ( _EEPROM_start = 0x402FB000 );
INCLUDE "local.eagle.app.v6.common.ld" |
This yields a 404 KiB filesystem instead of 400 KiB, touches no system sectors, and does not move the sketch's own origin — so it carries much less risk than reshaping the sketch/bootloader boundary.
The Sketch ↔ Filesystem Trade-off
Because the flash total is fixed, the sketch and filesystem sizes trade off directly against each other, subject to the reserved system areas at the end of the chip. The figures below are illustrative targets, not verified linker configurations — reaching them requires the sketch's own linker region, the filesystem image builder, and the uploader to all agree on the same boundary:
|
Layout |
Sketch |
LittleFS |
Reserved (EEPROM+RF_CAL+SYSTEM) |
|
Balanced |
600 KiB |
404 KiB |
20 KiB |
|
Filesystem-heavy |
500 KiB |
504 KiB |
20 KiB |
|
Application-heavy |
700 KiB |
304 KiB |
20 KiB |
|
Warning: Reclaiming the Final 12 KiB Is a Deeper Change Fully reclaiming the final 12 KiB of system/SDK parameter space is a deeper change. It requires coordinated changes across the linker script, the Arduino core, SDK partition registration, the flash-size information, the esptool image header, the filesystem uploader, and the boot process — because the core calculates RF/system sector locations from the physical flash size, not simply from the .ld file. Editing only the linker script is not sufficient to safely reclaim this space. |
|
||
11. Summary
● 0x402xxxxx addresses are memory-mapped flash; 0x3FFxxxxx addresses are RAM/peripherals.
● The MEMORY block defines three regions: a tiny DPORT segment, ~80 KiB of data RAM, and ~600 KiB of IROM flash for application code.
● _FS_page and _FS_block are filesystem configuration values (page size and block size), not addresses; _FS_block matches the 4 KiB hardware erase sector in this layout, but is not a fixed ESP8266-wide constant.
● The 4 KiB “empty” gap before the filesystem is unclaimed padding in this specific layout, safe to reclaim on its own terms — it is not itself an OTA image, even though OTA is the general reason ESP8266 layouts reserve extra space between sketch and filesystem.
● EEPROM's 4 KiB sector is more negotiable than RF_CAL or the system parameters, but on SDK 3.x it also carries PHY_DATA, so it should not be repurposed without checking the core version.
● A linker script's addresses, the physical flash offsets, the generated firmware image, and the Arduino flash-partition configuration are related but not interchangeable — any layout change should be validated through a full build/upload/boot cycle, not just checked arithmetically.
