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.


