Back to course

A Name Is Not a Symbol

16 minbeginnerModule 1: What a Symbol Is
16 min · Module 1: What a Symbol Is · Beginner

Why This Matters

Here is a program that does not compile, and the error message is the most informative one you will ever read:

$ cat main.c
#include <stdio.h>

int main(void) {
    printf("%d\n", answer);
    return 0;
}
$ clang -o prog main.c
main.c:4:20: error: use of undeclared identifier 'answer'

Now the same program with the definition supplied by a different file:

$ cat answer.c
int answer = 42;
$ clang -c answer.c -o answer.o
$ clang -o prog main.o answer.o
$ ./prog
42

Nothing in main.c changed, and the program went from an error to working. No header was included. No declaration was written. The name answer appeared in main.c with no definition anywhere near it, and the only thing that changed is that some other file on the command line happened to define it.

This is worth pausing on, because it is the whole subject of the course. A name in a program is not a thing. It is a question. And the question — what does this name mean here? — is not answered by the language, by the file, or by the line. It is answered by a process that runs after every file has been compiled, looks at all of them at once, and makes a decision.

A Simple Model

Strip away the file formats for a moment and state the problem as cleanly as it can be stated. The linker is given a bag of declarations and a bag of definitions, and it must answer one question for every declaration:

THE QUESTION for each name N mentioned anywhere in the link: which definition of N is THE definition of N? and then, a second question, which is the one people forget: for each name N that has NO definition: is that an error, or is it allowed?

The second question is not a corner case. An undefined symbol is legal in several quite different circumstances, and telling those circumstances apart is most of what the rest of this course is about. A printf in an object file is an undefined symbol and is perfectly fine — it is a request, and the request is satisfied by libc.so at link time or at load time. A misspelled function name is also an undefined symbol, and it is an error. Same encoding, same table, opposite outcomes.

So the model has two halves, and they are genuinely different halves:

TWO INDEPENDENT QUESTIONS, EASILY CONFLATED Q1 RESOLUTION which definition answers this name? -> needs binding, visibility, link order, archives Q2 PERMISSION is having no answer allowed at all? -> needs weak symbols, COMMON, dynamic linking

Almost every symbol concept in existence answers exactly one of these two questions, and the formats are strikingly bad at keeping them apart. ELF, for instance, answers Q1 with a binding nibble and answers Q2 with a section-index magic value — two completely different mechanisms in two completely different fields of the same 24-byte structure. You will meet that in Visibility Is Not Binding. Hold onto the split; it is the most useful thing in this concept.

Does It Actually Work?

Both questions, measured. First Q1 — the same name, two definitions, one error:

$ cat dup_a.c
int dup(void) { return 1; }
$ cat dup_b.c
int dup(void) { return 2; }
$ clang -c dup_a.c -o dup_a.o
$ clang -c dup_b.c -o dup_b.o
$ clang -o prog dup_a.o dup_b.o dup_m.o
dup_b.c:(.text+0x0): multiple definition of `dup'
dup_a.o:dup_a.c:(.text+0x0): first defined here

Now reverse the two files and read the message again, carefully:

$ clang -o prog dup_b.o dup_a.o dup_m.o
dup_a.c:(.text+0x0): multiple definition of `dup'
dup_b.o:dup_b.c:(.text+0x0): first defined here

The defect did not change. Only the order of two filenames in the sentence did. “first defined here” means first on the command line, not first in importance, not first in some priority order. This trips up almost everyone who reads a duplicate-symbol error as though it were ranking its candidates. It is not ranking anything; it is reporting a set of size two, and it happens to print them in the order it found them.

Now Q2 — an undefined symbol that is allowed, and the same shape that is not:

$ cat weak.c
extern int maybe(void) __attribute__((weak));
int main(void) { return maybe ? maybe() : -1; }   /* legal C */

$ clang -o prog weak.c
undefined reference to `maybe'                <-- ERROR

$ cat weak2.c
extern int maybe(void) __attribute__((weak));
int maybe(void) { return 7; }
$ clang -o prog weak2.c
$ ./prog ; echo $?
7

Both files contain the identical declaration of maybe. In one it is an error and in the other it is fine, and the difference is entirely whether some other file supplied a definition. That is Q2, and it is a genuinely different mechanism from Q1 — a weak symbol changes what the linker is allowed to do, not which definition it picks.

A Real Example

Why the split matters, concretely, in a situation you will meet. Consider a program that optionally uses a library:

/* main.c -- works with or without liboptional */
extern int opt_init(void) __attribute__((weak));

int main(void) {
    if (opt_init) {                 /* the whole feature is this test */
        return opt_init();
    }
    return 0;                       /* the fallback path */
}

Now imagine someone links this without liboptional, and the link succeeds — because the symbol is weak. Nothing crashes. Nothing is printed. The program takes the fallback path and behaves as though the feature were deliberately switched off.

That is the correct behaviour, and it is also the reason weak symbols are dangerous to rely on for anything you need to know about. A feature that is silently absent and a feature that is correctly disabled look identical from outside, and no amount of inspecting the output binary distinguishes them. If your build system needs to report “this was not compiled in”, it cannot get that information through a weak symbol; it has to come from somewhere else entirely.

This is worth stating as a general engineering shape, because it recurs far outside linkers. Any optional component whose provider might be missing has this property. The three general solutions are always the same: make the absence loud, record the decision in the output so it can be inspected later, or make the provider's presence a compile-time fact. Weak symbols give you none of the three, which is exactly why they are appropriate for the cases where a silent default is the intent.

Try It Yourself

Every measurement in this course is reproducible from one script. Run it and read the output — the numbers quoted in these lessons come from it:

$ cd courses/sym/assets/samples
$ ./build_samples.sh
$ python3 crosscheck.py
  ALL 84 CHECKS PASS

For this concept, section [1] of that run is the relevant one. Now do the experiment yourself, in this order, and write down what you see before you read on:

$ cat q1.c
int f(void) { return 1; }
$ cat q2.c
int f(void) { return 2; }
$ clang -c q1.c -o q1.o
$ clang -c q2.c -o q2.o
$ clang -r -o both.o q1.o q2.o
q2.c:(.text+0x0): multiple definition of `f'
q1.o:q1.c:(.text+0x0): first defined here

Note that -r did not help. A partial link, which produces a relocatable object rather than an executable, still enforces one-definition-per-name. That is worth knowing, because “it is only an intermediate, nothing is being decided yet” is a very natural assumption and it is false.

Then answer these three, from what you measured rather than from what you expect:

1. Does the ORDER of q1.o and q2.o change whether the link fails? (It cannot. Does it change the MESSAGE? Look.) 2. Does the order change which f() RUNS, if you make one of them weak? 3. Make q1.c's f() weak. Now does the order matter at all? Why not?

Connect

This concept is the frame for the course, so the connections from it are the connections between the other eleven. Binding, and the Two Tables answers Q1 with the mechanism every format agrees on — and shows that an object file carries two symbol tables because the linker and the loader need different things. Visibility Is Not Binding answers Q1's harder half, and the measured surprise there is that two of the four visibility values are not stored in the visibility field at all.

The other three modules are Q1 and Q2 at increasing distance from the source. The Algorithm, Measured and Order, Archives and the Map File are Q1 executed; Two Definitions and One Tentative is where Q1 and Q2 collide, and the storage class that resolved the collision for thirty years is the subject of that lesson. Two Hash Tables, One Job through Who Owns the Storage are the same two questions asked again at load time by a different program, against a different table, with a different set of rules — and the loader does not have all the information the linker had, which is the source of most of the interesting behaviour in that module.

One connection outside this course, because it is the strongest available. The object files course established that a relocation names a symbol by index and that the index is resolved to an address later. That later resolution is this course, and the symbol table is the only thing that makes it possible — the relocation says which name, and the table says what it means. Relocations is where the question is asked; this course is where it is answered.