Search code examples
assemblycpu-architectureriscvinstruction-setposition-independent-code

RISCV - How are jump instructions PC-relative?


In the RISC-V Unpriviliged spec V20191213, the following is stated, (page 21)

The unconditional jump instructions all use PC-relative addressing to help support position-independent code.

Looking at the definition of the JALR instruction,

The indirect jump instruction JALR (jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1, then setting the least-significant bit of the result to zero.

This address calculation is clearly not PC-relative. So, why does the spec claim that all jump instructions use PC-relative addressing?

Also, how can having PC-relative addressing support position-independent code? Shouldn't it be the exact opposite?


Solution

  • This address calculation is clearly not PC-relative. So, why does the spec claim that all jump instructions use PC-relative addressing?

    Yes, you are correct.  You have to appreciate some of the different uses of JALR:

    1. to return from subroutine,
    2. to make indirect function calls,
    3. to make long range (+/-2GB) function calls

    The first two do not require any offset at all, since the desired address is fully specified in the register, so there is no issue of needing to be pc-relative.

    The last uses a 2-instruction sequence, the first of which is AUIPC, which makes the sequence pc-relative.

    Also, how can having PC-relative addressing support position-independent code? Shouldn't it be the exact opposite?

    PC relative means that the code can be loaded anywhere in memory and will still function — which is to say that the code can find other parts of the code regardless of varying the initial address at which the block of code is loaded (or where, if in ROM, it appears in the address space).

    Data that has pointers that refer to code will need to be relocated (adjusted) depending on the final location the code is loaded.

    (Of course, position independence does not mean that the code can move once it has begun execution!  Once execution starts, data structures capture code addresses, so the code must remain fixed at the location chosen to load it for the duration of that execution.  Position independent means load-time position independent.)

    Thus, it is ok to capture return addresses on subroutine invocation as direct/absolute pointer values, which may or may not be saved to a stack frame, before being used to return from subroutine, e.g. using JALR.

    In this case, even though the return address is an absolute pointer value, it is dynamically captured from the position of the caller at runtime, and thus doesn't violate load-time position independence.

    (You wouldn't want the return operation via JALR to be pc-relative, because the pc address of the JALR, at the end of the returning callee, is irrelevant to the return linkage location in the caller.  What is important is the capture of the return address relative to the JAL that invoked the subroutine.)


    In a hypothetical mechanism, if we wanted some form of runtime-position independence, we might have the software/hardware compute two differences, namely between:

    • the caller's invocation instruction address and the address of the first instruction of the callee, which would be used as the return "offset" that would be passed to the callee in lieu of the return address, and,

    • the address of the first instruction of the callee and the address of an instruction of the callee that is returning to the caller.

    These two differences could be applied to a pc-relative return instruction, at the time of return (e.g. from the pc of the returning instruction).  This would provide some measure of runtime-position independence (e.g. for return linkage), but (a) would be overly complicated, and (b) would also not be nearly sufficient to make all pointers (or usages of pointers) runtime position independent.


    The step to load/run position independent code are as follows:

    1. load the code
    2. relocate the data
    3. run it!

    The relocation step 2. can be eliminated if the data already knows where the code will be loaded.  Otherwise, the data relocation needs to, in various places where it refers to the code (maybe nowhere), sum the base address of the loaded code to the data locations that refer to code.

    Once we pass step 2. the data is bound to the code using absolute addresses, and the code cannot be moved until the execution has terminated.

    An advantage of position independent code is that the such code does not require relocation, so can be read only, yet still loaded at different addresses in different processes, if needed.