Hello! In our last lesson, we mastered the C language idioms for manipulating individual bits in hardware registers. You now have the fundamental tools to set, clear, toggle, and test bits—the lowest level of software control over hardware.
This lesson addresses the next logical question: how does the C/C++ code you write, including those bitwise operations, transform into a program that a microcontroller can actually execute? On your development PC, compiling is often a single, straightforward command. For embedded systems, the process is more explicit and gives you, the developer, far more control. This is a critical concept that separates general software development from embedded engineering.
By the end of this lesson, you will be able to describe the entire embedded software build process, from source code to a running application on the target hardware. We will cover three cornerstone concepts: cross-compilation, the role of the linker script, and the purpose of the startup file.
The Big Picture: From Source Code to Target
In your computer science courses, you've likely compiled C++ code on your PC to run on that same PC. This is called native compilation. In embedded systems, we use cross-compilation: your powerful x86-based PC runs a compiler that generates machine code for a completely different processor architecture, like the ARM Cortex-M in a microcontroller.
The overall process can be visualized as a pipeline. Let's look at a diagram that shows the journey from your source files to the final program running on the target device.

Here's a breakdown of the steps shown in the diagram:
- Source Files: Your project consists of C/C++ files (
.c,.cpp), header files (.h), and sometimes assembly files (.s). Thestartup.sfile is a special assembly file we'll discuss shortly. - Compilation & Assembly: A cross-compiler (like
arm-none-eabi-gcc) translates each.cor.cppfile into an object file (.o). An assembler does the same for.sfiles. These object files contain machine instructions and data, but the memory addresses are not yet finalized. - Linking: The linker is the master builder. It takes all the individual object files, along with any pre-compiled libraries, and combines them into a single, complete executable file (often in a format like ELF, with an extension like
.axfor no extension). - Flashing: The final executable is converted into a raw binary format (
.bin) and written ("flashed") to the microcontroller's non-volatile Flash memory.
Two inputs to the linker are critically important in embedded development and are often hidden in desktop development: the linker script and the startup file.
Memory Sections: The Building Blocks of a Program
Before diving into the linker script, it's essential to understand how a compiler categorizes the different parts of your program. A video from the Artful Bytes channel provides a great overview of the build process and these memory sections.
How Microcontroller Memory Works | Embedded System Project Series #16
This video explains the entire process from source code to machine code running on a microcontroller. It provides the necessary context for understanding the linker script and startup code.
Please watch the segment from the process of going from source code to the end of the startup code explanation. As you watch, focus on the definitions of these key program sections: .text: The actual program instructions (code). .data: Global or static variables that have an initial value (e.g., int x = 10;). .bss: Global or static variables that are uninitialized or initialized to zero (e.g., int y;). Stack: Memory for local variables and function calls.
As you saw, the compiler sorts your code and variables into these standard sections. Now, how do these sections get placed into the microcontroller's physical memory? That's the job of the linker script.
The Linker's Blueprint: The Linker Script
A linker script is a configuration file that tells the linker how to arrange the output sections in memory. It is your map from the abstract sections (.text, .data) to the concrete memory regions of your microcontroller (FLASH, RAM), whose addresses you'd find in the datasheet.

A typical linker script has two main commands:
MEMORY: This command defines the physical memory blocks of your target device. You'll declare regions forFLASHandRAM, specifying their starting address (ORIGIN) and size (LENGTH). This information comes directly from the microcontroller's datasheet.SECTIONS: This is the core of the script. Here, you instruct the linker where to place each section from the input object files..text(code) and.rodata(constants) are placed inFLASH..bss(zero-initialized variables) reserves space inRAM..data(pre-initialized variables) is the most interesting. The variables themselves must reside inRAMso they can be changed. However, their initial values must be stored in the non-volatileFLASHmemory and copied toRAMat boot time. The linker script defines both the run-time address in RAM (the Virtual Memory Address, or VMA) and the load-time address in FLASH (the Load Memory Address, or LMA).
To see how this is done in practice, let's examine a real linker script.
Writing Linker Script for STM32 (Arm Cortex M3)🛠️
This article provides a step-by-step guide to creating a linker script from scratch, which will make these abstract commands concrete.
Please focus on these three parts: Start with the brief introduction explaining what a linker script is. Next, read the section on the MEMORY command. Notice how it defines the physical rom (FLASH) and ram regions and introduces the concepts of VMA and LMA. Finally, review the practical example. Don't worry about memorizing the syntax. The goal is to see how the MEMORY and SECTIONS commands work together to explicitly place the vector table (.isr_vector), code (.text), initialized data (.data), and uninitialized data (.bss) sections.
The linker script provides the map, but it doesn't perform any actions. A critical question remains: what actually copies the .data section from FLASH to RAM and zeroes out the .bss section before your program starts?
Before main(): The Startup File
Your CS background has trained you to see main() as the entry point of a program. In bare-metal embedded systems, this isn't true. The actual first code to run after the microcontroller powers on is a small piece of code defined in the startup file (e.g., startup.s or startup.c).
When an ARM Cortex-M processor resets, the first thing the hardware does is look at a specific address in memory for the vector table. From this table, it automatically loads two crucial values:
- The initial value for the Stack Pointer (SP) register.
- The address of the
Reset_Handlerfunction.
The CPU then immediately jumps to the Reset_Handler. This handler, found in the startup file, is the true entry point of your software.
Its primary jobs are:
- Initialize the C runtime environment:
- It copies the initial values for the
.datasection from their LMA in FLASH to their VMA in RAM. - It loops through the
.bsssection in RAM and writes zeros to every location.
The linker script exposes symbols (like_sdata,_edata,_sbss,_ebss) that mark the start and end of these sections, which the startup code uses to perform these loops.
- It copies the initial values for the
- Perform System Initialization: It may call a function (often
SystemInit()) to configure system clocks or other critical hardware before the main application runs. - Call
main(): Only after the memory is correctly prepared does it finally call yourmain()function, and your application begins.
The following video provides an outstanding walkthrough of building a startup file and linker script from scratch, demonstrating exactly why these steps are necessary.
This video is a masterclass in microcontroller startup. It shows the consequences of not having a proper startup sequence and then builds one piece by piece.
We'll focus on two key segments: First, watch how a minimal startup file is created in C. Pay attention to the definition of the Reset_Handler, the interrupt vector table, and how a special attribute is used to tell the linker where to place it. Watch from this clean slate. Next, see why this initial step is insufficient. The video triggers a hardware fault by trying to use variables before the runtime is initialized. It then shows the implementation of the crucial .data copy and .bss zero-fill loops. This segment is the heart of the lesson. Watch from our C program is already working.
For a concise textual summary of this process, the following article is an excellent companion to the video.
Understanding the Startup File on ARM Cortex-M
This article recaps the startup process, clearly showing the cooperative relationship between the startup file and the linker script.
Read the sections on the Role of the Startup File, the Startup Sequence itself (note the C code example), and how it all connects to the Linker Script.
Conclusion
You've now demystified the process that turns your C/C++ source code into a running program on a bare-metal system. This knowledge is fundamental for any serious embedded engineer, as it gives you control over your program's memory layout and initialization—a level of control required in the high-stakes environments you're aiming for.
Key Takeaways:
- Cross-compilation is the process of building a program on a host machine (like your PC) for a different target architecture (like an ARM microcontroller).
- The Linker is responsible for combining all compiled object files into a single executable.
- The Linker Script is a crucial configuration file that acts as a blueprint, telling the linker exactly how to place program sections (
.text,.data,.bss) into the microcontroller's physicalFLASHandRAM. - The Startup File contains the
Reset_Handler, the true entry point of the software. Its primary job is to initialize the C runtime environment—by copying initialized data from FLASH to RAM and zeroing the BSS section—beforemain()is ever called.
In our previous lessons, we've covered how to read datasheets, how to manipulate hardware registers with C, and now, how that C code gets built and loaded. In our next lesson, we'll put it all together by exploring memory-mapped I/O, the mechanism that allows your C code to control peripherals simply by writing to specific memory addresses.