Skip to content

Arm64: don't fold ldr page offset (PAGEOFFSET_12L) for NativeAOT#129966

Open
EgorBo wants to merge 3 commits into
dotnet:mainfrom
EgorBo:fix-129936-arm64-12l-naot
Open

Arm64: don't fold ldr page offset (PAGEOFFSET_12L) for NativeAOT#129966
EgorBo wants to merge 3 commits into
dotnet:mainfrom
EgorBo:fix-129936-arm64-12l-naot

Conversation

@EgorBo

@EgorBo EgorBo commented Jun 29, 2026

Copy link
Copy Markdown
Member

Fixes #129936.

PR #129589 added an Arm64 fold of adrp+add+ldr into adrp+ldr[#:lo12:] via a 64-bit PAGEOFFSET_12L reloc (R_AARCH64_LDST64_ABS_LO12_NC). That reloc encodes the offset scaled by 8, so the target must be 8-byte aligned. In R2R the relocatable loads always go through pointer-aligned indirection cells, but NativeAOT can fold a direct 64-bit load of byte-packed frozen data (4-byte aligned), which ld.lld rejects:

ld.lld : error : improper alignment for relocation R_AARCH64_LDST64_ABS_LO12_NC: 0x74A52C is not aligned to 8 bytes

The fold is purely a size optimization, so this restricts it to non-NativeAOT and falls back to the always-safe adrp+add+ldr.

Validation (SPMI, linux-arm64):

  • benchmarks.run (R2R, ~427K contexts): 0 asm diffs — the win is preserved.
  • smoke_tests.nativeaot (18891 ctx): clean replay, no diffs.

The adrp+add+ldr to adrp+ldr[#:lo12:] fold uses a 64-bit PAGEOFFSET_12L
reloc (R_AARCH64_LDST64_ABS_LO12_NC), whose scaled offset requires the
target to be 8-byte aligned. R2R loads always go through pointer-aligned
indirection cells, but NativeAOT can fold a direct 64-bit load of
byte-packed frozen data (4-byte aligned), which the linker rejects with
'improper alignment for relocation'. Restrict the fold to non-NativeAOT.

Fixes dotnet#129936

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 29, 2026 06:02
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke, @dotnet/ilc-contrib
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adjusts the Arm64 emitter’s TryFoldPageOffsetIntoLdr peephole optimization to avoid emitting PAGEOFFSET_12L relocations when targeting the NativeAOT ABI, preventing generation of linker-invalid R_AARCH64_LDST64_ABS_LO12_NC relocations in scenarios where the referenced symbol may not be 8-byte aligned.

Changes:

  • Disable adrp+add+ldradrp+ldr[#:lo12:] folding for NativeAOT by early-returning in TryFoldPageOffsetIntoLdr.
  • Add an in-code rationale describing the alignment requirement of PAGEOFFSET_12L / R_AARCH64_LDST64_ABS_LO12_NC and why NativeAOT differs.

Comment thread src/coreclr/jit/emitarm64.cpp Outdated

@jakobbotsch jakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Co-authored-by: Jakob Botsch Nielsen <Jakob.botsch.nielsen@gmail.com>
Copilot AI review requested due to automatic review settings July 1, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/jit/emitarm64.cpp Outdated
Comment thread src/coreclr/jit/emitarm64.cpp Outdated
return false;
}

// PAGEOFFSET_12L requires alignment which is not guaranteed for NAOT

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is it guaranteed for R2R?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkotas I'd assumed all relocatable constants in R2R go through (pointer-aligned) indirection slots. I tried to trick it with misaligned reads/writes to RVA data, but RVA blobs are always pointer-aligned too (requiredAlignment = targetPointerSize in GetRvaData). The failing case is actually NAOT statics: NAOT addresses non-GC statics directly and aligns the static region only to the largest static field's alignment (NonGCStaticFieldAlignment), which is 4 here (struct S { uint A, B; }). JIT's Physical promotion then does a 64-bit load of the struct at that 4-aligned address, which we fold into ldr [x],#:lo12: and LDST64_ABS_LO12 can't encode a non-8-aligned offset. R2R only ever directly addresses RVA data (pointer-aligned); regular statics go through helpers, so it's safe there.

If you think this introduces a fragility I can revert it, I just assumed the diffs (e.g. -200kb from corelib's size) was worth it.

@EgorBo EgorBo Jul 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A separate question is whether it makes sense to increase alignment for statics to 8 bytes (or even 16) as well for perf since JIT loves to optimize struct loads with wider loads🤔 (to avoid perf penalty from misalignment)
{9FEE1C01-DFFC-4C70-87A5-B82B66480784}
cc @MichalStrehovsky

On coreclr, presumably, all static fields holding structs are pointer-size aligned (boxed objects).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you think this introduces a fragility

Yes, I think this implementation is fragile. It assumes an invariant that is not guaranteed by the system. I think it is just a matter of time until we run into an invalid reloc with R2R too here. A better way to do this would be to e.g. add JIT/EE interface API that returns the alignment of the blob.

-200kb from corelib's size

I like the code size reduction

it makes sense to increase alignment for statics to 8 bytes

It may help a bit with NAOT statics, but it won't fix the problem for instance fields (for both JIT and AOT). Correct?

@github-actions github-actions Bot mentioned this pull request Jul 2, 2026
The adrp+add+ldr -> adrp+ldr[#:lo12:] fold uses R_AARCH64_LDST64_ABS_LO12_NC,
which encodes the :lo12: page offset scaled by 8, so the reloc target must be
8-byte aligned. Replace the blanket NativeAOT opt-out with a new JIT-EE API,
getAddressAlignment(void*), and only fold when the VM guarantees the target is
at least 8-byte aligned.

- VM (CoreCLR/NGen): reports the alignment of the absolute address.
- crossgen2 (R2R): reports pointer size; direct relocatable loads only target
  pointer-aligned data.
- NativeAOT: reports the exact alignment of non-GC statics (the failing case)
  and pointer size for GC-statics/RVA data, and fails closed otherwise so an
  unaligned relocation can never be emitted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@MichalStrehovsky MichalStrehovsky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for making it work for native AOT!

Comment on lines +4443 to +4456
case NonGCStaticsNode nonGcStatics:
// The non-GC static region is aligned only to the largest static field's
// alignment (plus the cctor-context's pointer alignment when present), which
// can be smaller than the pointer size for byte-packed layouts.
LayoutInt fieldAlignment = nonGcStatics.Type.NonGCStaticFieldAlignment;
int baseAlignment = fieldAlignment.IsIndeterminate ? pointerSize : fieldAlignment.AsInt;
alignment = nonGcStatics.HasCCtorContext ? Math.Max(baseAlignment, pointerSize) : baseAlignment;
break;

case GCStaticsNode:
case FieldRvaDataNode:
// These are laid out with at least pointer-size alignment.
alignment = pointerSize;
break;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a IObjectNodeWithAlignment interface with a int GetAlignment(NodeFactory factory) method and have these nodes implement it? Then make sure GetAlignment is actually the number that gets reported from GetObjectData/GetDehydratableData. This is duplicating the logic in GetXXXData and runs the risk of getting out of sync.

Comment on lines +4413 to +4432
private uint getAddressAlignment(void* address)
{
if (address == null)
{
return 1;
}
#if READYTORUN
// In R2R, relocatable direct loads only target pointer-aligned data (RVA blobs and
// indirection cells); statics and other data are reached through helpers. Report
// pointer-size alignment, matching how such targets are laid out.
return (uint)_compilation.TypeSystemContext.Target.PointerSize;
#else
if (HandleToObject(address) is ISymbolNode node)
{
return (uint)GetSymbolNodeAlignment(node);
}

// Unknown target: report unaligned so the JIT avoids alignment-sensitive relocations.
return 1;
#endif

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the body is this different, let's put these into CorInfoImpl.ReadyToRun.cs (R2R) and CorInfoImpl.RyuJIT.cs (native AOT) instead and skip the ifdefs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ci-scan] Build break: NativeAOT linker alignment error in PhysicalPromotion test (arm64)

5 participants