Absolute addresses in position-independent code (PIC)

Viewed 652

I am trying to build and link a single image to load as an OS kernel (ie. in QEMU) targeting aarch64-unknown-none-softfloat. I use a custom linker.ld file which sets the entry point for the kernel ENTRY(_reset) and positions the image

. = 0x40080000

where the program counter (PC) is on reset.

It works ok until I map the pages at 0x40080000 to high memory where the kernel will reside and enable virtual memory translation. To ensure the debugging information meshes after the switch, I change the nominal image position to

. = 0xffffff8200000000

and rebuild.

I have discovered that access:

  • to some (pub extern) statics, and
  • by certain core library functions

is by reading the absolute address from somewhere in .rodata. This breaks the code when it is running before mapping. And if I change it back it will break the code when I run it after mapping.

The code it is generating looks a bit like this at O1 (indirect through PC-relative page):

adrp  x0, 0x10000 // page offset from PC up to rodata
add   x0, 0x120   // byte offset from page in rodata
ldr   x0, [x0]    // use as address

What I need is truly position independent code across code and data so that it works at both locations in memory without referring to any stored absolute addresses, even if those addresses are available relative to PC.

I've tried the other relocation-models including Pic and RopiRwpi but I can't see it generating different code.

Thanks!

EDIT: Thanks very much for the suggestions to temporarily map. I have seen that used. I am more interested in compiler options that will enable -no-dynamic-linker to work, avoiding generating code that requires R_AARCH64_ABS64 relocations on the strength of a guarantee that code and data will be a set distance apart..

2 Answers

It has been my experience that most operating system kernels which want to live in high memory are just linked to their target high memory address, and one of the very first things the kernel does on startup is build the page tables to map itself into high memory. That way the OS kernel designer just needs to make sure that some chunk of the entrypoint is position independent (or actually, just have that startup code linked at its true low memory location by placing it in a special section and changing the linker script).

See example entrypoint and linker script (not a Rust kernel/aarch64).

A sophisticated enough kernel which has control of its memory allocator can then think about freeing the page(s) which contain the entrypoint code, as identified by the start and end of the entrypoint section (and indeed expand this to deal with all of the init code in the same way).

If you can afford using extra memory at startup, a nice rust option could also be to write a separate crate for the loader, tie everything together with the linker script, and just free its memory after startup.

You cannot rebase arbitrary code mid-way through its execution and expect it to continue to function. Literally every address in use at the point of rebasing will become invalid. Even if you got rid of all static references, how would you deal with dynamically allocated memory?

You will either have to load a clean copy of your binary after mapping at high addresses, or you will have to compile with a high load address and map yourself there before running any other code.

If you feel like your mapping logic is too long/complex to be done at such a stage, you can get away with trivial page tables that map everything as RWX first, then you can switch, bring up your libraries and other code, and then proceed to build the final page tables. Such "trivial" page tables can be built at compile time, should fit in no more than 4 pages of memory (two for each TTBR), and using them to switch to high addresses should take at most 20 instructions.

Related