What Happens When C Becomes Machine Code (10 Real Examples)

01-_-1 pts0 comments

What Actually Happens When C Becomes Machine Code (10 Real Examples) | Comuniq

This site requires Javascript to work properly. Please enable Javascript in your browser.

-->

/Programming

Learn to develop things, get out of debt, find answers and take part in coding and programming challenges.

Members: 10 Join

Moderated by: mozzapp

What Actually Happens When C Becomes Machine Code (10 Real Examples)

h--za1<br>1785440081<br>[Programming]<br>3 comments

All the assembly below was generated with `gcc -S -masm=att` on a real machine — it's not reconstructed from memory. Calling convention: System V AMD64 (Linux, 64-bit). No `-fasynchronous-unwind-tables`, so the CFI directives don't clutter the read.

## 1. The full pipeline (preprocessor → assembly → object file → binary)

Base code:

```c<br>#include

int soma(int a, int b) {<br>return a + b;

int main(void) {<br>printf("%d\n", soma(3, 4));<br>return 0;<br>```

**Step 1 — Preprocessor** (`gcc -E main.c`): resolves `#include`, macros, and directives. The output is plain C, minus the original `#include ` — it's already been expanded above (hundreds of lines from stdio.h, omitted here):

```c<br>int soma(int a, int b) {<br>return a + b;

int main(void) {<br>printf("%d\n", soma(3, 4));<br>return 0;<br>```

**Step 2 — Assembly** (`gcc -S main.c` → `main.s`):

```asm<br>soma:<br>endbr64<br>pushq %rbp<br>movq %rsp, %rbp<br>movl %edi, -4(%rbp)<br>movl %esi, -8(%rbp)<br>movl -4(%rbp), %edx<br>movl -8(%rbp), %eax<br>addl %edx, %eax<br>popq %rbp<br>ret<br>```

**Step 3 — Object file** (`gcc -c main.c` → `main.o`) + symbol table (`nm main.o`):

```<br>0000000000000018 T main<br>U printf<br>0000000000000000 T soma<br>```

`T` = symbol defined in the `.text` section. `U` = undefined symbol — `printf` only exists as a reference here; it gets resolved by the linker against libc.

**Step 4 — Disassembling the `.o`** (`objdump -d main.o`) — note that `soma` starts at address `0x0`, because the `.o` hasn't been placed in its final memory layout yet:

```<br>0000000000000000 :<br>0: f3 0f 1e fa endbr64<br>4: 55 push %rbp<br>5: 48 89 e5 mov %rsp,%rbp<br>8: 89 7d fc mov %edi,-0x4(%rbp)<br>b: 89 75 f8 mov %esi,-0x8(%rbp)<br>e: 8b 55 fc mov -0x4(%rbp),%edx<br>11: 8b 45 f8 mov -0x8(%rbp),%eax<br>14: 01 d0 add %edx,%eax<br>16: 5d pop %rbp<br>17: c3 ret<br>```

**Step 5 — Final linked binary** (`gcc -o main main.c` → `objdump -d main`) — now `soma` has a "real" address inside the binary (`0x1149`), since the linker has laid out every section:

```<br>0000000000001149 :<br>1149: f3 0f 1e fa endbr64<br>114d: 55 push %rbp<br>114e: 48 89 e5 mov %rsp,%rbp<br>1151: 89 7d fc mov %edi,-0x4(%rbp)<br>1154: 89 75 f8 mov %esi,-0x8(%rbp)<br>1157: 8b 55 fc mov -0x4(%rbp),%edx<br>115a: 8b 45 f8 mov -0x8(%rbp),%eax<br>115d: 01 d0 add %edx,%eax<br>115f: 5d pop %rbp<br>1160: c3 ret<br>```

The machine code is identical to the `.o` version (only the address changed) — good moment in the text to explain that the linker does *relocation*, it doesn't recompile anything.

## 2. `soma` — the simplest possible function

```c<br>int soma(int a, int b) {<br>return a + b;<br>```

**`-O0`:**<br>```asm<br>soma:<br>endbr64<br>pushq %rbp<br>movq %rsp, %rbp<br>movl %edi, -4(%rbp) # a arrives in edi, saved to the stack<br>movl %esi, -8(%rbp) # b arrives in esi, saved to the stack<br>movl -4(%rbp), %edx<br>movl -8(%rbp), %eax<br>addl %edx, %eax<br>popq %rbp<br>ret<br>```

**`-O2`:**<br>```asm<br>soma:<br>endbr64<br>leal (%rdi,%rsi), %eax # add directly using the lea trick, no stack frame at all<br>ret<br>```

This pair is a great opener for the optimization section: at `-O0` the compiler doesn't even try to be clever — it stores everything on the stack "just in case" (makes it easier for a debugger to map a variable to an address). At `-O2` it realizes it doesn't even need an `add`: it reuses `lea` (load effective address) — literally an address-arithmetic instruction repurposed to add two integers — without touching the stack.

## 3. `for` loop — summing an array

```c<br>int soma_array(int *arr, int n) {<br>int total = 0;<br>for (int i = 0; i b) {<br>return a;<br>} else {<br>return b;<br>```

**`-O0`:** (a real branch, with a jump)<br>```asm<br>maior:<br>endbr64<br>pushq %rbp<br>movq %rsp, %rbp<br>movl %edi, -4(%rbp)<br>movl %esi, -8(%rbp)<br>movl -4(%rbp), %eax<br>cmpl -8(%rbp), %eax<br>jle .L2<br>movl -4(%rbp), %eax<br>jmp .L3<br>.L2:<br>movl -8(%rbp), %eax<br>.L3:<br>popq %rbp<br>ret<br>```

**`-O2`:** (no branch at all — uses `cmov`)<br>```asm<br>maior:<br>endbr64<br>cmpl %esi, %edi<br>movl %esi, %eax<br>cmovge %edi, %eax # conditional move: eax = edi if edi >= esi<br>ret<br>```

One of my favorites for an article, because it's counterintuitive: people who learned "if turns into a jump" from a textbook will be surprised that `-O2` has no jump whatsoever. `cmov` (conditional move) exists specifically to avoid branch misprediction in small code paths — worth mentioning that this is a microarchitecture decision, not just "optimizing for code size."

## 5. Recursion — factorial

```c<br>int fatorial(int n) {<br>if (n

Comment

x1012<br>1785440909

honestly the overflow example is the one that should scare people more than it does. I've seen "defensive" checks like that in production code, written by people who genuinely believed they...

movl main soma code return endbr64

Related Articles