Multi-file Projects, Linkage & Storage Classes
Master how C programs are split across files, how names cross (or don't cross) file boundaries, and how these mechanisms underpin every Linux kernel driver and subsystem.
Overview
When you write a small C program, everything lives in one file and the compiler sees it all at once. But real software — from a 500-line utility to the Linux kernel with its 30 million lines — is split across hundreds or thousands of source files. Understanding how that works is not a bureaucratic detail; it is the foundational mental model that explains why static means something completely different at file scope versus inside a function, why you get "undefined reference" linker errors, why header guards exist, and how the kernel itself manages its massive symbol namespace.
This module teaches you the entire lifecycle of a multi-file C program: from the moment the compiler opens a .c file (a translation unit), through the object files it emits, to the linker that stitches them together into an executable. You will learn the precise rules that govern which names are visible where (linkage), how long objects live in memory (storage duration), and the One Definition Rule that keeps it all coherent.
These concepts are not abstract. Every device driver, every kernel subsystem, every library uses exactly these mechanisms. When a kernel developer marks a function static, they are deliberately hiding it from other modules. When they write extern, they are reaching across file boundaries. Mastering this is the difference between someone who runs a compiler and someone who understands what a compiler does.
By the end of this module you will be able to decompose a program into well-structured translation units, predict linker behavior from first principles, and read real kernel source with the eye of someone who knows exactly why each storage-class keyword was chosen.
The Linux kernel is a single large C program split across thousands of files. Its internal architecture depends entirely on the linkage and storage-class rules you will learn here. Functions marked static in one driver file are invisible to all other files — this is how the kernel achieves modular encapsulation without a class system. Global variables marked extern in headers tie subsystems together. The __init and __exit macros resolve to section attributes that exploit storage duration. When you read EXPORT_SYMBOL(some_func) in kernel source, you are looking at a mechanism that deliberately punches a symbol through the normal linkage boundary so loadable modules can call it.
Beyond kernel contribution, these rules underpin every non-trivial C project you will ever encounter. Misunderstanding them is the root cause of a huge fraction of beginner and intermediate bugs: multiple-definition link errors from forgetting static, undefined-behavior from accessing a freed stack frame via a stray pointer, subtle ODR violations when the same struct is defined differently in two translation units. Learning this material deeply — not just "header files hold declarations" — is what separates a C programmer from a C systems engineer.
From Source Files to Executable: Translation Units and the Linker
Each .c file is compiled independently into an object file. The compiler sees only its own translation unit; the linker resolves cross-file symbol references. Headers provide shared declarations without allocating storage — the glue that lets the compiler trust names it cannot yet see defined.
The concepts
01Translation Units: The Compiler's Unit of Work
The C standard does not compile files; it compiles translation units (TUs). A translation unit is what you get after the preprocessor has run on a single .c file: all #includes have been textually pasted in, all macros expanded, all conditional compilation resolved. The result is a single, self-contained stream of tokens that the compiler processes in isolation.
This isolation is fundamental. The compiler, when translating foo.c, has no idea that bar.c exists. It cannot see bar.c's variables or functions. All it can do is emit an object file (foo.o) that contains:
- Machine code for every function defined in the TU
- Read/write data for every global variable defined in the TU
- A symbol table listing what names this TU provides (definitions) and what names it needs from elsewhere (undefined references)
The linker (not the compiler) resolves those inter-TU references by matching undefined symbols from one object file to definitions in another.
Analogy: imagine each .c file is a contractor building a part of a house. Each contractor only knows about their own section. A site manager (the linker) looks at everyone's list of 'I need a door here' and 'I have a door to supply' and connects them. If nobody supplies a door you need, the build fails — that is your 'undefined reference' error.
/* See what the preprocessor produces before compilation */
/* command: gcc -E main.c -o main.i (the .i file IS the translation unit) */
/* Inspect what symbols an object file exports/imports: */
/* gcc -c foo.c -o foo.o (compile only, no link) */
/* nm foo.o (list symbols) */
/* 'U' = undefined (needed), 'T' = text/code (provided), 'D' = data (provided) */
02Headers vs Source Files: Declarations vs Definitions
A definition allocates storage or provides the body. A declaration merely announces that something exists (its type and name), without allocating anything.
This distinction is the entire reason headers exist. Because each translation unit is compiled in isolation, a .c file that wants to call a function from another .c file must first declare that function — tell the compiler its name and type — so the compiler can emit a correct call and record an undefined-symbol reference. It does NOT need the function's body (that is the linker's job).
A header file (.h) is nothing magical. It is a text file you #include to paste declarations into multiple translation units. Good header discipline:
- Headers contain declarations (function prototypes,
externvariable declarations, type definitions, macros, inline functions) - Source files contain definitions (function bodies, storage for global variables)
- A header should be self-contained (include whatever it needs) and idempotent (safe to include multiple times)
Violating this rule — putting a definition in a header and including it in two .c files — is the classic 'multiple definition' link error. The linker sees the same symbol defined twice and refuses to proceed.
The One Definition Rule (ODR) is the formal statement: in an entire program, each non-inline function and non-inline variable with external linkage must be defined exactly once. Declarations can be repeated freely.
/* math_utils.h — declarations only */
#ifndef MATH_UTILS_H /* header guard: prevents double-inclusion */
#define MATH_UTILS_H
/* declaration of a function (no body) */
int add(int a, int b);
/* declaration of an external variable (extern = no storage allocated here) */
extern int global_counter;
#endif
/* math_utils.c — definitions */
#include "math_utils.h"
int global_counter = 0; /* definition: storage allocated here */
int add(int a, int b) { /* definition: body provided here */
global_counter++;
return a + b;
}
/* main.c */
#include "math_utils.h" /* pastes in the declarations */
#include <stdio.h>
int main(void) {
printf("%d\n", add(3, 4)); /* compiler trusts the declaration */
printf("calls: %d\n", global_counter);
return 0;
}
03Linkage: Who Can See a Name?
Linkage determines the scope of a name across translation units. C defines three kinds:
External linkage: the name refers to a single, shared object across the entire program. All translation units that declare the same name with external linkage refer to the same thing. This is the default for functions and for variables defined at file scope without static.
Internal linkage: the name is visible only within its translation unit. Other TUs may define their own symbols with the same name; they are completely independent. Achieved with the static keyword at file scope.
No linkage: the name is visible only within its enclosing block. Local variables inside functions have no linkage.
The static keyword at file scope is one of C's most important tools for encapsulation. When you mark a helper function static, you are saying: 'this is an implementation detail; no other translation unit should even know it exists.' This is exactly how the Linux kernel hides driver internals. Consider drivers/block/nvme/core.c: it has dozens of static functions that handle NVMe-specific protocol details that no other driver should call.
A subtle but important point: static inside a function does NOT affect linkage at all — it changes storage duration (discussed next). The same keyword does two different things depending on context. This trips up almost everyone.
/* a.c */
#include <stdio.h>
static int secret = 42; /* internal linkage: only a.c can see this */
static void helper(void) { /* internal linkage: private to a.c */
printf("secret = %d\n", secret);
}
int shared_value = 100; /* external linkage: any TU can access */
void a_public_func(void) { /* external linkage: callable from main.c */
helper(); /* fine: helper is visible within a.c */
}
/* b.c — can also define its own 'secret' without conflict */
static int secret = 99; /* completely different variable from a.c's secret */
/* main.c */
extern void a_public_func(void); /* declare external symbol */
extern int shared_value; /* declare external variable */
/* extern int secret; would link to... which one? Undefined behavior territory. */
int main(void) {
a_public_func();
return 0;
}
04Storage Duration: How Long Does an Object Live?
Storage duration is about when memory is allocated and released for an object. C has four storage durations:
Automatic: the default for local variables. Memory is allocated on entry to the block and released on exit (effectively the stack frame). The variable lives only as long as the enclosing block is executing. This is fast (stack pointer arithmetic) but dangerous if you return a pointer to it — the memory is gone the moment the function returns.
Static: memory allocated at program start and freed at program end. For variables at file scope (with or without static), this is automatic. For local variables explicitly marked static, the variable retains its value between calls. The memory lives in the BSS segment (zero-initialized) or data segment (explicitly initialized).
Thread: each thread gets its own copy (C11 _Thread_local). Not covered in depth here but important for kernel development.
Allocated: from malloc/free. You manage the lifetime manually.
The most conceptually important is static storage duration for local variables (static inside a function). The variable is not on the stack. It sits in the data segment, permanently. Its value persists across calls. This is used in the kernel for things like call counters, lazily-initialized caches, and once-only initialization flags (static int initialized = 0; pattern).
The critical insight: linkage and storage duration are independent axes. A static local variable has static storage duration but no linkage. A file-scope variable without static has external linkage and static storage duration. Don't conflate them.
#include <stdio.h>
void counter(void) {
static int count = 0; /* static storage duration, no linkage */
/* 'count' is initialized ONCE (at program start), lives forever */
/* it is NOT re-initialized each call */
count++;
printf("called %d times\n", count);
}
void risky(void) {
int x = 10; /* automatic storage duration */
/* WARNING: never return &x — x dies when risky() returns */
}
/* global — static storage duration, external linkage by default */
int program_wide = 0;
/* global with static — static storage duration, INTERNAL linkage */
static int module_private = 0;
int main(void) {
counter(); /* prints: called 1 times */
counter(); /* prints: called 2 times */
counter(); /* prints: called 3 times */
return 0;
}
05The extern Keyword: Reaching Across Files
extern has one core meaning: 'I am declaring this name, but the definition (and thus the storage) is elsewhere.'
For variables, this is critical. Without extern, a file-scope variable declaration IS a definition — it allocates storage. With extern, it is a pure declaration — it tells the compiler 'trust me, this variable exists somewhere; emit a reference to it.'
For functions, extern is technically the default (file-scope functions without static have external linkage), so extern void foo(void); and void foo(void); are equivalent declarations. Most style guides omit extern for function prototypes in headers, but include it for variable declarations to make the intent explicit.
A common pattern in professional C code:
config.hdeclaresextern int debug_level;config.cdefinesint debug_level = 0;- Every
.cfile includesconfig.hand can read/writedebug_level
In the Linux kernel you see this constantly. include/linux/kernel.h and similar headers declare hundreds of extern variables and functions that are defined in various .c files across the tree.
A pitfall: writing extern int x = 5; — this is a definition (it has an initializer), and extern is silently ignored by most compilers. The declaration-vs-definition distinction wins over the extern keyword when an initializer is present.
/* shared.h */
#ifndef SHARED_H
#define SHARED_H
/* Declaration: no storage allocated here, exists in shared.c */
extern int debug_level;
/* Function declaration (extern optional but explicit here) */
extern void log_message(const char *msg);
#endif
/* shared.c */
#include "shared.h"
#include <stdio.h>
int debug_level = 1; /* Definition: storage allocated exactly once */
void log_message(const char *msg) {
if (debug_level > 0)
printf("[LOG] %s\n", msg);
}
/* main.c */
#include "shared.h"
int main(void) {
debug_level = 2; /* writes to the single shared instance */
log_message("hello"); /* calls the function defined in shared.c */
return 0;
}
06The One Definition Rule (ODR)
The One Definition Rule is the contract that makes the entire multi-file compilation model work. In simplified form:
- A variable or function with external linkage must be defined exactly once across the entire program. Declarations can appear any number of times.
- A type (struct, union, enum),
typedef, orinlinefunction may be defined in multiple translation units, but all definitions must be identical (same tokens, same meaning).
Violation of rule 1 gives you a linker error (usually 'multiple definition of foo'). Violation of rule 2 is undefined behavior — the compiler and linker are not required to detect it, making it one of the nastiest bugs in C. It can manifest as baffling crashes where a struct in one TU has a different layout than the compiler assumed in another TU.
Rule 2 is why you put type definitions in headers (so all TUs use the same, single source of truth) and why you should never put variable definitions in headers (they get copied into every including TU, violating rule 1).
The classic mistake: int x = 0; in a header. Every .c file that includes the header gets its own copy of the definition. The linker sees multiple definitions and complains. The fix: extern int x; in the header, int x = 0; in exactly one .c file.
Inline functions are the designed exception to rule 1: an inline function defined in a header can appear in multiple TUs. The compiler may or may not actually inline the call; if it doesn't, the linker is allowed to pick any one definition (they're all identical) and discard the rest.
/* BAD: this header will cause a multiple-definition link error */
/* bad_config.h */
int counter = 0; /* DEFINITION in a header — violation of ODR! */
/* When included by main.c AND utils.c, the linker sees: */
/* main.o: defines 'counter' */
/* utils.o: defines 'counter' */
/* linker error: multiple definition of 'counter' */
/* GOOD */
/* good_config.h */
extern int counter; /* pure declaration, no storage */
/* config.c — exactly one definition */
int counter = 0;
/* Inline function in header: ODR exception */
/* every TU gets its own copy, all identical — linker picks one */
static inline int max(int a, int b) {
return a > b ? a : b;
}
07Header Guards and #pragma once
Because #include is textual substitution, including the same header twice in one translation unit pastes its content twice. For most declarations this is harmless, but for type definitions (struct, typedef) it is a hard error — you cannot define the same type twice in one TU.
Header guards use the preprocessor to make a header idempotent:
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* ... header content ... */
#endif
On first inclusion, MY_HEADER_H is not defined, so the body is processed and MY_HEADER_H is defined. On any subsequent inclusion in the same TU, the #ifndef test fails and the body is skipped entirely.
The guard macro name should be unique. The conventional choice is the filename with . replaced by _ and all letters uppercased, optionally with a project prefix: LINUX_LIST_H, MYPROJECT_NET_UTILS_H.
#pragma once is a non-standard but nearly universally supported alternative that tells the compiler to include the file at most once per TU. It is shorter and avoids accidental macro name collisions. The Linux kernel uses traditional guards because it needs to be compilable by a wider range of compilers.
A subtle point: header guards protect against multiple inclusion in the same TU — they do nothing about the ODR problem of definitions in headers included by multiple TUs. These are separate problems with separate solutions.
/* Traditional header guard */
#ifndef VECTOR_H
#define VECTOR_H
typedef struct {
double x, y, z;
} Vec3;
Vec3 vec3_add(Vec3 a, Vec3 b);
double vec3_dot(Vec3 a, Vec3 b);
#endif /* VECTOR_H */
/* Alternative: #pragma once (non-standard but widely supported) */
#pragma once
typedef struct {
double x, y, z;
} Vec3;
Vec3 vec3_add(Vec3 a, Vec3 b);
double vec3_dot(Vec3 a, Vec3 b);
08static at Function Scope vs File Scope: Two Very Different Things
The static keyword is one of C's most overloaded constructs. It is critical to understand that it means something completely different in two contexts:
At file scope (outside any function): static restricts linkage from external to internal. The symbol is invisible outside its translation unit. This is about who can see the name.
Inside a function: static changes storage duration from automatic to static. The variable is allocated once (at program start), retains its value between calls, and is never on the stack. Linkage is unaffected — local variables have no linkage regardless.
Think of it this way:
- File-scope
static: 'keep this name private to this file' - Function-scope
static: 'keep this value alive between calls'
In the Linux kernel, function-scope static is used for patterns like:
static int __init mydriver_init(void) {
static bool first_run = true;
if (first_run) { /* one-time setup */ first_run = false; }
...
}
And file-scope static is used pervasively to enforce module encapsulation:
static irqreturn_t mydriver_irq_handler(int irq, void *dev_id) { ... }
This interrupt handler is a private implementation detail. No other kernel code should call it directly.
#include <stdio.h>
/* FILE SCOPE static: internal linkage — invisible outside this .c file */
static int call_count = 0;
void public_api(void) {
/* FUNCTION SCOPE static: static storage duration — persists between calls */
static int invocation = 0; /* initialized once, lives in data segment */
invocation++; /* retains value between calls */
call_count++; /* accesses file-scope static */
printf("invocation=%d, total_calls=%d\n", invocation, call_count);
}
/* Demonstrate the two meanings clearly: */
static void helper(void) { /* <-- static: internal linkage */
static int n = 0; /* <-- static: static storage duration */
printf("helper called %d times\n", ++n);
}
int main(void) {
public_api(); /* invocation=1, total_calls=1 */
public_api(); /* invocation=2, total_calls=2 */
helper(); /* helper called 1 times */
helper(); /* helper called 2 times */
return 0;
}
09The Build Process: Compile, Link, and the Symbol Table
Understanding linkage abstractly is useful; understanding what actually happens on disk makes it stick. The typical build has two distinct phases:
Phase 1 — Compilation: For each .c file, cc -c foo.c -o foo.o runs the preprocessor, parser, optimizer, and code generator, producing a relocatable object file (foo.o). This is an ELF file (on Linux) containing:
.textsection: machine code.datasection: initialized global/static variables.bsssection: zero-initialized global/static variables (no actual bytes stored; just a size).symtab(symbol table): names and whether they are defined here or needed from elsewhere.rel.text/.rela.text: relocation records saying 'at byte offset X, patch in the address of symbol Y'
Phase 2 — Linking: ld (or cc invoking ld) takes all .o files and resolves symbols. For each 'undefined' symbol in one object file, it searches other object files (and libraries) for a matching definition. When found, it patches the call/load instruction with the actual address. If a symbol is undefined in all inputs, you get 'undefined reference'. If defined in two inputs, 'multiple definition'.
You can inspect this yourself with nm, objdump, and readelf. Running nm foo.o shows each symbol with a type letter: T (defined in .text), D (defined in .data), B (in .bss), U (undefined, needed from elsewhere). This is not theoretical — every kernel developer reads these outputs when debugging link errors.
# Build each translation unit separately
gcc -c math_utils.c -o math_utils.o
gcc -c main.c -o main.o
# Inspect symbols before linking
nm math_utils.o
# Output (example):
# 0000000000000000 T add <- defined in .text
# 0000000000000000 D global_counter <- defined in .data
nm main.o
# Output (example):
# 0000000000000000 T main
# U add <- U = undefined, needs linking
# U global_counter
# U printf
# Link everything together
gcc math_utils.o main.o -o program
# Or equivalently (compiler drives the linker):
gcc math_utils.c main.c -o program
# See all symbols in final executable:
nm program | grep -E '(add|global_counter|main)'
# See sections in an object file:
objdump -h math_utils.o
10Kernel C: How These Rules Play Out in Real Code
The Linux kernel applies these rules at scale and the patterns are worth studying explicitly.
EXPORT_SYMBOL — punching through internal linkage: Kernel modules (.ko files) are loaded at runtime, after the kernel is built. They cannot be linked at build time. The kernel uses EXPORT_SYMBOL(func) to place a function's name and address in a special ELF section (.ksymtab). When a module loads, the kernel module loader resolves references against this table. Without EXPORT_SYMBOL, a kernel function is effectively file-scoped from a module's perspective even if it has external linkage.
static inline in headers: The kernel makes heavy use of static inline functions in headers. These are static (internal linkage per TU, no multiple-definition problem) and inline (hint to compiler to inline the call). Every TU gets its own copy. This is the standard way to put short, performance-critical functions in headers without ODR violations.
__init and __exit — storage class meets linker sections: __init is defined as __attribute__((__section__(".init.text"))). This places the function in a special ELF section. After the kernel boots and calls all __init functions, it frees the entire .init.text section. The function is gone from memory. Same concept as storage duration, but managed at section granularity by the linker script.
Per-file static encapsulation: Browse any driver in drivers/. You will see the pattern: the file_operations struct and all internal helper functions are static. Only the module init/exit functions and the file_operations struct pointer (or platform_driver) are non-static. This is idiomatic, rigorous C encapsulation.
/* Simplified kernel driver pattern — drivers/char/my_driver.c */
#include <linux/module.h>
#include <linux/fs.h>
/* All helpers are static — private to this translation unit */
static int my_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
return 0;
}
/* The ops table is static too — only passed via pointer, never by name */
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
};
/* These two are non-static: the module loader needs to call them */
static int __init my_driver_init(void) /* __init = .init.text section */
{
/* register_chrdev references my_fops by pointer, not by name */
return register_chrdev(0, "my_driver", &my_fops);
}
static void __exit my_driver_exit(void) /* __exit = .exit.text section */
{
unregister_chrdev(0, "my_driver");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
Key insights
- ✦The compiler works on translation units (post-preprocessor), not files. The linker works across translation units. These are different tools solving different problems, and conflating them is the source of most linkage confusion.
- ✦Linkage (who can see a name) and storage duration (how long an object lives) are independent axes. `static` changes both depending on context — at file scope it restricts linkage, inside a function it extends storage duration.
- ✦A header file is just a textual inclusion mechanism. It has no inherent 'declaration-only' magic — you must follow that convention yourself. The entire ODR system rests on the discipline of never putting definitions in headers.
- ✦The One Definition Rule has two faces: the linker enforces the 'exactly one definition' rule for external-linkage names and gives you a hard error; the 'all definitions must be identical' rule for types is not enforced and manifests as silent undefined behavior.
- ✦Every `static` function in the Linux kernel is an act of deliberate encapsulation — the developer is explicitly saying this is a private implementation detail. Reading kernel code with this in mind transforms apparent clutter into clear architectural intent.
- ✦`EXPORT_SYMBOL` is the kernel's mechanism for deliberately breaking the normal linkage boundary so loadable modules can call kernel functions at runtime — a neat inversion of the usual `static`-for-privacy pattern.
- ✦Object files are not just machine code: they carry symbol tables and relocation records. Understanding what `nm` and `objdump` show you turns linker errors from mysterious failures into straightforward symbol-matching problems.
Common pitfalls
- ⚠Putting a variable definition (not just `extern` declaration) in a header, then including that header in multiple `.c` files. The linker sees multiple definitions and fails. Fix: `extern` in the header, one definition in a `.c` file.
- ⚠Conflating `static` at file scope (internal linkage) with `static` inside a function (static storage duration). These are completely different meanings of the same keyword. Forgetting this leads to expecting a local `static` variable to be inaccessible from other files — it already is, for a different reason.
- ⚠Forgetting header guards (or `#pragma once`), then including a header transitively multiple times in one translation unit. Type redefinition errors result. Always guard headers.
- ⚠Returning a pointer to a local (automatic) variable. The variable's storage is reclaimed when the function returns, leaving you with a dangling pointer. Use `static`, heap allocation, or pass a buffer in as a parameter.
- ⚠Writing `extern int x = 5;` expecting it to be a pure declaration. An initializer makes it a definition regardless of `extern`. Most compilers warn but this is a frequent source of confusion.
- ⚠Defining the same struct differently in two translation units (e.g., adding a field in one TU's copy). The linker cannot detect this; the program compiles and links but has undefined behavior — typically manifesting as wrong field offsets and baffling data corruption.
- ⚠Using the same internal-linkage `static` function name in two `.c` files and being surprised they are independent. This is a feature, not a bug, but it can be confusing when debugging with a debugger that shows both symbols.
Practice
- 01easy
Build a two-file calculator. Create `calc.h` (declarations only, with a header guard), `calc.c` (definitions of `add`, `sub`, `mul`, `div_safe`, and a `static` helper `clamp`), and `main.c` (uses them). Compile with `gcc -c calc.c -o calc.o && gcc -c main.c -o main.o && gcc calc.o main.o -o calc`. Run `nm calc.o` and verify that `clamp` does NOT appear as an exported symbol (it should be absent or marked lowercase `t` meaning local static). Verify `add` appears as `T` (exported).
- 02medium
Deliberately trigger and then fix all three classic ODR errors: (1) Put `int counter = 0;` in a header, include it in two `.c` files, observe the linker error, then fix with `extern`. (2) Omit a header guard, include the header twice in one `.c` file via two different intermediate headers, observe the type-redefinition error, then add the guard. (3) Define `void foo(void) { }` in a header included by two `.c` files — observe the multiple-definition error, then fix with `static inline`.
- 03medium
Write a module-style C program that mimics kernel encapsulation: a `sensor.c` that has all functions `static` except `sensor_init()` and `sensor_read()`. Implement a `static int last_value` (static storage duration) that caches the last reading. Write `main.c` that can ONLY call the two public functions — it should not be able to access `last_value` directly. Verify this by trying to add `extern int last_value;` in `main.c` and observing the linker error (undefined reference, since `last_value` has internal linkage).
- 04hard
Explore a real kernel driver. Download the Linux kernel source (or browse it on https://elixir.bootlin.com/linux/latest/source). Navigate to `drivers/input/keyboard/gpio_keys.c`. Count how many functions are `static` vs non-static. Identify which functions are registered as callbacks (passed as function pointers) versus called by name. Read the `module_init`/`module_exit` pattern and explain why those two functions can be `static` even though the module loader needs to call them (hint: look at what `module_init` expands to).
- 05hard
Write a Makefile that compiles a three-file project (`vec3.h`, `vec3.c`, `main.c`) with separate compilation: each `.c` gets its own `gcc -c` rule, then a final link step. Add a rule `inspect` that runs `nm` on each `.o` file and pipes through `grep` to show only the symbols relevant to your project (exclude standard library symbols). Verify that recompiling only `vec3.c` after changing it does not recompile `main.c`.
Resources
The primary source of truth. Clause 6.2.2 defines internal/external/no linkage precisely. Draft N2176 is freely available online.
Readable, free, modern. Chapters on scope and storage duration are clear and accurate.
Chapter 7 on linking is the definitive textbook explanation of everything the linker does, with ELF details.
The classic deep-dive into linker internals, ELF format, symbol tables, and relocation.
Essential context for how linkage concepts manifest in the kernel module system.
Paste C code, see assembly, observe how `static` vs non-static affects the symbol table in the output panel.
The formal spec for the ELF object file format used on Linux — explains sections, symbol tables, and relocations.
Browse kernel source online with cross-referencing. Essential for studying how real drivers use static, extern, and EXPORT_SYMBOL.
LWN articles on kernel internals (e.g., module loading, symbol namespaces introduced in 5.4) show these concepts evolving in practice.
Checkpoints — you can now…
- ✓You can explain the difference between a declaration and a definition, and give one example of each for both variables and functions.
- ✓You can predict whether a link error will be 'undefined reference' or 'multiple definition' given a description of what is in two `.c` files.
- ✓You can use `nm foo.o` and correctly interpret the `T`, `D`, `B`, and `U` symbol type letters.
- ✓You can explain, without looking it up, the two completely different things `static` means depending on whether it appears at file scope or inside a function.
- ✓You can write a correct multi-file C project with a proper header (guard, extern declarations, no definitions) and corresponding source file, and build it with separate compilation steps.
- ✓You can identify, in real Linux kernel source code, which functions have internal linkage and explain why the developer made that choice.
- ✓You can explain why `EXPORT_SYMBOL` is necessary for kernel modules, connecting it to how external linkage and the linker work.