[PATCH v1 1/3] aarch64: Fix IMAGE_REL_ARM64_PAGEBASE_REL21 relocation and symbol reduction for relocations
Martin Storsjö
martin@martin.st
Fri Sep 6 14:43:04 GMT 2024
On Fri, 6 Sep 2024, Martin Storsjö wrote:
> On Mon, 26 Aug 2024, Evgeny Karpov wrote:
>
>> Initially, the change was to fix IMAGE_REL_ARM64_PAGEBASE_REL21,
>> which was addressing only 1MB instead of 4GB. This change triggered
>> multiple changes in binutils and gcc to support the small code model.
>
> Sorry, but this is wrong.
>
> The IMAGE_REL_ARM64_PAGEBASE_REL21 relocation supports an +/- 1 MB immediate
> offset encoded in the instruction, which is applied on the symbol that the
> relocation points to.
>
> This is how both LLVM and MSVC tools use this relocation - I've provided
> examples in
> https://gcc.gnu.org/pipermail/gcc-patches/2024-September/662277.html.
>
> You can't just redefine how the relocation is supposed to work, if you want
> your tools to remain with other consumers of the same object file format.
I just checked, and the current handling of IMAGE_REL_ARM64_PAGEBASE_REL21
in binutils-gdb git master seems correct, both within the assembler and
within the linker. But this patch breaks it.
To hopefully clarify things:
COFF-ARM64 object files supports referencing addresses anywhere in the 4
GB PE image, by using a single pair of adrp+add instructions.
An object file can reference a symbol anywhere in the 4 GB address range
of the PE image. The IMAGE_REL_ARM64_PAGEBASE_REL21 relocation on the adrp
instruction can encode a +/- 1 MB offset on top of any symbol. You still
can reach the whole 4 GB address space, but you can't reference an
arbitrary point unless there's a symbol nearby the spot you want to
reference.
First off: The MS armasm64 tool seems to be unable to actually assemble
code that does symbol references with an offset. This doesn't mean that
the object file format doesn't support it, it just means that this tool is
rather limited. (This tool is only used for external handwritten
assembly.)
Showcase:
$ cat adrp.asm
IMPORT |array|
EXPORT |func|
AREA |.text|, CODE, ARM64
func
adrp x8,array+0x12345
add x0,x8,array+0x12345
ret
END
$ armasm64 -nologo adrp.asm
$ dumpbin -nologo -disasm adrp.obj
Dump of file adrp.obj
File Type: COFF OBJECT
func:
0000000000000000: 90000008 adrp x8,array
0000000000000004: 91000100 add x0,x8,array
0000000000000008: D65F03C0 ret
Note how the tool didn't encode any offset, neither on the adrp nor on the
add instruction.
Then, as shown earlier as well, MSVC itself does generate such offsets for
some cases of code:
$ cat cref.c
extern char array[];
char *func(void) { return &array[0x123]; }
$ cl -nologo -c -O2 cref.c cref.c
$ dumpbin -nologo -disasm cref.obj
Dump of file cref.obj
File Type: COFF OBJECT
func:
0000000000000000: F0000908 adrp x8,array+#0x123
0000000000000004: 91048D00 add x0,x8,array+#0x123
0000000000000008: D65F03C0 ret
We can also inspect this with llvm-objdump or GNU objdump, which both
represent it mostly right:
$ llvm-objdump -d -r cref.obj
cref.obj: file format coff-arm64
Disassembly of section .text$mn:
0000000000000000 <func>:
0: f0000908 adrp x8, 0x123000 <func+0x123000>
0000000000000000: IMAGE_REL_ARM64_PAGEBASE_REL21 array
4: 91048d00 add x0, x8, #0x123
0000000000000004: IMAGE_REL_ARM64_PAGEOFFSET_12A array
8: d65f03c0 ret
$ aarch64-w64-mingw32-objdump -d -r cref.obj
cref.obj: file format pe-aarch64-little
Disassembly of section .text$mn:
0000000000000000 <func>:
0: f0000908 adrp x8, 123000 <array+0x123000>
0: IMAGE_REL_ARM64_PAGEBASE_REL21 array
4: 91048d00 add x0, x8, #0x123
4: IMAGE_REL_ARM64_PAGEOFFSET_12A array
8: d65f03c0 ret
The symbol offset, +0x123 bytes, in the relocatable adrp instruction is
misrepresented by both these tools, but the rest is correctly displayed.
We can also assemble the same, both with LLVM and GNU tools, where the GNU
assembler seems to be doing the right thing in the current git version:
$ cat adrp.s
.text
.globl func
func:
adrp x8,array+0x12345
add x0,x8,:lo12:array+0x12345
ret
$ clang -target aarch64-windows -c adrp.s
$ aarch64-w64-mingw32-as -c adrp.s
$ aarch64-w64-mingw32-objdump -d -r adrp.o
adrp.o: file format pe-aarch64-little
Disassembly of section .text:
0000000000000000 <func>:
0: b0091a28 adrp x8, 12345000 <array+0x12345000>
0: IMAGE_REL_ARM64_PAGEBASE_REL21 array
4: 910d1500 add x0, x8, #0x345
4: IMAGE_REL_ARM64_PAGEOFFSET_12A array
8: d65f03c0 ret
This is correct (except the immediate offset 0x12345000 which should be
interpreted as 0x12345). MS dumpbin also shows it correctly:
$ dumpbin -nologo -disasm adrp.o
Dump of file adrp.o
File Type: COFF OBJECT
func:
0000000000000000: B0091A28 adrp x8,array+#0x12345
0000000000000004: 910D1500 add x0,x8,array+#0x345
0000000000000008: D65F03C0 ret
Note how the offset 0x12345 is truncated into the lower 12 bits, 0x345, in
the immediate of the add instruction, while the adrp retains the full
offset.
Now for a tricky example on how these work out in practice:
$ cat example.s
.section .text$a, "xr"
.globl entry
entry:
adrp x0, symbol_in_same_page
add x0, x0, :lo12:symbol_in_same_page
adrp x0, symbol_in_same_page+108
add x0, x0, :lo12:symbol_in_same_page+108
adrp x0, .Ltemp_label
add x0, x0, :lo12:.Ltemp_label
.section .text$b, "xr"
.space 4000
.section .text$c, "xr"
symbol_in_same_page:
adrp x0, symbol_in_other_page
add x0, x0, :lo12:symbol_in_other_page
.section .text$d, "xr"
.space 100
.section .text$e, "xr"
symbol_in_other_page:
nop
.section .text$f, "xr"
.space 6000
.Ltemp_label:
ret
$ clang -target aarch64-windows -c example.s
$ aarch64-w64-mingw32-as -c example.s
We can disassemble this like before, and the output is the same,
regardless of which tool used to assemble it.
We can also link it with either of the three relevant linkers here, MS
link.exe, LLD or GNU ld.
$ link -nologo example.o -out:example.exe -entry:entry -subsystem:console
$ aarch64-w64-mingw32-ld.bfd example.o -o example.exe -e entry
$ ld.lld -m arm64pe example.o -o example.exe -e entry
And we can disassemble the output executable, looking at what the
final relocated instructions turned into:
$ aarch64-w64-mingw32-objdump -d example.exe
example.exe: file format pei-aarch64-little
Disassembly of section .text:
0000000140001000 <entry>:
140001000: 90000000 adrp x0, 140001000 <entry>
140001004: 913ee000 add x0, x0, #0xfb8
140001008: b0000000 adrp x0, 140002000 <symbol_in_same_page+0x48>
14000100c: 91009000 add x0, x0, #0x24
140001010: d0000000 adrp x0, 140003000 <symbol_in_other_page+0xfdc>
140001014: 911e6000 add x0, x0, #0x798
...
0000000140001fb8 <symbol_in_same_page>:
140001fb8: b0000000 adrp x0, 140002000 <symbol_in_same_page+0x48>
140001fbc: 91009000 add x0, x0, #0x24
...
0000000140002024 <symbol_in_other_page>:
140002024: d503201f nop
...
140003798: d65f03c0 ret
The output instruction sequence is the exact same in the case of all 3
linkers - the only thing that differs is the amount of symbols retained.
Now if you apply your patch and redo the last example, linked with GNU ld,
the output adrp relocations turn out wrong. Previously all 3 linkers
agree. Thus I think your patch is wrong, and the current handling of this
relocation in binutils is correct.
And one can use offsetted symbol references in adrp/add just fine, while
reaching the whole 4 GB image range, as long as it references a symbol
within 1 MB from the intended location.
// Martin
More information about the Binutils
mailing list