llvm / llvm/llvm-project

[AArch64] Scaled PAGEOFFSET_12L relocation emitted against COFF weak external symbols can reference under-aligned definitions

Open
#218,899 21 comments 0 reactions 0 assignees View on GitHub
backend:AArch64 lld:COFF platform:windows
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

I got build error for my aarch64-windows-gnu toolchain for the library and here is the summary. Please merge it asap because it breaks Windows on ARM for such as simple case

Summary
=======

When an `extern_weak` symbol is explicitly marked `dso_local` in IR, the
AArch64 backend lowers GOT-style references to it into
`adrp + ldr x8, [x8, :lo12:sym]`, which emits
`IMAGE_REL_ARM64_PAGEOFFSET_12L`. That relocation type applies to *scaled*
loads and requires the final resolution of the symbol to be aligned to the
access size. A COFF weak external can be resolved by the linker to a
definition from any object file -- including a weak-external ".default"
fallback synthesized by the assembler into an arbitrary, possibly
under-aligned section -- so the alignment assumption does not hold and
lld-link correctly rejects the link with:

ld.lld: error: misaligned ldr/str offset

This makes otherwise valid code unlinkable. The problem shows up in
practice with ThinLTO: LTO's symbol resolution marks prevailing weak
symbols as dso_local (llvm/lib/LTO/LTO.cpp calls GV->setDSOLocal(true)),
after which the backend takes this path.

Reduced C++ example
===================

The idiom is a library that exports entry points as GNU-style weak
symbols on MinGW/PE targets:

```c++
// a.cpp - defines the API as weak dllexport symbols
struct S { void *a; void *b; };
extern "C" __attribute__((dllexport, weak)) const struct S *foo(void) {
static const struct S s = {0, 0};
return &s;
}
extern "C" __attribute__((dllexport, weak)) const struct S *bar(void) {
static const struct S s = {0, 0};
return &s;
}

// b.cpp - another TU of the same library refers to them
struct S;
extern "C" __attribute__((dllexport, weak)) const struct S *foo(void);
extern "C" __attribute__((dllexport, weak)) const struct S *bar(void);
__attribute__((dllexport)) int use(void) {
return foo() != bar();
}
```

After ThinLTO promotion (weak definitions are prevailing within the link,
so LTO marks them dso_local), the extern_weak declarations in b.cpp carry
the explicit `dso_local` flag:

declare extern_weak dso_local ptr @foo()
declare extern_weak dso_local ptr @bar()

Minimal reproducer
==================

```llvm
; reduced.ll
target triple = "aarch64-windows-gnu"

declare extern_weak dso_local ptr @foo()

define dso_local ptr @use() {
entry:
%r = call ptr @foo()
ret ptr %r
}
```

```console
$ llc -mtriple=aarch64-windows-gnu < reduced.ll -filetype=obj -o bad.obj
$ llvm-readobj -r bad.obj | grep 12L
0xC IMAGE_REL_ARM64_PAGEOFFSET_12L foo
```

The emitted code is:

```asm
adrp x8, foo
ldr x8, [x8, :lo12:foo] ; PAGEOFFSET_12L against a weak external
blr x8
```

Per the PE/COFF specification PAGEOFFSET_12L applies to scaled loads, so
the linker is entitled to require &foo % 8 == 0 here. Nothing guarantees
that for a weak external: its resolution may come from any TU or from an
assembler-synthesized ".default" fallback placed in an under-aligned
section (observed in .rdata with 1-byte alignment in real projects).

Root cause
==========

- TargetMachine::shouldAssumeDSOLocal() checks `GV->isDSOLocal()` before
its `hasExternalWeakLinkage()` exclusion, so an explicitly dso_local
extern_weak returns true.
- ClassifyGlobalReference() therefore skips the MO_GOT|MO_COFFSTUB branch
and falls through to the small-code-model extern_weak case, returning
bare MO_GOT (no pointer slot).
- AArch64ExpandPseudoInsts.cpp expands the LOADgot pseudo unconditionally
into ADRP + scaled LDRXui, producing PAGEOFFSET_12L directly against
the weak external symbol.

Note that references which go through a pointer slot are fine: the
MO_COFFSTUB path (.refptr.) and the MO_DLLIMPORT path (__imp_)
reference locally-emitted/import-table cells with guaranteed 8-byte
alignment. Only the bare-symbol form is affected.

Proposed fix
============

In AArch64ExpandPseudoInsts.cpp, expand LOADgot into

```asm
adrp x8, sym
add x8, x8, :lo12:sym ; IMAGE_REL_ARM64_PAGEOFFSET_12A
ldr x8, [x8] ; no relocation
```

when the operand is a global with external weak linkage on a COFF target
and carries neither MO_COFFSTUB nor MO_DLLIMPORT. REL21/PAGEOFFSET_12A
carry no alignment requirement, so the sequence is valid regardless of
where the weak external resolves.

ELF is unaffected: undefined weak callees lower to a direct BL there, and
GOT entries themselves guarantee alignment where they are used.

A regression test is available (llvm/test/CodeGen/AArch64/
coff-loadgot-weak-extern.ll); the existing windows-extern-weak.ll test
continues to pass since the .refptr. path keeps its current form.

Contributor guide

Open the contributing guide

Research direction

Read AArch64ExpandPseudoInsts.cpp and the issue's proposed regression test, llvm/test/CodeGen/AArch64/coff-loadgot-weak-extern.ll. First run the reproducer through llc for aarch64-windows-gnu and inspect the emitted relocations; compare with windows-extern-weak.ll. Done means the weak COFF case avoids a scaled relocation against the weak symbol while the existing .refptr. case retains its behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.