本文使用如下的测试程序,观察动态链接时printf相关的链接逻辑实现。
1 | #include <stdio.h> |
riscv64-linux-gnu-gcc test.c编译后,riscv64-linux-gnu-objdump -D ./a.out > test.s做反汇编。
test.s里关键的内容如下。main函数里对printf的调用是:
1 | 646: f0bff0ef jal ra,550 <printf@plt> |
printf@plt是.plt段的一段,整个.plt如下:
1 | 0000000000000520 <.plt>: |
我们依次看下printf@plt里的指令,auipc和ld计算地址后把0x2028的数据load到t3,这里0x2028
是.got里printf函数的地址,这个位置存放动态链接完成后,printf的地址,但是第一次运行
到这里,可以看到0x2028上的内容是0x520,这个地址正好是.plt的基地址,所以printf@plt
中的jalr跳转到.plt开始处。.plt开始处准备t0-t3的数据,并跳到t3地址上执行,依次看下
t0-t3上保存的数据,0x530处的指令得到t0为.got表的基地址,所以0x538得到.got + 8处的
数据,linkmap的地址?0x524处t1 = t1 - t3, 得到0x55c - 0x520的差值,0x52c处把这个
差值再减去44(0x2c)得到0x10,这个值是什么?t2的值是0x2520,_dl_runtime_resolve没有
用到,t3是.got基地址上的数据,ld在装载时会用_dl_runtime_resolve的地址覆盖.got的
这个位置,所以0x53c就是直接跳到_dl_runtime_resolve函数执行。
如下是反汇编.got的内容:
1 | Disassembly of section .got: |
_dl_runtime_resolve在glibc里定义,具体位置在glibc/sysdeps/riscv/dl-trampoline.S,
这个函数是体系架构相关的,这里是riscv定义的地方。这个函数计算需要动态链接的函数
的地址,更新.got里对应的地址,然后把控制返回到.plt的对应域段,比如这里的printf@plt
域段,可见这里再次load到t3的值就是printf实际地址了。
需要注意的是,printf的入参被保存到a0-a7这样的参数寄存器上,而且没有保存恢复的动作,
所以这里.plt里两次过程调用都没有使用a0-a7寄存器,而是使用t0-t3这样的临时寄存器,
临时寄存器是caller save寄存器,只要过程调用后不再使用,caller就可以不必保存和恢复。
这里.plt里无法使用正常的使用a0-a7的过程调用,因为这里不是一个返回调用点后续指令的
调用行为。这里的调用从printf@plt进入,在0x558发生一圈调用后,又跳回到printf@plt
重复执行,而正常的调用应该返回到0x55c。