ELF PIC requires loader support. In absence of such, you must perform the work yourself. musl's dynlinker does this, for example. You require a pointer to the image base, and one to the dynamic section. For the latter, you can almost always encode a dynamic position independent lookup of the symbol _DYNAMIC, typically as either a PC-relative lookup or a self-call trick. For example, in AMD64 you could do
Code:
lea _DYNAMIC(%rip), %rsi
whereas in PowerPC it would be something like
Code:
bl 1f
.long _DYNAMIC-.
1: mflr r4 #run-time address of the above into r4
lwz r5,0(r4) #offset into r5
add r4,r4,r5 #run-time address of _DYNAMIC into r4
I don't know RISC-V, but these two approaches are typically sufficient.
Anyway, with the image base and the dynamic section in hand, you then look up the relocation tables, iterate over them, and process all the relative relocations by adding the image base to them. Since you are writing a kernel, you should not have any other relocations. But you can use "readelf -r" to verify the relocation type.
The code that does all of this must basically be the first thing after the _start label. Until it is done, you can pretty much only call other functions and refer to local (stack) variables. Global variables and constants only become available afterwards.
Code:
void _dlstart_c(char *base, size_t *dyn, void (*next_stage)())
{
size_t dynv[32];
memset(dynv, 0, sizeof dynv);
for (size_t i = 0; dyn[i]; i+= 2)
if (dyn[i] < 32)
dynv[dyn[i]] = dyn[i+1];
size_t *rel = (void*)(base + dynv[DT_REL]);
size_t rel_sz = dynv[DT_RELSZ];
for (; rel_sz; rel_sz -= 2 * sizeof (size_t), rel += 2) {
if (rel[1] == REL_RELATIVE) {
size_t *rel_addr = (void*)(base + rel[0]);
*rel_addr += (uintptr_t)base;
}
}
rel = (void*)(base + dynv[DT_RELA]);
rel_sz = dynv[DT_RELASZ];
for (; rel_sz; rel_sz -= 3 * sizeof (size_t), rel += 3) {
if (rel[1] == REL_RELATIVE) {
size_t *rel_addr = (void*)(base + rel[0]);
*rel_addr = (uintptr_t)base + rel[2];
}
}
next_stage();
}
This code assumes you defined REL_RELATIVE to R_<arch>_RELATIVE from elf.h. It generally assumes you have elf.h available. You must provide the base address, dynamic address, and address of the next stage function from the external assembler code. That last one prevents the compiler from improperly inlining functions and moving initializations using relocations above the place where those relocations get filled in. The code is currently not passing any parameters to the next stage, but you will probably need to do that. It also assumes that the loader has already correctly mapped the ELF file into virtual memory.
Anyway, I have never found it necessary to have the kernel be a PIE, because all of virtual address space is available at the time you load the kernel. The only thing that might be taken is physical address space, but the proper way to abstract that is to use virtual memory.