Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

Dissecting Linker Maps: Flash and RAM Placement

Recommended resources

Welcome to the final lesson of our first module on embedded systems foundations. Throughout this module, we've journeyed from the high-level architecture of a microcontroller and how to read its manuals, through the nuts and bolts of the build process and bitwise operations, to the crucial concept of memory-mapped I/O. You now understand how C++ code can reach out and control physical hardware by writing to specific memory addresses.

This lesson provides the capstone to that knowledge. We've discussed how the linker script tells the linker where to place code and data. But in a safety-critical field, assumptions are dangerous. How do we prove that our program's variables and functions have been placed correctly in Flash and RAM, and aren't accidentally overwriting a critical peripheral register? The answer lies in the linker map file, the ultimate ground-truth document produced by the build process.

By the end of this lesson, you will be able to inspect a linker map file to identify exactly how the primary program sections—.text, .data, and .bss—are placed in the microcontroller's Flash and RAM. This skill is foundational for debugging, optimizing memory usage, and building confidence in your embedded software.

A Tour of Your Program's Memory Footprint

Before we can read the map, we need to understand what it's mapping. When the linker assembles your final program, it doesn't just throw everything into one big blob. It organizes all the code and data from your various source files into standardized categories called sections. Your knowledge of computer architecture will be helpful here, as these concepts are fundamental to how compiled programs are structured.

The three most important sections for now are:

  • .text: This section contains the executable machine code—the actual CPU instructions for all your functions.
  • .data: This contains any global or static variables that have an explicit initial value (e.g., int global_counter = 100;).
  • .bss: This section is for global or static variables that are uninitialized or initialized to zero (e.g., int command_buffer[256];).

Let's also add .rodata for "read-only data", which stores constants like const int LUT[] = {...}; and string literals. The article "How C Code Really Ends Up in FLASH and RAM" provides an excellent breakdown of these sections.

How C Code Really Ends Up in FLASH and RAM

This article from dev.to clearly explains the purpose of each memory section. We'll focus on the descriptions of the sections themselves.

Please read the sections titled FLASH Memory Layout and RAM Memory Layout. Pay close attention to the descriptions of .text, .rodata, .data, and .bss.

As you read, you'll notice a fundamental division: some sections are destined for non-volatile Flash memory, while others belong in volatile RAM.

A typical memory layout in an embedded system. Code (`.text`) and read-only data reside in non-volatile Flash memory. Writable data sections (`.data`, `.bss`) and dynamic regions (Stack, Heap) occupy volatile RAM.

Here is a summary of where each section lives and why:

  • Flash (Non-Volatile):

    • .text (Code): Instructions are read from Flash and executed directly by the CPU.
    • .rodata (Constants): Since this data never changes, it can live permanently in Flash, saving precious RAM. This is why using const is so important for large lookup tables or configuration arrays in embedded systems.
  • RAM (Volatile):

    • .bss (Zero-initialized variables): These variables need to be writable, so they must be in RAM. The linker reserves space for them, and a special piece of code called the startup file fills this entire memory block with zeros before your main() function is ever called. This is a clever optimization: why waste Flash storing thousands of zeros when you can just create them on-the-fly at boot?
    • Stack: Used for local variables, function arguments, and return addresses. It grows and shrinks dynamically as functions are called and return.
    • Heap: Used for dynamic memory allocation (e.g., with new or malloc). This is often avoided in high-integrity systems, a topic we'll explore in depth later.
    • .data (Initialized variables): This is the most interesting case. A variable like int gain = 42; must be in RAM to be modifiable. But since RAM is volatile, where does the initial value 42 come from after a power cycle?

The Double Life of the .data Section

The .data section has a dual identity, which is managed by the linker using two different addresses:

  1. LMA (Load Memory Address): This is the address in Flash where the initial values (42 in our example) are stored. This is their permanent, non-volatile home.
  2. VMA (Virtual Memory Address): This is the address in RAM where the variable will actually live and be manipulated at runtime. The rest of your code knows the variable by its VMA.

Before main() starts, the startup code copies the initial data for the entire .data section from its LMA in Flash to its VMA in RAM. This "initialization dance" is a cornerstone of the embedded C/C++ boot process.

The "inpyjama" podcast provides a very clear explanation of this process.

ARM - M: Build process, role of linker and Linker scripts! | Embedded Systems podcast, in Pyjama!

This clip from an embedded systems podcast discusses the exact problem of initialized data and explains the RAM-at-Flash syntax used in linker scripts to solve it.

Watch the segment from the explanation where the presenters discuss why the .data section needs to be copied from Flash to RAM and how the linker script facilitates this.

This concept is so fundamental that it's worth seeing the formal definition in a technical article as well.

GNU Linker Scripts. Part 1. .data, .bss, and the Startup Contract - Alexey Karelin

This article from EmbeddedRelated.com provides the technical details behind the LMA and VMA concepts.

Please read the section that begins with the definitions of VMA and LMA. Then, look at the example linker script and C code that follows, showing how the copy_data function uses linker-generated symbols to perform the copy from Flash to RAM.


The Map File: Your Ground Truth

Now that you understand the sections, let's learn how to verify their placement. The linker can be instructed to generate a human-readable text file—the map file—that details its every decision.

To generate it, you typically add a flag to your linker command: -Wl,-Map=output.map. The -Wl, part tells the compiler driver (like gcc) to pass the subsequent command (-Map=output.map) directly to the linker (ld).

The Fastbit Embedded Brain Academy channel has an excellent, practical walkthrough of generating and interpreting a map file. This video is the core of this lesson, as it shows you exactly what to look for.

Bare metal embedded lecture-5: Linking and analyzing memory map file

This video provides a step-by-step guide to generating a map file and analyzing its contents to verify memory placement for each section.

Please watch the following segments carefully: Generating the map file: See how the -Wl,-Map flag is added to the Makefile. Analyzing .text and .rodata: Learn how to find the code and constant sections, their start addresses, sizes, and the object files that contributed to them. Analyzing .data and .bss: This part shows how to locate the RAM sections and track down which variables belong to them. It also covers a common issue with the COMMON section. Using linker symbols: The video demonstrates how linker-defined symbols (like _sdata, _edata) are used in C code to manage the startup copy routines.

After watching the video, let's put your knowledge to the test with a quick reference. The dev.to article we saw earlier has a fantastic "Truth Table" for predicting where a variable will land.

How C Code Really Ends Up in FLASH and RAM

This table is an invaluable quick-reference for predicting memory placement based on how a variable is declared.

Review the table under the heading The Truth Table. For each row, make sure you understand why the variable is placed in that specific segment based on what we've learned.

By cross-referencing this table with an actual map file, you can gain complete confidence in your program's memory layout. If you see a variable in the .data section of your map file, you know its initial value is stored in Flash and it occupies space in RAM at runtime. If you see it in .bss, you know it only takes up RAM and will be zeroed at boot.


Conclusion

This lesson closes the loop on the entire embedded software build process. You now know not only how to instruct the linker with a script but also how to verify its work by inspecting the map file. This is not just an academic exercise; it's a critical skill for professional embedded engineers, especially in aerospace and defense where resource constraints are tight and verifiable correctness is paramount.

Key Takeaways:

  • A compiled program is organized into sections: .text (code), .rodata (constants), .data (initialized data), and .bss (uninitialized data).
  • The .text and .rodata sections are placed in Flash.
  • The .data and .bss sections, along with the Stack and Heap, are placed in RAM.
  • The .data section has a unique lifecycle: its initial values are stored in Flash (at the LMA) and copied to RAM (at the VMA) during startup.
  • The linker map file is the definitive report showing the address, size, and composition of every section and symbol in your program.
  • You can generate a map file using the -Wl,-Map=<filename> linker flag and inspect it to debug memory issues and confirm your program's layout.

With this lesson, you have completed the foundational module of this course. You've built a solid mental model of how a microcontroller executes code and how our development tools translate C++ into a precisely placed binary image.

In our next module, "Development Environment and Bare-Metal Debugging," we will transition from theory to practice. You will install the toolchain, configure your IDE, connect to a real microcontroller, and use a hardware debugger to see these very memory sections and registers live on the target. It's time to get your hands on the hardware.