[lld] [LLD/AArch64] BTI link-time relaxation: NOP redundant `bti c` for internal functions (PR #187871)

Junjie Peng via llvm-commits llvm-commits at lists.llvm.org
Mon Apr 13 04:41:03 PDT 2026


https://github.com/HacoK updated https://github.com/llvm/llvm-project/pull/187871

>From b2e344774465ac5717888070be4cac9fba0f576c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot at users.noreply.github.com>
Date: Sat, 21 Mar 2026 15:12:46 +0000
Subject: [PATCH 1/5] Initial plan


>From 11262a0cdc575d887b7e3591016f0e266420a156 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot at users.noreply.github.com>
Date: Sat, 21 Mar 2026 16:04:20 +0000
Subject: [PATCH 2/5] Implement BTI link-time relaxation: NOP bti c for
 non-global, non-address-taken functions

Co-authored-by: HacoK <39146962+HacoK at users.noreply.github.com>
Agent-Logs-Url: https://github.com/HacoK/llvm-project/sessions/fb4b478d-d871-4245-9693-c5c384a39cc1
---
 lld/ELF/Arch/AArch64.cpp | 127 +++++++++++++++++++++++++++++++++++++++
 lld/ELF/Relocations.cpp  |   3 +
 lld/ELF/Target.h         |   1 +
 3 files changed, 131 insertions(+)

diff --git a/lld/ELF/Arch/AArch64.cpp b/lld/ELF/Arch/AArch64.cpp
index f85a3f48f2183..f595f4b8ab098 100644
--- a/lld/ELF/Arch/AArch64.cpp
+++ b/lld/ELF/Arch/AArch64.cpp
@@ -1147,6 +1147,133 @@ void AArch64::applyBranchToBranchOpt() const {
                              redirectControlTransferRelocations);
 }
 
+// Returns true if the AArch64 relocation type is a direct branch/call that
+// does not constitute an address-taken use of a function symbol.  A function
+// reached only via such relocations never has its address stored anywhere and
+// therefore does not need a BTI landing pad.
+static bool isAArch64DirectBranchReloc(RelType type) {
+  switch (type) {
+  case R_AARCH64_CALL26:
+  case R_AARCH64_JUMP26:
+  case R_AARCH64_CONDBR19:
+  case R_AARCH64_TSTBR14:
+  case R_AARCH64_PLT32:
+    return true;
+  default:
+    return false;
+  }
+}
+
+// BTI link-time relaxation: replace a redundant "bti c" instruction at the
+// entry of a function with a NOP when the linker can prove that the function
+// is never the target of an indirect branch.
+//
+// The compiler conservatively adds "bti c" to every function with external
+// linkage because it cannot know at compile time whether the function will be
+// called indirectly.  The linker, however, has whole-program visibility and
+// can determine which functions are only ever reached via direct BL/B
+// branches.  For those functions the "bti c" is redundant and can be replaced
+// with a NOP, reducing code size without compromising security.
+//
+// A function's "bti c" is KEPT when any of the following hold:
+//   isGlobal(sym):       preemptible (dynamic interposition), exported to
+//                        .dynsym, or in a shared library where all globally
+//                        visible symbols are potentially called indirectly.
+//   isAddressTaken(sym): has a PLT entry (PLT uses an indirect branch),
+//                        has a GOT entry (address can be loaded and used as a
+//                        function pointer), is an ifunc (always uses IPLT),
+//                        or is referenced by a non-direct-branch relocation
+//                        (address stored in data or computed in code for use
+//                        as a function pointer).
+void elf::relaxAArch64BTI(Ctx &ctx) {
+  if (!ctx.arg.relax)
+    return;
+  if (!(ctx.arg.andFeatures & GNU_PROPERTY_AARCH64_FEATURE_1_BTI))
+    return;
+
+  // Step 1: Scan all processed relocations across all input sections and
+  // collect the set of function symbols whose address is taken via a
+  // non-direct-branch relocation.  Such symbols cannot have their "bti c"
+  // removed because their address may be loaded and used to perform an
+  // indirect call (BLR) at runtime.
+  DenseSet<const Symbol *> addrTaken;
+  for (InputSectionBase *sec : ctx.inputSections) {
+    if (!(sec->flags & SHF_ALLOC) || sec == &InputSection::discarded)
+      continue;
+    for (const Relocation &rel : sec->relocs()) {
+      if (!rel.sym || !rel.sym->isDefined())
+        continue;
+      if (rel.sym->type != STT_FUNC && rel.sym->type != STT_GNU_IFUNC)
+        continue;
+      // A direct branch relocation does not expose the function's address;
+      // any other relocation type potentially does.
+      if (!isAArch64DirectBranchReloc(rel.type))
+        addrTaken.insert(rel.sym);
+    }
+  }
+
+  // Step 2: Collect (section, offset) pairs where a "bti c" instruction
+  // should be replaced with a NOP.
+  DenseMap<InputSection *, SmallVector<uint64_t, 4>> toNOP;
+
+  auto tryRelax = [&](Symbol &sym) {
+    auto *d = dyn_cast<Defined>(&sym);
+    if (!d)
+      return;
+
+    // isGlobal(sym): keep "bti c" for symbols visible outside this link unit.
+    if (d->isPreemptible || d->isExported)
+      return;
+    // In a shared library every non-hidden global symbol is reachable from
+    // outside and may be called indirectly.
+    if (ctx.arg.shared)
+      return;
+
+    // isAddressTaken(sym): keep "bti c" if the address is used indirectly.
+    if (d->isInPlt(ctx) || d->isInGot(ctx) || d->isGnuIFunc())
+      return;
+    if (addrTaken.count(d))
+      return;
+
+    auto *isec = dyn_cast_or_null<InputSection>(d->section);
+    if (!isec || !(isec->flags & SHF_EXECINSTR))
+      return;
+
+    uint64_t off = d->value;
+    if (off + 4 > isec->getSize())
+      return;
+
+    const uint8_t *buf = isec->content().begin();
+    if (!buf || read32le(buf + off) != 0xd503245f) // bti c
+      return;
+
+    toNOP[isec].push_back(off);
+  };
+
+  for (Symbol *sym : ctx.symtab->getSymbols())
+    tryRelax(*sym);
+
+  // Local symbols may also carry a "bti c" when their address was taken in
+  // the source.  The addrTaken set (built above) already accounts for this:
+  // if a local symbol's address is referenced by a non-branch relocation it
+  // will be in addrTaken and will not be relaxed.
+  for (ELFFileBase *file : ctx.objectFiles)
+    for (Symbol *sym : file->getLocalSymbols())
+      tryRelax(*sym);
+
+  // Step 3: For each section that needs modification, allocate a fresh
+  // mutable copy of the content, patch the "bti c" instructions to NOPs,
+  // and redirect the section to use the new buffer.
+  for (auto &[isec, offsets] : toNOP) {
+    ArrayRef<uint8_t> orig = isec->content();
+    uint8_t *newBuf = makeThreadLocalN<uint8_t>(orig.size());
+    memcpy(newBuf, orig.data(), orig.size());
+    for (uint64_t off : offsets)
+      write32le(newBuf + off, 0xd503201f); // nop
+    isec->content_ = newBuf;
+  }
+}
+
 // AArch64 may use security features in variant PLT sequences. These are:
 // Pointer Authentication (PAC), introduced in armv8.3-a and Branch Target
 // Indicator (BTI) introduced in armv8.5-a. The additional instructions used
diff --git a/lld/ELF/Relocations.cpp b/lld/ELF/Relocations.cpp
index ee6b14f2798ed..6da8e23981a86 100644
--- a/lld/ELF/Relocations.cpp
+++ b/lld/ELF/Relocations.cpp
@@ -1431,6 +1431,9 @@ void elf::postScanRelocations(Ctx &ctx) {
 
   if (ctx.arg.branchToBranch)
     ctx.target->applyBranchToBranchOpt();
+
+  if (ctx.arg.emachine == EM_AARCH64)
+    relaxAArch64BTI(ctx);
 }
 
 static bool mergeCmp(const InputSection *a, const InputSection *b) {
diff --git a/lld/ELF/Target.h b/lld/ELF/Target.h
index 8fa8cd13f1b67..c95d7ef39d1cf 100644
--- a/lld/ELF/Target.h
+++ b/lld/ELF/Target.h
@@ -259,6 +259,7 @@ void addPPC64SaveRestore(Ctx &);
 uint64_t getPPC64TocBase(Ctx &ctx);
 uint64_t getAArch64Page(uint64_t expr);
 bool isAArch64BTILandingPad(Ctx &, Symbol &s, int64_t a);
+void relaxAArch64BTI(Ctx &);
 template <typename ELFT> void writeARMCmseImportLib(Ctx &);
 uint64_t getLoongArchPageDelta(uint64_t dest, uint64_t pc, RelType type);
 void riscvFinalizeRelax(int passes);

>From 7b28fa2aa3788827482d58e3623322d20c168b29 Mon Sep 17 00:00:00 2001
From: Junjie Peng <877728156 at qq.com>
Date: Mon, 13 Apr 2026 16:00:10 +0800
Subject: [PATCH 3/5] Add include for SymbolTable in AArch64.cpp

---
 lld/ELF/Arch/AArch64.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lld/ELF/Arch/AArch64.cpp b/lld/ELF/Arch/AArch64.cpp
index f595f4b8ab098..e59aa649dc7bd 100644
--- a/lld/ELF/Arch/AArch64.cpp
+++ b/lld/ELF/Arch/AArch64.cpp
@@ -10,6 +10,7 @@
 #include "OutputSections.h"
 #include "RelocScan.h"
 #include "Symbols.h"
+#include "SymbolTable.h"
 #include "SyntheticSections.h"
 #include "Target.h"
 #include "TargetImpl.h"

>From f436cd7a05a5118f300737b46cd2604d9612f916 Mon Sep 17 00:00:00 2001
From: Junjie Peng <877728156 at qq.com>
Date: Mon, 13 Apr 2026 16:02:01 +0800
Subject: [PATCH 4/5] [lld][ELF] Gate AArch64 BTI relaxation and make it
 conservative

---
 lld/ELF/Arch/AArch64.cpp | 37 ++++++++++++++++++++++++++-----------
 lld/ELF/Config.h         |  1 +
 lld/ELF/Driver.cpp       |  2 ++
 lld/ELF/Options.td       |  3 +++
 lld/ELF/Relocations.cpp  |  2 +-
 5 files changed, 33 insertions(+), 12 deletions(-)

diff --git a/lld/ELF/Arch/AArch64.cpp b/lld/ELF/Arch/AArch64.cpp
index f595f4b8ab098..d882c9acd40c3 100644
--- a/lld/ELF/Arch/AArch64.cpp
+++ b/lld/ELF/Arch/AArch64.cpp
@@ -9,6 +9,7 @@
 #include "InputFiles.h"
 #include "OutputSections.h"
 #include "RelocScan.h"
+#include "SymbolTable.h"
 #include "Symbols.h"
 #include "SyntheticSections.h"
 #include "Target.h"
@@ -1185,6 +1186,11 @@ static bool isAArch64DirectBranchReloc(RelType type) {
 //                        or is referenced by a non-direct-branch relocation
 //                        (address stored in data or computed in code for use
 //                        as a function pointer).
+//   hasDirectBranchReloc(sym): at least one direct branch relocation was seen
+//                        to this symbol. This keeps the transform
+//                        conservative if branch instructions are emitted
+//                        without relocations or if relocations are expressed
+//                        through aliases/section symbols.
 void elf::relaxAArch64BTI(Ctx &ctx) {
   if (!ctx.arg.relax)
     return;
@@ -1192,11 +1198,14 @@ void elf::relaxAArch64BTI(Ctx &ctx) {
     return;
 
   // Step 1: Scan all processed relocations across all input sections and
-  // collect the set of function symbols whose address is taken via a
-  // non-direct-branch relocation.  Such symbols cannot have their "bti c"
-  // removed because their address may be loaded and used to perform an
-  // indirect call (BLR) at runtime.
+  // collect:
+  // - function symbols whose address is taken via a non-direct-branch
+  //   relocation;
+  // - function symbols that are the destination of a direct branch
+  //   relocation.
+  // Symbols without direct branch relocations are not relaxed.
   DenseSet<const Symbol *> addrTaken;
+  DenseSet<const Defined *> hasDirectBranchReloc;
   for (InputSectionBase *sec : ctx.inputSections) {
     if (!(sec->flags & SHF_ALLOC) || sec == &InputSection::discarded)
       continue;
@@ -1205,10 +1214,15 @@ void elf::relaxAArch64BTI(Ctx &ctx) {
         continue;
       if (rel.sym->type != STT_FUNC && rel.sym->type != STT_GNU_IFUNC)
         continue;
-      // A direct branch relocation does not expose the function's address;
-      // any other relocation type potentially does.
-      if (!isAArch64DirectBranchReloc(rel.type))
+      // A direct branch relocation does not expose the function's address.
+      // Record it as evidence that the function is reached by direct control
+      // transfer. Any other relocation potentially exposes the address.
+      if (isAArch64DirectBranchReloc(rel.type)) {
+        if (auto *d = dyn_cast<Defined>(rel.sym))
+          hasDirectBranchReloc.insert(d);
+      } else {
         addrTaken.insert(rel.sym);
+      }
     }
   }
 
@@ -1234,6 +1248,8 @@ void elf::relaxAArch64BTI(Ctx &ctx) {
       return;
     if (addrTaken.count(d))
       return;
+    if (!hasDirectBranchReloc.count(d))
+      return;
 
     auto *isec = dyn_cast_or_null<InputSection>(d->section);
     if (!isec || !(isec->flags & SHF_EXECINSTR))
@@ -1253,10 +1269,9 @@ void elf::relaxAArch64BTI(Ctx &ctx) {
   for (Symbol *sym : ctx.symtab->getSymbols())
     tryRelax(*sym);
 
-  // Local symbols may also carry a "bti c" when their address was taken in
-  // the source.  The addrTaken set (built above) already accounts for this:
-  // if a local symbol's address is referenced by a non-branch relocation it
-  // will be in addrTaken and will not be relaxed.
+  // Local symbols may also carry a "bti c". They are relaxed only when
+  // their address is not taken and at least one direct branch relocation
+  // targets the symbol.
   for (ELFFileBase *file : ctx.objectFiles)
     for (Symbol *sym : file->getLocalSymbols())
       tryRelax(*sym);
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index a9f74460f6f99..239d1dced5cf9 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -305,6 +305,7 @@ struct Config {
   bool bpDataOrderForCompression = false;
   bool bpVerboseSectionOrderer = false;
   bool branchToBranch = false;
+  bool aarch64BtiRelax = false;
   bool checkSections;
   bool checkDynamicRelocs;
   std::optional<llvm::DebugCompressionType> compressDebugSections;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index fac8fa2db04d6..9c9dc5c8b48df 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1664,6 +1664,8 @@ static void readConfigs(Ctx &ctx, opt::InputArgList &args) {
   ctx.arg.power10Stubs = args.getLastArgValue(OPT_power10_stubs_eq) != "no";
   ctx.arg.branchToBranch = args.hasFlag(
       OPT_branch_to_branch, OPT_no_branch_to_branch, ctx.arg.optimize >= 2);
+  ctx.arg.aarch64BtiRelax = args.hasFlag(
+      OPT_aarch64_bti_relax, OPT_no_aarch64_bti_relax, false);
 
   if (opt::Arg *arg = args.getLastArg(OPT_eb, OPT_el)) {
     if (arg->getOption().matches(OPT_eb))
diff --git a/lld/ELF/Options.td b/lld/ELF/Options.td
index c2111e58c12b9..d174a542d8db1 100644
--- a/lld/ELF/Options.td
+++ b/lld/ELF/Options.td
@@ -62,6 +62,9 @@ def : F<"build-id">, Alias<build_id>, AliasArgs<["sha1"]>, HelpText<"Alias for -
 defm branch_to_branch: BB<"branch-to-branch",
     "Enable branch-to-branch optimization (default at -O2)",
     "Disable branch-to-branch optimization (default at -O0 and -O1)">;
+defm aarch64_bti_relax: BB<"aarch64-bti-relax",
+    "Enable AArch64 BTI relaxation",
+    "Disable AArch64 BTI relaxation (default)">;
 
 defm check_sections: B<"check-sections",
     "Check section addresses for overlaps (default)",
diff --git a/lld/ELF/Relocations.cpp b/lld/ELF/Relocations.cpp
index 6da8e23981a86..bf9b90c37a9d4 100644
--- a/lld/ELF/Relocations.cpp
+++ b/lld/ELF/Relocations.cpp
@@ -1432,7 +1432,7 @@ void elf::postScanRelocations(Ctx &ctx) {
   if (ctx.arg.branchToBranch)
     ctx.target->applyBranchToBranchOpt();
 
-  if (ctx.arg.emachine == EM_AARCH64)
+  if (ctx.arg.emachine == EM_AARCH64 && ctx.arg.aarch64BtiRelax)
     relaxAArch64BTI(ctx);
 }
 

>From 9de1e3fa919a91ebde29a1e610359ea088214fff Mon Sep 17 00:00:00 2001
From: Junjie Peng <877728156 at qq.com>
Date: Mon, 13 Apr 2026 16:06:51 +0800
Subject: [PATCH 5/5] Remove duplicate include of SymbolTable.h

---
 lld/ELF/Arch/AArch64.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lld/ELF/Arch/AArch64.cpp b/lld/ELF/Arch/AArch64.cpp
index 6b83bb7763d7e..29c706628a79c 100644
--- a/lld/ELF/Arch/AArch64.cpp
+++ b/lld/ELF/Arch/AArch64.cpp
@@ -9,7 +9,6 @@
 #include "InputFiles.h"
 #include "OutputSections.h"
 #include "RelocScan.h"
-#include "SymbolTable.h"
 #include "Symbols.h"
 #include "SymbolTable.h"
 #include "SyntheticSections.h"



More information about the llvm-commits mailing list