Inconsistency between GCC and LLVM in mcmodel=large

I am a compiler engineer currently working on a project to run Grub2 with Clang + LLVM instead of GCC. I wanted to reach out to inquire about the difference in behavior between LLVM and GCC when using mcmodel=large. Here’s an example:

test.c:

const char *test(void){
	return "xx";
}

Execution with GCC:

$ gcc -o test.o -c test.c -mcmodel=large
$ readelf -r test.o

Relocation section '.rela.text' at offset 0x200 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000  000200000113 R_AARCH64_ADR_PRE 0000000000000000 .text + 10
000000000004  000200000115 R_AARCH64_ADD_ABS 0000000000000000 .text + 10
000000000010  000500000101 R_AARCH64_ABS64   0000000000000000 .rodata + 0

Relocation section '.rela.eh_frame' at offset 0x248 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000001c  000200000105 R_AARCH64_PREL32  0000000000000000 .text + 0

Execution with Clang:

$ clang -o test.o -c test.c -mcmodel=large
$ readelf -r test.o

Relocation section '.rela.text' at offset 0x1c0 contains 4 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000  000700000108 R_AARCH64_MOVW_UA 0000000000000000 .rodata.str1.1 + 0
000000000004  00070000010a R_AARCH64_MOVW_UA 0000000000000000 .rodata.str1.1 + 0
000000000008  00070000010c R_AARCH64_MOVW_UA 0000000000000000 .rodata.str1.1 + 0
00000000000c  00070000010d R_AARCH64_MOVW_UA 0000000000000000 .rodata.str1.1 + 0

Relocation section '.rela.eh_frame' at offset 0x220 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000001c  000600000104 R_AARCH64_PREL64  0000000000000000 .text + 0

Given this discrepancy, I would like to inquire about the reasons behind the difference in behavior between LLVM and GCC when it comes to supporting mcmodel=large. Understanding the rationale behind this distinction would greatly contribute to our project’s development and help us make informed decisions regarding compiler selection for Grub2.

Thank you for your attention to this matter and for your valuable contributions to the LLVM compiler technology. I appreciate any insights you can provide regarding this topic.

Welcome!

While it becomes clear that you’re talking about AArch64 upon getting to the readelf output, in the future, it would be helpful to state up front which target platform you’re talking about!

Anyhow, you can see the difference between clang and gcc more clearly by looking at objdump disassembly.

From aarch64-linux-gnu-gcc -fno-pic -c -mcmodel=large -o test.o /tmp/test.c && llvm-objdump -dr test.o

test.o:    file format elf64-littleaarch64

Disassembly of section .text:

0000000000000000 <test>:
       0: 00 00 00 90   adrp    x0, 0x0 <test>
                0000000000000000:  R_AARCH64_ADR_PREL_PG_HI21   .text+0x10
       4: 00 00 00 91   add     x0, x0, #0
                0000000000000004:  R_AARCH64_ADD_ABS_LO12_NC    .text+0x10
       8: 00 00 40 f9   ldr     x0, [x0]
       c: c0 03 5f d6   ret

0000000000000010 <$d>:
      10:       00 00 00 00     .word   0x00000000
                0000000000000010:  R_AARCH64_ABS64      .rodata
      14:       00 00 00 00     .word   0x00000000

vs clang --target=aarch64-linux-gnu -fno-pic -c -mcmodel=large -o test.o test.c && llvm-objdump -dr /tmp/test.o

test.o:    file format elf64-littleaarch64

Disassembly of section .text:

0000000000000000 <test>:
       0: 00 00 80 d2   mov     x0, #0
                0000000000000000:  R_AARCH64_MOVW_UABS_G0_NC    .rodata.str1.1
       4: 00 00 a0 f2   movk    x0, #0, lsl #16
                0000000000000004:  R_AARCH64_MOVW_UABS_G1_NC    .rodata.str1.1
       8: 00 00 c0 f2   movk    x0, #0, lsl #32
                0000000000000008:  R_AARCH64_MOVW_UABS_G2_NC    .rodata.str1.1
       c: 00 00 e0 f2   movk    x0, #0, lsl #48
                000000000000000c:  R_AARCH64_MOVW_UABS_G3       .rodata.str1.1
      10: c0 03 5f d6   ret

Here you can see that GCC has encoded the 64-bit address in a data island in the text segment, and then accesses that data island via a 32-bit-limited PC-relative load, to retrieve the underlying pointer. LLVM, on the other hand, uses 4 instructions to directly encode the 64-bit pointer value.

I’m not sure why each compiler made the choice it did, but they both seem like viable implementations!

Thank you for your comment! I appreciate your input and will consider it as I continue my work on this project!