[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