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.