C Programming 3 min read

From source to binary

A C file is only text. The preprocessor expands it, the compiler turns it into assembly, the assembler into an object file, and the linker joins everything into one image. On a microcontroller you flash that image; on a microprocessor the operating system loads it.

main.c goes through the preprocessor, which expands includes and macros, into the compiler, which produces assembly, then the assembler, which produces main.o. The linker joins main.o with startup code and libraries into firmware.elf. objcopy converts the ELF file to a .bin or .hex image, which a programmer writes into the microcontroller Flash. gcc runs the preprocessor and the compiler in one program, cc1.
Four stages turn main.c into an image the chip can run.

Four stages in a row

When you run gcc main.c, four stages run one after another (gcc runs the first two in one program, cc1). The preprocessor handles every line that starts with #: it pastes in #include files and replaces #define names. The compiler translates the result into assembly for one particular processor. The assembler turns that into machine code in an object file, main.o. The linker joins your object files with the startup code and libraries into one image, usually an ELF file.

Stop after any stage

You can halt the chain and read what a stage produced. -E stops after the preprocessor, -S after the compiler and -c after the assembler. Reading the preprocessed output is the quickest way to see what a macro really expanded to. Without -std, gcc uses its default dialect (GNU C23 since GCC 15); this series pins -std=c11.

main.cC
#include <stdio.h>int main(void) {    printf("Hello, firmware!\n");    return 0;}
Output
Hello, firmware!

Where main really starts

main() is not the first code that runs. Startup code runs first: on a microcontroller the reset handler sets up memory and then calls main; on a Linux-class processor the C runtime does the same job. Either way, the linker is what places that startup code in the image.