I was asked about a crash related to lld linked musl libc.so on PowerPC64. An executable using the built libc.so as the interpreter segfaulted while invoking libc.so to load the executable did not.
The section and program header dump from readelf looked like the following.
There were two PT_LOAD program headers with the PF_R|PF_W flags and the unusual property p_filesz < p_memsz. It turns out that when the Linux kernel loads an interpreter (PT_INTERP; see fs/binfmt_elf.c:load_elf_interp), it only supports one PT_LOAD with p_filesz < p_memsz.
Note: it is typical for lld output to have two RW PT_LOAD program headers, one for RELRO sections (PT_GNU_RELRO) and the other for non-RELRO sections. This may look unusual at the first glance but it avoids an alignment padding as used in GNU ld's single RW PT_LOAD layout. See Explain GNU style linker options#-z relro.
In the PowerPC ELFv2 ABI, .plt is like GOTPLT on other architectures (it holds resolved addresses for PLT entries) and has the SHT_NOBITS type. With -z now, .plt can be eagerly resolved and become read-only after relocation resolving, therefore it is part of PT_GNU_RELRO. When lld layouts sections, it is part of the first RW PT_LOAD.
Clang as of 15.0 passes -z now to ld for Alpine Linux. Chimera Linux has patched Clang Driver to pass -z now for all musl target triples. To override -z now, just build musl with LDFLAGS=-Wl,-z,lazy.
I made the suggestion and verified with a local cross-compilation build.
1 2 3
mkdir out/ppc64le && cd out/ppc64le ../../configure --target=powerpc64le-linux-gnu CC=clang CFLAGS='--target=powerpc64le-linux-gnu -mlong-double-64' LDFLAGS=-fuse-ld=lld make -j$(nproc)
(If you use GCC's powerpc64 port, avoid -Os. lld has not implemented _savefpr* and _restfpr* functions.)
An alternative workaround I suggested was linking libc.so with a linker script:
1
SECTIONS { .plt : {} } INSERT AFTER .bss;
-z now
musl does not implement lazy PLT resolution at call-time. You may ask: is -z now useful? The DF_1_NOW flag avoids an allocation in its rtld. See emulate lazy relocation as deferrable relocation.
The cons is likely size increase of .dynamic: it will always have a DT_FLAGS holding DF_NOW. In most cases DT_FLAGS can actually be absent if -z now is not used.
glibc
glibc adopts a separate rtld and libc.so design. Its rtld has no JUMP_SLOT (JMP_SLOT) relocations.
The powerpc64 port has been buildable since lld 13. There is no .plt section, therefore the first RW PT_LOAD has p_filesz == p_memsz. The built rtld works with Linux kernel.
I got powerpc64le-linux-gnu-gcc and binutils from system packages. I have installed /usr/local/bin/powerpc64le-linux-gnu-ld.lld so that powerpc64le-linux-gnu-gcc -fuse-ld=lld works.
1 2 3
mkdir out/ppc64le && cd out/ppc64le LDFLAGS=-fuse-ld=lld ../../configure --prefix=/tmp/glibc/ppc64le --host=powerpc64le-linux-gnu --with-default-link --enable-hardcoded-path-in-tests LDFLAGS=-fuse-ld=lld make -j $(nproc)