[lld] [ELF] Handle and optimize x86-64 PLTOFF64 TLS sequences (PR #216263)
Fangrui Song via llvm-commits
llvm-commits at lists.llvm.org
Mon Aug 17 09:27:33 PDT 2026
https://github.com/MaskRay updated https://github.com/llvm/llvm-project/pull/216263
>From 1cef81d5b052ec660019ee6040db5a00311505ed Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Thu, 13 Aug 2026 20:35:22 -0700
Subject: [PATCH] [ELF] Handle and optimize x86-64 PLTOFF64 TLS sequences
In `gcc -mcmodel=large` generated General Dynamic/Local Dynamic TLS
sequences, R_X86_64_TLSGD/R_X86_64_TLSLD is followed by a MOVABS loading
`__tls_get_addr at pltoff`, and the call goes through a register:
```
leaq x at tlsgd(%rip), %rdi # R_X86_64_TLSGD
movabsq $__tls_get_addr at pltoff, %rax # R_X86_64_PLTOFF64
addq %rbx, %rax
callq *%rax
```
`relaxTlsGdToLe` and `relaxTlsGdToIe` write the 16-byte direct call sequence at
loc-4, corrupting the preceding instruction, while `relaxTlsLdToLe` reports
"expected R_X86_64_PLT32 or R_X86_64_GOTPCRELX after R_X86_64_TLSLD".
Detect the MOVABS and rewrite the 22 bytes, matching GNU ld.
`__tls_get_addr` doesn't need to be defined when optimized to LE/IE,
LLM-aided
---
lld/ELF/Arch/X86_64.cpp | 45 +++++++++++++
lld/test/ELF/x86-64-tls-pltoff64.s | 100 +++++++++++++++++++++++++++++
2 files changed, 145 insertions(+)
create mode 100644 lld/test/ELF/x86-64-tls-pltoff64.s
diff --git a/lld/ELF/Arch/X86_64.cpp b/lld/ELF/Arch/X86_64.cpp
index fee33b03ffa6a..eb5e2140c85bd 100644
--- a/lld/ELF/Arch/X86_64.cpp
+++ b/lld/ELF/Arch/X86_64.cpp
@@ -778,9 +778,29 @@ void X86_64::scanSection(InputSectionBase &sec, unsigned shard) {
elf::scanSection1<X86_64, ELF32LE>(*this, sec, shard);
}
+// TLSGD/TLSLD can be directly followed by MOVABS (instead of CALL):
+// leaq x at tlsgd(%rip), %rdi # 48 8d 3d <disp32>
+// movabsq $__tls_get_addr at pltoff, %rax # 48 b8 <imm64>, R_X86_64_PLTOFF64
+// addq %REG, %rax
+// callq *%rax
+static bool isPltOff64Tls(const uint8_t *loc) {
+ return loc[4] == 0x48 && loc[5] == 0xb8;
+}
+
void X86_64::relaxTlsGdToLe(uint8_t *loc, const Relocation &rel,
uint64_t val) const {
if (rel.type == R_X86_64_TLSGD) {
+ if (isPltOff64Tls(loc)) {
+ const uint8_t inst[] = {
+ 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00,
+ 0x00, 0x00, // mov %fs:0x0,%rax
+ 0x48, 0x8d, 0x80, 0, 0, 0, 0, // lea x at tpoff,%rax
+ 0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00, // 6-byte nop
+ };
+ memcpy(loc - 3, inst, sizeof(inst));
+ write32le(loc + 9, val + 4);
+ return;
+ }
// Convert
// .byte 0x66
// leaq x at tlsgd(%rip), %rdi
@@ -830,6 +850,18 @@ void X86_64::relaxTlsGdToLe(uint8_t *loc, const Relocation &rel,
void X86_64::relaxTlsGdToIe(uint8_t *loc, const Relocation &rel,
uint64_t val) const {
if (rel.type == R_X86_64_TLSGD) {
+ if (isPltOff64Tls(loc)) {
+ const uint8_t inst[] = {
+ 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00,
+ 0x00, 0x00, // mov %fs:0x0,%rax
+ 0x48, 0x03, 0x05, 0, 0, 0, 0, // addq x at gottpoff(%rip),%rax
+ 0x66, 0x0f, 0x1f, 0x44, 0x00, 0x00, // nopw 0x0(%rax,%rax,1)
+ };
+ memcpy(loc - 3, inst, sizeof(inst));
+ // The new displacement is 9 bytes ahead of the original one.
+ write32le(loc + 9, val - 9);
+ return;
+ }
// Convert
// .byte 0x66
// leaq x at tlsgd(%rip), %rdi
@@ -1006,6 +1038,19 @@ void X86_64::relaxTlsLdToLe(uint8_t *loc, const Relocation &rel,
return;
}
+ if (isPltOff64Tls(loc)) {
+ // Convert the sequence to
+ // data16 data16 data16 cs nopw 0x0(%rax,%rax,1)
+ // movq %fs:0,%rax
+ const uint8_t inst[] = {
+ 0x66, 0x66, 0x66, 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00,
+ 0x00, 0x00, 0x00, 0x00, // 13-byte nop
+ 0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00, // movq %fs:0,%rax
+ };
+ memcpy(loc - 3, inst, sizeof(inst));
+ return;
+ }
+
ErrAlways(ctx)
<< getErrorLoc(ctx, loc - 3)
<< "expected R_X86_64_PLT32 or R_X86_64_GOTPCRELX after R_X86_64_TLSLD";
diff --git a/lld/test/ELF/x86-64-tls-pltoff64.s b/lld/test/ELF/x86-64-tls-pltoff64.s
new file mode 100644
index 0000000000000..d3409353008f1
--- /dev/null
+++ b/lld/test/ELF/x86-64-tls-pltoff64.s
@@ -0,0 +1,100 @@
+# REQUIRES: x86
+## Test dynamic TLS sequences that call __tls_get_addr indirectly through
+## R_X86_64_PLTOFF64 instead of a direct call.
+
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64 b.s -o b.o
+# RUN: ld.lld -shared b.o -soname=b.so -o b.so
+
+# RUN: ld.lld a.o b.so -o out
+# RUN: llvm-readelf -Sr out | FileCheck %s --check-prefix=SEC
+# RUN: llvm-objdump -d --no-show-raw-insn --no-print-imm-hex out | FileCheck %s --check-prefix=EXE
+
+# RUN: ld.lld -shared a.o b.so -o out.so
+# RUN: llvm-readelf -r out.so | FileCheck %s --check-prefix=SDYN
+# RUN: llvm-objdump -d --no-show-raw-insn --no-print-imm-hex out.so | FileCheck %s --check-prefix=SHARED
+
+# SEC: .got PROGBITS 0000000000202418
+# SEC: Relocation section '.rela.dyn' {{.*}} contains 1 entries:
+# SEC: 0000000000202418 {{.*}} R_X86_64_TPOFF64 {{.*}} y + 0
+
+## The TLS block is 15 bytes. x1 is at DTPOFF 7 and TPOFF -8, and x2 at DTPOFF 11
+## and TPOFF -4. Each optimized sequence is padded with a nop to the original 22 bytes.
+# EXE-LABEL: <_start>:
+# EXE: movq %fs:0, %rax
+# EXE-NEXT: leaq -8(%rax), %rax
+# EXE-NEXT: nopw (%rax,%rax)
+
+## y is preemptible. Its GD sequence is optimized to IE.
+# EXE-NEXT: movq %fs:0, %rax
+# EXE-NEXT: addq 4331(%rip), %rax # 0x202418
+# EXE-NEXT: nopw (%rax,%rax)
+
+# EXE-NEXT: nopw %cs:(%rax,%rax)
+# EXE-NEXT: movq %fs:0, %rax
+# EXE-NEXT: leaq -8(%rax), %rcx
+# EXE-NEXT: leaq -4(%rax), %rdx
+
+# SDYN: Relocation section '.rela.dyn' {{.*}} contains 4 entries:
+# SDYN: 00000000000024f0 {{.*}} R_X86_64_DTPMOD64 0
+# SDYN-NEXT: 0000000000002500 {{.*}} R_X86_64_DTPMOD64 0
+# SDYN-NEXT: 0000000000002510 {{.*}} R_X86_64_DTPMOD64 {{.*}} y + 0
+# SDYN-NEXT: 0000000000002518 {{.*}} R_X86_64_DTPOFF64 {{.*}} y + 0
+# SDYN: Relocation section '.rela.plt' {{.*}} contains 1 entries:
+# SDYN: {{.*}} R_X86_64_JUMP_SLOT {{.*}} __tls_get_addr + 0
+
+# SHARED-LABEL: <_start>:
+# SHARED: leaq 4458(%rip), %rdi # 0x2500
+# SHARED-NEXT: movabsq $-8496, %rax
+# SHARED-NEXT: addq %rbx, %rax
+# SHARED-NEXT: callq *%rax
+# SHARED-NEXT: leaq 4452(%rip), %rdi # 0x2510
+# SHARED-NEXT: movabsq $-8496, %rax
+# SHARED-NEXT: addq %rbx, %rax
+# SHARED-NEXT: callq *%rax
+# SHARED-NEXT: leaq 4398(%rip), %rdi # 0x24f0
+# SHARED-NEXT: movabsq $-8496, %rax
+# SHARED-NEXT: addq %r15, %rax
+# SHARED-NEXT: callq *%rax
+## x1 is at DTPOFF 7
+# SHARED-NEXT: leaq 7(%rax), %rcx
+# SHARED-NEXT: leaq 11(%rax), %rdx
+
+#--- a.s
+.globl _start
+_start:
+.L2:
+ leaq .L2(%rip), %rbx
+ movabsq $_GLOBAL_OFFSET_TABLE_-.L2, %r11
+ addq %r11, %rbx
+ movq %rbx, %r15
+
+ leaq x1 at tlsgd(%rip), %rdi
+ movabsq $__tls_get_addr at PLTOFF, %rax
+ addq %rbx, %rax
+ callq *%rax
+
+ leaq y at tlsgd(%rip), %rdi
+ movabsq $__tls_get_addr at PLTOFF, %rax
+ addq %rbx, %rax
+ callq *%rax
+
+ leaq x1 at tlsld(%rip), %rdi
+ movabsq $__tls_get_addr at PLTOFF, %rax
+ addq %r15, %rax
+ callq *%rax
+ leaq x1 at dtpoff(%rax), %rcx
+ leaq x2 at dtpoff(%rax), %rdx
+
+.section .tbss,"awT", at nobits
+.globl x1, x2
+.hidden x1, x2
+.space 7
+x1: .zero 4
+x2: .zero 4
+
+#--- b.s
+.section .tbss,"awT", at nobits
+.globl y
+y: .zero 4
More information about the llvm-commits
mailing list