A linker script looks like C and is not C. That is the source of nearly every confusion a reader has with one, because C-trained eyes supply rules the language does not have — and the differences are all in the direction of more being true than you expect, not less.
Here is a complete, working script. It is small, it links a real program, and it uses every construct that matters in the default one:
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64", "elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/lib/x86_64-linux-gnu")
SECTIONS
{
. = 0x400000 + SIZEOF_HEADERS;
.text : { *(.text .stub .text.*) }
. = ALIGN(0x1000);
.rodata : { *(.rodata .rodata.*) }
.data : { *(.data .data.*) }
.bss : { *(.bss .bss.*) }
/DISCARD/ : { *(.note.GNU-stack) }
}
Eleven lines, and every one of them is doing something. This concept is the tour.
Two phases, and almost every misunderstanding is a confusion about which phase a construct belongs to.
Phase 1 is why you can put ENTRY before SECTIONS and nothing breaks, and why putting it inside SECTIONS is a syntax error rather than a surprise. The commands are not a sequence of statements to be executed in order; they are a set of settings, plus one block that is walked in order.
Now the four things you can do inside SECTIONS, which is the entire inner language.
There is no if, no loop, and no variable other than the dot. That is the entire control-flow surface. A linker script is a straight line with a wildcard matcher, and if you are looking for the C in it, this is the list of things that is not there.
Every construct above, exercised. Start from the eleven-line script and check each one actually did what it looks like it did.
$ cat > probe.ld <<'EOF'
OUTPUT_FORMAT("elf64-x86-64")
ENTRY(_start)
SECTIONS
{
. = SEGMENT_START("text-segment", 0xDEADBEEF) + SIZEOF_HEADERS;
.text : { *(.text .stub .text.*) }
}
EOF
$ clang -O1 -fno-pie -no-pie hello.o -o probe.out -T probe.ld \
-Wl,-u,SEGMENT_START 2>/dev/null || true
That is not the right way to read a value out. The previous concept established the technique — PROVIDE plus -u — and it is worth doing properly for the two functions that most people cannot predict, because both have names that promise more than they deliver.
$ python3 - <<'PY'
s = open('default.ld').read()
s = s.replace('SECTIONS\n{', 'SECTIONS\n{\n'
' PROVIDE(__p_text = SEGMENT_START("text-segment", 0xDEADBEEF));\n'
' PROVIDE(__p_max = CONSTANT(MAXPAGESIZE));\n'
' PROVIDE(__p_hdr = SIZEOF_HEADERS);', 1)
open('probe.ld','w').write(s)
PY
$ clang -O1 -fno-pie -no-pie hello.o -o probe.out -T probe.ld \
-Wl,-u,__p_text -Wl,-u,__p_max -Wl,-u,__p_hdr
$ readelf -sW probe.out | grep __p_
7: 00000000deadbeef 0 OBJECT LOCAL DEFAULT 1 __p_text
8: 0000000000001000 0 OBJECT LOCAL DEFAULT 2 __p_max
9: 0000000000000350 0 OBJECT LOCAL DEFAULT 3 __p_hdr
Three results, and two of them correct the obvious reading of the name.
SEGMENT_START("text-segment", 0xDEADBEEF)
returned 0xdeadbeef -- the SECOND argument.
you would bet the first is a query to the target
emulation and the second is a fallback. on
x86-64 Linux the emulation has no opinion about
"text-segment", so the fallback IS the answer.
the name is a hook for targets that really do
have named segments. the location counter
concept is entirely about this function.
CONSTANT(MAXPAGESIZE) returned 0x1000. CORRECT,
and the control is the point: this one DID ask
the emulation, and got a real answer. so the
probe mechanism works. the two functions differ.
SIZEOF_HEADERS returned 0x350 -- the ELF
header, the program headers, and the section
headers. this is the gap between "where the image
starts" and "where your first byte of code is".
That gap is the thing to take away. The address you write is where the image goes, not where your code goes, and the difference is 0x350 bytes on this build. The . = on line 2 of the eleven-line script is not decoration; it is the whole reason the first byte of .text is not at 0x400000.
Now the operators, all from the real default script, each with what it is for:
*(.text .stub .text.* .gnu.linkonce.t.*)
the workhorse. a WILDCARD over input section
names, matched against every section of every
object. .text.* is a suffix match. the order of
the names inside the parens is the order they are
laid out -- that is the next concept.
KEEP (*(SORT_NONE(.init)))
KEEP makes the section a ROOT for --gc-sections.
SORT_NONE means "leave the order alone". the
.init rule needs it because the init code must
come first.
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) ...))
a SORT operator. priority numbers live in the
input section NAME (.init_array.65535), and this
rule makes constructors run in priority order.
SORT_NONE, SORT, SORT_BY_NAME, SORT_BY_INIT_PRIORITY
and SORT_WITH_SIZE all exist.
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o ...)))
EXCLUDE_FILE subtracts. the .ctors section of
crtbegin.o must come FIRST and crtend.o's must
come LAST, so both are excluded from the ordinary
rule and placed by dedicated KEEP rules above it.
*crtbegin.o(.ctors)
a FILE PATTERN rather than a section wildcard.
"the .ctors section of crtbegin.o, whichever
directory it is in."
*(.eh_frame) ONLY_IF_RO { ... }
the ONE conditional in the language. the
emulation substitutes ONLY_IF_RO or ONLY_IF_RW
when it reads the script, which is why .eh_frame
appears twice in the default script: once for a
read-only segment, once for a read-write one,
and exactly one of the two survives.
That last one is the neatest thing in the file and almost nobody knows it is there. The script does not contain an if about whether a section is read-only. It contains the same section twice, guarded by a placeholder the target fills in. The orphan concept returns to what happens to the rules that match nothing.
The construct that separates a working script from a working embedded script: giving a section an explicit address.
$ python3 - <<'PY'
s = open('default.ld').read()
s = s.replace(' .text :\n', ' .text 0x900000 :\n', 1)
open('abs.ld','w').write(s)
PY
$ clang -O1 hello.o -o place_abs -T abs.ld
$ readelf -SW place_abs | awk '/ \.text /{print " .text addr="$4}'
.text addr=0000000000900000
$ readelf -sW place_abs | awk '/ answer$/{print " answer =" $2}'
answer =00000000009000f0
$ ./place_abs; echo " ran, exit=$?"
ran, exit=0
One number changed, the section moved, and the program still worked. Note that answer is at 0x9000f0 and not 0x900000: the input section for answer is not the first thing in .text, because _start and the C runtime's code come first. Setting the address of an output section sets the address of its first byte, not of your function.
That is worth a second measurement, because it is the single most common surprise when writing a real script: which input section ends up first is decided by the rules, and the rules are matched in order.
.text : { *(.text.cold) *(.text) *(.text.*) }
vs
.text : { *(.text) *(.text.*) *(.text.cold) }
the first puts .text.cold at offset 0. the second
puts .text at offset 0. same total size, different
addresses for every function, and the program runs
identically either way -- which is exactly why the
mistake is silent.
And one more thing the language does that C-trained eyes get wrong, which is that a script is not required to name every section. Delete the .rodata rule from the eleven-line script and the link still succeeds, because the linker places what the script did not mention somewhere sensible of its own choosing. It will not tell you. That behaviour has a name and a flag, and it is the whole of the orphan concept — and the measurement there finds five sections in a trivial hello-world that even the 276-line default script does not name, four of them from the C runtime.
$ cd courses/link/assets/samples $ ./build_samples.sh 2>&1 | sed -n '/F1/,/F3/p' $ python3 crosscheck.py 2>&1 | sed -n '/\\[1\\]/,/\\[3\\]/p' $ python3 linklab.py base 0x3000000
Then write the eleven-line script yourself, from an empty file, in this order:
1. Start with NOTHING. Link with -T empty.ld and
read the error. (It will complain about PHDRs
before it complains about your sections. Note the
order of the complaints -- that is the phase
order from the model.)
2. Add OUTPUT_FORMAT and ENTRY. Link again. What is
the next complaint?
3. Add a SECTIONS block with .text only. Link. Then
run it. Then add .rodata, .data, .bss one at a
time, running after each. Which one, if any, is
REQUIRED for the program to run?
4. Replace the literal 0x400000 with
SEGMENT_START("text-segment", 0x400000). Does the
binary change? (It should be identical, because on
this target the function returns its second
argument -- F2. Verify with cmp, not with size.)
5. Now add ONLY_IF_RO to your .eh_frame rule and
link. Does it work? What did the emulation
substitute? (Use ld -v if you want to see it.)
Step 1 is the one that teaches the most, and it is the exercise I would put first in any linker course. Starting from a genuinely empty script and letting the linker tell you what it needs is how you learn what is required versus what is conventional. The answer, when you get there, is that almost nothing in the eleven-line script is required — ENTRY has a default, OUTPUT_FORMAT has a default, and sections you do not name get placed anyway. What the script is for is being a deliberate statement of layout rather than an accident of defaults, and knowing which parts are load-bearing is what tells you which parts you may safely omit.
Step 4 is the trap this course has already hit twice. cmp, not stat. The round trip and the four-cell ordering experiment both produced a right-sized but wrong file at some point during writing, and both were caught only by comparing bytes.
This concept supplies the grammar, and its only job is to make the next one possible. The Program That Placed Your Binary gave you the file; this one gives you the eight constructs it is written in. Those three concepts together are a complete reading ability — enough for any linker script in the wild, which is the practical test.
Forward within the module, the grammar immediately raises the question the previous concept deferred. The .text rule is *(.text .stub .text.* .gnu.linkonce.t.*) and the .init_array rule is KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) ...)), and both contain several patterns. Which input section does a section matching two of them get? Which Rule Wins answers that with a four-cell experiment, and the answer is that the order inside the parentheses is itself a decision with consequences — it is how the hot/cold partition works.
Two connections backwards, both about a distinction the grammar forces you to notice. First: the dot is a variable and SIZEOF_HEADERS is a function, which is a different kind of thing from an OBJECT in the symbol table. The Object Files course called the zero displacement in an object file “the hole” and showed the linker filling it. This concept is where the hole is measured: the counter has a concrete value, 0x350 bytes past the image base, and the distance between “where the image is” and “where your code is” is a real, queryable number rather than a gap in understanding.
Second: PROVIDE is a script construct, and the linker-defined symbols concept treated those symbols as a linker feature without asking where they were made. They are made here, by a line of script, and the difference between PROVIDE and a bare assignment is the difference between a symbol that exists in your binary and one that does not. If you have ever wondered why __start_SEC is missing from a particular binary, that is the answer, and it is one word in a grammar rather than a linker mystery.
And the connection into the placement module is the sharpest one in the course. . = SEGMENT_START(...) + SIZEOF_HEADERS on line 2 of the default script is a statement about the relationship between the image base and the first byte of code, and the two summands are not interchangeable. The Location Counter is entirely about that statement: what the dot holds, when it moves, and why the address you typed and the address you got are 0x350 bytes apart. The grammar says the dot exists; that concept says what it is for.