LLVM 23.1 has been released. As usual, I maintain lld/ELF and as volunteer work have added some notes to https://github.com/llvm/llvm-project/blob/release/23.x/lld/docs/ReleaseNotes.rst. I've reviewed almost all the patches not authored by me.
Like last time, I used Claude Code to summarize
git log llvmorg-23-init..origin/release/23.x -- lld/ELF,
excluding changes cherry-picked into 22.x
(git rev-list llvmorg-23-init..llvmorg-22.1.8 -- lld), and
then edited the draft.
This was a busy cycle: 141 commits landed in lld/ELF between the branch point (2026-01-13) and 23.1.0-rc1 (2026-07-16), compared with 72 in the 22 cycle. Much of the increase is performance work, which I described in Recent lld/ELF performance improvements. lld 23 is the first release that ships all of it.
- Input file loading is now parallelized, meaningfully reducing link time for large links. (#191690)
--gc-sectionsmark phase is now parallelized. (#189321)- Relocation scanning was rewritten as target-specific scanners for all targets with shared library support, devirtualizing the hot relocation-classification path.
- Added
--bp-compression-sort-section=<glob>[=<layout_priority>[=<match_priority>]], replacing the old coarse--bp-compression-sortmodes with a way to split input sections into multiple compression groups, run balanced partitioning independently per group, and leave out sections that are poor candidates for BP. (#185661) - Added
-z memtag-{mode,heap,stack}as generic replacements for the Android-specific--android-memtag-*flags;--android-memtag-notekeeps the Android-specific memtag note opt-in. (#188205) - Unused space in executable output sections is now filled with trap
instructions, primarily for
-z separate-codemode. (#176845) .eh_frame_hdrnow supports theDW_EH_PE_sdata8encoding, auto-upgrading fromsdata4when a table entry or the frame pointer exceeds the 32-bit range, instead of erroring out for large executables. (#179089)vna_flagsis now set toVER_FLG_WEAKwhen all undefined references to a version are weak, allowing glibc's dynamic loader to warn instead of error when the version is missing at runtime. (#176673).ltext.*input sections are now merged into a single.ltextoutput section, matching the existing.ldata.*/.lrodata.*/.lbss.*handling for the large code model with-ffunction-sections. (#190305).gnu.build.attributes.*input sections are now concatenated into one output section, matching GNU ld. (#208737).tbssoutput sections may now use an explicit address expression; previously it was silently overridden to follow the preceding.tbss. (#196447)--discard-locals/--discard-allcombined with-r/--emit-relocsno longer discard local symbols that are referenced only from retained non-SHF_ALLOCsections (e.g..Lsymbols referenced by.debug_info), fixing DWARF corruption in the output. (#209035) (#209042)--retain-symbols-filenow filters.symtabinstead of.dynsym, matching GNU ld. (#209063)- When
-ois-, the output is written to thelld::outs()stream instead of the process's stdout, so that library users can capture it. (#209064) INCLUDEin linker scripts now fully parses its own content instead of sharing a lexer buffer stack with the includer, fixing spurious acceptance of malformed scripts. (#193427)- The
OVERLAYlinker script command now accepts any output-section-command (e.g. symbol assignments), not just input section descriptions. (#203524) - Thunks are no longer reused across an
OVERLAYboundary unless the target output section is guaranteed to be resident at the same time. (#200415) - When a
SECTIONScommand interleaves relro and non-relro sections, lld now emits onePT_GNU_RELROsegment per contiguous run of relro sections instead of reporting anot contiguous with other relro sectionserror. (#203675) SHT_NOBITSsections are now excluded from LMA overlap checks, matching GNU ld and allowing e.g. a startup section to share an LMA with.bssin embedded linker scripts. (#196423)- LTO: the middle-end no longer emits new references to, or internalizes, symbols defined in bitcode after the extracted-bitcode set has been fixed, preventing undefined symbol references from transforms that run after linking has determined which bitcode files to extract. (#164916)
- DTLTO: significantly improved the performance of adding backend output files to the link, especially on Windows. (#186366)
- For AArch64, fixed
.relr.auth.dyn->.rela.dynmovement to properly adjust__rela_iplt_start/__rela_iplt_endand size the.dynamicsection for both tags. (#195649) - For AArch64, handle Memtag globals for
R_AARCH64_AUTH_ABS64. (#173291) - For AArch64, fixed TLS GD against non-preemptible dynamic symbols
(e.g.
protectedor-Bsymbolic) in DSOs, which previously produced an inconsistent GOT entry and spurious preemption. (#207881) - For AArch64,
adrp+ldrGOT relaxation is now decided per-symbol, all-or-nothing, avoiding invalid relaxation when a branch target sits between theadrpandldrof a pair. (#208396) - For AArch64, a redundant local-exec TLS
addwith a zero high-12-bits immediate is now relaxed to anop. (#204286) - For AArch64,
-z bti-report=noneand-z gcs-report=nonenow silence the warnings implied by-z force-btiand-z gcs=always. (#186343) - For Hexagon, fixed out-of-range PLT branch thunks and TLS GD PLT entry creation. (#186545) (#180297)
- For LoongArch, fixed range checking of
R_LARCH_*_PCADD_HI20relocations on 64-bit and DTPREL relocations in debug sections. (#183233) (#199327) - For MIPS, fixed the addend for preemptible static TLS. (#150729)
- For x86-64, CFI jump table relaxation reduces the runtime overhead of indirect calls under Control Flow Integrity by opportunistically moving eligible function bodies into the jump table itself. (#147424)
Breaking changes:
- The symbol partition feature has been removed. lld no longer
recognizes
SHT_LLVM_SYMPARTsections, which are now treated as ordinary sections. (#198718) (#199186) - An output section that has an address expression, and is also
assigned to a
MEMORYregion, now uses the address expression in preference to the next available location in the region, matching GNU ld. (#197293) - The default extension for time trace files is now
.time-trace.json. (#122207)
Jessica Clarke (jrtc27) and Peter Smith (smithp35) continued to
contribute patches and reviews. jrtc27 fixed several TLS corner cases
(AArch64 TLS GD against non-preemptible symbols, MIPS static TLS
addends, SystemZ R_390_TLS_LDO in
non-SHF_ALLOC sections). Brian Cain fixed a batch of
Hexagon issues.
Performance
The parallelization work from the first half of 2026 all landed before release/23.x was branched in July:
- Parallel input file loading (#191690).
createFilesrecords a job per non-script input, andloadFilesfans the jobs out to worker threads. Linker scripts stay on the main thread sinceINPUT()/GROUP()recurse into the driver. - Parallel
--gc-sectionsmark (#189321), a level-synchronized BFS with per-shard queues. A follow-up replacedparallel::getThreadIndexwith a shared counter and explicit shards, which improved load balancing (#208974). - Relocation scanning is parallel for
-z nocombrelocand PPC64 as well (#190309); only MIPS still scans serially. - Target-specific relocation scanning: each target's
scanSectionImplinlinesgetRelExprand TLS optimization dispatch. x86 (#178846), AArch64 (#181099), ARM (#182440), RISC-V (#181332), LoongArch (#182236), Hexagon (#181596), SystemZ (#181563), PPC32 (#181517), PPC64 (#181496), and SPARCV9 (#206284). - Faster
getSectionPiecefor merge sections (#187916). - Parallel
demoteAndCopyLocalSymbols(#187970) anddemoteSymbolsAndComputeIsPreemptible(#207310), parallel orphan output section name computation (#207321), and reusingSHT_GROUPselection verdicts ininitializeSections(#207437). llvm/lib/Support/Parallel.cppimprovements:parallelForspawns onlyThreadCountworkers that claim chunks with an atomic counter,TaskGroup::spawnavoids a mutex, theExecutorvirtual base class is gone, and nestedTaskGroups use work stealing instead of degenerating to serial execution.
Two of these needed extra care after they landed.
--reproduce output became non-deterministic with parallel
loading, fixed in #196773.
handleTlsIe could call the unsynchronized
addRelativeReloc from concurrent scan tasks
(R_386_TLS_IE in -shared links), fixed in #208956; the
test was restructured so that a ThreadSanitizer build detects the
race.
Here is lld 22.1.8 versus lld 23.1.1 on an Intel i7-14700K, both
built from the release tags with the same compiler and configuration
(-DLLVM_TARGETS_TO_BUILD=X86, release, no assertions). The
workloads are nine x86-64 release-mode links captured with
--reproduce (Clang 21.1.8, Chromium 145, Firefox 149
libxul.so, Blender 5.2, ClickHouse 26.1, Godot 4.6,
LibreOffice libmergedlo.so, PyTorch
libtorch_cpu.so, TensorFlow
libtensorflow_cc.so), linked with --threads=8,
pinned to the 8 performance cores, output written to tmpfs,
hyperfine -w 2 -r 10. The debug bundles have 13-19 GiB of
input and were I/O bound on this machine, so I left them out.
| Workload | lld 22.1.8 | lld 23.1.1 | Speedup |
|---|---|---|---|
| Clang 21.1.8 | 242.6 ms | 207.4 ms | 1.17x |
Chromium 145 (--gc-sections --icf=all) |
5.252 s | 4.749 s | 1.11x |
Firefox 149 libxul.so (--gc-sections) |
516.1 ms | 424.2 ms | 1.22x |
| Blender 5.2 | 521.1 ms | 493.7 ms | 1.06x |
ClickHouse 26.1 (--gc-sections) |
1.920 s | 1.669 s | 1.15x |
| Godot 4.6 | 221.0 ms | 169.7 ms | 1.30x |
LibreOffice libmergedlo.so |
463.4 ms | 398.5 ms | 1.16x |
PyTorch libtorch_cpu.so |
294.9 ms | 278.2 ms | 1.06x |
TensorFlow libtensorflow_cc.so
(--gc-sections) |
5.966 s | 5.608 s | 1.06x |
lld 23.1 is 1.06x to 1.30x as fast as lld 22.1 on these links.
For the full breakdown, including comparisons with mold and wild, see Recent lld/ELF performance improvements.
CFI jump table relaxation
Clang's Control-Flow Integrity (-fsanitize=cfi-icall and
friends) implements indirect call checks with jump tables. For every
address-taken function f, the LowerTypeTests
pass renames the body to f.cfi and creates a jump table
entry f that is just jmp f.cfi. Functions with
the same type signature get consecutive entries, so checking that an
indirect call target has the expected type is a range and alignment
check on the address. The address-taken symbol f refers to
the jump table entry, not the body.
On x86-64 each entry is 8 bytes (a 5-byte jmp padded
with int3):
1 | .section .text.cfi.jumptable,"ax",@llvm_cfi_jump_table,8 |
Every indirect call pays an extra jmp, and the jump
tables are clustered together, typically far from the callees, which
hurts the i-cache and the iTLB. Google's internal benchmarking
attributed around 30% of the total CFI overhead to this indirection. lld
21 added --branch-to-branch (enabled at -O2),
which retargets a direct call f to f.cfi (#145579),
but indirect calls still went through the table.
lld 23 adds CFI jump table relaxation (#147424),
also controlled by --branch-to-branch. LLVM 22 defined a
new section type SHT_LLVM_CFI_JUMP_TABLE with
sh_entsize set to the entry size, and Clang 23 emits CFI
jump tables with this type. X86_64::relaxCFIJumpTables
walks such sections and eliminates the indirection in two cases:
- If
f.cfiis at mostsh_entsizebytes (and no more aligned than that), the function body is moved into the jump table in place of the entry. The jump table is split into slices around the moved bodies. - The last entry's target is handled specially: the jump table is
placed immediately before the target function, and the last
jmpis deleted so that control falls through. The function stays where it is, on the assumption that functions of the same type (and their callees) are more likely to share a page with each other than with the jump table's original location.
The eligibility check is conservative: the branch must be a
R_X86_64_PC32/R_X86_64_PLT32 to the start of a
non-preemptible, non-IFUNC Defined symbol, the section may
not be over-aligned relative to the entry size, and a function body is
only moved once even if multiple jump tables reference it. The commit
message reports a 0.2 to 0.5 percentage point reduction in CFI overhead
(10-25% of the overhead) on a large internal Google benchmark.
Symbol partitions removed
The symbol partition feature (2019, D60242 and D60353) allowed an executable
or shared object to be split into a main partition and several loadable
partitions. Symbols were assigned to partitions with
SHT_LLVM_SYMPART sections,
llvm-objcopy --extract-partition extracted each partition
into a separate file, and the loader could dlopen a feature
partition on demand, sharing the main partition's memory. It was built
for Chrome on Android to lazily load features.
The feature touched a surprising amount of the linker: per-partition
MarkLive runs, per-partition
.dynamic/.dynsym/.gnu.hash/.eh_frame/.ARM.exidx,
cross-partition pull in InputSection::replace, thunk
compatibility checks, PartitionIndexSection, and more.
Chrome hasn't needed it for years, and I am not aware of any other user.
#198718
reduced the machinery to a shim, and #199186
removed the rest. SHT_LLVM_SYMPART sections are now treated
as ordinary sections, and
ctx.mainPart/ctx.partitions are gone from the
code base.
Removing an experimental feature that never found a second user simplifies a lot of code paths, particularly synthetic sections, which no longer need to be instantiated per partition.
Linker script robustness
Several linker script fixes landed this cycle.
INCLUDE used to push a buffer onto the lexer's buffer
stack, so a construct started in the included file could be closed by
the parent. This accepted malformed scripts (a missing ; in
the include, terminated by the parent's ;) and computed
undefined-behavior spans in readAssignment. #193427
makes each INCLUDE parse its own content like a call frame;
the top-level, SECTIONS, output section, and
MEMORY call sites pass a context-appropriate parser
callback.
OVERLAY now accepts any output-section-command, e.g.
symbol assignments, not just input section descriptions (#203524).
Thunks in an OVERLAY are no longer reused by callers in a
different output section, since the thunk may not be resident when the
caller runs (#200415).
Peter Smith notes that Tightly Coupled Memory regions could be modeled
the same way.
When a SECTIONS command interleaves relro and non-relro
sections, lld used to error with
not contiguous with other relro sections (D40359). glibc only honors
the first PT_GNU_RELRO, but Bionic and FreeBSD rtld protect
every PT_GNU_RELRO segment, so #203675
emits one PT_GNU_RELRO per contiguous run of relro
sections. Each relro-to-non-relro transition still starts a fresh
PT_LOAD.
SHT_NOBITS sections are excluded from LMA overlap checks
(#196423),
matching GNU ld, which lets embedded scripts place a startup section at
the same load address as .bss.
The MEMORY change is worth calling out as a behavior
change. For s02 . : { ... } > FLASH, lld evaluated
. against the memory region's current position instead of
the global location counter. #197293
fixes this to match GNU ld. If you relied on the old behavior, the
section address may move.
GNU ld compatibility
--retain-symbols-file was implemented in 2017 using the
symbol version machinery ({local: *; global: listed;}),
which removed unlisted symbols from .dynsym and left
.symtab untouched. GNU ld does the opposite: it keeps only
the listed symbols in .symtab and does not touch
.dynsym. #209063
reimplements the option on top of the
--discard-{locals,all} mechanism. Use
--export-dynamic-symbol or a version script to control
.dynsym.
While reworking --discard-*, I found that
-r/--emit-relocs with
--discard-locals could discard .L symbols
referenced only from retained non-SHF_ALLOC sections (e.g.
.debug_str_offsets referenced by .debug_info),
rewriting the relocations to reference the null symbol and corrupting
DWARF (#209042).
This is common on RISC-V, where the assembler prefers symbol-relative
relocations, but reachable on any target with
--reloc-section-sym=none.
.gnu.build.attributes.* sections (emitted by annobin)
are now concatenated into .gnu.build.attributes (#208737).
GNU ld has done this since 2018; without it, an output with tens of
thousands of sections broke tools like file.
vna_flags is set to VER_FLG_WEAK when all
undefined references to a version are weak (#176673).
glibc since 2.30 tolerates a missing versioned symbol when the shared
object defines the version; with this flag it also tolerates a
completely missing version, printing
weak version 'v1' not found instead of failing. This is
useful for optional dependencies.
Link: lld 22 ELF changes