[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