Pages

Wednesday, February 4, 2026

RiscV Assembly Got/PLT index Calculation

I searched many online document about RiscV PLT table and how the relocation works. Eventually I got my own version working. This is my notes in case one day I will forget. 

Based on the PLT/GOT RISC V code, the lazy binding code is like the following two tables. The first one is the PLT[0] code stub where the _dl_runtime_resolver code is being invoked. The t3 stores the _dl_runtime_resolver address and the jr t3 is to call that function. That function will requires two parameters: t0 holds the link map address, and t1 holds the function (e.g. printf) offset in the .got.plt. In the code, t2 is used as temporary register.

The summary is below

·       t3 is the _dl_runtime_resolver function address stored in GOT

·       t0 is the link map address in GOT

·       t1 is the function (e.g. printf) offset in .got.plt

·       t2 is temporary register

The hdr_size is the PLT[0] size which is two 16 bytes (32 bytes) size section. It has 8 instructions. In the following code, PTRSIZE = 4 if 32bits and PTRSIZE = 8 if 64bits

Table 1 PLT[0] Code Stub

1:  auipc  t2, %pcrel_hi(.got.plt)

    sub    t1, t1, t3               # shifted .got.plt offset + hdr size + 12

    l[w|d] t3, %pcrel_lo(1b)(t2)    # _dl_runtime_resolve

    addi   t1, t1, -(hdr size + 12) # shifted .got.plt offset, hdr_size is PLT0 size (32 bytes)

    addi   t0, t2, %pcrel_lo(1b)    # &.got.plt

    srli   t1, t1, log2(16/PTRSIZE) # .got.plt offset

    l[w|d] t0, PTRSIZE(t0)          # link map

    jr     t3

 

And the above code is invoked by the following code. The following code is a function (e.g. printf) stub in plt section.

Table 2 PLT[N] Code Stub

1:  auipc   t3, %pcrel_hi(function@.got.plt)

    l[w|d]  t3, %pcrel_lo(1b)(t3)

    jalr    t1, t3

    nop

 

How to get offset in t1 register

The t1 is used to compute the got.plt offset from the plt code stub.

When the code called from Table 2 PLT[N] Code Stub to Table 1 PLT[0] Code Stub. The first time the Table 2 PLT[N] Code Stub is called, the function@.got.plt is points to PLT[0]. The PLT[0]’s job is to update the function@.got.plt to actually function address.

Table 2 PLT[N] Code Stub set t1 and t3 to the following value

·       t1 = &nop = &PLT[N] + 12,

o   12 is from 3 instruction and each instruction is 4 bytes

·       t3 = &PLT[0]

In the Table 1 PLT[0] Code Stub, the t1 was computed

t1 = [t1 – t3 – (hdr_size + 12)] >>   

     = [&PLT[N] + 12 - &PLT[0] – hdr_size – 12] >>  

= [&PLT[N] - &PLT[0] – hdr_size] >>   (where hdr_size is the PLT[0] size, it is 32bytes, refer to the diagram below)

= [(N-1)*16] >>  

=  =  

= (N-1)PTRSIZE

 

 

N starts from 1 because PLT[0] is reserved for dynamic resolver. So the 1st function is at 0 and 2nd function is at PTRSIZE. The PTRSIZE is the address data size.

·       If 32bit address, the PTRSIZE is 4, which is 4 bytes equals 32 bits.

·       If 64bit address, the PTRSIZE is 8, which is 8 bytes equals 64 bits.

Please note the GOT.PLT has 1st and 2nd element to be reserved value. The 1st element is PLT[0] address and the 2nd element is link map address.

Link map into t0

The link map is a reserve entry in got.plt section. The link_map is a data structure used internally by the Linux dynamic linker (ld.so) to keep track of all shared libraries (shared objects, .so files) loaded into a running process.

Executable (.got.plt)

├── [0] => address of resolver entry (__dl_runtime_resolve)

├── [1] => pointer to link_map (used by resolver)

├── [2] => address of function (e.g., printf)

 

Please note this is not the end. If you want to call printf, you will need to implement the gnu version table and hash algorithm. I tried both old hash algorithm and new hash algorithm, both works. My environemnt is QEMU and StarFive2 Ubuntu Linux.