[lld] [lld][MachO] Don't reject TLV relocations against dynamic-lookup symbols (PR #221364)
Peter Rong via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 10 21:22:08 PDT 2026
https://github.com/DataCorrupted updated https://github.com/llvm/llvm-project/pull/221364
>From 3707c64af7894db636fcd2d2e80ad34bfab44e8a Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Fri, 4 Sep 2026 16:04:28 -0700
Subject: [PATCH 1/4] [lld][MachO] Don't reject TLV relocations against
dynamic-lookup symbols
Reading an `extern _Thread_local` variable whose definition lives in
another image fails to link under `-undefined dynamic_lookup`. This
long-standing bug will be exposed more often by C++20's `constinit`, which
lets the compiler elide the wrapper function a cross-TU `thread_local`
access normally routes through: without the wrapper, the referencing
object emits a direct TLV relocation instead of an ordinary call.
```
$ echo 'extern _Thread_local int tlsVal; int read_tls(void) { return tlsVal; }' > /tmp/a.c
$ clang -target arm64-apple-macos11 -fuse-ld=lld -dynamiclib -nostdlib \
-Wl,-undefined,dynamic_lookup /tmp/a.c -o /tmp/a.dylib
ld64.lld: error: /tmp/a-<hash>.o:(symbol read_tls+0x8): TLVP_LOAD_PAGE21
relocation requires that symbol _tlsVal be thread-local
ld64.lld: error: /tmp/a-<hash>.o:(symbol read_tls+0xc): TLVP_LOAD_PAGEOFF12
relocation requires that symbol _tlsVal be thread-local
```
`validateSymbolRelocation` requires that a relocation carrying the TLV
attribute point at a symbol that `isTlv()`, which is reasonable.
However, a dynamic-lookup symbol fails the check because it has no defining dylib, and Mach-O
has no field for thread-locality on an undefined symbol reference, so there is no way to know.
`addDynamicLookup` creates the symbol with `isTlv = false` because that is the only value available.
Therefore, we relax the check by skipping it if the symbol came from dynamic lookup.
---
lld/MachO/Relocations.cpp | 7 ++++-
lld/test/MachO/tlv-dynamic-lookup.s | 45 +++++++++++++++++++++++++++++
2 files changed, 51 insertions(+), 1 deletion(-)
create mode 100644 lld/test/MachO/tlv-dynamic-lookup.s
diff --git a/lld/MachO/Relocations.cpp b/lld/MachO/Relocations.cpp
index 0945e7a96dee2..319443b631a66 100644
--- a/lld/MachO/Relocations.cpp
+++ b/lld/MachO/Relocations.cpp
@@ -68,7 +68,12 @@ bool macho::validateSymbolRelocation(const Symbol *sym,
.str();
};
- if (relocAttrs.hasAttr(RelocAttrBits::TLV) != sym->isTlv())
+ // A dynamic-lookup symbol's thread-locality is unknown at link time but
+ // resolved by dyld at load time; rejecting it would refuse a valid link.
+ const auto *dysym = dyn_cast<DylibSymbol>(sym);
+ bool tlvKindIsKnown = !(dysym && dysym->isDynamicLookup());
+
+ if (tlvKindIsKnown && relocAttrs.hasAttr(RelocAttrBits::TLV) != sym->isTlv())
error(message(Twine("requires that symbol ") + sym->getName() + " " +
(sym->isTlv() ? "not " : "") + "be thread-local"));
diff --git a/lld/test/MachO/tlv-dynamic-lookup.s b/lld/test/MachO/tlv-dynamic-lookup.s
new file mode 100644
index 0000000000000..5b1cb5706ebfe
--- /dev/null
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -0,0 +1,45 @@
+# REQUIRES: aarch64
+# RUN: rm -rf %t; split-file %s %t
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/consumer.s -o %t/consumer.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/notlv.s -o %t/notlv.o
+
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/dylookup.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/dylookup.dylib | FileCheck %s
+
+# RUN: %lld -arch arm64 -dylib -U _tlv -o %t/u.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/u.dylib | FileCheck %s
+
+# RUN: %lld -arch arm64 -dylib -flat_namespace -undefined suppress -o %t/flat.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/flat.dylib | FileCheck %s
+
+# CHECK: __thread_ptrs
+# CHECK-LABEL: Bind table:
+# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## The import gets the flat-lookup ordinal.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -fixup_chains \
+# RUN: -o %t/chained.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --chained-fixups %t/chained.dylib | FileCheck %s --check-prefix=CHAINED
+# CHAINED: lib_ordinal = -2 (flat-namespace)
+
+## A dylib that does define the symbol still tells us its thread-locality, so
+## the mismatch check keeps working there.
+# RUN: %lld -arch arm64 -dylib -install_name @rpath/libnotlv.dylib -o %t/libnotlv.dylib %t/notlv.o
+# RUN: not %lld -arch arm64 -dylib -o /dev/null %t/consumer.o %t/libnotlv.dylib 2>&1 | \
+# RUN: FileCheck %s --check-prefix=ERR
+# ERR: error: {{.*}}TLVP_LOAD_PAGE21 relocation requires that symbol _tlv be thread-local
+
+#--- consumer.s
+.globl _readTlv
+.p2align 2
+_readTlv:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+
+#--- notlv.s
+.globl _tlv
+.data
+_tlv:
+ .quad 0
>From 88ff7755921281471fada24ef0b245c14df63c21 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Thu, 10 Sep 2026 13:07:31 -0700
Subject: [PATCH 2/4] 2nd try
---
lld/MachO/InputSection.cpp | 9 +-
lld/MachO/MapFile.cpp | 2 +-
lld/MachO/Symbols.cpp | 10 +-
lld/MachO/Symbols.h | 13 +-
lld/MachO/SyntheticSections.cpp | 7 +-
lld/MachO/SyntheticSections.h | 18 +-
lld/test/MachO/tlv-dynamic-lookup-x86.s | 51 +++++
lld/test/MachO/tlv-dynamic-lookup.s | 241 ++++++++++++++++++++++--
8 files changed, 324 insertions(+), 27 deletions(-)
create mode 100644 lld/test/MachO/tlv-dynamic-lookup-x86.s
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index d977830161a8e..d2841a0f73c39 100644
--- a/lld/MachO/InputSection.cpp
+++ b/lld/MachO/InputSection.cpp
@@ -248,8 +248,13 @@ void ConcatInputSection::writeTo(uint8_t *buf) {
}
referentVA = minuendVA - fromSym->getVA();
} else if (auto *referentSym = r.referent.dyn_cast<Symbol *>()) {
- if (target->hasAttr(r.type, RelocAttrBits::LOAD) &&
- !referentSym->isInGot())
+ // Relaxing a load means "there is no slot; compute the address
+ // directly". Ask about the section this relocation actually uses:
+ // __thread_ptrs for TLV relocations, __got for everything else.
+ bool hasSlot = target->hasAttr(r.type, RelocAttrBits::TLV)
+ ? referentSym->isInTlvPointers()
+ : referentSym->isInGot();
+ if (target->hasAttr(r.type, RelocAttrBits::LOAD) && !hasSlot)
target->relaxGotLoad(loc, r.type);
// For dtrace symbols, do not handle them as normal undefined symbols
if (referentSym->getName().starts_with("___dtrace_")) {
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 29ebcdcf9a832..4d5e543c24c8d 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -149,7 +149,7 @@ static void printNonLazyPointerSection(raw_fd_ostream &os,
// associations.
for (const Symbol *sym : osec->getEntries())
os << format("0x%08llX\t0x%08zX\t[ 0] non-lazy-pointer-to-local: %s\n",
- osec->addr + sym->gotIndex * target->wordSize,
+ osec->addr + osec->getIndex(*sym) * target->wordSize,
target->wordSize, sym->getName().str().data());
}
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index 27419caf9de1e..b622dbcb1901a 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -50,7 +50,15 @@ uint64_t Symbol::getLazyPtrVA() const {
return in.lazyPointers->getVA(stubsIndex);
}
uint64_t Symbol::getGotVA() const { return in.got->getVA(gotIndex); }
-uint64_t Symbol::getTlvVA() const { return in.tlvPointers->getVA(gotIndex); }
+bool Symbol::isInTlvPointers() const {
+ return in.tlvPointers->getIndex(*this) != UINT32_MAX;
+}
+uint64_t Symbol::getTlvVA() const {
+ return in.tlvPointers->getVA(in.tlvPointers->getIndex(*this));
+}
+uint64_t Symbol::resolveTlvVA() const {
+ return isInTlvPointers() ? getTlvVA() : getVA();
+}
Defined::Defined(StringRef name, InputFile *file, InputSection *isec,
uint64_t value, uint64_t size, bool isWeakDef, bool isExternal,
diff --git a/lld/MachO/Symbols.h b/lld/MachO/Symbols.h
index 9fc7d6b079058..6c89e7a77266a 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,9 +65,14 @@ class Symbol {
virtual bool isTlv() const { return false; }
- // Whether this symbol is in the GOT or TLVPointer sections.
+ // Whether this symbol has a __got slot.
bool isInGot() const { return gotIndex != UINT32_MAX; }
+ // Whether this symbol has a __thread_ptrs slot. Unlike the GOT index this
+ // is kept in a side table owned by TlvPointerSection: thread-local imports
+ // are rare, and Symbol is instantiated in the millions.
+ bool isInTlvPointers() const;
+
// Whether this symbol is in the StubsSection.
bool isInStubs() const { return stubsIndex != UINT32_MAX; }
@@ -80,11 +85,9 @@ class Symbol {
return isInStubs() ? getStubVA() : getVA();
}
uint64_t resolveGotVA() const { return isInGot() ? getGotVA() : getVA(); }
- uint64_t resolveTlvVA() const { return isInGot() ? getTlvVA() : getVA(); }
+ uint64_t resolveTlvVA() const;
- // The index of this symbol in the GOT or the TLVPointer section, depending
- // on whether it is a thread-local. A given symbol cannot be referenced by
- // both these sections at once.
+ // The index of this symbol in the GOT.
uint32_t gotIndex = UINT32_MAX;
uint32_t lazyBindOffset = UINT32_MAX;
uint32_t stubsHelperIndex = UINT32_MAX;
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index c4277d54edd38..8503e0ed6a452 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -338,10 +338,11 @@ void macho::addNonLazyBindingEntries(const Symbol *sym,
void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
if (entries.insert(sym)) {
- assert(!sym->isInGot());
- sym->gotIndex = entries.size() - 1;
+ assert(getIndex(*sym) == UINT32_MAX);
+ uint32_t index = entries.size() - 1;
+ setIndex(*sym, index);
- addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize);
+ addNonLazyBindingEntries(sym, isec, index * target->wordSize);
}
}
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 9acbd73d277e8..5c9006d3104b6 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -120,9 +120,13 @@ class NonLazyPointerSectionBase : public SyntheticSection {
}
void writeTo(uint8_t *buf) const override;
void addEntry(Symbol *sym);
- uint64_t getVA(uint32_t gotIndex) const {
- return addr + gotIndex * target->wordSize;
+ uint64_t getVA(uint32_t index) const {
+ return addr + index * target->wordSize;
}
+ // This symbol's slot index within this section, or UINT32_MAX. __got and
+ // __thread_ptrs track indices separately so a symbol may appear in both.
+ virtual uint32_t getIndex(const Symbol &sym) const = 0;
+ virtual void setIndex(Symbol &sym, uint32_t index) = 0;
private:
llvm::SetVector<const Symbol *> entries;
@@ -131,11 +135,21 @@ class NonLazyPointerSectionBase : public SyntheticSection {
class GotSection final : public NonLazyPointerSectionBase {
public:
GotSection();
+ uint32_t getIndex(const Symbol &sym) const override { return sym.gotIndex; }
+ void setIndex(Symbol &sym, uint32_t index) override { sym.gotIndex = index; }
};
class TlvPointerSection final : public NonLazyPointerSectionBase {
public:
TlvPointerSection();
+ uint32_t getIndex(const Symbol &sym) const override {
+ auto it = indices.find(&sym);
+ return it == indices.end() ? UINT32_MAX : it->second;
+ }
+ void setIndex(Symbol &sym, uint32_t index) override { indices[&sym] = index; }
+
+private:
+ llvm::DenseMap<const Symbol *, uint32_t> indices;
};
struct Location {
diff --git a/lld/test/MachO/tlv-dynamic-lookup-x86.s b/lld/test/MachO/tlv-dynamic-lookup-x86.s
new file mode 100644
index 0000000000000..80ef492218373
--- /dev/null
+++ b/lld/test/MachO/tlv-dynamic-lookup-x86.s
@@ -0,0 +1,51 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/movq.s -o %t/movq.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/leaq.s -o %t/leaq.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/notlv.s -o %t/notlv.o
+
+# RUN: %lld -dylib -undefined dynamic_lookup -o %t/movq.dylib %t/movq.o
+# RUN: llvm-objdump --macho --section-headers --bind -d --no-show-raw-insn %t/movq.dylib | \
+# RUN: FileCheck %s --check-prefix=MOVQ
+# MOVQ-LABEL: _f:
+# MOVQ-NEXT: movq
+# MOVQ: __thread_ptrs
+# MOVQ-LABEL: Bind table:
+# MOVQ: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## FIXME: leaq computes the address of the __thread_ptrs slot, but the ABI
+## requires the descriptor the slot holds, so this is wrong code. It is accepted
+## because X86_64::relaxGotLoad's MOVQ check only runs on the relax path, and a
+## slot exists here. Pre-existing and not specific to dynamic lookup: the same
+## leaq against a dylib that really exports _tlv as thread-local miscompiles
+## identically on stock lld. Pinned as-is so the separate fix has a baseline.
+# RUN: %lld -dylib -undefined dynamic_lookup -o %t/leaq.dylib %t/leaq.o
+# RUN: llvm-objdump --macho -d --no-show-raw-insn %t/leaq.dylib | \
+# RUN: FileCheck %s --check-prefix=LEAQ
+# LEAQ-LABEL: _f:
+# LEAQ-NEXT: leaq
+
+# RUN: %lld -dylib -install_name @rpath/libnotlv.dylib -o %t/libnotlv.dylib %t/notlv.o
+# RUN: not %lld -dylib -o /dev/null %t/movq.o %t/libnotlv.dylib 2>&1 | \
+# RUN: FileCheck %s --check-prefix=DEFINED
+# DEFINED: error: {{.*}}TLV relocation requires that symbol _tlv be thread-local
+
+#--- movq.s
+.globl _f
+_f:
+ movq _tlv at TLVP(%rip), %rax
+ retq
+.subsections_via_symbols
+
+#--- leaq.s
+.globl _f
+_f:
+ leaq _tlv at TLVP(%rip), %rax
+ retq
+.subsections_via_symbols
+
+#--- notlv.s
+.globl _tlv
+.data
+_tlv:
+ .quad 0
diff --git a/lld/test/MachO/tlv-dynamic-lookup.s b/lld/test/MachO/tlv-dynamic-lookup.s
index 5b1cb5706ebfe..96fb12e1541d1 100644
--- a/lld/test/MachO/tlv-dynamic-lookup.s
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -1,35 +1,149 @@
# REQUIRES: aarch64
# RUN: rm -rf %t; split-file %s %t
-
# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/consumer.s -o %t/consumer.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/twotlv.s -o %t/twotlv.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/mixed.s -o %t/mixed.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/branch.s -o %t/branch.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/cfi.s -o %t/cfi.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/binder.s -o %t/binder.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/initoff.s -o %t/initoff.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/ep.s -o %t/ep.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/unsmix.s -o %t/unsmix.o
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/deftlv.s -o %t/deftlv.o
# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/notlv.s -o %t/notlv.o
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/dylookup.dylib %t/consumer.o
# RUN: llvm-objdump --macho --section-headers --bind %t/dylookup.dylib | FileCheck %s
+# CHECK: __thread_ptrs
+# CHECK-LABEL: Bind table:
+# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-# RUN: %lld -arch arm64 -dylib -U _tlv -o %t/u.dylib %t/consumer.o
-# RUN: llvm-objdump --macho --section-headers --bind %t/u.dylib | FileCheck %s
+## -U is per-symbol: _tlv is exempt, _other is left undefined.
+# RUN: not %lld -arch arm64 -dylib -U _tlv -o /dev/null %t/twotlv.o 2>&1 | \
+# RUN: FileCheck %s --check-prefix=USCOPE
+# USCOPE-NOT: _tlv
+# USCOPE: error: undefined symbol: _other
+# USCOPE-NOT: _tlv
-# RUN: %lld -arch arm64 -dylib -flat_namespace -undefined suppress -o %t/flat.dylib %t/consumer.o
-# RUN: llvm-objdump --macho --section-headers --bind %t/flat.dylib | FileCheck %s
+# RUN: %lld -arch arm64 -dylib -U _tlv -U _other -o %t/u.dylib %t/twotlv.o
+# RUN: llvm-objdump --macho --bind %t/u.dylib | FileCheck %s --check-prefix=UBOTH
+# UBOTH-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UBOTH-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _other
-# CHECK: __thread_ptrs
-# CHECK-LABEL: Bind table:
-# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# RUN: %no-fatal-warnings-lld -arch arm64 -dylib -flat_namespace -undefined suppress \
+# RUN: -o %t/flat.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --bind %t/flat.dylib | FileCheck %s --check-prefix=FLAT
+# FLAT: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## The import gets the flat-lookup ordinal.
+## Chained fixups must produce a __thread_ptrs slot, not just a flat import --
+## a GOT-only binary yields a byte-identical import table.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -fixup_chains \
# RUN: -o %t/chained.dylib %t/consumer.o
-# RUN: llvm-objdump --macho --chained-fixups %t/chained.dylib | FileCheck %s --check-prefix=CHAINED
-# CHAINED: lib_ordinal = -2 (flat-namespace)
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/chained.dylib | \
+# RUN: FileCheck %s --check-prefix=CHAINED
+# CHAINED: __thread_ptrs
+# CHAINED-NOT: __got
+# CHAINED: lib_ordinal = -2 (flat-namespace)
+# CHAINED: _tlv
-## A dylib that does define the symbol still tells us its thread-locality, so
-## the mismatch check keeps working there.
# RUN: %lld -arch arm64 -dylib -install_name @rpath/libnotlv.dylib -o %t/libnotlv.dylib %t/notlv.o
+# RUN: %lld -arch arm64 -dylib -install_name @rpath/libtlv.dylib -undefined dynamic_lookup \
+# RUN: -o %t/libtlv.dylib %t/deftlv.o
+
# RUN: not %lld -arch arm64 -dylib -o /dev/null %t/consumer.o %t/libnotlv.dylib 2>&1 | \
# RUN: FileCheck %s --check-prefix=ERR
# ERR: error: {{.*}}TLVP_LOAD_PAGE21 relocation requires that symbol _tlv be thread-local
+# RUN: %lld -arch arm64 -dylib -o %t/good.dylib %t/consumer.o %t/libtlv.dylib
+# RUN: llvm-objdump --macho --bind %t/good.dylib | FileCheck %s --check-prefix=GOOD
+# GOOD: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libtlv _tlv
+
+## A dynamic-lookup symbol has no thread-locality to publish, so it must not be
+## re-exported -- a downstream link would read the missing flag as a definite
+## "not thread-local". _readTlv is the positive control: the trie is not simply
+## empty for every input.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -exported_symbol _tlv \
+# RUN: -exported_symbol _readTlv -o %t/reexport.dylib %t/consumer.o
+# RUN: llvm-objdump --macho --exports-trie %t/reexport.dylib | \
+# RUN: FileCheck %s --check-prefix=REEXPORT
+# REEXPORT: _readTlv
+# REEXPORT-NOT: _tlv{{$}}
+
+## The remaining cases each reach NonLazyPointerSectionBase::addEntry by a route
+## that does not pass through prepareSymbolRelocation. None may abort the
+## linker. Where lld synthesizes the competing entry itself the user has no way
+## to act on a diagnostic, so these must link.
+
+## StubsSection::addEntry -> in.got->addEntry under chained fixups, which is the
+## default at iOS 16 / macOS 13.
+## Chained fixups records binds in LC_DYLD_CHAINED_FIXUPS, so --bind is empty
+## here; the stub's __got slot and the TLV's __thread_ptrs slot must coexist.
+# RUN: %no-arg-lld -arch arm64 -platform_version ios 16.0 16.0 -dylib \
+# RUN: -undefined dynamic_lookup -o %t/branch-chained.dylib %t/branch.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/branch-chained.dylib | \
+# RUN: FileCheck %s --check-prefix=BRANCHC
+# BRANCHC-DAG: __got
+# BRANCHC-DAG: __thread_ptrs
+# BRANCHC: lib_ordinal = -2 (flat-namespace)
+
+## Same source without chained fixups must not reach a different verdict.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -no_fixup_chains \
+# RUN: -o %t/branch-lazy.dylib %t/branch.o
+# RUN: llvm-objdump --macho --bind %t/branch-lazy.dylib | FileCheck %s --check-prefix=BRANCH
+# BRANCH: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## UnwindInfoSection::prepare, after the relocation scan.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/cfi.dylib %t/cfi.o
+# RUN: llvm-objdump --macho --bind %t/cfi.dylib | FileCheck %s --check-prefix=CFI
+# CFI: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _pers
+
+## StubHelperSection::setUp, which runs after Writer::run's errorCount() bail
+## and so can never be protected by an error() emitted earlier.
+# RUN: %no-lsystem-lld -arch arm64 -dylib -undefined dynamic_lookup -no_fixup_chains \
+# RUN: -o %t/binder.dylib %t/binder.o
+# RUN: llvm-objdump --macho --bind %t/binder.dylib | FileCheck %s --check-prefix=BINDER
+# BINDER: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace dyld_stub_binder
+
+## InitOffsetsSection::setUp.
+# RUN: %no-arg-lld -arch arm64 -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem -dylib -undefined dynamic_lookup \
+# RUN: -init_offsets -o %t/initoff.dylib %t/initoff.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/initoff.dylib | \
+# RUN: FileCheck %s --check-prefix=INITOFF
+# INITOFF-DAG: __got
+# INITOFF-DAG: __thread_ptrs
+# INITOFF: lib_ordinal = -2 (flat-namespace)
+# INITOFF: _ctor
+
+## The entry-point stub is synthesized before the relocation scan; the only
+## reference in the source is the TLV one, so there is nothing for a user to fix.
+# RUN: %no-arg-lld -arch arm64 -platform_version macos 13.0 13.0 \
+# RUN: -syslibroot %S/Inputs/MacOSX.sdk -lSystem -e _ep -undefined dynamic_lookup \
+# RUN: -o %t/ep.out %t/ep.o
+# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/ep.out | \
+# RUN: FileCheck %s --check-prefix=EP
+# EP-DAG: __got
+# EP-DAG: __thread_ptrs
+# EP: lib_ordinal = -2 (flat-namespace)
+# EP: _ep
+
+## JUDGMENT CALL. A data pointer and a TLV reference to one dynamic-lookup
+## symbol cannot both describe whatever dyld finds, but neither can lld tell
+## which is wrong, and the UNSIGNED path allocates no slot for a conflict check
+## to inspect. Accepting both binds matches how every other unverifiable
+## dynamic-lookup mismatch is already treated.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/unsmix.dylib %t/unsmix.o
+# RUN: llvm-objdump --macho --bind %t/unsmix.dylib | FileCheck %s --check-prefix=UNS
+# UNS-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UNS-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
+## JUDGMENT CALL. Same question for a GOT reference, which does allocate a slot.
+## Treated the same way for consistency with UNS and BRANCH above.
+# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/mixed.dylib %t/mixed.o
+# RUN: llvm-objdump --macho --bind %t/mixed.dylib | FileCheck %s --check-prefix=MIXED
+# MIXED-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# MIXED-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+
#--- consumer.s
.globl _readTlv
.p2align 2
@@ -37,6 +151,107 @@ _readTlv:
adrp x8, _tlv at TLVPPAGE
ldr x8, [x8, _tlv at TLVPPAGEOFF]
ret
+.subsections_via_symbols
+
+#--- twotlv.s
+.globl _two
+.p2align 2
+_two:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ adrp x9, _other at TLVPPAGE
+ ldr x9, [x9, _other at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- mixed.s
+.globl _viaTlv, _viaGot
+.p2align 2
+_viaTlv:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+_viaGot:
+ adrp x8, _tlv at GOTPAGE
+ ldr x8, [x8, _tlv at GOTPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- branch.s
+.globl _g
+.p2align 2
+_g:
+ bl _tlv
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- cfi.s
+.globl _f
+.p2align 2
+_f:
+ .cfi_startproc
+ .cfi_personality 155, _pers
+ adrp x8, _pers at TLVPPAGE
+ ldr x8, [x8, _pers at TLVPPAGEOFF]
+ ret
+ .cfi_endproc
+.subsections_via_symbols
+
+#--- binder.s
+.globl _f
+.p2align 2
+_f:
+ adrp x8, dyld_stub_binder at TLVPPAGE
+ ldr x8, [x8, dyld_stub_binder at TLVPPAGEOFF]
+ bl _lazy
+ ret
+.subsections_via_symbols
+
+#--- initoff.s
+.globl _f
+.p2align 2
+_f:
+ adrp x8, _ctor at TLVPPAGE
+ ldr x8, [x8, _ctor at TLVPPAGEOFF]
+ ret
+.section __DATA,__mod_init_func,mod_init_funcs
+.quad _ctor
+.subsections_via_symbols
+
+#--- ep.s
+.globl _main
+.p2align 2
+_main:
+ adrp x8, _ep at TLVPPAGE
+ ldr x8, [x8, _ep at TLVPPAGEOFF]
+ ret
+.subsections_via_symbols
+
+#--- unsmix.s
+.globl _a
+.p2align 2
+_a:
+ adrp x8, _tlv at TLVPPAGE
+ ldr x8, [x8, _tlv at TLVPPAGEOFF]
+ ret
+.data
+.globl _p
+_p:
+ .quad _tlv
+.subsections_via_symbols
+
+#--- deftlv.s
+.globl _tlv
+.section __DATA,__thread_data,thread_local_regular
+_tlv$tlv$init:
+ .quad 0
+.section __DATA,__thread_vars,thread_local_variables
+_tlv:
+ .quad __tlv_bootstrap
+ .quad 0
+ .quad _tlv$tlv$init
#--- notlv.s
.globl _tlv
>From 35d2dbb23fece43bf1962f6d16df2dd765ad8431 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Thu, 10 Sep 2026 13:50:36 -0700
Subject: [PATCH 3/4] [lld][MachO] Give a TLV symbol one non-lazy pointer slot,
as ld64 does
A thread-local symbol's __thread_ptrs slot and its __got slot hold the
very same value -- the address of its TLV descriptor -- so a symbol
referenced both ways needs only one of them, and either reference kind
can read it. Route both through a single slot:
- a known thread-local gets its slot in __thread_ptrs,
- anything else, including a dynamic-lookup symbol whose
thread-locality is unknowable at link time, gets one in __got.
This fixes two bugs at once.
Since 3707c64af789 relaxed validateSymbolRelocation for dynamic-lookup
symbols, a TLV and a GOT relocation against one such symbol could both
allocate a slot. Symbol::gotIndex served both sections, so the second
addEntry tripped `assert(!sym->isInGot())`, or in a release build
silently overwrote the first index. Sending both to __got makes them
coalesce. ld64 lowers this input identically -- one __got slot, both
references reading it -- so the output is now instruction-for-
instruction what the reference implementation emits.
Second, only one direction of the TLV/non-TLV relocation mismatch is
really an error. A GOT relocation against a thread-local is meaningful:
it asks for the descriptor's address, which is what the slot holds.
Rejecting it also left the reciprocal half of the original bug unfixed.
A TLV relocation against a symbol known not to be thread-local stays an
error, because the TLV sequence would call the referent's first word as
if it were a resolver function.
That second change inverts two long-standing tests rather than dropping
them, so the claim that their diagnostic was overreach is measured rather
than argued. Against
Apple's ld-1230.1 on macOS 26.6.2, a GOT relocation targeting a
thread-local links with no diagnostic in every configuration the deleted
tests covered:
- defined in the same image (bad-got-to-tlv-reference.s): exit 0, no
slot at all, the load relaxed to a direct address computation.
- exported by a dylib (bad-got-to-dylib-tlv-reference.s): exit 0 on
both x86_64 and arm64, one __got slot bound to libtlv/_foo.
Both tests keep their inputs and now assert the new behaviour, moving out
of invalid/ to got-to-tlv-reference.s and got-to-dylib-tlv-reference.s since
neither input is invalid any more. (git scores them at 26-28% similarity, so
-M20% is needed to see the moves as renames.)
lld now links both too. For the dylib case it puts the slot in __thread_ptrs rather than __got,
since unlike ld64 it does track that the symbol is thread-local; the two sections hold the same
descriptor address, and a two-thread harness confirms a GOT reference served out of a __thread_ptrs
slot yields a descriptor whose resolver returns the calling thread's storage.
The dynamic-lookup case was verified end to end the same way: lld emits
the same single __got slot and the same instructions as ld64, and dyld
binds it to the descriptor with per-thread reads resolving correctly.
---
lld/MachO/InputSection.cpp | 18 +++---
lld/MachO/MapFile.cpp | 2 +-
lld/MachO/Relocations.cpp | 11 ++--
lld/MachO/Symbols.cpp | 10 +---
lld/MachO/Symbols.h | 22 ++++---
lld/MachO/SyntheticSections.cpp | 16 +++--
lld/MachO/SyntheticSections.h | 26 ++++----
lld/MachO/Writer.cpp | 5 +-
lld/test/MachO/got-to-dylib-tlv-reference.s | 48 +++++++++++++++
lld/test/MachO/got-to-tlv-reference.s | 24 ++++++++
.../invalid/bad-got-to-dylib-tlv-reference.s | 23 --------
.../MachO/invalid/bad-got-to-tlv-reference.s | 14 -----
lld/test/MachO/tlv-dynamic-lookup-x86.s | 15 ++---
lld/test/MachO/tlv-dynamic-lookup.s | 59 ++++++++++---------
lld/test/MachO/tlv-synthetic-got-entry.s | 44 ++++++++++++++
15 files changed, 210 insertions(+), 127 deletions(-)
create mode 100644 lld/test/MachO/got-to-dylib-tlv-reference.s
create mode 100644 lld/test/MachO/got-to-tlv-reference.s
delete mode 100644 lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s
delete mode 100644 lld/test/MachO/invalid/bad-got-to-tlv-reference.s
create mode 100644 lld/test/MachO/tlv-synthetic-got-entry.s
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index d2841a0f73c39..beeeb8e7d5c64 100644
--- a/lld/MachO/InputSection.cpp
+++ b/lld/MachO/InputSection.cpp
@@ -100,10 +100,11 @@ uint64_t macho::resolveSymbolOffsetVA(const Symbol *sym, uint8_t type,
// function's layout, which an interposed replacement wouldn't preserve.
// There's no meaningful way to "interpose" an interior offset.
symVA = (offset != 0) ? sym->getVA() : sym->resolveBranchVA();
- } else if (relocAttrs.hasAttr(RelocAttrBits::GOT)) {
- symVA = sym->resolveGotVA();
- } else if (relocAttrs.hasAttr(RelocAttrBits::TLV)) {
- symVA = sym->resolveTlvVA();
+ } else if (relocAttrs.hasAttr(RelocAttrBits::GOT) ||
+ relocAttrs.hasAttr(RelocAttrBits::TLV)) {
+ // Both kinds read the symbol's single non-lazy pointer slot; for a
+ // thread-local that slot holds the address of its TLV descriptor.
+ symVA = sym->resolveNonLazyPtrVA();
} else {
symVA = sym->getVA();
}
@@ -248,13 +249,8 @@ void ConcatInputSection::writeTo(uint8_t *buf) {
}
referentVA = minuendVA - fromSym->getVA();
} else if (auto *referentSym = r.referent.dyn_cast<Symbol *>()) {
- // Relaxing a load means "there is no slot; compute the address
- // directly". Ask about the section this relocation actually uses:
- // __thread_ptrs for TLV relocations, __got for everything else.
- bool hasSlot = target->hasAttr(r.type, RelocAttrBits::TLV)
- ? referentSym->isInTlvPointers()
- : referentSym->isInGot();
- if (target->hasAttr(r.type, RelocAttrBits::LOAD) && !hasSlot)
+ if (target->hasAttr(r.type, RelocAttrBits::LOAD) &&
+ !referentSym->isInGot())
target->relaxGotLoad(loc, r.type);
// For dtrace symbols, do not handle them as normal undefined symbols
if (referentSym->getName().starts_with("___dtrace_")) {
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 4d5e543c24c8d..29ebcdcf9a832 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -149,7 +149,7 @@ static void printNonLazyPointerSection(raw_fd_ostream &os,
// associations.
for (const Symbol *sym : osec->getEntries())
os << format("0x%08llX\t0x%08zX\t[ 0] non-lazy-pointer-to-local: %s\n",
- osec->addr + osec->getIndex(*sym) * target->wordSize,
+ osec->addr + sym->gotIndex * target->wordSize,
target->wordSize, sym->getName().str().data());
}
diff --git a/lld/MachO/Relocations.cpp b/lld/MachO/Relocations.cpp
index 319443b631a66..419ccf36e81d6 100644
--- a/lld/MachO/Relocations.cpp
+++ b/lld/MachO/Relocations.cpp
@@ -68,14 +68,15 @@ bool macho::validateSymbolRelocation(const Symbol *sym,
.str();
};
- // A dynamic-lookup symbol's thread-locality is unknown at link time but
- // resolved by dyld at load time; rejecting it would refuse a valid link.
+ // Only a TLV relocation against a symbol that is not thread-local should be
+ // banned; as long as the sym is dynamic lookup, `isTlv` is just a default
+ // false and we can still link.
const auto *dysym = dyn_cast<DylibSymbol>(sym);
bool tlvKindIsKnown = !(dysym && dysym->isDynamicLookup());
- if (tlvKindIsKnown && relocAttrs.hasAttr(RelocAttrBits::TLV) != sym->isTlv())
- error(message(Twine("requires that symbol ") + sym->getName() + " " +
- (sym->isTlv() ? "not " : "") + "be thread-local"));
+ if (tlvKindIsKnown && relocAttrs.hasAttr(RelocAttrBits::TLV) && !sym->isTlv())
+ error(message(Twine("requires that symbol ") + sym->getName() +
+ " be thread-local"));
return valid;
}
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index b622dbcb1901a..27419caf9de1e 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -50,15 +50,7 @@ uint64_t Symbol::getLazyPtrVA() const {
return in.lazyPointers->getVA(stubsIndex);
}
uint64_t Symbol::getGotVA() const { return in.got->getVA(gotIndex); }
-bool Symbol::isInTlvPointers() const {
- return in.tlvPointers->getIndex(*this) != UINT32_MAX;
-}
-uint64_t Symbol::getTlvVA() const {
- return in.tlvPointers->getVA(in.tlvPointers->getIndex(*this));
-}
-uint64_t Symbol::resolveTlvVA() const {
- return isInTlvPointers() ? getTlvVA() : getVA();
-}
+uint64_t Symbol::getTlvVA() const { return in.tlvPointers->getVA(gotIndex); }
Defined::Defined(StringRef name, InputFile *file, InputSection *isec,
uint64_t value, uint64_t size, bool isWeakDef, bool isExternal,
diff --git a/lld/MachO/Symbols.h b/lld/MachO/Symbols.h
index 6c89e7a77266a..025cc6668840d 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,14 +65,10 @@ class Symbol {
virtual bool isTlv() const { return false; }
- // Whether this symbol has a __got slot.
+ // Whether this symbol has a non-lazy pointer slot. A symbol gets at most
+ // one: in __thread_ptrs if it is a known thread-local, in __got otherwise.
bool isInGot() const { return gotIndex != UINT32_MAX; }
- // Whether this symbol has a __thread_ptrs slot. Unlike the GOT index this
- // is kept in a side table owned by TlvPointerSection: thread-local imports
- // are rare, and Symbol is instantiated in the millions.
- bool isInTlvPointers() const;
-
// Whether this symbol is in the StubsSection.
bool isInStubs() const { return stubsIndex != UINT32_MAX; }
@@ -84,10 +80,18 @@ class Symbol {
assert(isa<Defined>(this) || isa<DylibSymbol>(this));
return isInStubs() ? getStubVA() : getVA();
}
- uint64_t resolveGotVA() const { return isInGot() ? getGotVA() : getVA(); }
- uint64_t resolveTlvVA() const;
+ // The address of this symbol's non-lazy pointer slot, or the symbol's own
+ // address if it has none. A thread-local's slot holds the address of its
+ // TLV descriptor, which is also what a GOT reference to it wants, so GOT
+ // and TLV relocations both resolve through here and share the one slot.
+ uint64_t resolveNonLazyPtrVA() const {
+ if (!isInGot())
+ return getVA();
+ return isTlv() ? getTlvVA() : getGotVA();
+ }
- // The index of this symbol in the GOT.
+ // The index of this symbol's non-lazy pointer slot within whichever of
+ // __got / __thread_ptrs holds it.
uint32_t gotIndex = UINT32_MAX;
uint32_t lazyBindOffset = UINT32_MAX;
uint32_t stubsHelperIndex = UINT32_MAX;
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index 8503e0ed6a452..8e5e78fc3f810 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -338,11 +338,12 @@ void macho::addNonLazyBindingEntries(const Symbol *sym,
void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
if (entries.insert(sym)) {
- assert(getIndex(*sym) == UINT32_MAX);
- uint32_t index = entries.size() - 1;
- setIndex(*sym, index);
+ // A symbol belongs to at most one non-lazy pointer section, so the index
+ // is unambiguous even though both sections share the field.
+ assert(!sym->isInGot());
+ sym->gotIndex = entries.size() - 1;
- addNonLazyBindingEntries(sym, isec, index * target->wordSize);
+ addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize);
}
}
@@ -399,6 +400,13 @@ GotSection::GotSection()
flags = S_NON_LAZY_SYMBOL_POINTERS;
}
+void GotSection::addEntry(Symbol *sym) {
+ if (sym->isTlv())
+ in.tlvPointers->addEntry(sym);
+ else
+ NonLazyPointerSectionBase::addEntry(sym);
+}
+
TlvPointerSection::TlvPointerSection()
: NonLazyPointerSectionBase(segment_names::data,
section_names::threadPtrs) {
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 5c9006d3104b6..956e77b5c4f41 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -120,13 +120,9 @@ class NonLazyPointerSectionBase : public SyntheticSection {
}
void writeTo(uint8_t *buf) const override;
void addEntry(Symbol *sym);
- uint64_t getVA(uint32_t index) const {
- return addr + index * target->wordSize;
+ uint64_t getVA(uint32_t gotIndex) const {
+ return addr + gotIndex * target->wordSize;
}
- // This symbol's slot index within this section, or UINT32_MAX. __got and
- // __thread_ptrs track indices separately so a symbol may appear in both.
- virtual uint32_t getIndex(const Symbol &sym) const = 0;
- virtual void setIndex(Symbol &sym, uint32_t index) = 0;
private:
llvm::SetVector<const Symbol *> entries;
@@ -135,21 +131,19 @@ class NonLazyPointerSectionBase : public SyntheticSection {
class GotSection final : public NonLazyPointerSectionBase {
public:
GotSection();
- uint32_t getIndex(const Symbol &sym) const override { return sym.gotIndex; }
- void setIndex(Symbol &sym, uint32_t index) override { sym.gotIndex = index; }
+
+ // Redirects a thread-local to __thread_ptrs instead of allocating a slot
+ // here. Both sections would hold the same value -- the address of the
+ // symbol's TLV descriptor -- and Symbol::gotIndex can only describe one
+ // slot, so a symbol must never be in both. Several synthetic sections add
+ // GOT entries without passing through the relocation scan, so the choice
+ // has to live here, where every caller reaches it.
+ void addEntry(Symbol *sym);
};
class TlvPointerSection final : public NonLazyPointerSectionBase {
public:
TlvPointerSection();
- uint32_t getIndex(const Symbol &sym) const override {
- auto it = indices.find(&sym);
- return it == indices.end() ? UINT32_MAX : it->second;
- }
- void setIndex(Symbol &sym, uint32_t index) override { indices[&sym] = index; }
-
-private:
- llvm::DenseMap<const Symbol *, uint32_t> indices;
};
struct Location {
diff --git a/lld/MachO/Writer.cpp b/lld/MachO/Writer.cpp
index 49415ccb70af8..d8d1b5c8bb005 100644
--- a/lld/MachO/Writer.cpp
+++ b/lld/MachO/Writer.cpp
@@ -684,8 +684,11 @@ static void prepareSymbolRelocation(Symbol *sym, const InputSection *isec,
if (relocAttrs.hasAttr(RelocAttrBits::POINTER) || needsBinding(sym))
in.got->addEntry(sym);
} else if (relocAttrs.hasAttr(RelocAttrBits::TLV)) {
+ // GotSection::addEntry redirects a thread-local to __thread_ptrs. A
+ // dynamic-lookup symbol, whose thread-locality cannot be known here,
+ // keeps its slot in __got -- which is how ld64 lowers it too.
if (needsBinding(sym))
- in.tlvPointers->addEntry(sym);
+ in.got->addEntry(sym);
} else if (relocAttrs.hasAttr(RelocAttrBits::UNSIGNED)) {
// References from thread-local variable sections are treated as offsets
// relative to the start of the referent section, and therefore have no
diff --git a/lld/test/MachO/got-to-dylib-tlv-reference.s b/lld/test/MachO/got-to-dylib-tlv-reference.s
new file mode 100644
index 0000000000000..94ad56f07031a
--- /dev/null
+++ b/lld/test/MachO/got-to-dylib-tlv-reference.s
@@ -0,0 +1,48 @@
+# REQUIRES: x86
+# RUN: rm -rf %t; split-file %s %t
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/libtlv.s -o %t/libtlv.o
+# RUN: %lld -dylib -install_name @executable_path/libtlv.dylib \
+# RUN: -lSystem -o %t/libtlv.dylib %t/libtlv.o
+
+## Imported from a dylib, a GOT reference to a thread-local binds through the
+## __thread_ptrs slot -- the section lld uses for a symbol it knows to be
+## thread-local. lld used to reject this outright; ld64 accepts it, and puts
+## the slot in __got instead. Both hold the descriptor's address.
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/test.s -o %t/test.o
+# RUN: %lld -dylib -lSystem -L%t -ltlv -o %t/test.dylib %t/test.o
+# RUN: llvm-objdump --macho --bind %t/test.dylib | FileCheck %s
+# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+
+## Reaching the same thread-local through both a GOT and a TLV relocation must
+## produce one slot, not two bound to the same address. A second would appear
+## as a __got section alongside __thread_ptrs.
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/both.s -o %t/both.o
+# RUN: %lld -dylib -lSystem -L%t -ltlv -o %t/both.dylib %t/both.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/both.dylib | \
+# RUN: FileCheck %s --check-prefix=BOTH
+# BOTH-NOT: __got
+# BOTH: __thread_ptrs
+# BOTH-LABEL: Bind table:
+# BOTH: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libtlv _foo
+# BOTH-NOT: _foo
+
+#--- libtlv.s
+.section __DATA,__thread_vars,thread_local_variables
+.globl _foo
+_foo:
+
+#--- test.s
+.text
+.globl _main
+_main:
+ movq _foo at GOTPCREL(%rip), %rax
+ ret
+
+#--- both.s
+.text
+.globl _main
+_main:
+ movq _foo at GOTPCREL(%rip), %rax
+ movq _foo at TLVP(%rip), %rcx
+ ret
diff --git a/lld/test/MachO/got-to-tlv-reference.s b/lld/test/MachO/got-to-tlv-reference.s
new file mode 100644
index 0000000000000..00d273bdc0b84
--- /dev/null
+++ b/lld/test/MachO/got-to-tlv-reference.s
@@ -0,0 +1,24 @@
+# REQUIRES: x86
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %s -o %t.o
+# RUN: %lld -dylib -o %t.dylib %t.o
+# RUN: llvm-objdump --macho --section-headers -d --no-show-raw-insn %t.dylib | FileCheck %s
+
+## A GOT relocation against a thread-local asks for the address of that symbol's
+## TLV descriptor, which is what its non-lazy pointer slot holds. lld used to
+## reject this outright; ld64 accepts it. Defined in the same image the
+## reference needs no slot at all -- the load relaxes to a direct address
+## computation, which is also what ld64 emits.
+
+# CHECK-NOT: __got
+# CHECK-NOT: __thread_ptrs
+# CHECK-LABEL: _main:
+# CHECK-NEXT: leaq _foo(%rip), %rax
+
+.text
+.globl _main
+_main:
+ movq _foo at GOTPCREL(%rip), %rax
+ ret
+
+.section __DATA,__thread_vars,thread_local_variables
+_foo:
diff --git a/lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s b/lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s
deleted file mode 100644
index 463a044465172..0000000000000
--- a/lld/test/MachO/invalid/bad-got-to-dylib-tlv-reference.s
+++ /dev/null
@@ -1,23 +0,0 @@
-# REQUIRES: x86
-# RUN: rm -rf %t; split-file %s %t
-
-# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/libtlv.s -o %t/libtlv.o
-# RUN: %lld -dylib -install_name @executable_path/libtlv.dylib \
-# RUN: -lSystem -o %t/libtlv.dylib %t/libtlv.o
-
-# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %t/test.s -o %t/test.o
-# RUN: not %lld -lSystem -L%t -ltlv -o /dev/null %t/test.o 2>&1 | FileCheck %s -DFILE=%t/test.o
-
-# CHECK: error: [[FILE]]:(symbol _main+0x3): GOT_LOAD relocation requires that symbol _foo not be thread-local
-
-#--- libtlv.s
-.section __DATA,__thread_vars,thread_local_variables
-.globl _foo
-_foo:
-
-#--- test.s
-.text
-.globl _main
-_main:
- movq _foo at GOTPCREL(%rip), %rax
- ret
diff --git a/lld/test/MachO/invalid/bad-got-to-tlv-reference.s b/lld/test/MachO/invalid/bad-got-to-tlv-reference.s
deleted file mode 100644
index 8934bc892a474..0000000000000
--- a/lld/test/MachO/invalid/bad-got-to-tlv-reference.s
+++ /dev/null
@@ -1,14 +0,0 @@
-# REQUIRES: x86
-# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-darwin %s -o %t.o
-# RUN: not %lld -o /dev/null %t.o 2>&1 | FileCheck %s -DFILE=%t.o
-
-# CHECK: error: [[FILE]]:(symbol _main+0x3): GOT_LOAD relocation requires that symbol _foo not be thread-local
-
-.text
-.globl _main
-_main:
- movq _foo at GOTPCREL(%rip), %rax
- ret
-
-.section __DATA,__thread_vars,thread_local_variables
-_foo:
diff --git a/lld/test/MachO/tlv-dynamic-lookup-x86.s b/lld/test/MachO/tlv-dynamic-lookup-x86.s
index 80ef492218373..c39622c4fd169 100644
--- a/lld/test/MachO/tlv-dynamic-lookup-x86.s
+++ b/lld/test/MachO/tlv-dynamic-lookup-x86.s
@@ -9,15 +9,16 @@
# RUN: FileCheck %s --check-prefix=MOVQ
# MOVQ-LABEL: _f:
# MOVQ-NEXT: movq
-# MOVQ: __thread_ptrs
+# MOVQ-NOT: __thread_ptrs
+# MOVQ: __got
# MOVQ-LABEL: Bind table:
-# MOVQ: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# MOVQ: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## FIXME: leaq computes the address of the __thread_ptrs slot, but the ABI
-## requires the descriptor the slot holds, so this is wrong code. It is accepted
-## because X86_64::relaxGotLoad's MOVQ check only runs on the relax path, and a
-## slot exists here. Pre-existing and not specific to dynamic lookup: the same
-## leaq against a dylib that really exports _tlv as thread-local miscompiles
+## FIXME: leaq computes the address of the slot itself, but the ABI requires the
+## descriptor the slot holds, so this is wrong code. It is accepted because
+## X86_64::relaxGotLoad's MOVQ check only runs on the relax path, and a slot
+## exists here. Pre-existing and not specific to dynamic lookup: the same leaq
+## against a dylib that really exports _tlv as thread-local miscompiles
## identically on stock lld. Pinned as-is so the separate fix has a baseline.
# RUN: %lld -dylib -undefined dynamic_lookup -o %t/leaq.dylib %t/leaq.o
# RUN: llvm-objdump --macho -d --no-show-raw-insn %t/leaq.dylib | \
diff --git a/lld/test/MachO/tlv-dynamic-lookup.s b/lld/test/MachO/tlv-dynamic-lookup.s
index 96fb12e1541d1..3c1a147ad7ea4 100644
--- a/lld/test/MachO/tlv-dynamic-lookup.s
+++ b/lld/test/MachO/tlv-dynamic-lookup.s
@@ -12,11 +12,14 @@
# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/deftlv.s -o %t/deftlv.o
# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/notlv.s -o %t/notlv.o
+## A dynamic-lookup symbol's thread-locality is unknown, so its slot goes in
+## __got. ld64 lowers this case identically.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/dylookup.dylib %t/consumer.o
# RUN: llvm-objdump --macho --section-headers --bind %t/dylookup.dylib | FileCheck %s
-# CHECK: __thread_ptrs
+# CHECK-NOT: __thread_ptrs
+# CHECK: __got
# CHECK-LABEL: Bind table:
-# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# CHECK: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
## -U is per-symbol: _tlv is exempt, _other is left undefined.
# RUN: not %lld -arch arm64 -dylib -U _tlv -o /dev/null %t/twotlv.o 2>&1 | \
@@ -27,22 +30,21 @@
# RUN: %lld -arch arm64 -dylib -U _tlv -U _other -o %t/u.dylib %t/twotlv.o
# RUN: llvm-objdump --macho --bind %t/u.dylib | FileCheck %s --check-prefix=UBOTH
-# UBOTH-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-# UBOTH-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _other
+# UBOTH-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UBOTH-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _other
# RUN: %no-fatal-warnings-lld -arch arm64 -dylib -flat_namespace -undefined suppress \
# RUN: -o %t/flat.dylib %t/consumer.o
# RUN: llvm-objdump --macho --bind %t/flat.dylib | FileCheck %s --check-prefix=FLAT
-# FLAT: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# FLAT: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## Chained fixups must produce a __thread_ptrs slot, not just a flat import --
-## a GOT-only binary yields a byte-identical import table.
+## Chained fixups must produce a real slot, not just a flat import.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -fixup_chains \
# RUN: -o %t/chained.dylib %t/consumer.o
# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/chained.dylib | \
# RUN: FileCheck %s --check-prefix=CHAINED
-# CHAINED: __thread_ptrs
-# CHAINED-NOT: __got
+# CHAINED-NOT: __thread_ptrs
+# CHAINED: __got
# CHAINED: lib_ordinal = -2 (flat-namespace)
# CHAINED: _tlv
@@ -71,38 +73,38 @@
## The remaining cases each reach NonLazyPointerSectionBase::addEntry by a route
## that does not pass through prepareSymbolRelocation. None may abort the
-## linker. Where lld synthesizes the competing entry itself the user has no way
-## to act on a diagnostic, so these must link.
+## linker: the symbol already has a __got slot by the time the TLV reference
+## asks for one, so addEntry must coalesce rather than allocate a second.
## StubsSection::addEntry -> in.got->addEntry under chained fixups, which is the
## default at iOS 16 / macOS 13.
## Chained fixups records binds in LC_DYLD_CHAINED_FIXUPS, so --bind is empty
-## here; the stub's __got slot and the TLV's __thread_ptrs slot must coexist.
+## here; the stub and the TLV reference share the one __got slot.
# RUN: %no-arg-lld -arch arm64 -platform_version ios 16.0 16.0 -dylib \
# RUN: -undefined dynamic_lookup -o %t/branch-chained.dylib %t/branch.o
# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/branch-chained.dylib | \
# RUN: FileCheck %s --check-prefix=BRANCHC
-# BRANCHC-DAG: __got
-# BRANCHC-DAG: __thread_ptrs
+# BRANCHC-NOT: __thread_ptrs
+# BRANCHC: __got
# BRANCHC: lib_ordinal = -2 (flat-namespace)
## Same source without chained fixups must not reach a different verdict.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -no_fixup_chains \
# RUN: -o %t/branch-lazy.dylib %t/branch.o
# RUN: llvm-objdump --macho --bind %t/branch-lazy.dylib | FileCheck %s --check-prefix=BRANCH
-# BRANCH: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# BRANCH: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
## UnwindInfoSection::prepare, after the relocation scan.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/cfi.dylib %t/cfi.o
# RUN: llvm-objdump --macho --bind %t/cfi.dylib | FileCheck %s --check-prefix=CFI
-# CFI: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _pers
+# CFI: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _pers
## StubHelperSection::setUp, which runs after Writer::run's errorCount() bail
## and so can never be protected by an error() emitted earlier.
# RUN: %no-lsystem-lld -arch arm64 -dylib -undefined dynamic_lookup -no_fixup_chains \
# RUN: -o %t/binder.dylib %t/binder.o
# RUN: llvm-objdump --macho --bind %t/binder.dylib | FileCheck %s --check-prefix=BINDER
-# BINDER: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace dyld_stub_binder
+# BINDER: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace dyld_stub_binder
## InitOffsetsSection::setUp.
# RUN: %no-arg-lld -arch arm64 -platform_version macos 13.0 13.0 \
@@ -110,8 +112,8 @@
# RUN: -init_offsets -o %t/initoff.dylib %t/initoff.o
# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/initoff.dylib | \
# RUN: FileCheck %s --check-prefix=INITOFF
-# INITOFF-DAG: __got
-# INITOFF-DAG: __thread_ptrs
+# INITOFF-NOT: __thread_ptrs
+# INITOFF: __got
# INITOFF: lib_ordinal = -2 (flat-namespace)
# INITOFF: _ctor
@@ -122,8 +124,8 @@
# RUN: -o %t/ep.out %t/ep.o
# RUN: llvm-objdump --macho --section-headers --chained-fixups %t/ep.out | \
# RUN: FileCheck %s --check-prefix=EP
-# EP-DAG: __got
-# EP-DAG: __thread_ptrs
+# EP-NOT: __thread_ptrs
+# EP: __got
# EP: lib_ordinal = -2 (flat-namespace)
# EP: _ep
@@ -135,14 +137,17 @@
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/unsmix.dylib %t/unsmix.o
# RUN: llvm-objdump --macho --bind %t/unsmix.dylib | FileCheck %s --check-prefix=UNS
# UNS-DAG: __DATA __data 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-# UNS-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# UNS-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-## JUDGMENT CALL. Same question for a GOT reference, which does allocate a slot.
-## Treated the same way for consistency with UNS and BRANCH above.
+## Referencing one symbol both ways yields a single slot, as ld64 emits here.
# RUN: %lld -arch arm64 -dylib -undefined dynamic_lookup -o %t/mixed.dylib %t/mixed.o
-# RUN: llvm-objdump --macho --bind %t/mixed.dylib | FileCheck %s --check-prefix=MIXED
-# MIXED-DAG: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
-# MIXED-DAG: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# RUN: llvm-objdump --macho --section-headers --bind %t/mixed.dylib | \
+# RUN: FileCheck %s --check-prefix=MIXED
+# MIXED-NOT: __thread_ptrs
+# MIXED: __got
+# MIXED-LABEL: Bind table:
+# MIXED: __DATA_CONST __got 0x{{[0-9a-f]+}} pointer 0 flat-namespace _tlv
+# MIXED-NOT: _tlv
#--- consumer.s
.globl _readTlv
diff --git a/lld/test/MachO/tlv-synthetic-got-entry.s b/lld/test/MachO/tlv-synthetic-got-entry.s
new file mode 100644
index 0000000000000..a2db2f4fbdda0
--- /dev/null
+++ b/lld/test/MachO/tlv-synthetic-got-entry.s
@@ -0,0 +1,44 @@
+# REQUIRES: aarch64
+# RUN: rm -rf %t; split-file %s %t
+
+## UnwindInfoSection adds a GOT entry for a CFI personality directly, without
+## passing through the relocation scan. If that personality is a thread-local
+## already given a __thread_ptrs slot by a TLV relocation, the symbol would end
+## up in both sections -- tripping assert(!sym->isInGot()), or silently
+## overwriting Symbol::gotIndex in a release build. It must yield one slot.
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/libtlv.s -o %t/libtlv.o
+# RUN: %lld -arch arm64 -dylib -install_name @executable_path/libtlv.dylib \
+# RUN: -lSystem -o %t/libtlv.dylib %t/libtlv.o
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-darwin %t/cfi.s -o %t/cfi.o
+# RUN: %lld -arch arm64 -dylib -lSystem -L%t -ltlv -o %t/cfi.dylib %t/cfi.o
+# RUN: llvm-objdump --macho --section-headers --bind %t/cfi.dylib | FileCheck %s
+# CHECK-NOT: __got
+# CHECK: __thread_ptrs
+# CHECK-LABEL: Bind table:
+# CHECK: __DATA __thread_ptrs 0x{{[0-9a-f]+}} pointer 0 libtlv _pers
+# CHECK-NOT: _pers
+
+#--- libtlv.s
+.globl _pers
+.section __DATA,__thread_data,thread_local_regular
+_pers$tlv$init:
+ .quad 0
+.section __DATA,__thread_vars,thread_local_variables
+_pers:
+ .quad __tlv_bootstrap
+ .quad 0
+ .quad _pers$tlv$init
+
+#--- cfi.s
+.globl _f
+.p2align 2
+_f:
+ .cfi_startproc
+ .cfi_personality 155, _pers
+ adrp x8, _pers at TLVPPAGE
+ ldr x8, [x8, _pers at TLVPPAGEOFF]
+ ret
+ .cfi_endproc
+.subsections_via_symbols
>From 4061bde2a78d079b9f9878c29d727090271cb012 Mon Sep 17 00:00:00 2001
From: Peter Rong <PeterRong at meta.com>
Date: Thu, 10 Sep 2026 17:09:22 -0700
Subject: [PATCH 4/4] [NFC][lld][MachO] Rename gotIndex/isInGot to reflect both
sections
A symbol's non-lazy pointer slot lives in __thread_ptrs when the symbol is
a known thread-local and in __got otherwise, so the `got` in these two
names no longer matches what they mean. It read as an outright bug in
Symbol::getTlvVA, which indexed __thread_ptrs with something called
`gotIndex`.
Rename to match the vocabulary already used for these sections --
NonLazyPointerSectionBase, addNonLazyBindingEntries, resolveNonLazyPtrVA:
Symbol::gotIndex -> Symbol::nonLazyPtrIndex
Symbol::isInGot -> Symbol::hasNonLazyPtr
NonLazyPointerSectionBase::getVA's parameter becomes `index`, since the
base class serves both sections. The comments that existed to explain away
the old names are dropped or reduced to stating the invariant. A few lines
are reflowed by clang-format because the new name is longer.
---
lld/MachO/Arch/ARM64.cpp | 2 +-
lld/MachO/Arch/ARM64Common.h | 2 +-
lld/MachO/Arch/X86_64.cpp | 6 +++---
lld/MachO/InputSection.cpp | 2 +-
lld/MachO/MapFile.cpp | 2 +-
lld/MachO/Symbols.cpp | 6 ++++--
lld/MachO/Symbols.h | 13 ++++++-------
lld/MachO/SyntheticSections.cpp | 9 +++++----
lld/MachO/SyntheticSections.h | 12 ++++++------
lld/MachO/UnwindInfoSection.cpp | 2 +-
10 files changed, 29 insertions(+), 27 deletions(-)
diff --git a/lld/MachO/Arch/ARM64.cpp b/lld/MachO/Arch/ARM64.cpp
index 7acf48cbbf123..2caa2a56f8ec2 100644
--- a/lld/MachO/Arch/ARM64.cpp
+++ b/lld/MachO/Arch/ARM64.cpp
@@ -131,7 +131,7 @@ void ARM64::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
if (config->objcStubsMode == ObjCStubsMode::fast) {
objcStubSize = target->objcStubsFastSize;
objcMsgSendAddr = in.got->addr;
- objcMsgSendIndex = objcMsgSend->gotIndex;
+ objcMsgSendIndex = objcMsgSend->nonLazyPtrIndex;
::writeObjCMsgSendFastStub<LP64>(buf, objcStubsFastCode, sym, stubsAddr,
stubOffset, selrefVA, objcMsgSendAddr,
objcMsgSendIndex);
diff --git a/lld/MachO/Arch/ARM64Common.h b/lld/MachO/Arch/ARM64Common.h
index f20cd4b170249..cb2a1f9784910 100644
--- a/lld/MachO/Arch/ARM64Common.h
+++ b/lld/MachO/Arch/ARM64Common.h
@@ -134,7 +134,7 @@ inline void writeStubHelperHeader(uint8_t *buf8,
encodePageOff12(&buf32[1], d, stubHelperHeaderCode[1], loaderVA);
buf32[2] = stubHelperHeaderCode[2];
uint64_t binderVA =
- in.got->addr + in.stubHelper->stubBinder->gotIndex * LP::wordSize;
+ in.got->addr + in.stubHelper->stubBinder->nonLazyPtrIndex * LP::wordSize;
encodePage21(&buf32[3], d, stubHelperHeaderCode[3],
pageBits(binderVA) - pcPageBits(3));
encodePageOff12(&buf32[4], d, stubHelperHeaderCode[4], binderVA);
diff --git a/lld/MachO/Arch/X86_64.cpp b/lld/MachO/Arch/X86_64.cpp
index c0bdffa22f254..6e9472a5838ec 100644
--- a/lld/MachO/Arch/X86_64.cpp
+++ b/lld/MachO/Arch/X86_64.cpp
@@ -174,8 +174,8 @@ void X86_64::writeStubHelperHeader(uint8_t *buf) const {
writeRipRelative(d, buf, in.stubHelper->addr, 7,
in.imageLoaderCache->getVA());
writeRipRelative(d, buf, in.stubHelper->addr, 0xf,
- in.got->addr +
- in.stubHelper->stubBinder->gotIndex * LP64::wordSize);
+ in.got->addr + in.stubHelper->stubBinder->nonLazyPtrIndex *
+ LP64::wordSize);
}
static constexpr uint8_t stubHelperEntry[] = {
@@ -200,7 +200,7 @@ void X86_64::writeObjCMsgSendStub(uint8_t *buf, Symbol *sym, uint64_t stubsAddr,
uint64_t &stubOffset, uint64_t selrefVA,
Symbol *objcMsgSend) const {
uint64_t objcMsgSendAddr = in.got->addr;
- uint64_t objcMsgSendIndex = objcMsgSend->gotIndex;
+ uint64_t objcMsgSendIndex = objcMsgSend->nonLazyPtrIndex;
memcpy(buf, objcStubsFastCode, sizeof(objcStubsFastCode));
SymbolDiagnostic d = {sym, sym->getName()};
diff --git a/lld/MachO/InputSection.cpp b/lld/MachO/InputSection.cpp
index beeeb8e7d5c64..74c0b2a2bff17 100644
--- a/lld/MachO/InputSection.cpp
+++ b/lld/MachO/InputSection.cpp
@@ -250,7 +250,7 @@ void ConcatInputSection::writeTo(uint8_t *buf) {
referentVA = minuendVA - fromSym->getVA();
} else if (auto *referentSym = r.referent.dyn_cast<Symbol *>()) {
if (target->hasAttr(r.type, RelocAttrBits::LOAD) &&
- !referentSym->isInGot())
+ !referentSym->hasNonLazyPtr())
target->relaxGotLoad(loc, r.type);
// For dtrace symbols, do not handle them as normal undefined symbols
if (referentSym->getName().starts_with("___dtrace_")) {
diff --git a/lld/MachO/MapFile.cpp b/lld/MachO/MapFile.cpp
index 29ebcdcf9a832..47e0ec06fa9ef 100644
--- a/lld/MachO/MapFile.cpp
+++ b/lld/MachO/MapFile.cpp
@@ -149,7 +149,7 @@ static void printNonLazyPointerSection(raw_fd_ostream &os,
// associations.
for (const Symbol *sym : osec->getEntries())
os << format("0x%08llX\t0x%08zX\t[ 0] non-lazy-pointer-to-local: %s\n",
- osec->addr + sym->gotIndex * target->wordSize,
+ osec->addr + sym->nonLazyPtrIndex * target->wordSize,
target->wordSize, sym->getName().str().data());
}
diff --git a/lld/MachO/Symbols.cpp b/lld/MachO/Symbols.cpp
index 27419caf9de1e..18ab4bc3c79cb 100644
--- a/lld/MachO/Symbols.cpp
+++ b/lld/MachO/Symbols.cpp
@@ -49,8 +49,10 @@ uint64_t Symbol::getStubVA() const { return in.stubs->getVA(stubsIndex); }
uint64_t Symbol::getLazyPtrVA() const {
return in.lazyPointers->getVA(stubsIndex);
}
-uint64_t Symbol::getGotVA() const { return in.got->getVA(gotIndex); }
-uint64_t Symbol::getTlvVA() const { return in.tlvPointers->getVA(gotIndex); }
+uint64_t Symbol::getGotVA() const { return in.got->getVA(nonLazyPtrIndex); }
+uint64_t Symbol::getTlvVA() const {
+ return in.tlvPointers->getVA(nonLazyPtrIndex);
+}
Defined::Defined(StringRef name, InputFile *file, InputSection *isec,
uint64_t value, uint64_t size, bool isWeakDef, bool isExternal,
diff --git a/lld/MachO/Symbols.h b/lld/MachO/Symbols.h
index 025cc6668840d..f904310756bd9 100644
--- a/lld/MachO/Symbols.h
+++ b/lld/MachO/Symbols.h
@@ -65,9 +65,7 @@ class Symbol {
virtual bool isTlv() const { return false; }
- // Whether this symbol has a non-lazy pointer slot. A symbol gets at most
- // one: in __thread_ptrs if it is a known thread-local, in __got otherwise.
- bool isInGot() const { return gotIndex != UINT32_MAX; }
+ bool hasNonLazyPtr() const { return nonLazyPtrIndex != UINT32_MAX; }
// Whether this symbol is in the StubsSection.
bool isInStubs() const { return stubsIndex != UINT32_MAX; }
@@ -85,14 +83,15 @@ class Symbol {
// TLV descriptor, which is also what a GOT reference to it wants, so GOT
// and TLV relocations both resolve through here and share the one slot.
uint64_t resolveNonLazyPtrVA() const {
- if (!isInGot())
+ if (!hasNonLazyPtr())
return getVA();
return isTlv() ? getTlvVA() : getGotVA();
}
- // The index of this symbol's non-lazy pointer slot within whichever of
- // __got / __thread_ptrs holds it.
- uint32_t gotIndex = UINT32_MAX;
+ // A symbol has at most one non-lazy pointer slot: in __thread_ptrs if it is
+ // a known thread-local, in __got otherwise. This indexes whichever of the
+ // two holds it.
+ uint32_t nonLazyPtrIndex = UINT32_MAX;
uint32_t lazyBindOffset = UINT32_MAX;
uint32_t stubsHelperIndex = UINT32_MAX;
uint32_t stubsIndex = UINT32_MAX;
diff --git a/lld/MachO/SyntheticSections.cpp b/lld/MachO/SyntheticSections.cpp
index 8e5e78fc3f810..61fe58a24bc76 100644
--- a/lld/MachO/SyntheticSections.cpp
+++ b/lld/MachO/SyntheticSections.cpp
@@ -340,10 +340,11 @@ void NonLazyPointerSectionBase::addEntry(Symbol *sym) {
if (entries.insert(sym)) {
// A symbol belongs to at most one non-lazy pointer section, so the index
// is unambiguous even though both sections share the field.
- assert(!sym->isInGot());
- sym->gotIndex = entries.size() - 1;
+ assert(!sym->hasNonLazyPtr());
+ sym->nonLazyPtrIndex = entries.size() - 1;
- addNonLazyBindingEntries(sym, isec, sym->gotIndex * target->wordSize);
+ addNonLazyBindingEntries(sym, isec,
+ sym->nonLazyPtrIndex * target->wordSize);
}
}
@@ -940,7 +941,7 @@ void ObjCStubsSection::setUp() {
objcMsgSend->used = true;
if (config->objcStubsMode == ObjCStubsMode::fast) {
in.got->addEntry(objcMsgSend);
- assert(objcMsgSend->isInGot());
+ assert(objcMsgSend->hasNonLazyPtr());
} else {
assert(config->objcStubsMode == ObjCStubsMode::small);
// In line with ld64's behavior, when objc_msgSend is a direct symbol,
diff --git a/lld/MachO/SyntheticSections.h b/lld/MachO/SyntheticSections.h
index 956e77b5c4f41..9631de5451180 100644
--- a/lld/MachO/SyntheticSections.h
+++ b/lld/MachO/SyntheticSections.h
@@ -120,8 +120,8 @@ class NonLazyPointerSectionBase : public SyntheticSection {
}
void writeTo(uint8_t *buf) const override;
void addEntry(Symbol *sym);
- uint64_t getVA(uint32_t gotIndex) const {
- return addr + gotIndex * target->wordSize;
+ uint64_t getVA(uint32_t index) const {
+ return addr + index * target->wordSize;
}
private:
@@ -134,10 +134,10 @@ class GotSection final : public NonLazyPointerSectionBase {
// Redirects a thread-local to __thread_ptrs instead of allocating a slot
// here. Both sections would hold the same value -- the address of the
- // symbol's TLV descriptor -- and Symbol::gotIndex can only describe one
- // slot, so a symbol must never be in both. Several synthetic sections add
- // GOT entries without passing through the relocation scan, so the choice
- // has to live here, where every caller reaches it.
+ // symbol's TLV descriptor -- and a symbol has only one slot, so it must
+ // never be in both. Several synthetic sections add GOT entries without
+ // passing through the relocation scan, so the choice has to live here,
+ // where every caller reaches it.
void addEntry(Symbol *sym);
};
diff --git a/lld/MachO/UnwindInfoSection.cpp b/lld/MachO/UnwindInfoSection.cpp
index 9775a723a92fd..ff3c7b2a0eb9a 100644
--- a/lld/MachO/UnwindInfoSection.cpp
+++ b/lld/MachO/UnwindInfoSection.cpp
@@ -305,7 +305,7 @@ void UnwindInfoSectionImpl::prepareRelocations(ConcatInputSection *isec) {
Defined *const *gotEntry =
llvm::find_if(referentIsec->symbols, [&](Defined const *d) {
return d->value == static_cast<uint64_t>(r.addend) &&
- d->isInGot();
+ d->hasNonLazyPtr();
});
if (gotEntry != referentIsec->symbols.end()) {
s = *gotEntry;
More information about the llvm-commits
mailing list