MIPS 动态符号的延迟绑定

  1. 概述
MIPS平台中动态库的装载与解析原理,网上资料并不多,而且很多讲解都是错误的,甚至MIPS宝书《see mips run》中也没有提供详细的方案。因此本人只好通过代码走读和运行验证的方法来详细了解其原理。之所以要了解mips动态符号的解析调用,是因为之前在实现mips backtrace时,对一些调整gp的指令存在疑惑,顾而深入研究一下。

先上结论:MIPS动态库中的外部符号调用,是依赖.got段和.MIPS.stubs段来共同实现的。
.got中保存着外部符号的地址,但由于采用了延迟绑定技术,某外部符号在第一次被调用前,其地址是未知的,ld.so在装载动态库阶段并没有立刻将外部函数解析并填充到.got中,目的是为了加快启动速度,也是为了不必要的时间浪费,因为在进程运行期间,某些外部符号可能永远都不会被调用到,所以装载阶段解析所有符号是一个浪费时间的行为。
因此,.got中的未决议符号项内,先指向了.MIPS.stubs中的一段指令(每个符号都对应一段单独的指令),这段指令的作用是跳转至符号解析函数_dl_runtime_resolve。
.got段的第一项比较特殊,它保存的符号正是函数"_dl_runtime_resolve",用objdump查看时此项为0,但运行起来之后,会被ld.so填充进_dl_runtime_resolve的地址。

下面简单举例说明。
hello.c:
#include <stdio.h>
#include "hello.h"
int hello(int i)
{
printf("hello %d \n",i);
return 0;
}

main.c:
#include <stdio.h>
#include "hello.h"
int main(int argc, char** argv)
{
int i = 0;

hello(i);
i++;

hello(i);
return 0;
}

mipsel-linux-gcc hello.c -g -fPIC -rdynamic -fasynchronous-unwind-tables -shared -o libhello.so
mipsel-linux-gcc main.c -g -fPIC -rdynamic -fasynchronous-unwind-tables -L./ -lhello -o main.elf
  1. 流程图
  1. main首次调用hello
mips_lazy_banding.png
图1 main首次调hello
  1. main再次调用hello
mips_after_banding.png

图2 main再次调hello
  1. 汇编代码走读
main函数汇编代码走读:
00400810 <main>:
400810: 3c1c0002 lui gp,0x2 //gp = 0x20000
400814: 279c81b0 addiu gp,gp,-32336 //gp = gp -32336;
400818: 0399e021 addu gp,gp,t9 //gp == &_gp,此时gp寄存器指向符号_gp(_gp_disp),也就是 &_gp = 4189c0 关于_gp的详细解释请见附录,t9存的是本函数地址
40081c: 27bdffd8 addiu sp,sp,-40 //调整栈顶指针
400820: afbf0024 sw ra,36(sp) //保存返回地址
400824: afbe0020 sw s8,32(sp) //保存caller fp
400828: 03a0f021 move s8,sp //调整fp
40082c: afbc0010 sw gp,16(sp) //保存本函数gp
400830: afc40028 sw a0,40(s8) //保存入参
400834: afc5002c sw a1,44(s8) //保存入参
400838: afc00018 sw zero,24(s8) //本地变量初始化
40083c: 8fc40018 lw a0,24(s8) //准备将本地变量作为入参传给子函数
400840: 8f828018 lw v0,-32744(gp) //v0 = *(&_gp - 32744) = *(4109d8) = 4008f0,也就是.got内第3项存的是.MIPS.stub的地址,稍后经过binding之后,此处会got第3项变成hello的地址
400844: 0040c821 move t9,v0 //经计算,此处t9寄存器的值是4008f0,也即.MIPS.stubs的地址
400848: 0320f809 jalr t9 //跳转到.MIPS.stubs处执行,返回地址已自动存入ra寄存器
40084c: 00000000 nop
...

004008f0 <.MIPS.stubs>:
4008f0: 8f998010 lw t9,-32752(gp) //t9 = *(4109d0),为 .got的首地址
4008f4: 03e07821 move t7,ra //将返回地址存到t7,此处的返回地址显然是main中的指令 "jalr t9"的下下条指令。_dl_runtime_resolve中会用t7寄存器来返回到main。
4008f8: 0320f809 jalr t9 //跳转执行.got首地址的指令,也就是_dl_runtime_resolve函数地址
4008fc: 24180015 li t8,21 //t8=21 此处是将hello的符号表(.dynsym)的索引21存入t8
...

Disassembly of section .got:
004109d0 <_GLOBAL_OFFSET_TABLE_>:
4109d0: 00000000 nop //此处保存的是_dl_runtime_resolve,编译后值为0,但运行后会被ld.so填充上_dl_runtime_resolve的地址,已用gdb验证
4109d4: 80000000 lb zero,0(zero)
4109d8: 004008f0 tge v0,zero,0x23


ld-uClibc.so中的_dl_runtime_resolve函数:
00007f50 <_dl_runtime_resolve>:
7f50: 03801821 move v1,gp
7f54: 27bdffd8 addiu sp,sp,-40
7f58: 2739000c addiu t9,t9,12
7f5c: 3c1c0002 lui gp,0x2
7f60: 279ca0b4 addiu gp,gp,-24396
7f64: 0399e021 addu gp,gp,t9
7f68: 03e01021 move v0,ra
7f6c: afbc0020 sw gp,32(sp)
7f70: afaf0024 sw t7,36(sp) //将.MIPS.stubs中保存的main中的返回地址保存起来
7f74: afa40010 sw a0,16(sp)
7f78: afa50014 sw a1,20(sp)
7f7c: afa60018 sw a2,24(sp)
7f80: afa7001c sw a3,28(sp)
7f84: 03002021 move a0,t8 //将t8的值做为入参传给函数<__dl_runtime_resolve>,从其函数原型看,t8存的是hello在符号表中的index,__dl_runtime_resolve会根据这个index来更新got(通过对hello在symtab中的index进行一定的计算和转换,能得到hello在got表中的位置)
7f88: 00602821 move a1,v1
7f8c: 8f99814c lw t9,-32436(gp)
7f90: 0411ec02 bal 2f9c <__dl_runtime_resolve>
7f94: 00000000 nop
7f98: 8fbc0020 lw gp,32(sp)
7f9c: 8fbf0024 lw ra,36(sp) //此处将main中调用hello的返回地址加载到ra
7fa0: 8fa40010 lw a0,16(sp)
7fa4: 8fa50014 lw a1,20(sp)
7fa8: 8fa60018 lw a2,24(sp)
7fac: 8fa7001c lw a3,28(sp)
7fb0: 27bd0028 addiu sp,sp,40
7fb4: 0040c821 move t9,v0 //v0做为__dl_runtime_resolve的返回值,保存的是hello的实际地址
7fb8: 03200008 jr t9 //执行hello函数,由于此时ra存的是main调hello的返回地址,因此hello函数返回时正好跳转到main中。这也是此处使用jr而不是jal的原因,因为jal会自动更新ra寄存器!
7fbc: 00000000 nop


uclinux-rootfs/lib/uClibc/ldso/ldso/mips/elfinterp.c:
unsigned long __dl_runtime_resolve(unsigned long sym_index,
unsigned long old_gpreg)
{
......
return new_addr;
}
  1. 运行验证
# ./gdb ./main.elf
Reading symbols from /mnt/usb/main.elf...done.
(gdb) b main
Breakpoint 1 at 0x400838: file ./main.c, line 8.
(gdb) r
Starting program: /mnt/usb/main.elf
[Thread debugging using libthread_db enabled]

Breakpoint 1, main (argc=1, argv=0x7fff6e04) at ./main.c:8
8 ./main.c: No such file or directory.
in ./main.c
(gdb) info address main
Symbol "main" is a function at address 0x400810.
(gdb) x/xw &main
0x400810 <main>: 0x3c1c0002 //此处main的地址还是0x400810,说明main.elf虽然以fPIC编译的,但是做为主程序,依然被加载到了默认地址。
(gdb) x/32xw &main
0x400810 <main>: 0x3c1c0002 0x279c81b0 0x0399e021 0x27bdffd8
0x400820 <main+16>: 0xafbf0024 0xafbe0020 0x03a0f021 0xafbc0010
0x400830 <main+32>: 0xafc40028 0xafc5002c 0xafc00018 0x8fc40018
0x400840 <main+48>: 0x8f828018 0x0040c821 0x0320f809 0x00000000
0x400850 <main+64>: 0x8fdc0010 0x8fc20018 0x24420001 0xafc20018
0x400860 <main+80>: 0x8fc40018 0x8f828018 0x0040c821 0x0320f809
0x400870 <main+96>: 0x00000000 0x8fdc0010 0x00001021 0x03c0e821
0x400880 <main+112>: 0x8fbf0024 0x8fbe0020 0x27bd0028 0x03e00008
(gdb) x/32xw 0x4109d0 //既然main的地址没发生偏移,那么说明.got的地址也没变,依然是编译后的0x4109d0,打印显示.got的第一项是0x77fe6780
0x4109d0 <_GLOBAL_OFFSET_TABLE_>: 0x77fe6780 0x77ff5028 0x004008f0 0x00000000 //.got第3项是4008f0,也即.MIPS.stubs内的地址,此时还没真正调用到hello()
0x4109e0 <completed.5347>: 0x00000000 0x00000000 0xffffffff 0x00000000
0x4109f0 <object.5359+8>: 0x00000000 0x0041095c 0x000007f8 0x77fba898
0x410a00: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a10: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a20: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a30: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a40: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) info symbol 0x77fe6780 //此地址真的是_dl_runtime_resolve!猜想得以验证!
_dl_runtime_resolve in section .text of /lib/ld-uClibc.so.0
(gdb) info address hello
Symbol "hello" is a function at address 0x77faa690.
(gdb) b main.c:11
Breakpoint 2 at 0x400854: file ./main.c, line 11.
(gdb) c
Continuing.
hello,world, i=0

Breakpoint 2, main (argc=1, argv=0x7fff6e04) at ./main.c:11
11 in ./main.c
(gdb) x/32xw 0x4109d0 //此时已经调用过一次hello函数了,再次查看.got的第3项:已经变成hello的地址了!!
0x4109d0 <_GLOBAL_OFFSET_TABLE_>: 0x77fe6780 0x77ff5028 0x77faa690 0x00000000
0x4109e0 <completed.5347>: 0x00000000 0x00000000 0xffffffff 0x00000000
0x4109f0 <object.5359+8>: 0x00000000 0x0041095c 0x000007f8 0x77fba898
0x410a00: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a10: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a20: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a30: 0x00000000 0x00000000 0x00000000 0x00000000
0x410a40: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) info symbol 0x77faa690
hello in section .text of /mnt/usb/libhello.so
 附录 关于寄存器gp与符号_gp
我们知道PIC的函数入口处,首先会调整gp寄存器,很多资料上都说gp指向的是.got,实际上并不是,gp寄存器指向的是符号_gp,这个_gp绝不是got段的地址,而是一个常数区的中间位置,它是在编译阶段生成的,它的身份是做为gp相对寻址的“基准”位置。
《see mips run》p260 9.4.1 相对于gp寻址:
要求compiler,assembler,linker以及运行时激活代码(runtime startup code)偕同配合,把程序中的’小’变量和常数汇集到一个独立的内存区间;然后设置register $28(通常称为全局量指针global pointer或简写为gp)指向该区间的中央(linker生成一个特殊符号_gp,其地址为该区间的中央,激活代码负责将_gp的地址加载到gp寄存 器,这一动作在第一个load/store指令运行之前完成).只要这些全局变量\静态编量\常量加起来不占用超过64k大小的空间,这些资料相对该区间 的中点也就不超过正负32k(偏移量15bit+符号位1bit,参见mips机器码格式),那幺我们就可以在一条指令中完成对它们的 load/store操作:
lw $2, addr ---> lw $2, addr-_gp(at)
在同一个动态模块中,每个函数入口处的gp寄存器调整,都将使gp指向符号_gp的地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章