> For the complete documentation index, see [llms.txt](https://kmanu225.gitbook.io/cs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kmanu225.gitbook.io/cs/software-engineering/compilation/fundamentals.md).

# Fundamentals

Compilation turns source code you can read into instructions a processor can execute. This page is the theory behind the practical [cross-compilation](/cs/software-engineering/compilation/cross-compilation.md) case study: what compiling actually does, how programs are assembled and linked, how you target a given machine, and how a finished binary can be protected or read back.

## What is compilation?

A CPU only runs **machine code**: raw bytes encoding instructions for one specific architecture. Compilation is the translation from a high-level language into that machine code.

Not every language is compiled the same way, and "compiled vs interpreted" is more a spectrum than a hard line:

| Model                         | Examples                      | When translation happens                                                      |
| ----------------------------- | ----------------------------- | ----------------------------------------------------------------------------- |
| Ahead-of-time (AOT) to native | C, C++, Rust, Go              | Fully, before running                                                         |
| To bytecode + VM              | Java, C#                      | Source → bytecode at build time, bytecode → machine code by the VM at runtime |
| JIT-compiled                  | JavaScript (V8), Java HotSpot | Hot paths compiled to native **while** the program runs                       |
| Interpreted                   | Python, Ruby, Bash            | Translated statement by statement at runtime                                  |

So do all languages need to be compiled? Not to native machine code. But almost all go through **some** translation step. "Interpreted" languages like Python still compile source to bytecode (`.pyc`) before their VM executes it. The real distinction is **when** the translation happens and **what** runs the result.

The rest of this page focuses on AOT native compilation (the C/C++ model), where the trade-offs are most visible.

## The build pipeline

Turning a `.c` file into an executable is not one step but four. Understanding them is what makes **"compilation vs assembly"** clear: assembly is just one stage inside the larger process.

```
source.c
  │  1. Preprocessing      cpp         (#include, #define, #ifdef expanded)
  ▼
source.i
  │  2. Compilation        cc1         (C → assembly for the target arch)
  ▼
source.s
  │  3. Assembly           as          (assembly → machine code object)
  ▼
source.o
  │  4. Linking            ld          (objects + libraries → executable)
  ▼
a.out
```

* **Preprocessing** handles the `#` directives: it pastes in headers, expands macros and resolves conditional compilation. Pure text manipulation.
* **Compilation** (in the narrow sense) translates preprocessed C into **assembly**, a human-readable mnemonic form of the target's machine instructions.
* **Assembly** is the mechanical translation of that assembly text into binary **object code** (`.o`): machine instructions, but with addresses still unresolved.
* **Linking** stitches the object files together with the libraries they use and resolves every symbol into a final address, producing the executable.

With `gcc` you can stop at any stage:

```bash
gcc -E source.c -o source.i   # preprocess only
gcc -S source.c -o source.s   # stop after compilation (emit assembly)
gcc -c source.c -o source.o   # stop after assembly (object file)
gcc source.c -o program       # all the way through linking
```

## Linking: static vs dynamic

Linking is where most build surprises live, because a program rarely stands alone: it calls into libraries (the C standard library at the very least). How those libraries are attached is the **static vs dynamic** choice.

### Dynamic linking (the default)

The executable keeps only a **reference** to each shared library (`.so` on Linux, `.dll` on Windows). The library is loaded and the symbols resolved at startup by the dynamic loader.

```bash
gcc source.c -o program          # dynamically linked by default
ldd program                      # list the shared libraries it needs
```

The loader finds libraries via a search order: paths baked into the binary (`rpath`/`runpath`), the `LD_LIBRARY_PATH` environment variable, then the system cache (`ldconfig`, `/etc/ld.so.conf`). If a required `.so` is missing or the wrong version, the program fails to start.

* Smaller binaries; one copy of a library shared by many programs.
* Security fixes to a library benefit every program that uses it, with no rebuild.
* But the target **must** provide the right libraries, at compatible versions.

### Static linking

Every library is copied **into** the executable at build time. The result is self-contained.

```bash
gcc source.c -o program -static
ldd program                      # "not a dynamic executable"
```

* Runs anywhere with the matching architecture, regardless of what libraries the target has, exactly the property exploited in the [cross-compilation](/cs/software-engineering/compilation/cross-compilation.md) case study.
* But larger binaries, and a library fix means rebuilding and redeploying.

## Targeting: architecture and libc

Machine code is specific to a **target**, usually written as a *target triple* like `arm-linux-gnueabihf` or `x86_64-linux-musl`: `architecture-vendor-os-abi`. A binary built for one architecture (x86) is meaningless to another (ARM).

Two parts of the target matter most in practice:

* **Architecture / ABI**: the instruction set and calling conventions. `eabihf`, for instance, means the ARM target uses hardware floating-point registers.
* **The C library (libc)**: the bridge between your code and the kernel. `glibc` is the full-featured standard on most desktops; `musl` is a lightweight implementation that links statically cleanly, which is why it is the go-to for portable static binaries.

Building for a target other than the machine you are on is **cross-compilation**, covered end to end in its [own article](/cs/software-engineering/compilation/cross-compilation.md).

## The toolchain

### Choosing a compiler

For C/C++ the two mainstream compilers are **GCC** and **Clang/LLVM**. They are largely command-line compatible, so swapping is usually just changing `gcc` to `clang`. Clang tends to give clearer diagnostics and powers many tooling and sanitizer workflows; GCC is the long-standing default on Linux and sometimes produces faster code for a given target. For cross-compilation you use a toolchain built for the target, e.g. `arm-linux-gnueabihf-gcc`.

### Flags and libraries: the pthread example

Many libraries need a flag at **both** compile and link time, and threading is the classic case. POSIX threads are not pulled in automatically:

```bash
gcc program.c -o program -pthread
```

`-pthread` is preferred over linking `-lpthread` by hand because it also sets the preprocessor and codegen options the threading library expects, not just the link step. It is a good reminder that a "library" can affect every stage of the pipeline, not only linking.

## Build provenance (brief)

Sometimes you need to **prove** a binary was built from specific sources, to trust a release, or to audit a supply chain. **Reproducible builds** make this possible: given the same source, toolchain and build environment, the compiler must produce a byte-for-byte identical output, so anyone can rebuild and compare hashes.

Getting there means removing non-determinism: embedded timestamps (`SOURCE_DATE_EPOCH`), absolute build paths, and anything else that varies run to run. Projects like Debian and the Reproducible Builds initiative track this at scale.

## Going backwards: reverse engineering

Everything above runs the pipeline forwards. Reverse engineering runs it backwards, recovering meaning from a finished binary.

* **Disassembly**: turn machine code back into assembly. The inverse of the assembly stage, and almost always possible: the bytes *are* instructions. Tools: `objdump -d`, Ghidra, IDA, radare2.
* **Decompilation**: reconstruct higher-level C-like source from the binary. The inverse of compilation, but only approximate: compilation discards variable names, comments and structure that cannot be recovered, so decompilers produce readable but imperfect code. Tools: Ghidra, IDA's Hex-Rays.

### Obfuscation

Obfuscation deliberately makes that backward path harder, trading some performance for resistance to analysis: control-flow flattening, opaque predicates, string encryption, dead-code insertion, and symbol stripping (`strip`). It slows a reverse engineer down; it never makes analysis impossible, since the CPU must ultimately execute real instructions that can be observed.
