[lld] [lld][MachO] Warn on underaligned arm64 functions (PR #221636)

Chak-Kuen Lam via llvm-commits llvm-commits at lists.llvm.org
Thu Sep 10 03:50:49 PDT 2026


https://github.com/dukecat0 updated https://github.com/llvm/llvm-project/pull/221636

>From a4cea76a50be67fe9be83fabd88fc54eddbfc369 Mon Sep 17 00:00:00 2001
From: meowmeowcat <68463158+meowmeowmeowcat at users.noreply.github.com>
Date: Mon, 7 Sep 2026 13:21:39 +0800
Subject: [PATCH 1/2] [lld][MachO] Warn on underaligned arm64 functions

arm64 instructions must be 4-byte aligned, so a function symbol with a
weaker alignment guarantee names code that can never be executed. ld64
warns about this; ld64.lld was silent. Reproducing the ld64 behaviour on
a recent host needs -ld_classic, as the current Apple linker no longer
emits the warning.

Warn when an arm64 symbol in an S_ATTR_PURE_INSTRUCTIONS section has an
alignment guarantee below 4, derived from the section alignment and the
symbol's offset within it. The check is per-symbol so that alt-entry
symbols, which induce no new subsection, are still covered. Local
symbols named with an 'l' or 'L' prefix are exempt, since the assembler
emits an ltmp0 anchor in every section that ld64 does not warn about;
external symbols using that prefix are still diagnosed.

Several existing tests assemble arm64 code without an alignment
directive and link with -fatal_warnings. Those inputs are genuinely
underaligned, so add .p2align 2 rather than suppressing the warning.

Fixes #55177

Co-Authored-By: Claude Opus 5 <noreply at anthropic.com>
---
 lld/MachO/InputFiles.cpp                      | 21 ++++++
 lld/docs/ReleaseNotes.md                      |  5 ++
 lld/test/MachO/adhoc-codesign.s               |  2 +
 lld/test/MachO/arm64-dtrace.s                 |  1 +
 lld/test/MachO/arm64-objc-stubs-dead.s        |  1 +
 lld/test/MachO/arm64-objc-stubs-dyn.s         |  1 +
 lld/test/MachO/arm64-objc-stubs-fix.s         |  1 +
 lld/test/MachO/arm64-objc-stubs.s             |  1 +
 lld/test/MachO/arm64-reloc-got-load.s         |  1 +
 lld/test/MachO/arm64-stubs.s                  |  2 +
 lld/test/MachO/arm64-thunk-for-alignment.s    |  1 +
 lld/test/MachO/arm64-thunk-stubs-multi-text.s |  2 +
 lld/test/MachO/arm64-thunk-stubs.s            |  1 +
 lld/test/MachO/arm64-unaligned-function.s     | 66 +++++++++++++++++++
 lld/test/MachO/bp-section-orderer-cold.s      |  1 +
 lld/test/MachO/bp-section-orderer-errs.s      |  1 +
 lld/test/MachO/bp-section-orderer-icf.s       |  1 +
 lld/test/MachO/bp-section-orderer-stress.s    |  1 +
 lld/test/MachO/bp-section-orderer.s           |  1 +
 lld/test/MachO/compression-order-sections.s   |  1 +
 lld/test/MachO/dwarf-no-compile-unit.s        |  1 +
 lld/test/MachO/dyld-stub-binder.s             |  2 +
 lld/test/MachO/eh-frame-dead-strip.s          |  2 +
 lld/test/MachO/fat-arch.s                     |  1 +
 lld/test/MachO/header.s                       |  1 +
 lld/test/MachO/icf-fold-dwarf-frame.s         |  1 +
 .../MachO/implicit-and-allowable-clients.test |  1 +
 lld/test/MachO/invalid/arm64-unaligned-load.s |  1 +
 lld/test/MachO/invalid/incompatible-arch.s    |  1 +
 lld/test/MachO/invalid/invalid-loh.s          |  1 +
 lld/test/MachO/loh-adrp-ldr-got.s             |  2 +
 lld/test/MachO/loh-parsing.s                  |  1 +
 lld/test/MachO/objc-methname.s                |  1 +
 .../objc-stubs-order-file-icf-safe-thunks.s   |  1 +
 lld/test/MachO/order-file-cstring.s           |  1 +
 lld/test/MachO/order-file-strip-hashes.s      |  1 +
 lld/test/MachO/read-workers-order-obj.s       |  1 +
 lld/test/MachO/read-workers-order.s           |  1 +
 lld/test/MachO/reexport-with-symlink.s        |  1 +
 lld/test/MachO/reexport-without-rpath.s       |  1 +
 lld/test/MachO/tapi-rpath.s                   |  1 +
 41 files changed, 136 insertions(+)
 create mode 100644 lld/test/MachO/arm64-unaligned-function.s

diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index 5279eb0e1fea7..b8928ee4a4752 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -896,6 +896,23 @@ void ObjFile::parseSymbols(ArrayRef<typename LP::section> sectionHeaders,
       return StringRef(strtab + sym.n_strx);
     };
 
+    // arm64 instructions must be 4-byte aligned, so a symbol in a code section
+    // whose guaranteed alignment is lower than that names a function that can
+    // never be executed. ld64 diagnoses this, so we do too. Note that we check
+    // each symbol rather than each subsection: an alt-entry symbol does not
+    // induce a new subsection, but it is still an entry point that has to be
+    // aligned.
+    const bool checkCodeAlign = target->cpuType == CPU_TYPE_ARM64 &&
+                                (sections[i]->flags & S_ATTR_PURE_INSTRUCTIONS);
+
+    // Mach-O reserves the 'l' and 'L' prefixes for labels that the assembler
+    // generates for its own use, such as the ltmp0 anchor it emits for each
+    // section. Those do not name functions, so ld64 does not warn about them.
+    auto isAssemblerTemp = [](const NList &sym, StringRef name) {
+      return !(sym.n_type & N_EXT) &&
+             (name.starts_with("l") || name.starts_with("L"));
+    };
+
     // Calculate symbol sizes and create subsections by splitting the sections
     // along symbol boundaries.
     // We populate subsections by repeatedly splitting the last (highest
@@ -913,6 +930,10 @@ void ObjFile::parseSymbols(ArrayRef<typename LP::section> sectionHeaders,
       const uint32_t symIndex = symbolIndices[j];
       const NList &sym = nList[symIndex];
       StringRef name = getSymName(sym);
+      if (checkCodeAlign && !isAssemblerTemp(sym, name) &&
+          MinAlign(sectionAlign, sym.n_value - sectionAddr) < 4)
+        warn("arm64 function not 4-byte aligned: " + name + " from " +
+             toString(this));
       Subsection &subsec = subsections.back();
       InputSection *isec = subsec.isec;
 
diff --git a/lld/docs/ReleaseNotes.md b/lld/docs/ReleaseNotes.md
index 00c9f5c85af8f..7332553b9b6f6 100644
--- a/lld/docs/ReleaseNotes.md
+++ b/lld/docs/ReleaseNotes.md
@@ -41,6 +41,11 @@ from the [LLVM releases web site](https://llvm.org/releases/).
   call them, so that stubs reached from prioritized code are laid out together.
   This applies whenever section priorities exist, such as with `-order_file`.
 
+* arm64 function symbols that are not guaranteed to be 4-byte aligned now
+  produce a warning, matching ld64. Hand-written assembly that omits an
+  explicit `.p2align 2` may need one, and links that pass `-fatal_warnings`
+  may start to fail.
+
 ### WebAssembly Improvements
 
 * Added support for resolving and merging common data symbols (allocating them
diff --git a/lld/test/MachO/adhoc-codesign.s b/lld/test/MachO/adhoc-codesign.s
index 79503e0dd2624..210be195eb8d8 100644
--- a/lld/test/MachO/adhoc-codesign.s
+++ b/lld/test/MachO/adhoc-codesign.s
@@ -103,10 +103,12 @@
 
 #--- foo.s
 .globl _foo
+.p2align 2
 _foo:
   ret
 
 #--- main.s
 .globl _main
+.p2align 2
 _main:
   ret
diff --git a/lld/test/MachO/arm64-dtrace.s b/lld/test/MachO/arm64-dtrace.s
index 36854195e3142..a79b1565bd69e 100644
--- a/lld/test/MachO/arm64-dtrace.s
+++ b/lld/test/MachO/arm64-dtrace.s
@@ -13,6 +13,7 @@
 
 #--- arm64-dtrace.s
 	.globl	_main
+.p2align 2
 _main:
 	bl	___dtrace_isenabled$Foo$added$v1
 	.reference	___dtrace_typedefs$Foo$v2
diff --git a/lld/test/MachO/arm64-objc-stubs-dead.s b/lld/test/MachO/arm64-objc-stubs-dead.s
index 5dcb171c17eac..d6c728bab0104 100644
--- a/lld/test/MachO/arm64-objc-stubs-dead.s
+++ b/lld/test/MachO/arm64-objc-stubs-dead.s
@@ -16,6 +16,7 @@
 .section __TEXT,__text
 
 .globl _foo
+.p2align 2
 _foo:
   bl  _objc_msgSend$length
   ret
diff --git a/lld/test/MachO/arm64-objc-stubs-dyn.s b/lld/test/MachO/arm64-objc-stubs-dyn.s
index 9358fc5b31c2b..fbbc84e3aaedd 100644
--- a/lld/test/MachO/arm64-objc-stubs-dyn.s
+++ b/lld/test/MachO/arm64-objc-stubs-dyn.s
@@ -69,6 +69,7 @@ lselref2:
 .text
 
 .globl _main
+.p2align 2
 _main:
   bl  _objc_msgSend$length
   bl  _objc_msgSend$foo
diff --git a/lld/test/MachO/arm64-objc-stubs-fix.s b/lld/test/MachO/arm64-objc-stubs-fix.s
index 0dbec361f4b7e..3cf17db7d62ff 100644
--- a/lld/test/MachO/arm64-objc-stubs-fix.s
+++ b/lld/test/MachO/arm64-objc-stubs-fix.s
@@ -25,6 +25,7 @@
 
 .text
 .globl _objc_msgSend
+.p2align 2
 _objc_msgSend:
   ret
 
diff --git a/lld/test/MachO/arm64-objc-stubs.s b/lld/test/MachO/arm64-objc-stubs.s
index da25b1292faa6..3292602f8036b 100644
--- a/lld/test/MachO/arm64-objc-stubs.s
+++ b/lld/test/MachO/arm64-objc-stubs.s
@@ -80,6 +80,7 @@ lselref2:
 
 .text
 .globl _objc_msgSend
+.p2align 2
 _objc_msgSend:
   ret
 
diff --git a/lld/test/MachO/arm64-reloc-got-load.s b/lld/test/MachO/arm64-reloc-got-load.s
index 0186d41dd4250..409f6f4b361be 100644
--- a/lld/test/MachO/arm64-reloc-got-load.s
+++ b/lld/test/MachO/arm64-reloc-got-load.s
@@ -45,6 +45,7 @@ _main:
 
 #--- foobar.s
 .globl _foo, _bar
+.p2align 2
 _foo:
   .space 0
 _bar:
diff --git a/lld/test/MachO/arm64-stubs.s b/lld/test/MachO/arm64-stubs.s
index 55e0f0613a6ec..a9a228e9c8a67 100644
--- a/lld/test/MachO/arm64-stubs.s
+++ b/lld/test/MachO/arm64-stubs.s
@@ -48,10 +48,12 @@
 
 #--- foo.s
 .globl _foo
+.p2align 2
 _foo:
 
 #--- bar.s
 .globl _bar
+.p2align 2
 _bar:
 
 #--- test.s
diff --git a/lld/test/MachO/arm64-thunk-for-alignment.s b/lld/test/MachO/arm64-thunk-for-alignment.s
index f497b81f705b2..5fe933f61f051 100644
--- a/lld/test/MachO/arm64-thunk-for-alignment.s
+++ b/lld/test/MachO/arm64-thunk-for-alignment.s
@@ -18,6 +18,7 @@
 
 #--- foo.s
 
+.p2align 2
 _foo:
   b _bar
 
diff --git a/lld/test/MachO/arm64-thunk-stubs-multi-text.s b/lld/test/MachO/arm64-thunk-stubs-multi-text.s
index 033c1a61afd2e..8bb68193ce971 100644
--- a/lld/test/MachO/arm64-thunk-stubs-multi-text.s
+++ b/lld/test/MachO/arm64-thunk-stubs-multi-text.s
@@ -23,6 +23,7 @@
 .text
 
 .globl _main
+.p2align 2
 _main:
   bl _foo
   bl _extern_sym
@@ -40,6 +41,7 @@ _spacer1:
 .space 0x4000000
 
 .section __TEXT,__lcxx_override,regular,pure_instructions
+.p2align 2
 _bar:
   bl _extern_sym
   ret
diff --git a/lld/test/MachO/arm64-thunk-stubs.s b/lld/test/MachO/arm64-thunk-stubs.s
index 046d3965525d0..1eefba1256cb7 100644
--- a/lld/test/MachO/arm64-thunk-stubs.s
+++ b/lld/test/MachO/arm64-thunk-stubs.s
@@ -39,6 +39,7 @@ lselref2:
 
 .text
 .globl _objc_msgSend
+.p2align 2
 _objc_msgSend:
   ret
 
diff --git a/lld/test/MachO/arm64-unaligned-function.s b/lld/test/MachO/arm64-unaligned-function.s
new file mode 100644
index 0000000000000..3a8ae4dcbfcb3
--- /dev/null
+++ b/lld/test/MachO/arm64-unaligned-function.s
@@ -0,0 +1,66 @@
+# REQUIRES: aarch64, x86
+## Like ld64, warn about arm64 function symbols that are not 4-byte aligned,
+## since arm64 instructions cannot be executed from an unaligned address.
+
+# RUN: rm -rf %t; split-file %s %t
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-macos %t/unaligned.s -o %t/unaligned.o
+# RUN: %no-arg-lld -dylib -arch arm64 -platform_version macos 11.0 11.0 \
+# RUN:   -o /dev/null %t/unaligned.o 2>&1 | FileCheck %s --check-prefix=WARN
+# WARN-DAG: warning: arm64 function not 4-byte aligned: _underaligned_section from {{.*}}unaligned.o
+# WARN-DAG: warning: arm64 function not 4-byte aligned: _misaligned_offset from {{.*}}unaligned.o
+# WARN-DAG: warning: arm64 function not 4-byte aligned: _misaligned_alt_entry from {{.*}}unaligned.o
+## _aligned and _underaligned_data must not be diagnosed.
+# WARN-NOT: warning: arm64 function not 4-byte aligned: _aligned
+# WARN-NOT: warning: arm64 function not 4-byte aligned: _underaligned_data
+
+## Aligned functions, data symbols and non-arm64 targets are not diagnosed.
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-macos %t/aligned.s -o %t/aligned.o
+# RUN: %no-arg-lld -dylib -arch arm64 -platform_version macos 11.0 11.0 \
+# RUN:   -o /dev/null %t/aligned.o 2>&1 | count 0
+
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-macos %t/unaligned.s -o %t/unaligned-x86_64.o
+# RUN: %no-arg-lld -dylib -arch x86_64 -platform_version macos 11.0 11.0 \
+# RUN:   -o /dev/null %t/unaligned-x86_64.o 2>&1 | count 0
+
+#--- unaligned.s
+.subsections_via_symbols
+
+## The section itself is only 1-byte aligned.
+.section __TEXT,__underaligned,regular,pure_instructions
+.globl _underaligned_section
+_underaligned_section:
+  ret
+
+.text
+.p2align 2
+.globl _aligned
+_aligned:
+  ret
+  .byte 0
+
+## Aligned section, but the symbol sits at a misaligned offset within it.
+.globl _misaligned_offset
+_misaligned_offset:
+  ret
+  .byte 0
+
+## An alt_entry does not start a new subsection, but is still an entry point.
+.globl _misaligned_alt_entry
+.alt_entry _misaligned_alt_entry
+_misaligned_alt_entry:
+  ret
+
+## Underaligned data is not a function, so it is not diagnosed.
+.section __DATA,__mydata
+.globl _underaligned_data
+_underaligned_data:
+  .byte 1
+
+#--- aligned.s
+.subsections_via_symbols
+.text
+.p2align 2
+.globl _aligned
+_aligned:
+  ret
diff --git a/lld/test/MachO/bp-section-orderer-cold.s b/lld/test/MachO/bp-section-orderer-cold.s
index b7d2234b7d0db..f14320681f613 100644
--- a/lld/test/MachO/bp-section-orderer-cold.s
+++ b/lld/test/MachO/bp-section-orderer-cold.s
@@ -60,6 +60,7 @@
 .text
 
 .globl _main
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/bp-section-orderer-errs.s b/lld/test/MachO/bp-section-orderer-errs.s
index ebd5debe6f168..a5e3a815b31df 100644
--- a/lld/test/MachO/bp-section-orderer-errs.s
+++ b/lld/test/MachO/bp-section-orderer-errs.s
@@ -40,5 +40,6 @@
 # MISSING: error: [[MSG]]
 
 .globl _main
+.p2align 2
 _main:
   ret
diff --git a/lld/test/MachO/bp-section-orderer-icf.s b/lld/test/MachO/bp-section-orderer-icf.s
index b8efd69b56d8d..dee76ad14c077 100644
--- a/lld/test/MachO/bp-section-orderer-icf.s
+++ b/lld/test/MachO/bp-section-orderer-icf.s
@@ -45,6 +45,7 @@
 
 .text
 .globl _main
+.p2align 2
 _main:
   bl _hot_a
   bl _hot_b
diff --git a/lld/test/MachO/bp-section-orderer-stress.s b/lld/test/MachO/bp-section-orderer-stress.s
index 0bfd99eb3dd86..785e6b9f9745d 100644
--- a/lld/test/MachO/bp-section-orderer-stress.s
+++ b/lld/test/MachO/bp-section-orderer-stress.s
@@ -78,6 +78,7 @@ with open(assembly_filepath, "w") as f:
 .text
 .globl _main
 
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/bp-section-orderer.s b/lld/test/MachO/bp-section-orderer.s
index b8c2ac6865b5b..92250b91c0735 100644
--- a/lld/test/MachO/bp-section-orderer.s
+++ b/lld/test/MachO/bp-section-orderer.s
@@ -60,6 +60,7 @@
 .text
 .globl _main, A, _B, l_C.__uniq.111111111111111111111111111111111111111.llvm.2222222222222222222
 
+.p2align 2
 _main:
   ret
 A:
diff --git a/lld/test/MachO/compression-order-sections.s b/lld/test/MachO/compression-order-sections.s
index 40ceaf7cc4ca9..f3e5bb9c5dff7 100644
--- a/lld/test/MachO/compression-order-sections.s
+++ b/lld/test/MachO/compression-order-sections.s
@@ -81,6 +81,7 @@
 #--- a.s
   .text
   .globl _main
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/dwarf-no-compile-unit.s b/lld/test/MachO/dwarf-no-compile-unit.s
index ced2467ca95d0..1384659e16164 100644
--- a/lld/test/MachO/dwarf-no-compile-unit.s
+++ b/lld/test/MachO/dwarf-no-compile-unit.s
@@ -8,6 +8,7 @@
 
 .text
 .globl _main
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/dyld-stub-binder.s b/lld/test/MachO/dyld-stub-binder.s
index 170fe8abd89bd..1e728f274fc1d 100644
--- a/lld/test/MachO/dyld-stub-binder.s
+++ b/lld/test/MachO/dyld-stub-binder.s
@@ -46,11 +46,13 @@
 
 #--- foo.s
 .globl _foo
+.p2align 2
 _foo:
 
 #--- bar.s
 .text
 .globl _bar
+.p2align 2
 _bar:
   bl _foo
   ret
diff --git a/lld/test/MachO/eh-frame-dead-strip.s b/lld/test/MachO/eh-frame-dead-strip.s
index c9eb8c167720d..357c71782b26e 100644
--- a/lld/test/MachO/eh-frame-dead-strip.s
+++ b/lld/test/MachO/eh-frame-dead-strip.s
@@ -28,6 +28,7 @@
 
 #--- strong.s
 .globl _fun
+.p2align 2
 _fun:
   .cfi_startproc
   ## cfi_escape cannot be encoded in compact unwind
@@ -38,6 +39,7 @@ _fun:
 #--- weak.s
 .globl _fun
 .weak_definition _fun
+.p2align 2
 _fun:
   .cfi_startproc
   ## cfi_escape cannot be encoded in compact unwind
diff --git a/lld/test/MachO/fat-arch.s b/lld/test/MachO/fat-arch.s
index 59b82cd900ae6..efbee0a73af0a 100644
--- a/lld/test/MachO/fat-arch.s
+++ b/lld/test/MachO/fat-arch.s
@@ -41,5 +41,6 @@
 
 .text
 .global _main
+.p2align 2
 _main:
   ret
diff --git a/lld/test/MachO/header.s b/lld/test/MachO/header.s
index e7ddf94568c81..0838fbe4834ca 100644
--- a/lld/test/MachO/header.s
+++ b/lld/test/MachO/header.s
@@ -25,4 +25,5 @@
 # DYLIB-NEXT: MH_MAGIC_64{{(_64)?}}  [[CPU]]  [[SUBTYPE]]  [[CAPS]]  DYLIB    {{.*}} NOUNDEFS DYLDLINK TWOLEVEL NO_REEXPORTED_DYLIBS{{$}}
 
 .globl _main
+.p2align 2
 _main:
diff --git a/lld/test/MachO/icf-fold-dwarf-frame.s b/lld/test/MachO/icf-fold-dwarf-frame.s
index 34fdc3cc55f42..2f915123ecd83 100644
--- a/lld/test/MachO/icf-fold-dwarf-frame.s
+++ b/lld/test/MachO/icf-fold-dwarf-frame.s
@@ -30,6 +30,7 @@
 # Due to padding, we need to emit a throwaway FDE for each personality, so that
 # subsequent FDEs are the same size and can be folded
 # TODO: Could we detect padding differences and fold anyway?
+.p2align 2
 _padA:
   .cfi_startproc
   .cfi_personality 155, _p0
diff --git a/lld/test/MachO/implicit-and-allowable-clients.test b/lld/test/MachO/implicit-and-allowable-clients.test
index f5c13c250ea4a..191785132afd0 100644
--- a/lld/test/MachO/implicit-and-allowable-clients.test
+++ b/lld/test/MachO/implicit-and-allowable-clients.test
@@ -43,6 +43,7 @@ exports:
 .text
 .globl _main
 
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/invalid/arm64-unaligned-load.s b/lld/test/MachO/invalid/arm64-unaligned-load.s
index 60e89f3447525..9b4353fe2dd6d 100644
--- a/lld/test/MachO/invalid/arm64-unaligned-load.s
+++ b/lld/test/MachO/invalid/arm64-unaligned-load.s
@@ -9,6 +9,7 @@
 # CHECK-DAG: error: {{.*}}:(symbol _main+0x1c): 128-bit LDR/STR to 0x[[#%X,]] (_quad) is not 16-byte aligned
 
 .globl _main
+.p2align 2
 _main:
   adrp x0, _half at PAGE
   ldrh w0, [x0, _half at PAGEOFF]
diff --git a/lld/test/MachO/invalid/incompatible-arch.s b/lld/test/MachO/invalid/incompatible-arch.s
index d5cb403a51026..09e9c2a3360a2 100644
--- a/lld/test/MachO/invalid/incompatible-arch.s
+++ b/lld/test/MachO/invalid/incompatible-arch.s
@@ -49,5 +49,6 @@
 # CROSS-SIM2: {{.*}}test_x86_ios_sim.o has platform iOS Simulator, which is different from target platform watchOS Simulator
 
 .globl _main
+.p2align 2
 _main:
   ret
diff --git a/lld/test/MachO/invalid/invalid-loh.s b/lld/test/MachO/invalid/invalid-loh.s
index 20da17fa29edc..dc0f3684ad050 100644
--- a/lld/test/MachO/invalid/invalid-loh.s
+++ b/lld/test/MachO/invalid/invalid-loh.s
@@ -7,6 +7,7 @@
 # CHECK: error: {{.*}}test.o: linker optimization hint spans multiple sections
 
 .globl _main
+.p2align 2
 _main:
 L1:
   adrp x0, _target at PAGE
diff --git a/lld/test/MachO/loh-adrp-ldr-got.s b/lld/test/MachO/loh-adrp-ldr-got.s
index 5363a11677704..84452387b9c3a 100644
--- a/lld/test/MachO/loh-adrp-ldr-got.s
+++ b/lld/test/MachO/loh-adrp-ldr-got.s
@@ -11,6 +11,7 @@
 .text
 .globl _main
 # CHECK-LABEL: _main:
+.p2align 2
 _main:
 ## The referenced symbol is local
 L1: adrp x0, _local at GOTPAGE
@@ -32,4 +33,5 @@ _local:
 
 #--- dylib.s
 .globl _external
+.p2align 2
 _external:
diff --git a/lld/test/MachO/loh-parsing.s b/lld/test/MachO/loh-parsing.s
index aad1af359fe19..b7dfc71caee77 100644
--- a/lld/test/MachO/loh-parsing.s
+++ b/lld/test/MachO/loh-parsing.s
@@ -9,6 +9,7 @@
 ## __debug_info is one such section that gets special-case handling.
 
 .text
+.p2align 2
 _foo:
 
 .section __DWARF,__debug_info,regular,debug
diff --git a/lld/test/MachO/objc-methname.s b/lld/test/MachO/objc-methname.s
index 3d06472971c80..0f3fd5712bbd6 100644
--- a/lld/test/MachO/objc-methname.s
+++ b/lld/test/MachO/objc-methname.s
@@ -34,6 +34,7 @@
 #--- main.s
 .text
 .globl _objc_msgSend
+.p2align 2
 _objc_msgSend:
   ret
 
diff --git a/lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s b/lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s
index 249b14f48a1af..1b7b8a34931c5 100644
--- a/lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s
+++ b/lld/test/MachO/objc-stubs-order-file-icf-safe-thunks.s
@@ -23,6 +23,7 @@
 #--- input.s
 .text
 .globl _cold
+.p2align 2
 _cold:
   bl _objc_msgSend$cold
   ret
diff --git a/lld/test/MachO/order-file-cstring.s b/lld/test/MachO/order-file-cstring.s
index ca3c32bb1e4f5..9d272e704c657 100644
--- a/lld/test/MachO/order-file-cstring.s
+++ b/lld/test/MachO/order-file-cstring.s
@@ -191,6 +191,7 @@ CSTR;0x2032D362
 .text
 .globl _main
 
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/order-file-strip-hashes.s b/lld/test/MachO/order-file-strip-hashes.s
index 42d1e1b98f49e..56b399538bc14 100644
--- a/lld/test/MachO/order-file-strip-hashes.s
+++ b/lld/test/MachO/order-file-strip-hashes.s
@@ -10,6 +10,7 @@
 .text
 .globl _main, A, _B, C.__uniq.111111111111111111111111111111111111111.llvm.2222222222222222222
 
+.p2align 2
 _main:
   ret
 A:
diff --git a/lld/test/MachO/read-workers-order-obj.s b/lld/test/MachO/read-workers-order-obj.s
index d7a3967077508..aec3887a78f71 100644
--- a/lld/test/MachO/read-workers-order-obj.s
+++ b/lld/test/MachO/read-workers-order-obj.s
@@ -33,6 +33,7 @@ _foo:
 
 #--- test.s
 .globl _main
+.p2align 2
 _main:
   bl _foo
   ret
diff --git a/lld/test/MachO/read-workers-order.s b/lld/test/MachO/read-workers-order.s
index 861c12b197333..0ae4107c0096a 100644
--- a/lld/test/MachO/read-workers-order.s
+++ b/lld/test/MachO/read-workers-order.s
@@ -34,6 +34,7 @@ _foo:
 
 #--- test.s
 .globl _main
+.p2align 2
 _main:
   bl _foo
   ret
diff --git a/lld/test/MachO/reexport-with-symlink.s b/lld/test/MachO/reexport-with-symlink.s
index c9cde5bc4f18b..55c63b87a823e 100644
--- a/lld/test/MachO/reexport-with-symlink.s
+++ b/lld/test/MachO/reexport-with-symlink.s
@@ -67,6 +67,7 @@
 .globl _main
 .linker_option "-lDeveloperSupport"
 
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/reexport-without-rpath.s b/lld/test/MachO/reexport-without-rpath.s
index a204c140c4a70..9fb2ab7202ab3 100644
--- a/lld/test/MachO/reexport-without-rpath.s
+++ b/lld/test/MachO/reexport-without-rpath.s
@@ -112,6 +112,7 @@
 .globl _main
 .linker_option "-lDeveloperSupport"
 
+.p2align 2
 _main:
   ret
 
diff --git a/lld/test/MachO/tapi-rpath.s b/lld/test/MachO/tapi-rpath.s
index 23187e797466b..8daead6e875cf 100644
--- a/lld/test/MachO/tapi-rpath.s
+++ b/lld/test/MachO/tapi-rpath.s
@@ -81,6 +81,7 @@
 .text
 .globl _main
 
+.p2align 2
 _main:
   ret
 

>From 151e4da1cf29383a781a41bc138c5120342b2893 Mon Sep 17 00:00:00 2001
From: meowmeowcat <68463158+meowmeowmeowcat at users.noreply.github.com>
Date: Mon, 7 Sep 2026 13:21:39 +0800
Subject: [PATCH 2/2] [lld][MachO] Warn on underaligned arm64 functions

arm64 and arm64_32 instructions must be 4-byte aligned, so a function
symbol with a weaker alignment guarantee names code that can never be
executed. ld64 warns about this; ld64.lld was silent. Reproducing the
ld64 behaviour on a recent host needs -ld_classic, as the current Apple
linker no longer emits the warning.

Warn when a symbol in a non-empty code section (per sections::isCodeSection)
has an alignment guarantee below 4, derived from the section alignment and
the symbol's offset within it, on both arm64 and arm64_32. The check is
per-symbol so that alt-entry symbols, which induce no new subsection, are
still covered. L-prefixed symbols and the assembler's non-external ltmp<N>
anchors are exempt, matching ld64; user-defined 'l'-prefixed symbols and
global ltmp<N> symbols are diagnosed. An empty section has no instructions,
so its symbols are not diagnosed.

Several existing tests assemble arm64/arm64_32 code without an alignment
directive and link with -fatal_warnings. Those inputs are genuinely
underaligned, so add .p2align 2 rather than suppressing the warning. In
bind-opcodes.s, move data placeholder symbols that lived in the default
.text section into a data section instead.

Fixes #55177

Co-Authored-By: Claude Opus 5 <noreply at anthropic.com>
---
 lld/MachO/InputFiles.cpp                   | 41 +++++++++-----
 lld/docs/ReleaseNotes.md                   |  8 +--
 lld/test/MachO/arm64-32-dtrace.s           |  1 +
 lld/test/MachO/arm64-thunk-for-alignment.s |  9 ++-
 lld/test/MachO/arm64-unaligned-function.s  | 66 ++++++++++++++++++++--
 lld/test/MachO/bind-opcodes.s              |  4 ++
 lld/test/MachO/header.s                    |  1 -
 lld/test/MachO/loh-parsing.s               |  1 -
 lld/test/MachO/segments.s                  |  1 +
 9 files changed, 100 insertions(+), 32 deletions(-)

diff --git a/lld/MachO/InputFiles.cpp b/lld/MachO/InputFiles.cpp
index b8928ee4a4752..f1ab910334c8b 100644
--- a/lld/MachO/InputFiles.cpp
+++ b/lld/MachO/InputFiles.cpp
@@ -51,6 +51,7 @@
 #include "ObjC.h"
 #include "OutputSection.h"
 #include "OutputSegment.h"
+#include "Sections.h"
 #include "SymbolTable.h"
 #include "Symbols.h"
 #include "SyntheticSections.h"
@@ -868,6 +869,7 @@ void ObjFile::parseSymbols(ArrayRef<typename LP::section> sectionHeaders,
     std::vector<uint32_t> &symbolIndices = symbolsBySection[i];
     uint64_t sectionAddr = sectionHeaders[i].addr;
     uint32_t sectionAlign = 1u << sectionHeaders[i].align;
+    uint64_t sectionSize = sectionHeaders[i].size;
 
     // Some sections have already been split into subsections during
     // parseSections(), so we simply need to match Symbols to the corresponding
@@ -896,21 +898,26 @@ void ObjFile::parseSymbols(ArrayRef<typename LP::section> sectionHeaders,
       return StringRef(strtab + sym.n_strx);
     };
 
-    // arm64 instructions must be 4-byte aligned, so a symbol in a code section
-    // whose guaranteed alignment is lower than that names a function that can
-    // never be executed. ld64 diagnoses this, so we do too. Note that we check
-    // each symbol rather than each subsection: an alt-entry symbol does not
-    // induce a new subsection, but it is still an entry point that has to be
-    // aligned.
-    const bool checkCodeAlign = target->cpuType == CPU_TYPE_ARM64 &&
-                                (sections[i]->flags & S_ATTR_PURE_INSTRUCTIONS);
-
-    // Mach-O reserves the 'l' and 'L' prefixes for labels that the assembler
-    // generates for its own use, such as the ltmp0 anchor it emits for each
-    // section. Those do not name functions, so ld64 does not warn about them.
+    // arm64 and arm64_32 instructions must be 4-byte aligned, so a symbol in
+    // a code section whose guaranteed alignment is lower than that names a
+    // function that can never be executed. ld64 diagnoses this, so we do too.
+    // Note that we check each symbol rather than each subsection: an alt-entry
+    // symbol does not induce a new subsection, but it is still an entry point
+    // that has to be aligned.
+    const bool checkCodeAlign =
+        is_contained({CPU_TYPE_ARM64, CPU_TYPE_ARM64_32}, target->cpuType) &&
+        lld::macho::sections::isCodeSection(sections[i]->name,
+                                            sections[i]->segname,
+                                            sections[i]->flags);
+
+    // Mach-O reserves the 'L' prefix for labels that the assembler generates
+    // for its own use, such as the Lloh<N> labels that anchor LOH groups, and
+    // ld64 does not warn about them. It also does not warn about the
+    // assembler's non-external ltmp<N> section anchors. User-defined
+    // 'l'-prefixed symbols are diagnosed, whether local or external.
     auto isAssemblerTemp = [](const NList &sym, StringRef name) {
-      return !(sym.n_type & N_EXT) &&
-             (name.starts_with("l") || name.starts_with("L"));
+      return name.starts_with("L") ||
+             (!(sym.n_type & N_EXT) && name.starts_with("ltmp"));
     };
 
     // Calculate symbol sizes and create subsections by splitting the sections
@@ -930,8 +937,12 @@ void ObjFile::parseSymbols(ArrayRef<typename LP::section> sectionHeaders,
       const uint32_t symIndex = symbolIndices[j];
       const NList &sym = nList[symIndex];
       StringRef name = getSymName(sym);
+        // A symbol in an empty section names no instruction bytes, so it is not
+        // diagnosed.
+      uint64_t sectionOffset = sym.n_value - sectionAddr;
       if (checkCodeAlign && !isAssemblerTemp(sym, name) &&
-          MinAlign(sectionAlign, sym.n_value - sectionAddr) < 4)
+          sectionSize != 0 &&
+          MinAlign(sectionAlign, sectionOffset) < 4)
         warn("arm64 function not 4-byte aligned: " + name + " from " +
              toString(this));
       Subsection &subsec = subsections.back();
diff --git a/lld/docs/ReleaseNotes.md b/lld/docs/ReleaseNotes.md
index 7332553b9b6f6..13e2fe023a12b 100644
--- a/lld/docs/ReleaseNotes.md
+++ b/lld/docs/ReleaseNotes.md
@@ -41,10 +41,10 @@ from the [LLVM releases web site](https://llvm.org/releases/).
   call them, so that stubs reached from prioritized code are laid out together.
   This applies whenever section priorities exist, such as with `-order_file`.
 
-* arm64 function symbols that are not guaranteed to be 4-byte aligned now
-  produce a warning, matching ld64. Hand-written assembly that omits an
-  explicit `.p2align 2` may need one, and links that pass `-fatal_warnings`
-  may start to fail.
+* arm64 and arm64_32 function symbols that are not guaranteed to be 4-byte
+  aligned now produce a warning, matching ld64. Hand-written assembly that
+  omits an explicit `.p2align 2` may need one, and links that pass
+  `-fatal_warnings` may start to fail.
 
 ### WebAssembly Improvements
 
diff --git a/lld/test/MachO/arm64-32-dtrace.s b/lld/test/MachO/arm64-32-dtrace.s
index 26c91bd28831d..32b6dfe437bb3 100644
--- a/lld/test/MachO/arm64-32-dtrace.s
+++ b/lld/test/MachO/arm64-32-dtrace.s
@@ -13,6 +13,7 @@
 
 #--- arm64-32-dtrace.s
 	.globl	_main
+	.p2align 2
 _main:
 	bl	___dtrace_isenabled$Foo$added$v1
 	.reference	___dtrace_typedefs$Foo$v2
diff --git a/lld/test/MachO/arm64-thunk-for-alignment.s b/lld/test/MachO/arm64-thunk-for-alignment.s
index 5fe933f61f051..56d63ed137331 100644
--- a/lld/test/MachO/arm64-thunk-for-alignment.s
+++ b/lld/test/MachO/arm64-thunk-for-alignment.s
@@ -10,11 +10,10 @@
 ## Regression test for PR59259. Previously, we neglected to check section
 ## alignments when deciding when to create thunks.
 
-## If we ignore alignment, the total size of _spacer1 + _spacer2 below is just
-## under the limit at which we attempt to insert thunks between the spacers.
-## However, with alignment accounted for, their total size ends up being
-## 0x8000000, which is just above the max forward branch range, making thunk
-## insertion necessary. Thus, not accounting for alignment led to an error.
+## If we ignore alignment, _bar is 0x7fff3fb bytes after _foo, just within the
+## max forward branch range. However, aligning _spacer2's section adds 0xc05
+## bytes of padding, placing _bar 0x8000000 bytes after _foo. This requires a
+## thunk, and previously not accounting for that alignment led to an error.
 
 #--- foo.s
 
diff --git a/lld/test/MachO/arm64-unaligned-function.s b/lld/test/MachO/arm64-unaligned-function.s
index 3a8ae4dcbfcb3..26b3ebdacdea1 100644
--- a/lld/test/MachO/arm64-unaligned-function.s
+++ b/lld/test/MachO/arm64-unaligned-function.s
@@ -1,18 +1,46 @@
 # REQUIRES: aarch64, x86
-## Like ld64, warn about arm64 function symbols that are not 4-byte aligned,
-## since arm64 instructions cannot be executed from an unaligned address.
+## Like ld64, warn about arm64/arm64_32 function symbols that are not 4-byte
+## aligned, since arm64 instructions cannot be executed from an unaligned
+## address.
 
 # RUN: rm -rf %t; split-file %s %t
 
 # RUN: llvm-mc -filetype=obj -triple=arm64-apple-macos %t/unaligned.s -o %t/unaligned.o
+## Run twice with separate prefixes: WARN-NOT would otherwise only apply to
+## output after the WARN-DAG matches, not the whole output.
 # RUN: %no-arg-lld -dylib -arch arm64 -platform_version macos 11.0 11.0 \
-# RUN:   -o /dev/null %t/unaligned.o 2>&1 | FileCheck %s --check-prefix=WARN
+# RUN:   -o /dev/null %t/unaligned.o 2>&1 | tee %t/warnings.txt | \
+# RUN:   FileCheck %s --check-prefix=WARN
+# RUN: FileCheck %s --check-prefix=NOWARN < %t/warnings.txt
+# RUN: llvm-objcopy --redefine-sym lmisaligned_local=Lmisaligned_local \
+# RUN:   %t/unaligned.o %t/unaligned-L.o
+# RUN: %no-arg-lld -dylib -arch arm64 -platform_version macos 11.0 11.0 \
+# RUN:   -o /dev/null %t/unaligned-L.o 2>&1 | FileCheck %s --check-prefix=L
 # WARN-DAG: warning: arm64 function not 4-byte aligned: _underaligned_section from {{.*}}unaligned.o
 # WARN-DAG: warning: arm64 function not 4-byte aligned: _misaligned_offset from {{.*}}unaligned.o
 # WARN-DAG: warning: arm64 function not 4-byte aligned: _misaligned_alt_entry from {{.*}}unaligned.o
-## _aligned and _underaligned_data must not be diagnosed.
-# WARN-NOT: warning: arm64 function not 4-byte aligned: _aligned
-# WARN-NOT: warning: arm64 function not 4-byte aligned: _underaligned_data
+# WARN-DAG: warning: arm64 function not 4-byte aligned: lmisaligned_local from {{.*}}unaligned.o
+# WARN-DAG: warning: arm64 function not 4-byte aligned: lmisaligned_ext from {{.*}}unaligned.o
+# WARN-DAG: warning: arm64 function not 4-byte aligned: ltmp9 from {{.*}}unaligned.o
+# WARN-DAG: warning: arm64 function not 4-byte aligned: _section_end from {{.*}}unaligned.o
+## _aligned, _underaligned_data, the assembler's non-external anchors (each
+## section gets an ltmp<N>; __underaligned's own ltmp1 is itself misaligned
+## yet must not be diagnosed), L-prefixed labels, and empty sections are exempt.
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: _aligned
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: _underaligned_data
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: ltmp0
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: ltmp1
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: ltmp2
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: ltmp3
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: Lloh
+# NOWARN-NOT: warning: arm64 function not 4-byte aligned: _empty_section
+# L-NOT: warning: arm64 function not 4-byte aligned: Lmisaligned_local
+
+## arm64_32 has the same 4-byte instruction alignment requirement.
+# RUN: llvm-mc -filetype=obj -triple=arm64_32-apple-watchos %t/unaligned.s -o %t/unaligned-arm64_32.o
+# RUN: %no-arg-lld -dylib -arch arm64_32 -platform_version watchos 4.0 4.0 \
+# RUN:   -o /dev/null %t/unaligned-arm64_32.o 2>&1 | FileCheck %s --check-prefix=WARN32
+# WARN32: warning: arm64 function not 4-byte aligned: _underaligned_section from {{.*}}unaligned-arm64_32.o
 
 ## Aligned functions, data symbols and non-arm64 targets are not diagnosed.
 # RUN: llvm-mc -filetype=obj -triple=arm64-apple-macos %t/aligned.s -o %t/aligned.o
@@ -51,12 +79,38 @@ _misaligned_offset:
 _misaligned_alt_entry:
   ret
 
+## User-defined l-prefixed symbols are diagnosed, whether local or external;
+## ld64 only exempts the assembler's own non-external ltmp* anchors, so a
+## global ltmp9 is diagnosed while the assembler's ltmp0 is not.
+lmisaligned_local:
+  ret
+.globl lmisaligned_ext
+lmisaligned_ext:
+  ret
+.globl ltmp9
+ltmp9:
+  ret
+
 ## Underaligned data is not a function, so it is not diagnosed.
 .section __DATA,__mydata
 .globl _underaligned_data
 _underaligned_data:
   .byte 1
 
+## An empty section contains no instructions, so a symbol in it names no
+## function and is not diagnosed.
+.section __TEXT,__empty,regular,pure_instructions
+.globl _empty_section
+_empty_section:
+
+## A symbol at the very end of a non-empty section is diagnosed.
+.text
+.p2align 2
+  ret
+  .byte 0
+.globl _section_end
+_section_end:
+
 #--- aligned.s
 .subsections_via_symbols
 .text
diff --git a/lld/test/MachO/bind-opcodes.s b/lld/test/MachO/bind-opcodes.s
index cf294f2f7f427..5aec1c4f5ad96 100644
--- a/lld/test/MachO/bind-opcodes.s
+++ b/lld/test/MachO/bind-opcodes.s
@@ -152,6 +152,9 @@
 # BIND-EMPTY:
 
 #--- foo.s
+## These are data placeholders, not functions; keep them out of a code
+## section.
+.section __TEXT,__datafoo,regular
 .globl _foo, _bar
 _foo:
   .space 4
@@ -182,5 +185,6 @@ ptr _bar
 ptr _foo
 
 .globl _main
+.p2align 2
 .text
 _main:
diff --git a/lld/test/MachO/header.s b/lld/test/MachO/header.s
index 0838fbe4834ca..e7ddf94568c81 100644
--- a/lld/test/MachO/header.s
+++ b/lld/test/MachO/header.s
@@ -25,5 +25,4 @@
 # DYLIB-NEXT: MH_MAGIC_64{{(_64)?}}  [[CPU]]  [[SUBTYPE]]  [[CAPS]]  DYLIB    {{.*}} NOUNDEFS DYLDLINK TWOLEVEL NO_REEXPORTED_DYLIBS{{$}}
 
 .globl _main
-.p2align 2
 _main:
diff --git a/lld/test/MachO/loh-parsing.s b/lld/test/MachO/loh-parsing.s
index b7dfc71caee77..aad1af359fe19 100644
--- a/lld/test/MachO/loh-parsing.s
+++ b/lld/test/MachO/loh-parsing.s
@@ -9,7 +9,6 @@
 ## __debug_info is one such section that gets special-case handling.
 
 .text
-.p2align 2
 _foo:
 
 .section __DWARF,__debug_info,regular,debug
diff --git a/lld/test/MachO/segments.s b/lld/test/MachO/segments.s
index b167813d40188..d2c09e201eee2 100644
--- a/lld/test/MachO/segments.s
+++ b/lld/test/MachO/segments.s
@@ -67,6 +67,7 @@
 
 .text
 .global _main
+.p2align 2
 _main:
   ret
 



More information about the llvm-commits mailing list