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.
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.
#include <stdio.h>int main(void) { printf("Hello, firmware!\n"); return 0;}
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.