thoroughly
back to writing
2026 · 08 · 09 · 3 min read · compilers

What the Compiler Leaves Blank: How a Function Call Finds Its Way

A call instruction the compiler can't finish: why linking has to be a separate step, and what actually happens to the zeroed-out bytes it leaves behind.

When main calls sum, and sum lives in a different file, how does the call know where to go?

The CPU doesn’t look up names. It jumps to a number. So somewhere between your source code and the running program, the name sum has to become an address, and the compiler that produced the call never saw the file sum lives in.

I don’t write C. But C is where you can see it.

cat > sum.c <<'EOF'
int sum(int a, int b) { return a + b; }
EOF

cat > main.c <<'EOF'
int sum(int a, int b);
int main() { return sum(1, 2); }
EOF

Compiling in isolation

First, compile main.c on its own and dump it.

$ gcc -c main.c
$ objdump -dr main.o
main.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   be 02 00 00 00          mov    $0x2,%esi
   9:   bf 01 00 00 00          mov    $0x1,%edi
   e:   e8 00 00 00 00          call   13 <main+0x13>
                        f: R_X86_64_PLT32       sum-0x4
  13:   5d                      pop    %rbp
  14:   c3                      ret

Look at the call instruction. e8 means call, and the four bytes after it hold the address. They’re zeros.

The compiler had to emit all five bytes, because leaving a gap would shift everything after it. But it had no number to write, since it only reads one file at a time. So it wrote zeros and, one line below, a note: byte f of this file needs patching, look up sum.

After linking

Now link it and dump again.

$ gcc -o prog main.o sum.c
$ objdump -dr prog | grep -A8 '<main>:'
0000000000401106 <main>:
  401106:       55                      push   %rbp
  401107:       48 89 e5                mov    %rsp,%rbp
  40110a:       be 02 00 00 00          mov    $0x2,%esi
  40110f:       bf 01 00 00 00          mov    $0x1,%edi
  401114:       e8 02 00 00 00          call   40111b <sum>
  401119:       5d                      pop    %rbp
  40111a:       c3                      ret

Three things changed. main now sits at 0x401106, because the linker picked a spot for it. The zeros became 02, so the hole is filled. And the note is gone, because it was used up. Everything else is byte for byte identical.

The 2 is a distance, not an address. The call sits at 0x401114 and is 5 bytes long, so the next instruction is at 0x401119. sum is at 0x40111b, and the difference is 2, which is exactly what the linker wrote. The CPU adds it to where it’s about to be and lands on sum.

Notice that sum starts at 0x40111b, immediately after main ends at 0x40111a. The linker stacked them, which is how it knew where sum was in the first place.

Why the split has to happen

Four of the five instructions came out identical because there was nothing in them to resolve. Pushing a register, moving the number 1 into a register, returning. The fifth needed to know where sum sits in memory, and there was no way for the compiler to know. main.c doesn’t contain sum, and the compiler only reads one file at a time.

That’s why linking is a separate step. Something has to see all the files at once, decide where everything goes, and fill in what the compiler left blank.

Every language has to do this somewhere. C does it before the program exists, which is why you can watch it happen, and why nothing can change afterward.