Here is a complete linker script, and it is a shape you will meet in firmware, in kernels, and in anything that has to know where it lives in physical memory:
MEMORY {
rom (rx) : ORIGIN = 0x08000000, LENGTH = 1M
ram (rw) : ORIGIN = 0x20000000, LENGTH = 8M
}
SECTIONS {
.text : { *(.text .text.*) } > rom
.data : { *(.data .data.*) } > ram
.bss : { *(.bss .bss.* ) } > ram
}
Two regions, four attributes between them, and one operator (> rom) that says which region each output section is spending from. It links, and the result is at 0x08000000:
$ ld -T mem.ld -o mem.out hello.o -e helper
$ readelf -SW mem.out | sed -n 's/^ *\[[ 0-9]*\] *\(\.text\|\.data\).*/ \1/p' | head -2
.text
$ readelf -SW mem.out | awk '/ \.text /{print " .text addr = "$4}'
.text addr = 0000000008000000
$ readelf -hW mem.out | awk '/Entry/{print " entry = "$4}'
entry = 8000000
It works, and the reason it works is not that the linker obeyed an address.ORIGIN = 0x08000000 is a number, but nothing in the script asked for that address. The linker was told “there is a region starting here, one megabyte long” and asked to put .text in it. The address is a consequence of the budget, not an instruction.
What a region is, in one sentence, and the four letters in parentheses.
That last line is the one that matters and almost nobody knows it. A MEMORY block on its own constrains nothing. It is a set of ranges; sections only get checked against them if you write > region. A script with a MEMORY block and no operators is a script with a comment at the top.
And the reason regions exist at all is worth stating, because it is not convenience:
That is the real value, and it is easy to miss because on Linux the script “just works” either way. The enforcement is what you are buying, not the placement.
The enforcement, which is the entire point. Same object, same script, one number changed:
$ cat tiny_mem.ld <<'EOF'
MEMORY { rom (rx) : ORIGIN = 0x08000000, LENGTH = 64 }
SECTIONS { .text : { *(.text .text.*) } > rom }
EOF
$ ld -T tiny_mem.ld -o tm.out hello.o -e helper
/usr/bin/x86_64-linux-gnu-ld.bfd: tm.out section `.eh_frame' will not fit in region `rom'
/usr/bin/x86_64-linux-gnu-ld.bfd: region `rom' overflowed by 24 bytes
$ sed 's/LENGTH = 64/LENGTH = 4M/' tiny_mem.ld > big_mem.ld
$ ld -T big_mem.ld -o bm.out hello.o -e helper && echo "linked"
linked
Read the two lines of that error, because each one is doing a different job. The first names the section — .eh_frame, a section you did not mention in your one-line rule and did not know was being placed there. The second names the region and gives a byte count: 24 bytes over.
A 64-byte region cannot hold the ELF headers, .text, and .eh_frame, and the linker says so precisely. This is the only diagnostic in a whole link that is about the script rather than about the symbols, and it is worth noticing what a plain link would have done: placed everything, overlapping nothing, and produced a binary that runs perfectly on this machine and is wrong on a device with 64 bytes of ROM.
Two more behaviours worth measuring, because both are quiet:
$ readelf -SW mem.out | awk '/ \.strtab| \.symtab| \.shstrtab/{print " "$2" addr="$4}'
.strtab addr=0000000000000000
.symtab addr=0000000000000000
.shstrtab addr=0000000000000000
^ sections that are NOT loaded into memory sit at
address 0, and stay there. a region holds sections
that will be MAPPED. debug sections, the symbol
table, the string table -- none of those exist at
run time, so ORIGIN has nothing to say about them.
this is the same fact the default script states by
writing `.debug_info 0 :` -- an address of 0 is a
claim that a section is not in memory.
And the attribute claim, which is a hint and not a setting:
$ cat wrong.ld <<'EOF'
MEMORY { rw (w) : ORIGIN = 0x08000000, LENGTH = 1M }
SECTIONS { .text : { *(.text .text.*) } > rw }
EOF
$ ld -T wrong.ld -o wrong.out hello.o -e helper 2>&1 | head -3
$ echo "exit=$?"
exit=0 <-- no diagnostic at all
$ readelf -lW wrong.out | grep '^ LOAD'
LOAD 0x001000 0x0000000008000000 ... R E 0x1000
^^^ R E, NOT R W E
$ # the same script with (rx) instead:
$ readelf -lW right.out | grep '^ LOAD'
LOAD 0x001000 0x0000000008000000 ... R E 0x1000
^^^ identical
Two results, and the second is the one I did not expect. The first is unsurprising: .text in a region marked (w) produces no diagnostic whatsoever. The second is that changing the attribute from (w) to (rx) changed nothing — both builds produced the same R E segment.
So in GNU ld 2.46 the region attributes are, in this measurement, completely inert: they did not reject the mismatch, and they did not influence the program header flags either. The flags came from the section, not the region. A script that relies on (rx) to keep code out of a writable segment is relying on a field that, here, did nothing at all — and the safe thing is to treat the attributes as documentation for the reader rather than as an input to the linker.
The AT> operator, which solves a problem that appears the moment you have two regions and one of them is not address zero.
MEMORY {
rom (rx) : ORIGIN = 0x08000000, LENGTH = 1M
ram (rw) : ORIGIN = 0x20000000, LENGTH = 8M
}
SECTIONS {
.text : { *(.text .text.*) } > rom
.data : { *(.data .data.*) } > ram AT> rom
}
the two addresses are DIFFERENT and they are both
correct:
VMA where the section is when the CPU runs it.
for .data, 0x20000000 -- because that is where
the RAM is and the CPU can only reach RAM.
LMA where the section's BYTES live in the file.
for .data, 0x08000000+ -- because the bytes
have to be stored in the flash image, and the
flash is what the CPU boots from.
a bootloader copies from the LMA to the VMA at
startup. AT> is how the script tells it where to
copy FROM, and the address it writes into the binary
is the VMA because that is what the running program
needs.
Two addresses for one section is the whole reason AT> exists, and it is a genuinely different idea from anything else in the script language: a section is not a thing, it is a mapping between a file offset and an address, and on a fixed target those are allowed to disagree.
Where you see it in practice, and it is a reliable marker of a real firmware build:
__data_load = LOADADDR(.data);
__data_start = ADDR(.data);
and at startup, in C:
extern uint32_t __data_load, __data_start, __data_end;
uint32_t *src = &__data_load, *dst = &__data_start;
while (dst < &__data_end) *dst++ = *src++;
LOADADDR() and ADDR() are script FUNCTIONS that become
linker-defined SYMBOLS. the script computes two numbers
and the C code reads them. that is the interface, and
it is the same PROVIDE mechanism the
symbol course covered.
And the pattern that catches people: a script with MEMORY and no AT> puts initialised data in RAM, and the RAM contents are whatever was there at reset, not your initialisers. The program starts, runs, and reads zeroes or garbage — and nothing warns you, because from the linker's point of view the layout is perfectly legal. AT> is not an optimisation. It is the difference between a linker script that works and one that produces a device that boots to a random number.
$ cd courses/link/assets/samples $ ./build_samples.sh 2>&2 | sed -n '/F7/,/F8/p' $ python3 crosscheck.py 2>&2 | sed -n '/\\[7\\]/,/\\[8\\]/p' $ python3 linklab.py budget 4M
Then find the boundary, which is more interesting than it sounds:
1. Bisect LENGTH. Start at 1M, halve it, keep
halving until the link fails. What is the exact
size at which this object stops fitting, and how
does that compare to .text + .eh_frame + headers?
(Use readelf -SW on a build that DID fit.)
2. Add a section to your rule that you did not
measure, and watch the error name it. Then
exclude it with EXCLUDE_FILE and watch the error
change name. (The error tells you what it could
not place, which is a free list of what your script
did not account for.)
3. Write a MEMORY block with THREE regions, and
give two of them the same ORIGIN. Does ld object?
If not, what happens when a section is assigned
to each?
4. Set (w) on a region and put .text in it. Does the
linker object? Then change it to (rx) and compare
the program header flags of the two builds. (The
measured answer is on this page: no diagnostic, and
IDENTICAL flags. Find out whether the attributes
do anything at all, or whether the flags came from
the section.)
5. Add AT> to your .data rule and re-read the
binary. What are the VMA and the LMA? Which one
appears in the section header, and where does the
other one appear?
Exercise 2 is the one that pays, and for a reason that is not obvious until you run it. The overflow error is a list of the things your script did not account for, and it is the closest thing a linker has to a warning about incomplete layout. A script that has been linking for months and has never overflowed a region may simply have generous regions; try a tight one and it will tell you, in section names, exactly what it had to place.
Exercise 4 has an answer that corrected a claim this page originally made. The obvious story is that the attributes drive the program header flags — (rx) gives R E, (w) gives R W — and that is certainly how they are documented to be read. Measured, they are inert: .text in a (w) region produced the same R E segment as .text in an (rx) region, and neither produced a diagnostic. The flags came from the section. The correction is above rather than deleted, because “the documentation says X and the measurement says not-X” is exactly the kind of thing a learner needs to see handled rather than hidden.
This is the second placement concept and it is the one that turns placement from a description into a constraint. The Location Counter gave you a variable with a value; this gives it a maximum. Together they are the difference between saying where something goes and saying what happens if it does not fit — and the second is the only one that catches a mistake.
The connection to the ELF course's segment-types concept is the AT> operator, and it is a genuine gap that concept could not fill. p_vaddr and p_paddr are two different fields in a program header, and on every hosted target they are equal, so there is nothing to learn about them from a Linux binary. AT> is the one place where a real difference between them is expressed, and it is expressed in a linker script. Two addresses for one section is a real capability of the format that a course built on Linux examples cannot demonstrate, and a script is the only place it appears.
The strongest connection is forward, and it is about enforcement rather than placement. PHDRS and the -T Surprise is about the other half of what a script controls: not which sections go where, but which segments exist. > region picks a region and, with AT>, a segment. PHDRS names the segments outright. Together they are the two ways a script says “this is how memory is laid out”, and the difference is that regions constrain and PHDRS describes.
And there is a connection back to a course you have already finished that reframes something in it. The Algorithm, Measured established that GNU ld resolves symbols in a demand-driven pass and that a wrong answer produces no diagnostic — a working binary with a wrong number in it. This concept is the same shape of failure and the same shape of defence. An unconstrained layout overlaps silently on a device and produces garbage at run time; a constrained one fails the link and names the section. Neither the resolution pass nor a plain link can tell you your layout is wrong, because as far as the linker is concerned it is not. MEMORY is the only mechanism in the toolchain that turns that class of bug into a message, and it is worth knowing that the safety net exists and is opt-in.
One connection outward, for the hardware framing. The ORIGIN = 0x08000000 in the example is a real convention: on STM32-family parts, flash is mapped at 0x08000000 and SRAM at 0x20000000. Those are not linker script choices; they are what the chip's memory map says. A script that puts .text anywhere else on such a part produces a binary that flashes cleanly and does not boot, and no tool on the machine can tell you — because the correctness of the address is a property of the silicon, and no part of the toolchain has been told what the silicon is. That is the honest limit of what a linker script can be verified against, and it is why this concept's experiments all run on a hosted target and say so.