[lld] [LLD] Allow linker scripts to override non-SHF_ALLOC->VMA=0 rule (resolve #202266) (PR #203788)

via llvm-commits llvm-commits at lists.llvm.org
Sun Jun 14 13:31:10 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld-elf

Author: Sam Edwards (CFSworks)

<details>
<summary>Changes</summary>

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.

---
Full diff: https://github.com/llvm/llvm-project/pull/203788.diff


4 Files Affected:

- (modified) lld/ELF/LinkerScript.cpp (+24-9) 
- (modified) lld/test/ELF/linkerscript/compress-sections.s (+2-4) 
- (modified) lld/test/ELF/linkerscript/memory-nonalloc-no-warn.test (+3-3) 
- (modified) lld/test/ELF/linkerscript/symbols-non-alloc.test (+11-3) 


``````````diff
diff --git a/lld/ELF/LinkerScript.cpp b/lld/ELF/LinkerScript.cpp
index 0c1652b7e3a45..1ac01f3946ad6 100644
--- a/lld/ELF/LinkerScript.cpp
+++ b/lld/ELF/LinkerScript.cpp
@@ -1177,15 +1177,12 @@ 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;
 
-  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
@@ -1217,7 +1233,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()
@@ -1227,12 +1242,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;
   }
@@ -1315,7 +1330,7 @@ bool LinkerScript::assignOffsets(OutputSection *sec) {
     state->tbssAddr = dot;
     dot = savedDot;
   }
-  return addressChanged;
+  return sec->addr != oldAddress;
 }
 
 static bool isDiscardable(const OutputSection &sec) {
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) }
 }
 
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 = .;
 }

``````````

</details>


https://github.com/llvm/llvm-project/pull/203788


More information about the llvm-commits mailing list