The Linker | Internals for Interns 📚 Understanding the Go Compiler (8 of 8) ▼ 1. The Scanner 2. The Parser 3. The Type Checker 4. The Unified IR Format 5. The IR 6. The SSA Phase 7. From SSA to Machine Code 8. The Linker You are here In the previous post , we watched the compiler transform optimized SSA into machine code bytes and package them into object files. Each .o file contains the compiled code for one package—complete with machine instructions, symbol definitions, and relocations marking addresses that need fixing. But your program isn’t just one package. Even a simple “hello world” imports fmt , which imports io , os , reflect , and dozens of other packages. Each package is compiled separately into its own object file. None of these files can run on their own. This is where the linker comes in. The linker’s job is to take all these separate object files and combine them into a single executable that your operating system can run. Let me show you what the linker does and how it does it. What the Linker Does At a high level, the linker performs four main tasks: 1. Symbol Resolution : Your code calls fmt.Println , but that function is defined in a different object file. The linker finds all these cross-file references and connects them. 2. Relocation : Remember those placeholder addresses in the machine code? The linker patches them with actual addresses now that it knows where everything will live in memory. 3. Dead Code Elimination : If you import a package but only use one function, the linker removes all the unused functions. This keeps your binary small. 4. Layout and Executable Generation : The linker decides where in memory each piece of code and data will live, then writes out an executable in the format your OS expects (ELF on Linux, Mach-O on macOS, PE on Windows). Let’s walk through each of these steps, starting with how the linker figures out what symbols exist and where they live. Symbol Resolution Every object file contains symbols —names that identify functions, global variables, and other program elements. Some symbols are defined in a file (the actual code or data lives there), while others are just referenced (the code uses them, but they live somewhere else). Let me show you what I mean: // main.go package main import “fmt” func main () { fmt . Println ( “Hello” ) } When compiled, your main.o contains main.main —that’s your function, complete with machine code. But it also references fmt.Println , and that code isn’t here. It’s just a name pointing somewhere else. Note: In practice, fmt.Println gets inlined by the compiler, so there’s no actual cross-package reference in this case. But the concept holds for functions that don’t get inlined. Over in fmt.o , you’ll find the actual fmt.Println implementation. But that file references io.Writer , os.Stdout , and dozens more symbols from other packages. Each package defines some symbols and references others. The linker needs to match all these references with their definition
Source: Hacker News | Original Link