[lld] [LLD] Allow linker scripts to override non-SHF_ALLOC->VMA=0 rule (resolve #202266) (PR #203788)
Sam Edwards via llvm-commits
llvm-commits at lists.llvm.org
Sun Jun 14 13:30:14 PDT 2026
https://github.com/CFSworks created https://github.com/llvm/llvm-project/pull/203788
As discussed in #202266, there are linker scripts that use non-SHF_ALLOC sections -- generally (ab)using `(OVERLAY)` -- to perform pointer arithmetic to plan the runtime memory layout via marker symbols. In those cases, forcibly putting the section at address zero causes miscalculations.
I can also imagine some hypothetical user might want to use `(OVERLAY)` to create overlays that should exist informationally in the file (for extraction by a later tool, or use at runtime by a self-modifying program inspecting the ELF) without the overlays being covered by PT_LOAD as an `OVERLAY { ... }` declaration would do. Those users would probably want the overlay to have a section VMA but not a LMA.
Therefore, this PR adds two exceptions to the non-SHF_ALLOC->VMA=0 rule:
- A section with an explicit start address is always at that explicit start address; the zero-address rule does not take precedence over the linker script.
- Perhaps more controversially, a non-SHF_ALLOC section that contains some symbol assignments (real symbols, not `. += ...;` expressions) is interpreted as evidence of the "planning memory layout" kind of nonalloc, and therefore intent that the section have a "real" address.
As @smithp35 pointed out, `(OVERLAY)` is kind of an obscure/obsolete way of doing overlays. And especially if #203524 is merged, an acceptable resolution might very well be "tell affected users to use `OVERLAY { ... }`" instead of merging this PR. However, the change is minor, so I'm sending it anyway. :)
The first two patches are small/nonfunctional cleanups I did while hacking on this, included as a courtesy. The third patch corrects a test that inadvertently would trigger this new behavior. The fourth patch is the actual implementation.
>From 842b49d224b4c5e535955ed2b9251f6cb959ec51 Mon Sep 17 00:00:00 2001
From: Sam Edwards <CFSworks at gmail.com>
Date: Sun, 14 Jun 2026 10:02:05 -0700
Subject: [PATCH 1/4] [LLD][ELF] Simplify assignOffsets() change tracking (NFC)
This function returns `true` iff it changed the current section's
address. Rather than track this change "by hand" with a bool, simply
note the old address at function entry and compare upon return.
---
lld/ELF/LinkerScript.cpp | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/lld/ELF/LinkerScript.cpp b/lld/ELF/LinkerScript.cpp
index 0c1652b7e3a45..171c86a2ffd97 100644
--- a/lld/ELF/LinkerScript.cpp
+++ b/lld/ELF/LinkerScript.cpp
@@ -1177,8 +1177,8 @@ bool LinkerScript::assignOffsets(OutputSection *sec) {
const bool isTbss = (sec->flags & SHF_TLS) && sec->type == SHT_NOBITS;
const bool sameMemRegion = state->memRegion == sec->memRegion;
const bool prevLMARegionIsDefault = state->lmaRegion == nullptr;
+ const uint64_t oldAddress = sec->addr;
const uint64_t savedDot = dot;
- bool addressChanged = false;
state->memRegion = sec->memRegion;
state->lmaRegion = sec->lmaRegion;
@@ -1217,7 +1217,6 @@ bool LinkerScript::assignOffsets(OutputSection *sec) {
dot = alignToPowerOf2(dot, sec->addralign);
expandMemoryRegions(dot - pos);
}
- addressChanged = sec->addr != dot;
sec->addr = dot;
// state->lmaOffset is LMA minus VMA. If LMA is explicitly specified via AT()
@@ -1315,7 +1314,7 @@ bool LinkerScript::assignOffsets(OutputSection *sec) {
state->tbssAddr = dot;
dot = savedDot;
}
- return addressChanged;
+ return sec->addr != oldAddress;
}
static bool isDiscardable(const OutputSection &sec) {
>From 10e75784c7ff776e4df3dfacc9fa845909512f0c Mon Sep 17 00:00:00 2001
From: Sam Edwards <CFSworks at gmail.com>
Date: Sun, 14 Jun 2026 10:02:40 -0700
Subject: [PATCH 2/4] [LLD][ELF] Simplify lmaOffset computation (NFC)
As the source says, "state->lmaOffset is LMA minus VMA." The VMA happens
to be equal to `dot` at the time, but the authoritative value for the
section's VMA is `sec->addr`; therefore, compute the offset from that.
---
lld/ELF/LinkerScript.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lld/ELF/LinkerScript.cpp b/lld/ELF/LinkerScript.cpp
index 171c86a2ffd97..065e0a85c868b 100644
--- a/lld/ELF/LinkerScript.cpp
+++ b/lld/ELF/LinkerScript.cpp
@@ -1226,12 +1226,12 @@ bool LinkerScript::assignOffsets(OutputSection *sec) {
// heuristics described in
// https://sourceware.org/binutils/docs/ld/Output-Section-LMA.html
if (sec->lmaExpr) {
- state->lmaOffset = sec->lmaExpr().getValue() - dot;
+ state->lmaOffset = sec->lmaExpr().getValue() - sec->addr;
} else if (MemoryRegion *mr = sec->lmaRegion) {
uint64_t lmaStart = alignToPowerOf2(mr->curPos, sec->addralign);
if (mr->curPos < lmaStart)
expandMemoryRegion(mr, lmaStart - mr->curPos, sec->name);
- state->lmaOffset = lmaStart - dot;
+ state->lmaOffset = lmaStart - sec->addr;
} else if (!sameMemRegion || !prevLMARegionIsDefault) {
state->lmaOffset = 0;
}
>From e4a6e6a440cc190283c95c16cf4600b7816f0ae1 Mon Sep 17 00:00:00 2001
From: Sam Edwards <CFSworks at gmail.com>
Date: Sun, 14 Jun 2026 10:03:24 -0700
Subject: [PATCH 3/4] [LLD][ELF][test] In --compress-sections test, get size
directly
The next commit is about to change the memory semantics for
non-SHF_ALLOC sections, so that they have "real" addresses if the linker
script puts any symbol assignments within them. This test (incidentally)
uses symbol assignments and depends on the section being placed at
memory address zero. However, since the test is only concerned with the
ultimate size of the nonalloc section, just use SIZEOF(...) instead to
avoid triggering the new addressing behavior.
The check that the section has a zero address has been left intact, as
this is intended behavior.
---
lld/test/ELF/linkerscript/compress-sections.s | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/lld/test/ELF/linkerscript/compress-sections.s b/lld/test/ELF/linkerscript/compress-sections.s
index 5131fa7542243..d44825956c1f3 100644
--- a/lld/test/ELF/linkerscript/compress-sections.s
+++ b/lld/test/ELF/linkerscript/compress-sections.s
@@ -9,8 +9,7 @@
# CHECK: nonalloc PROGBITS 0000000000000000 [[#%x,]] [[#%x,]] 00 C 0 0 1
# CHECK-NEXT: str PROGBITS 0000000000000000 [[#%x,]] [[#%x,]] 01 MSC 0 0 1
-# CHECK: 0000000000000000 0 NOTYPE GLOBAL DEFAULT [[#]] (nonalloc) nonalloc_start
-# CHECK: 0000000000000063 0 NOTYPE GLOBAL DEFAULT [[#]] (nonalloc) nonalloc_end
+# CHECK: 0000000000000063 0 NOTYPE GLOBAL DEFAULT ABS nonalloc_size
# CHECK: String dump of section 'str':
# CHECK-NEXT: [ 0] AAA
# CHECK-NEXT: [ 4] {{a+}}
@@ -47,12 +46,11 @@ SECTIONS {
b = c+1;
a = b+1;
nonalloc : {
- nonalloc_start = .;
## In general, using data commands is error-prone. This case is correct, though.
*(nonalloc*) QUAD(SIZEOF(.text))
. += a;
- nonalloc_end = .;
}
+ nonalloc_size = SIZEOF(nonalloc);
str : { *(str) }
}
>From 878d86eb7da2c0e175485cccbd5fcbe6643ec63d Mon Sep 17 00:00:00 2001
From: Sam Edwards <CFSworks at gmail.com>
Date: Sun, 14 Jun 2026 10:03:51 -0700
Subject: [PATCH 4/4] [LLD][ELF] Allow scripts to give non-SHF_ALLOC sections
nonzero VMA
Non-SHF_ALLOC sections are not part of the memory image, so they
generally should have VMA zeroed. However, some linker scripts (such as
those used by Das U-Boot) use (OVERLAY) to perform pointer arithmetic
and define some marker symbols to plan runtime memory layout; forcing
those sections to live at address zero breaks the computation.
This commit adds two exceptions to the non-SHF_ALLOC-means-zero rule:
1) Explicit address expressions are still respected.
2) The presence of symbol assignments (not to `.`) is considered user
intent for the section to have an address.
The location counter (`.`) remains unaffected by the presence of a
preceding non-SHF_ALLOC output section.
Also give the regression test `symbols-non-alloc.test` symbols to
trigger case 2 (while still checking that `.` is unaffected), and update
`memory-nonalloc-no-warn.test` to expect that the empty sections have
the chosen explicit start address (per case 1).
Closes https://github.com/llvm/llvm-project/issues/202266
---
lld/ELF/LinkerScript.cpp | 24 +++++++++++++++----
.../linkerscript/memory-nonalloc-no-warn.test | 6 ++---
.../ELF/linkerscript/symbols-non-alloc.test | 14 ++++++++---
3 files changed, 34 insertions(+), 10 deletions(-)
diff --git a/lld/ELF/LinkerScript.cpp b/lld/ELF/LinkerScript.cpp
index 065e0a85c868b..1ac01f3946ad6 100644
--- a/lld/ELF/LinkerScript.cpp
+++ b/lld/ELF/LinkerScript.cpp
@@ -1182,10 +1182,7 @@ bool LinkerScript::assignOffsets(OutputSection *sec) {
state->memRegion = sec->memRegion;
state->lmaRegion = sec->lmaRegion;
- if (!(sec->flags & SHF_ALLOC)) {
- // Non-SHF_ALLOC sections have zero addresses.
- dot = 0;
- } else if (isTbss) {
+ if (isTbss) {
// Allow consecutive SHF_TLS SHT_NOBITS output sections. The address range
// starts from the end address of the previous tbss section.
if (state->tbssAddr == 0)
@@ -1209,6 +1206,25 @@ bool LinkerScript::assignOffsets(OutputSection *sec) {
sec->name);
}
+ if (!(sec->flags & SHF_ALLOC)) {
+ // Non-SHF_ALLOC sections usually have zero addresses, with two exceptions:
+ // 1) Explicit address expressions are still respected.
+ // 2) The presence of symbol assignments (not to `.`) is considered user
+ // intent for the section to have an address.
+ bool hasAssign = false;
+ for (SectionCommand *cmd : sec->commands) {
+ if (auto *assign = dyn_cast<SymbolAssignment>(cmd)) {
+ if (assign->name != ".") {
+ hasAssign = true;
+ break;
+ }
+ }
+ }
+
+ if (!(hasSectionsCommand && sec->addrExpr) && !hasAssign)
+ dot = 0;
+ }
+
state->outSec = sec;
if (!(sec->addrExpr && hasSectionsCommand)) {
// ALIGN is respected. sec->alignment is the max of ALIGN and the maximum of
diff --git a/lld/test/ELF/linkerscript/memory-nonalloc-no-warn.test b/lld/test/ELF/linkerscript/memory-nonalloc-no-warn.test
index eabdf75fcf935..81a2f8085c17f 100644
--- a/lld/test/ELF/linkerscript/memory-nonalloc-no-warn.test
+++ b/lld/test/ELF/linkerscript/memory-nonalloc-no-warn.test
@@ -21,9 +21,9 @@
# CHECK-NEXT: [ 0] NULL 0000000000000000 000000 000000 00 0 0 0
# CHECK-NEXT: [ 1] .nonalloc PROGBITS 0000000000000000 001064 001000 00 W 0 0 1
# CHECK-NEXT: [ 2] .dat PROGBITS 0000000000000000 002064 000004 00 W 0 0 1
-# CHECK-NEXT: [ 3] .intvec0_out PROGBITS 0000000000000000 002068 000000 00 W 0 0 1
-# CHECK-NEXT: [ 4] .intvec1_out PROGBITS 0000000000000000 002068 000000 00 W 0 0 1
-# CHECK-NEXT: [ 5] .intvec2_out PROGBITS 0000000000000000 002068 000000 00 W 0 0 1
+# CHECK-NEXT: [ 3] .intvec0_out PROGBITS 00000000803fe000 002068 000000 00 W 0 0 1
+# CHECK-NEXT: [ 4] .intvec1_out PROGBITS 00000000803fe020 002068 000000 00 W 0 0 1
+# CHECK-NEXT: [ 5] .intvec2_out PROGBITS 00000000803fe040 002068 000000 00 W 0 0 1
# CHECK-NEXT: [ 6] .intvec3_out PROGBITS 00000000803fe060 001060 000004 00 AX 0 0 1
# CHECK-NEXT: [ 7] .text PROGBITS 00000000803fe064 001064 000000 00 AX 0 0 4
# CHECK-NEXT: [ 8] .comment PROGBITS 0000000000000000 {{.*}} {{.*}} 01 MS 0 0 1
diff --git a/lld/test/ELF/linkerscript/symbols-non-alloc.test b/lld/test/ELF/linkerscript/symbols-non-alloc.test
index ca47b2bfbcac6..b52bc1ec4e6be 100644
--- a/lld/test/ELF/linkerscript/symbols-non-alloc.test
+++ b/lld/test/ELF/linkerscript/symbols-non-alloc.test
@@ -1,6 +1,8 @@
# REQUIRES: x86
## The address of a symbol assignment after a non-SHF_ALLOC section equals the
-## end address of the last SHF_ALLOC section.
+## end address of the last SHF_ALLOC section. However, script-defined symbols
+## appearing within the non-SHF_ALLOC section still behave as if the section
+## were SHF_ALLOC.
# RUN: echo '.section .nonalloc,""; .quad 0' \
# RUN: | llvm-mc -filetype=obj -triple=x86_64-unknown-linux - -o %t
@@ -9,14 +11,20 @@
# CHECK: Sections:
# CHECK: .text 00000000 0000000000000120
-# CHECK: .nonalloc 00000008 0000000000000000
+# CHECK: .nonalloc 00000008 0000000000000120
# CHECK: SYMBOL TABLE:
+# CHECK: 0000000000000120 g .nonalloc 0000000000000000 start_nonalloc
+# CHECK: 0000000000000128 g .nonalloc 0000000000000000 end_nonalloc
# CHECK: 0000000000000120 g .nonalloc 0000000000000000 Sym
SECTIONS {
. = SIZEOF_HEADERS;
.text : { *(.text) }
- .nonalloc : { *(.nonalloc) }
+ .nonalloc : {
+ start_nonalloc = .;
+ *(.nonalloc);
+ end_nonalloc = .;
+ }
Sym = .;
}
More information about the llvm-commits
mailing list