UNIT 04 · LESSON 1 OF 6

From Source Code to Machine Code

What did each one do, and why can the number 466 only be computed at the very end?

INTERACTIVEOne function through every build stage
A C function shown at one stage of the buildled.o (gcc -c)00: b510       push {r4, lr}02: 2101       movs r1, #104: 2019       movs r0, #2506: f7ff fffe  bl   gpio_put0a: 4a02       ldr  r2, [pc, #8]0c: 6813       ldr  r3, [r2, #0]0e: 1c5b       adds r3, r3, #110: 6013       str  r3, [r2, #0]12: bd10       pop  {r4, pc}14: 0000 0000  .word 0x00000000relocations:  06  R_ARM_THM_CALL gpio_put  14  R_ARM_ABS32 led_countMachine code at offsets from 0. The call and the address of led_count areplaceholders, each with a relocation entry telling the linker what to patch.
A C function shown at one stage of the buildled.o (gcc -c)00: b510       push {r4, lr}02: 2101       movs r1, #104: 2019       movs r0, #2506: f7ff fffe  bl   gpio_put0a: 4a02       ldr  r2, [pc, #8]0c: 6813       ldr  r3, [r2, #0]0e: 1c5b       adds r3, r3, #110: 6013       str  r3, [r2, #0]12: bd10       pop  {r4, pc}14: 0000 0000  .word 0x00000000relocations:  06  R_ARM_THM_CALL gpio_put  14  R_ARM_ABS32 led_countMachine code at offsets from 0. The call and theaddress of led_count are placeholders, each with arelocation entry telling the linker what to patch.

Try this

Stage
Object file (led.o (gcc -c)): Machine code at offsets from 0. The call and the address of led_count are placeholders, each with a relocation entry telling the linker what to patch.

The same C function after preprocessing, compilation to assembly, assembly to an object file and linking. The object file’s bytes are exact Thumb encodings; the placeholders are filled by relocations (lesson 2). The final addresses in the linked stage are an illustrative layout, and the BL encoding is computed from them.

What you will be able to do
  • Explain what a cross toolchain is and read a target triple such as arm-none-eabi.
  • Name the four build stages, the file each produces (.i, .s, .o, .elf) and the gcc option that stops after each.
  • Describe what is still unknown in an object file and why the linker must run once for the whole program.
  • Compute the offset a Thumb BL instruction encodes from its own address and its target’s address.
  • Distinguish the ELF output of the linker from the raw binary or hex file written to flash, and say what objcopy does.
Before you start
  • Instructions, registers and the program counter (unit 3, lesson 2).
  • Flash and RAM address ranges on a microcontroller (unit 3, lessons 1 and 4).
Steps in this lesson
  1. Compiling for a different machine
  2. One command, four stages
  3. What each stage knows
  4. Worked example: computing a branch
  5. From ELF to flash
  6. Common misconceptions

The puzzle

You write gpio_put(LED_PIN, 1); in a file called led.c and press build. A second later a file of a few kilobytes appears, and somewhere inside it are the four bytes f000 f8e9, which the processor will execute as “call the function 466 bytes beyond the point the program counter reads as”. The name gpio_put, the macro LED_PIN, the idea of a function at all: none of it survives. Four stages of the toolchain touched the code on the way. What did each one do, and why can the number 466 only be computed at the very end?

STEP 1

Compiling for a different machine

A microcontroller cannot compile its own firmware; it has no room for a compiler and nowhere to run one. You build on a PC and run on the target, so the compiler must be a cross-compiler: it runs on one architecture and generates code for another. GNU cross toolchains are named by a target triple. In arm-none-eabi-gcc, arm is the architecture, none says there is no operating system underneath (the program owns the machine, unit 3), and eabi is the Arm Embedded ABI, the calling and object-file conventions from unit 3. Flags then narrow the target further: -mcpu=cortex-m0plus -mthumb selects the exact core and its Thumb instruction set, so the compiler never emits an instruction that core lacks.

STEP 2

One command, four stages

arm-none-eabi-gcc is a driver. For each source file it takes the code through three stages, and at the end it runs a fourth once for the whole program:

  1. Preprocessing. Handles every line beginning with #: pastes the contents of #include files in, replaces macros such as LED_PIN with their text, and removes code excluded by #if. Output: a single, long C file. GCC normally does this inside the compiler proper as it reads the source, so the .i file exists only if you ask for it.
  2. Compiler proper (cc1). Parses the C, optimises it and generates assembly language for the target (.s). This is where -O2 or -Os does its work.
  3. Assembler (as). Encodes each assembly instruction into its binary form, producing an object file (.o).
  4. Linker (ld). Combines all object files and libraries into one executable image (.elf), deciding every address.

Three options stop the pipeline early (and -E writes out the preprocessed text), and they are the fastest way to see what a stage does: -E stops after preprocessing, -S after compilation, -c after assembly (GCC’s Overall Options). -save-temps runs the whole build but keeps all the intermediate files.

DIAGRAMThe programs behind one gcc command
Preprocessor, compiler, assembler, linker and objcopy chained togetherled.cpreproc.led.icc1led.sasled.omain.olibc.amemmap.ldldapp.elfobjcopyapp.binapp.hex-E stops after preprocessing, -S after cc1, -c after as;without them gcc runs all four steps.
Preprocessor, compiler, assembler, linker and objcopy chained togetherled.cpreprocess (gcc -E)led.icc1 (gcc -S)led.sas (gcc -c)led.o + main.o + libc.a + memmap.ldldapp.elfobjcopyapp.bin / app.hex
led.c → preprocessing → led.i → cc1 → led.s → as → led.o; all objects, libraries and the linker script → ld → app.elf → objcopy → app.bin or app.hex.

A single arm-none-eabi-gcc invocation takes each source file through preprocessing, compilation and assembly, then runs the linker once for the whole program. The stages are drawn separately; GCC normally preprocesses inside the compiler proper (cc1), so led.i exists only with -E or -save-temps. objcopy converts the ELF result into the raw binary or hex format a programmer or boot loader expects.

STEP 3

What each stage knows

Follow one function through the stages in the figure below. The preprocessed text has lost the macro: the compiler proper never sees LED_PIN, only 25, which is why a debugger cannot show you a macro’s name. The assembly has registers and instructions but still uses names for things outside this file. The object file has real machine code, with offsets counted from zero, because the assembler has no idea where in memory this code will end up or where gpio_put lives: gpio_put might be in another file, in a library, or not exist at all.

↑ This step uses the figure at the top of the page.

So the object file contains placeholders plus instructions for filling them in, called relocations (lesson 2). Only the linker sees every object file at once, so only the linker can choose addresses and fill the placeholders. That is why linking happens once, after everything else, and why “undefined reference” is a link error, not a compile error: each file compiled perfectly well on its own.

STEP 4

Worked example: computing a branch

A Thumb BL (branch with link) does not store the target address. It stores the distance from the branch to the target, measured from the branch’s own address plus 4 (the value the program counter reads as while the instruction executes):

offset=S−(P+4)\text{offset} = S - (P + 4)

where SS is the target’s address and PP the address of the BL itself. In the figure’s linked layout, led_on starts at 0x1000_0234, so the BL six bytes in is at PP = 0x1000_023A, and the linker has placed gpio_put at SS = 0x1000_0410:

offset=0x1000_0410−(0x1000_023A+4)=0x1D2=466\text{offset} = \text{0x1000\_0410} - (\text{0x1000\_023A} + 4) = \text{0x1D2} = 466

The BL encoding splits that offset (halved, since instructions are 2-byte aligned) across two 16-bit halfwords: 466 / 2 = 233 = 0xE9 goes into the low 11 bits of the second halfword, the rest of the fields are zero or their “positive” values, and the instruction becomes f000 f8e9. In the object file the same instruction was the placeholder f7ff fffe, which the ELF for the Arm Architecture specification gives as the encoding of BL ., a branch to itself, representing an initial addend of −4. Move either function at all in the link and the four bytes change; that is why they cannot be known until the end.

STEP 5

From ELF to flash

The linker’s output is an ELF file: the machine code and data, plus section headers, symbol tables, debug information and each section’s load address. A debugger wants all of that. A chip’s flash wants only the bytes, at the right addresses. objcopy converts between the two: arm-none-eabi-objcopy -O binary app.elf app.bin writes a raw memory image, starting at the load address of the lowest section and discarding symbols and relocations; -O ihex writes Intel HEX, a text format in which every line carries its own address and a checksum. Programming tools and boot loaders accept one or more of these; some chips define their own container, such as the RP2040’s UF2 files, which carry the target address in every block.

The raw binary has one trap. It is a single contiguous dump from the lowest to the highest load address, so if any section is ever given a load address far away (say, in RAM at 0x2000_0000 while code sits at 0x1000_0000), the “binary” becomes hundreds of megabytes of padding. Lesson 4 shows how the linker script keeps every load address in flash.

MYTHS AND FACTS

Common misconceptions

The compiler makes the executable

The compiler makes assembly; the assembler makes object files; the linker makes the executable. gcc is a driver that runs all of them.

An undefined reference means the code did not compile

Each file compiled. The linker could not find a definition for a name one of them uses.

The object file contains the final addresses

It contains section-relative offsets and placeholders. Final addresses exist only after linking.

-O0 code is the ‘real’ code and optimised code is a distortion

Both implement the same C semantics. -O0 is merely easier to map back to source lines.

The .elf file is what goes into flash

The ELF describes the image; the programmer or objcopy extracts the loadable bytes from it.

Macros exist at run time

They are expanded by the preprocessor before compilation; the compiled code contains only their expansions.

Check yourself

Answer in your head, then open the card.

Which gcc option would you use to see what a macro-heavy header expands to, and which to see the instructions a loop compiles to?

-E stops after preprocessing and shows the expanded C. -S stops after compilation and writes the assembly.

A BL at 0x0800_1000 calls a function at 0x0800_0F00. What offset does the instruction encode?

S−(P+4)=0x0800_0F00−0x0800_1004=−0x104=−260S - (P + 4) = \text{0x0800\_0F00} - \text{0x0800\_1004} = -\text{0x104} = -260 bytes: a backward branch.

Why might the assembler leave the BL to gpio_put for the linker even when gpio_put is defined in the same source file?

With functions in separate sections (as -ffunction-sections arranges, lesson 3) the linker may place them anywhere, or discard one, so the distance is unknown until link time. And even within one section, gpio_put is a global symbol: a definition in another file could take its place (lesson 2), so the GNU assembler keeps a relocation for the call rather than fixing the distance itself.

A project’s .text is at 0x0000_0000 and a section wrongly given a load address of 0x2000_0000. How big is the raw binary objcopy produces?

At least 0x2000_0000 bytes, 512 MiB, almost all padding, because a raw binary is one contiguous dump from the lowest to the highest load address.

Sources (6)
  1. GCC manual, “Options Controlling the Kind of Output” (-E, -S, -c, -o) — -E: “Stop after the preprocessing stage”; -S: “Stop after the stage of compilation proper; do not assemble”; -c: “Compile or assemble the source files, but do not link”
  2. GCC manual, “Options That Control Optimization” (-O0, -Og, -O2, -Os) and “ARM Options” (-mcpu, -mthumb) — -O0 is the default and “make[s] debugging produce the expected results”; -Og is recommended for the edit-compile-debug cycle; -Os enables -O2 optimisations except those that often increase size; -mcpu accepts cortex-m0plus among others
  3. GCC manual, “GCC Developer Options”: -save-temps — keeps the intermediate .i, .s and .o files that a normal build deletes
  4. Arm, ELF for the Arm Architecture (AAELF32), “Relocation” and “Addends and PC-bias compensation” — an unrelocated Thumb BL to itself is encoded 0xf7ff, 0xfffe with R_ARM_THM_CALL and initial addend −4; R_ARM_THM_CALL computes ((S + A) | T) – P
  5. GNU Binutils manual, “objcopy” — “-O binary” produces “a memory dump of the contents of the input object file”; symbols and relocation information are discarded and the dump starts at the load address of the lowest section copied
  6. Microsoft, UF2 file format specification (README) — a UF2 file is a sequence of self-contained 512-byte blocks, each carrying the flash address its data should be written to (offset 12)