[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