Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions include/eld/Diagnostics/DiagVerbose.inc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ DIAG(relax_to_compress, DiagnosticEngine::Verbose,
"%3 in section %4+0x%5 file %6")
DIAG(trace_relax_gotpcrelx, DiagnosticEngine::Trace,
"%0: relaxed GOTPCRELX (%1) for symbol '%2' to PC-relative access")
DIAG(trace_relax_gottpoff, DiagnosticEngine::Trace,
"relaxed GOTTPOFF (%0) for symbol '%1' to TP-relative immediate in "
"section %2+0x%3 file %4")
DIAG(verbose_ehframe_remove_fde, DiagnosticEngine::Verbose, "EhFrame %0")
DIAG(verbose_ehframe_read_fde, DiagnosticEngine::Verbose, "EhFrame %0")
DIAG(verbose_ehframe_read_cie, DiagnosticEngine::Verbose, "EhFrame %0")
Expand Down
155 changes: 140 additions & 15 deletions lib/Target/X86/x86_64LDBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,46 @@ bool x86_64LDBackend::isGOTPCRELXRelaxable(const Relocation *reloc) const {
}

bool x86_64LDBackend::shouldIgnoreRelocSync(Relocation *reloc) const {
return isGOTPCRELXRelaxCandidate(reloc);
return isGOTPCRELXRelaxCandidate(reloc) || isTLSIERelaxCandidate(reloc);
}

bool x86_64LDBackend::isTLSIERelaxable(const Relocation *reloc) const {
// Partial links have no final layout; keep the GOT slot
if (config().isLinkPartial())
return false;
if (reloc->type() != llvm::ELF::R_X86_64_GOTTPOFF)
return false;
// GNU as emits GOTTPOFF with addend -4. An object with a different addend
// does not encode a standard gottpoff(%rip) load and cannot be rewritten.
if (static_cast<int64_t>(reloc->addend()) != -4)
return false;
// TLS symbols (STT_TLS) cannot be STT_GNU_IFUNC, so no IFUNC guard is
// needed here unlike isGOTPCRELXRelaxable.
auto *RF = llvm::dyn_cast<RegionFragment>(reloc->targetRef()->frag());
if (!RF)
return false;
uint32_t offset = reloc->targetRef()->offset();
// The rewrite reads REX at loc[-3], opcode at loc[-2], ModR/M at loc[-1].
if (offset < 3)
return false;
const uint8_t *loc =
reinterpret_cast<const uint8_t *>(RF->getRegion().data()) + offset;
uint8_t opcode = loc[-2];
// Only MOV (0x8b) and ADD (0x03) GOTTPOFF forms are relaxable.
return opcode == 0x8b || opcode == 0x03;
}

bool x86_64LDBackend::shouldRelaxTLSIEToLE(const Relocation *reloc,
bool pPreemptible) const {
// Unifies the two scan-call-sites: local (pPreemptible=false) and global.
// IE->LE is a mandatory ABI TLS transition.
if (!isTLSIERelaxable(reloc))
return false;
// Relax when the TP offset is a link-time constant: any executable
// (static, dynamic, or PIE) for non-preemptible symbols. Shared libraries
// are never relaxed even for hidden symbols — the loader places the DSO TLS
// block at a runtime-determined offset.
return !pPreemptible && config().isBuildingExecutable();
}

eld::Expected<void>
Expand All @@ -160,29 +199,31 @@ x86_64LDBackend::postProcessing(llvm::FileOutputBuffer &pOutput) {

// Relaxation rewrites bytes in the laid-out output image; it is not
// applicable to partial links, which have no final layout.
if (config().options().getRelax() && !config().isLinkPartial())
if (!config().isLinkPartial())
ELDEXP_RETURN_DIAGENTRY_IF_ERROR(doRelax(pOutput));

return {};
}

eld::Expected<void> x86_64LDBackend::doRelax(llvm::FileOutputBuffer &pOutput) {
uint8_t *buf = pOutput.getBufferStart();
// The scan phase already identified every relaxation candidate (skipping
// debug relocations and internal files, which never carry a relaxable
// relocation). Iterate that cached set instead of re-walking all
// relocations, and dispatch on the relocation type. Future relaxable types
// (e.g. R_X86_64_REX_GOTPCRELX, TLS relaxations) add a case here.
for (Relocation *reloc : m_GOTPCRELXRelaxCandidates) {
switch (reloc->type()) {
case llvm::ELF::R_X86_64_GOTPCRELX:
ELDEXP_RETURN_DIAGENTRY_IF_ERROR(relaxGOTPCRELXReloc(reloc, buf));
break;
default:
// Only relaxable types are recorded as candidates; skip anything else.
break;
// The scan phase already identified every relaxation candidate.
// GOTPCRELX relaxation is optional (gated on --relax); IE→LE is a mandatory
// ABI TLS transition and runs unconditionally (mirrors GD→LE).
if (config().options().getRelax()) {
for (Relocation *reloc : m_GOTPCRELXRelaxCandidates) {
switch (reloc->type()) {
case llvm::ELF::R_X86_64_GOTPCRELX:
ELDEXP_RETURN_DIAGENTRY_IF_ERROR(relaxGOTPCRELXReloc(reloc, buf));
break;
default:
// Only relaxable types are recorded as candidates; skip anything else.
break;
}
}
}
for (Relocation *reloc : m_TLSIERelaxCandidates)
ELDEXP_RETURN_DIAGENTRY_IF_ERROR(relaxTLSIEReloc(reloc, buf));
return {};
}

Expand Down Expand Up @@ -285,6 +326,90 @@ eld::Expected<void> x86_64LDBackend::relaxGOTPCRELXReloc(Relocation *reloc,
return {};
}

eld::Expected<void> x86_64LDBackend::relaxTLSIEReloc(Relocation *reloc,
uint8_t *buf) {
// Only RegionFragment relocations can carry GOTTPOFF (object file code
// sections), and offset >= 3 is guaranteed by isTLSIERelaxable in scan.
auto *RF = llvm::cast<RegionFragment>(reloc->targetRef()->frag());

uint32_t offset = reloc->targetRef()->offset();
assert(offset >= 3 && "GOTTPOFF IE→LE relax candidate offset must be >= 3");

const uint8_t *loc =
reinterpret_cast<const uint8_t *>(RF->getRegion().data()) + offset;

uint64_t TLSTemplateSize = getTLSTemplateSize();
if (TLSTemplateSize == 0) {
config().raise(Diag::no_pt_tls_segment);
return {};
}

// finalizeTLSSymbol gives the PT_TLS-relative offset: sym_VA - tls_vaddr.
// The TP-relative offset for Variant 2 (x86-64) is S - TLSTemplateSize.
// The GOTTPOFF addend (-4) is a PC-relative displacement artifact of the
// original GOT-indirect load; it must NOT be included in the immediate.
uint64_t S = finalizeTLSSymbol(reloc->symInfo()->outSymbol());
int64_t tpoff =
static_cast<int64_t>(S) - static_cast<int64_t>(TLSTemplateSize);
uint32_t imm32 = static_cast<uint32_t>(tpoff);

FragmentRef::Offset off = reloc->targetRef()->getOutputOffset(m_Module);
if (off == (FragmentRef::Offset)-1)
return {};
size_t out_off = reloc->targetRef()->getOutputELFSection()->offset() + off;

uint8_t rex = loc[-3];
uint8_t opcode = loc[-2];
uint8_t modrm = loc[-1];
uint8_t reg = (modrm >> 3) & 0x7;

if (opcode == 0x8b) {
// movq gottpoff(%rip), %reg -> movq $tpoff, %reg
// REX.R (encodes reg field source) is no longer needed; REX.B (bit 0)
// selects r8-r15 in the 0xc0|reg ModRM. Map: 0x4c -> 0x49, else 0x48.
buf[out_off - 3] = (rex == 0x4c) ? 0x49 : 0x48;
buf[out_off - 2] = 0xc7;
buf[out_off - 1] = 0xc0 | reg;
llvm::support::endian::write32le(buf + out_off, imm32);
} else {
assert(opcode == 0x03 &&
"isTLSIERelaxable only passes 0x8b and 0x03 opcodes");
if (modrm == 0x25) {
// addq gottpoff(%rip), %rsp/%r12 -> addq $tpoff, %rsp/%r12
// leaq would need a SIB byte (8 bytes total); addq $imm stays at 7.
buf[out_off - 3] = (rex == 0x4c) ? 0x49 : 0x48;
buf[out_off - 2] = 0x81;
buf[out_off - 1] = 0xc4;
llvm::support::endian::write32le(buf + out_off, imm32);
} else {
// addq gottpoff(%rip), %reg -> leaq tpoff(%reg), %reg
// For r8-r15 (REX was 0x4c), the new form needs REX.W + REX.R + REX.B
// because both the base and destination are the same extended register.
buf[out_off - 3] = (rex == 0x4c) ? 0x4d : 0x48;
buf[out_off - 2] = 0x8d;
buf[out_off - 1] = 0x80 | (reg << 3) | reg;
llvm::support::endian::write32le(buf + out_off, imm32);
}
}

if (m_Module.getPrinter()->traceRelax()) {
ResolveInfo *rsym = reloc->symInfo();
ELFSection *traceSect = RF->getOwningSection();
assert(traceSect &&
"RegionFragment in relax candidate must have an owning section");
std::string fileName;
if (InputFile *F = traceSect->getInputFile())
if (auto *I = F->getInput())
fileName = I->decoratedPath();
const char *kind = (opcode == 0x8b) ? "mov->movimm" : "add->lea/addimm";
config().raise(Diag::trace_relax_gottpoff)
<< kind << rsym->name() << traceSect->name() << llvm::utohexstr(offset)
<< fileName;
}

return {};
}

/// finalizeSymbol - finalize the symbol value
bool x86_64LDBackend::finalizeTargetSymbols() {
if (config().codeGenType() == LinkerConfig::Object)
Expand Down
33 changes: 33 additions & 0 deletions lib/Target/X86/x86_64LDBackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ class x86_64LDBackend : public GNULDBackend {
/// fragment). Relaxation is disabled for partial links.
bool isGOTPCRELXRelaxable(const Relocation *reloc) const;

/// Returns true if a GOTTPOFF relocation is eligible for IE->LE relaxation.
/// Checks: not a partial link, type R_X86_64_GOTTPOFF, addend == -4,
/// RegionFragment with offset >= 3, and opcode is MOV (0x8b) or ADD (0x03).
/// Non-preemptibility and output-is-executable are checked in the scan.
bool isTLSIERelaxable(const Relocation *reloc) const;

/// Returns true if the GOTTPOFF scan should record pReloc as an IE->LE
/// candidate and skip GOT creation. Combines isTLSIERelaxable and the
/// link-type / preemptibility condition. IE->LE is a mandatory ABI
/// transition.
bool shouldRelaxTLSIEToLE(const Relocation *reloc, bool pPreemptible) const;

/// Records a relocation that the scan phase has decided to relax, so that
/// postProcessing can iterate only these candidates instead of re-walking
/// every relocation. Thread-safe: called from the parallel scan under the
Expand All @@ -124,6 +136,19 @@ class x86_64LDBackend : public GNULDBackend {
return m_GOTPCRELXRelaxCandidates.count(reloc) != 0;
}

/// Records a GOTTPOFF relocation whose scan phase decided on IE→LE
/// relaxation (a non-preemptible symbol in an executable). The GOT slot is
/// not allocated; postProcessing rewrites the instruction bytes.
void recordTLSIERelaxCandidate(Relocation *reloc) {
m_TLSIERelaxCandidates.insert(reloc);
}

/// O(1) membership check used by shouldIgnoreRelocSync and the apply-path
/// guard in relocGOTRelative.
bool isTLSIERelaxCandidate(Relocation *reloc) const {
return m_TLSIERelaxCandidates.count(reloc) != 0;
}

DynRelocType getDynRelocType(const Relocation *X) const override {
if (X->type() == llvm::ELF::R_X86_64_GLOB_DAT)
return DynRelocType::GLOB_DAT;
Expand Down Expand Up @@ -179,6 +204,10 @@ class x86_64LDBackend : public GNULDBackend {
/// signed 32-bit field.
eld::Expected<void> relaxGOTPCRELXReloc(Relocation *reloc, uint8_t *buf);

/// Rewrites a single GOTTPOFF IE→LE candidate: patches REX, opcode,
/// ModR/M, and the 4-byte immediate in the output buffer.
eld::Expected<void> relaxTLSIEReloc(Relocation *reloc, uint8_t *buf);

/// Iterates the cached relaxation candidates and rewrites each one.
eld::Expected<void> doRelax(llvm::FileOutputBuffer &pOutput);

Expand All @@ -198,6 +227,10 @@ class x86_64LDBackend : public GNULDBackend {
/// postProcessing. unordered_set for O(1) membership checks in
/// shouldIgnoreRelocSync and the apply-path guard.
std::unordered_set<Relocation *> m_GOTPCRELXRelaxCandidates;

/// Relocations selected for GOTTPOFF IE→LE relaxation during the scan
/// phase (non-preemptible symbols in executables).
std::unordered_set<Relocation *> m_TLSIERelaxCandidates;
};
} // namespace eld

Expand Down
21 changes: 18 additions & 3 deletions lib/Target/X86/x86_64Relocator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,11 @@ void x86_64Relocator::scanLocalReloc(InputFile &pInputFile, Relocation &pReloc,
return;
case llvm::ELF::R_X86_64_GOTTPOFF: {
std::lock_guard<std::mutex> relocGuard(m_RelocMutex);
// Local symbols are never preemptible; pass false directly.
if (m_Target.shouldRelaxTLSIEToLE(&pReloc, /*preemptible=*/false)) {
m_Target.recordTLSIERelaxCandidate(&pReloc);
return;
}
if (rsym->reserved() & ReserveGOT)
return;
x86_64GOT *G = m_Target.createGOT(GOT::TLS_IE, Obj, rsym);
Expand Down Expand Up @@ -459,12 +464,17 @@ void x86_64Relocator::scanGlobalReloc(InputFile &pInputFile, Relocation &pReloc,
}
case llvm::ELF::R_X86_64_GOTTPOFF: {
std::lock_guard<std::mutex> relocGuard(m_RelocMutex);
const bool preemptible = m_Target.isSymbolPreemptible(*rsym);
if (m_Target.shouldRelaxTLSIEToLE(&pReloc, preemptible)) {
m_Target.recordTLSIERelaxCandidate(&pReloc);
return;
}
if (rsym->reserved() & ReserveGOT)
return;
x86_64GOT *G = m_Target.createGOT(GOT::TLS_IE, Obj, rsym);
const bool isExec = config().isBuildingExecutable();
const bool preemptible = m_Target.isSymbolPreemptible(*rsym);
if (isExec && !preemptible) {
if (config().isBuildingExecutable() && !preemptible) {
// TP offset is fixed at link time for non-preemptible symbols in
// executables. Compute it at layout; no dynamic reloc needed.
G->setValueType(GOT::TLSStaticSymbolValue);
} else {
helper_DynRel_init(Obj, &pReloc, rsym, G, 0x0,
Expand Down Expand Up @@ -738,6 +748,11 @@ Relocator::Result eld::relocGOTRelative(Relocation &pReloc,
if (pParent.getTarget().isGOTPCRELXRelaxCandidate(&pReloc))
return Relocator::OK;

// IE→LE candidates also skip GOT lookup; the byte rewrite already happened
// in postProcessing.
if (pParent.getTarget().isTLSIERelaxCandidate(&pReloc))
return Relocator::OK;

Relocator::DWord A = pReloc.addend();
Relocator::DWord P = pReloc.place(pParent.module());
x86_64GOT *gotEntry = pParent.getTarget().findEntryInGOT(symInfo);
Expand Down
17 changes: 11 additions & 6 deletions test/x86_64/linux/TLSIELocalSymbol/TLSIELocalSymbol.test
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
/// Verifies that R_X86_64_GOTTPOFF against a file-local TLS symbol emits a
/// R_X86_64_TPOFF64 dynamic relocation in -shared output, and is statically
/// resolved into the GOT slot in -pie.
/// R_X86_64_TPOFF64 dynamic relocation in -shared output. In -pie the symbol
/// is non-preemptible, so IE->LE relaxation (a mandatory ABI transition, not
/// gated on --relax) rewrites the GOT-indirect load to a TP-relative
/// immediate and drops the GOT slot entirely, matching lld.

// RUN: %clang %clangopts -c -x c %s -fPIC -ftls-model=initial-exec -o %t.o
// RUN: %link %linkopts -shared %t.o -o %t.shared.so
// RUN: %readelf -r -x .got -d %t.shared.so | %filecheck %s --check-prefix=SHARED
// RUN: %link %linkopts -pie %t.o -o %t.pie --defsym __libc_start_main=0
// RUN: %readelf -r -x .got %t.pie | %filecheck %s --check-prefix=PIE
// RUN: %objdump -d %t.pie | %filecheck %s --check-prefix=PIE
// RUN: %readelf -r %t.pie | %filecheck %s --check-prefix=PIERELOC

// SHARED: STATIC_TLS
// SHARED: R_X86_64_TPOFF64
// SHARED-LABEL: Hex dump of section '.got':
// SHARED-NEXT: 0x{{[0-9a-f]+}} 00000000 00000000

// PIE-NOT: R_X86_64_TPOFF64
// PIE-LABEL: Hex dump of section '.got':
// PIE-NEXT: 0x{{[0-9a-f]+}} fcffffff ffffffff
// IE->LE relaxes the movq gottpoff(%rip),%rcx load to movq $tpoff,%rcx
// (48 c7 c1 <imm32>); no GOT slot and no dynamic relocation remain.
// PIE: <foo>:
// PIE: {{[0-9a-f]+}}: 48 c7 c1 {{.*}}
// PIERELOC-NOT: R_X86_64_TPOFF64

static __thread int a = 10;
int foo(void) { return a; }
11 changes: 11 additions & 0 deletions test/x86_64/linux/TLSIERelaxation/Inputs/tls_ie_add.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.globl tls_var
.section .tbss,"awT",@nobits
.align 4
tls_var:
.long 0

.text
.globl get_tls
get_tls:
addq tls_var@GOTTPOFF(%rip), %rax
ret
11 changes: 11 additions & 0 deletions test/x86_64/linux/TLSIERelaxation/Inputs/tls_ie_add_rsp.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.globl tls_var
.section .tbss,"awT",@nobits
.align 4
tls_var:
.long 0

.text
.globl get_tls
get_tls:
addq tls_var@GOTTPOFF(%rip), %rsp
ret
15 changes: 15 additions & 0 deletions test/x86_64/linux/TLSIERelaxation/Inputs/tls_ie_addend_nonm4.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# GOTTPOFF with addend 0 (non-standard). GNU as always emits addend -4
# for GOTTPOFF; use .reloc to inject addend 0 explicitly. The linker
# must keep the GOT slot when the addend is not -4.
.globl tls_var_bad
.hidden tls_var_bad
.section .tbss,"awT",@nobits
tls_var_bad: .long 0

.text
.globl get_tls_bad
.type get_tls_bad,@function
get_tls_bad:
movq 0(%rip), %rax
.reloc .-4, R_X86_64_GOTTPOFF, tls_var_bad+0
ret
12 changes: 12 additions & 0 deletions test/x86_64/linux/TLSIERelaxation/Inputs/tls_ie_hidden.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.globl tls_hidden
.hidden tls_hidden
.section .tbss,"awT",@nobits
.align 4
tls_hidden:
.long 0

.text
.globl get_tls
get_tls:
movq tls_hidden@GOTTPOFF(%rip), %rax
ret
11 changes: 11 additions & 0 deletions test/x86_64/linux/TLSIERelaxation/Inputs/tls_ie_mov.s
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.globl tls_var
.section .tbss,"awT",@nobits
.align 4
tls_var:
.long 0

.text
.globl get_tls
get_tls:
movq tls_var@GOTTPOFF(%rip), %rax
ret
Loading