[lld] [lld][MachO] Key branch-extension thunks on (referent, addend) (PR #191808)

via llvm-commits llvm-commits at lists.llvm.org
Mon Apr 13 05:54:25 PDT 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld

Author: Zhaoxuan Jiang (nocchijiang)

<details>
<summary>Changes</summary>

TextOutputSection::finalize ignored branch relocation addends. Two call sites branching to the same symbol with different addends therefore collapsed onto a single thunk.

Key thunkMap on (isec, value, addend) so two call sites with different addends get independent thunks. The addend is encoded in the thunk's relocs and is zeroed at the call site after the callee is redirected to the thunk. Thunk names carry a `+N` suffix when the addend is non-zero.

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


6 Files Affected:

- (modified) lld/MachO/Arch/ARM64.cpp (+6-4) 
- (modified) lld/MachO/ConcatOutputSection.cpp (+35-8) 
- (modified) lld/MachO/ConcatOutputSection.h (+44-16) 
- (modified) lld/MachO/Target.h (+2-1) 
- (added) lld/test/MachO/arm64-thunk-branch-addend-interposable.s (+62) 
- (added) lld/test/MachO/arm64-thunk-branch-addend.s (+55) 


``````````diff
diff --git a/lld/MachO/Arch/ARM64.cpp b/lld/MachO/Arch/ARM64.cpp
index 04da702b48764..7acf48cbbf123 100644
--- a/lld/MachO/Arch/ARM64.cpp
+++ b/lld/MachO/Arch/ARM64.cpp
@@ -33,7 +33,8 @@ struct ARM64 : ARM64Common {
   void writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
                             uint64_t &stubOffset, uint64_t selrefVA,
                             Symbol *objcMsgSend) const override;
-  void populateThunk(InputSection *thunk, Symbol *funcSym) override;
+  void populateThunk(InputSection *thunk, Symbol *funcSym,
+                     int64_t addend) override;
 
   void initICFSafeThunkBody(InputSection *thunk,
                             Symbol *targetSym) const override;
@@ -160,17 +161,18 @@ static constexpr uint32_t thunkCode[] = {
     0xd61f0200, // 08: br    x16
 };
 
-void ARM64::populateThunk(InputSection *thunk, Symbol *funcSym) {
+void ARM64::populateThunk(InputSection *thunk, Symbol *funcSym,
+                          int64_t addend) {
   thunk->align = 4;
   thunk->data = {reinterpret_cast<const uint8_t *>(thunkCode),
                  sizeof(thunkCode)};
   thunk->relocs.emplace_back(/*type=*/ARM64_RELOC_PAGEOFF12,
                              /*pcrel=*/false, /*length=*/2,
-                             /*offset=*/4, /*addend=*/0,
+                             /*offset=*/4, /*addend=*/addend,
                              /*referent=*/funcSym);
   thunk->relocs.emplace_back(/*type=*/ARM64_RELOC_PAGE21,
                              /*pcrel=*/true, /*length=*/2,
-                             /*offset=*/0, /*addend=*/0,
+                             /*offset=*/0, /*addend=*/addend,
                              /*referent=*/funcSym);
 }
 // Just a single direct branch to the target function.
diff --git a/lld/MachO/ConcatOutputSection.cpp b/lld/MachO/ConcatOutputSection.cpp
index fe67308942e75..34907ae1b62b6 100644
--- a/lld/MachO/ConcatOutputSection.cpp
+++ b/lld/MachO/ConcatOutputSection.cpp
@@ -112,7 +112,7 @@ void ConcatOutputSection::addInput(ConcatInputSection *input) {
 //   the inputs and thunks vectors (both ordered by ascending address), which
 //   is simple and cheap.
 
-DenseMap<Symbol *, ThunkInfo, ThunkMapKeyInfo> lld::macho::thunkMap;
+DenseMap<ThunkKey, ThunkInfo, ThunkMapKeyInfo> lld::macho::thunkMap;
 
 // Determine whether we need thunks, which depends on the target arch -- RISC
 // (i.e., ARM) generally does because it has limited-range branch/call
@@ -155,7 +155,7 @@ bool TextOutputSection::needsThunks() const {
       // Pre-populate the thunkMap and memoize call site counts for every
       // InputSection and ThunkInfo. We do this for the benefit of
       // estimateBranchTargetThresholdVA().
-      ThunkInfo &thunkInfo = thunkMap[sym];
+      ThunkInfo &thunkInfo = thunkMap[ThunkKey{sym, r.addend}];
       // Knowing ThunkInfo call site count will help us know whether or not we
       // might need to create more for this referent at the time we are
       // estimating distance to __stubs in estimateBranchTargetThresholdVA().
@@ -351,9 +351,10 @@ void TextOutputSection::finalize() {
       uint64_t highVA = callVA + forwardBranchRange;
       // Calculate our call referent address
       auto *funcSym = cast<Symbol *>(r.referent);
-      ThunkInfo &thunkInfo = thunkMap[funcSym];
+      ThunkInfo &thunkInfo = thunkMap[ThunkKey{funcSym, r.addend}];
       // The referent is not reachable, so we need to use a thunk ...
-      if ((funcSym->isInStubs() ||
+      if (r.addend == 0 &&
+          (funcSym->isInStubs() ||
            (in.objcStubs && in.objcStubs->isNeeded() &&
             ObjCStubsSection::isObjCStubSymbol(funcSym))) &&
           callVA >= branchTargetThresholdVA) {
@@ -361,9 +362,25 @@ void TextOutputSection::finalize() {
         // ... Oh, wait! We are close enough to the end that branch target
         // sections (__stubs, __objc_stubs) are now within range of a simple
         // forward branch.
+        //
+        // For non-zero addends, the writer's resolveSymbolOffsetVA() resolves
+        // interior branches against the symbol body rather than the stub, so
+        // __stubs reachability says nothing about whether the call can be
+        // emitted directly.
+        // See arm64-thunk-branch-addend-interposable.s test.
         continue;
       }
-      uint64_t funcVA = funcSym->resolveBranchVA();
+      // For non-zero addends, branch directly to the symbol body rather than
+      // through any stub (`resolveBranchVA()` would prefer the stub VA when
+      // present). This mirrors the writer-side rule in `resolveSymbolOffsetVA`:
+      // branching to the interior of a stub trampoline is meaningless, so for
+      // `bl _func+N` we resolve against `_func`'s own body. The two helpers
+      // only diverge when `funcSym` is a Defined that is *also* in stubs (e.g.
+      // an interposable extern), but `finalize()` has to agree with the writer
+      // in that case or reachability analysis drifts.
+      // See arm64-thunk-branch-addend-interposable.s test.
+      uint64_t funcVA = r.addend != 0 ? funcSym->getVA() + r.addend
+                                      : funcSym->resolveBranchVA();
       ++thunkInfo.callSitesUsed;
       if (lowVA <= funcVA && funcVA <= highVA) {
         // The referent is reachable with a simple call instruction.
@@ -376,6 +393,9 @@ void TextOutputSection::finalize() {
         uint64_t thunkVA = thunkInfo.isec->getVA();
         if (lowVA <= thunkVA && thunkVA <= highVA) {
           r.referent = thunkInfo.sym;
+          // The thunk itself bakes in the addend, so the call-site reloc must
+          // branch to the thunk start with no extra offset.
+          r.addend = 0;
           continue;
         }
       }
@@ -394,8 +414,12 @@ void TextOutputSection::finalize() {
       thunkInfo.isec->parent = this;
       assert(thunkInfo.isec->live);
 
-      StringRef thunkName = saver().save(funcSym->getName() + ".thunk." +
-                                         std::to_string(thunkInfo.sequence++));
+      std::string addendSuffix;
+      if (r.addend != 0)
+        addendSuffix = "+" + std::to_string(r.addend);
+      StringRef thunkName =
+          saver().save(funcSym->getName() + addendSuffix + ".thunk." +
+                       std::to_string(thunkInfo.sequence++));
       if (!isa<Defined>(funcSym) || cast<Defined>(funcSym)->isExternal()) {
         r.referent = thunkInfo.sym = symtab->addDefined(
             thunkName, /*file=*/nullptr, thunkInfo.isec, /*value=*/0, thunkSize,
@@ -410,7 +434,10 @@ void TextOutputSection::finalize() {
             /*noDeadStrip=*/false, /*isWeakDefCanBeHidden=*/false);
       }
       thunkInfo.sym->used = true;
-      target->populateThunk(thunkInfo.isec, funcSym);
+      target->populateThunk(thunkInfo.isec, funcSym, r.addend);
+      // The thunk itself bakes in the addend, so the call-site reloc must
+      // branch to the thunk start with no extra offset.
+      r.addend = 0;
       finalizeOne(thunkInfo.isec);
       thunks.push_back(thunkInfo.isec);
       ++thunkCount;
diff --git a/lld/MachO/ConcatOutputSection.h b/lld/MachO/ConcatOutputSection.h
index f09d1cdbaba01..dd27743199649 100644
--- a/lld/MachO/ConcatOutputSection.h
+++ b/lld/MachO/ConcatOutputSection.h
@@ -112,32 +112,60 @@ NamePair maybeRenameSection(NamePair key);
 // of ConcatOutputSection, so must have deterministic iteration order.
 extern llvm::MapVector<NamePair, ConcatOutputSection *> concatOutputSections;
 
-// After ICF, multiple Defined symbols may point to the same (isec, value) yet
-// remain as distinct Symbol pointers.  Using bare Symbol* as thunkMap keys
-// would create redundant thunks for the same target. This DenseMapInfo
+// Branch-extension thunks are keyed by both the target referent and the
+// branch relocation's addend.  Two call sites that branch to the same
+// symbol with different addends (e.g. `bl _func` and `bl _func+8`) target
+// distinct addresses and therefore need distinct thunks.
+//
+// After ICF, multiple Defined symbols may point to the same (isec, value)
+// yet remain as distinct Symbol pointers.  The equality predicate below
 // canonicalizes Defined symbols by (isec, value) so that ICF-folded copies
-// share a single thunkMap entry.
-struct ThunkMapKeyInfo : llvm::DenseMapInfo<Symbol *> {
-  static unsigned getHashValue(const Symbol *sym) {
-    if (const auto *d = dyn_cast<Defined>(sym))
-      return llvm::hash_combine(d->isec(), d->value);
-    return llvm::DenseMapInfo<Symbol *>::getHashValue(sym);
+// still share a single thunkMap entry when their addends match.
+struct ThunkKey {
+  Symbol *sym;
+  int64_t addend;
+
+  static ThunkKey getEmptyKey() {
+    return {llvm::DenseMapInfo<Symbol *>::getEmptyKey(), 0};
+  }
+  static ThunkKey getTombstoneKey() {
+    return {llvm::DenseMapInfo<Symbol *>::getTombstoneKey(), 0};
   }
-  static bool isEqual(const Symbol *lhs, const Symbol *rhs) {
-    if (lhs == rhs)
+  bool isSentinel() const {
+    return sym == llvm::DenseMapInfo<Symbol *>::getEmptyKey() ||
+           sym == llvm::DenseMapInfo<Symbol *>::getTombstoneKey();
+  }
+  bool operator==(const ThunkKey &other) const {
+    if (addend != other.addend)
+      return false;
+    if (sym == other.sym)
       return true;
-    if (lhs == getEmptyKey() || lhs == getTombstoneKey() ||
-        rhs == getEmptyKey() || rhs == getTombstoneKey())
+    if (isSentinel() || other.isSentinel())
       return false;
-    const auto *dl = dyn_cast<Defined>(lhs);
-    const auto *dr = dyn_cast<Defined>(rhs);
+    const auto *dl = dyn_cast<Defined>(sym);
+    const auto *dr = dyn_cast<Defined>(other.sym);
     if (dl && dr)
       return dl->isec() == dr->isec() && dl->value == dr->value;
     return false;
   }
 };
 
-extern llvm::DenseMap<Symbol *, ThunkInfo, ThunkMapKeyInfo> thunkMap;
+struct ThunkMapKeyInfo {
+  static ThunkKey getEmptyKey() { return ThunkKey::getEmptyKey(); }
+  static ThunkKey getTombstoneKey() { return ThunkKey::getTombstoneKey(); }
+  static unsigned getHashValue(const ThunkKey &k) {
+    if (k.isSentinel())
+      return llvm::hash_value(k.sym);
+    if (const auto *d = dyn_cast<Defined>(k.sym))
+      return llvm::hash_combine(d->isec(), d->value, k.addend);
+    return llvm::hash_combine(k.sym, k.addend);
+  }
+  static bool isEqual(const ThunkKey &lhs, const ThunkKey &rhs) {
+    return lhs == rhs;
+  }
+};
+
+extern llvm::DenseMap<ThunkKey, ThunkInfo, ThunkMapKeyInfo> thunkMap;
 
 } // namespace lld::macho
 
diff --git a/lld/MachO/Target.h b/lld/MachO/Target.h
index 145c14037995a..0c7c0c3817c97 100644
--- a/lld/MachO/Target.h
+++ b/lld/MachO/Target.h
@@ -99,7 +99,8 @@ class TargetInfo {
 
   virtual uint64_t getPageSize() const = 0;
 
-  virtual void populateThunk(InputSection *thunk, Symbol *funcSym) {
+  virtual void populateThunk(InputSection *thunk, Symbol *funcSym,
+                             int64_t addend) {
     llvm_unreachable("target does not use thunks");
   }
 
diff --git a/lld/test/MachO/arm64-thunk-branch-addend-interposable.s b/lld/test/MachO/arm64-thunk-branch-addend-interposable.s
new file mode 100644
index 0000000000000..5cee2b7c4c312
--- /dev/null
+++ b/lld/test/MachO/arm64-thunk-branch-addend-interposable.s
@@ -0,0 +1,62 @@
+# REQUIRES: aarch64
+
+# RUN: rm -rf %t; mkdir %t
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %s -o %t/input.o
+# RUN: %lld -arch arm64 -lSystem -interposable -o %t/out %t/input.o
+# RUN: llvm-objdump --no-print-imm-hex -d --no-show-raw-insn %t/out | FileCheck %s
+
+## Regression test for the narrow corner case where the referent of a
+## branch relocation is a Defined symbol that *also* has a __stubs entry
+## (here forced via -interposable, which creates a stub for every extern
+## Defined so callers can be interposed at runtime).
+
+.subsections_via_symbols
+
+.text
+
+## _target will become an interposable extern Defined under `-interposable`: it
+## has a real body *and* a __stubs entry.
+.globl _target
+## Align it to a 4K boundary to make the test a little bit easier to write.
+.p2align 12
+_target:
+  mov w0, #1
+  ret
+  mov w0, #2
+  ret
+
+## Spacer to push _main out of branch range of _target.
+.globl _spacer
+.p2align 2
+_spacer:
+  .space 0x8000000
+  ret
+
+.globl _main
+.p2align 2
+_main:
+  bl _target
+  bl _target+8
+  ret
+
+## Capture _target's full VA.
+# CHECK-LABEL: <_target>:
+# CHECK-NEXT:   [[#%x, TARGET_VA:]]: mov w0, #1
+
+## The addend=0 call takes the __stubs fast-path, so its bl target is an
+## address inside the __stubs section. The addend=8 call must resolve against
+## the local body.
+# CHECK-LABEL: <_main>:
+# CHECK-NEXT:   bl 0x[[#%x, STUB_CALL:]]
+# CHECK-NEXT:   bl 0x{{[0-9a-f]+}} <_target+8.thunk.0>
+# CHECK-NEXT:   ret
+
+## The +8 thunk must load _target+8 into x16: adrp loads _target (which is
+## 4K-aligned), add supplies the +8.
+# CHECK-LABEL: <_target+8.thunk.0>:
+# CHECK-NEXT:   adrp  x16, 0x[[#%x, TARGET_VA]]
+# CHECK-NEXT:   add   x16, x16, #8
+# CHECK-NEXT:   br    x16
+
+# CHECK-LABEL: <__stubs>:
+# CHECK:        [[#%x, STUB_CALL]]: adrp x16,
diff --git a/lld/test/MachO/arm64-thunk-branch-addend.s b/lld/test/MachO/arm64-thunk-branch-addend.s
new file mode 100644
index 0000000000000..114009f88ef32
--- /dev/null
+++ b/lld/test/MachO/arm64-thunk-branch-addend.s
@@ -0,0 +1,55 @@
+# REQUIRES: aarch64
+
+# RUN: rm -rf %t; mkdir %t
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %s -o %t/input.o
+# RUN: %lld -arch arm64 -lSystem -o %t/out %t/input.o
+# RUN: llvm-objdump --no-print-imm-hex -d --no-show-raw-insn %t/out | FileCheck %s
+
+## When two call sites branch to the same target symbol with different addends
+## and each needs a branch-extension thunk, the thunks must be distinct.
+
+.subsections_via_symbols
+
+.text
+
+## _target has an interior entry point at +8. A distant caller branches
+## to both entry points, forcing a branch-extension thunk for each.
+.globl _target
+.p2align 2
+_target:
+  mov w0, #1
+  ret
+  mov w0, #2
+  ret
+
+## Spacer to push _main out of branch range of _target.
+.globl _spacer
+.p2align 2
+_spacer:
+  .space 0x8000000
+  ret
+
+.globl _main
+.p2align 2
+_main:
+  bl _target
+  bl _target+8
+  ret
+
+# CHECK-LABEL: <_main>:
+# CHECK-NEXT:   bl 0x{{[0-9a-f]+}} <_target.thunk.0>
+# CHECK-NEXT:   bl 0x{{[0-9a-f]+}} <_target+8.thunk.0>
+# CHECK-NEXT:   ret
+
+## The zero-addend thunk loads the page offset of _target.
+# CHECK-LABEL: <_target.thunk.0>:
+# CHECK-NEXT:   adrp  x16, 0x[[#%x, PAGE:]]
+# CHECK-NEXT:   add   x16, x16, #[[#%u, OFF0:]]
+# CHECK-NEXT:   br    x16
+
+## The +8 thunk lives on the same page but loads an offset exactly 8
+## bytes higher, so that `br x16` lands on `_target+8`.
+# CHECK-LABEL: <_target+8.thunk.0>:
+# CHECK-NEXT:   adrp  x16, 0x[[#PAGE]]
+# CHECK-NEXT:   add   x16, x16, #[[#OFF0 + 8]]
+# CHECK-NEXT:   br    x16

``````````

</details>


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


More information about the llvm-commits mailing list