原文地址:http://tsecer.blog.163.com/blog/static/150181720124993733491/
一、动态链接
在Linux(unix族谱)下,共享目标文件称为so文件,它和windows下的DLL机制对应,该功能在节省物理内存使用量上有重要意义,但是更重要的它还是一种扩展框架,也就是很多所谓的“插件”的实现基础。
从它的出现频率上来看,它和Linux下的多线程具有同等重要的地位,甚至更高。因为很多可执行文件都没有使用pthread库,但是几乎所有的Linux发行版本的可执行文件都是动态链接生成的可执行文件。
从实现机制上看,两种机制也有相同之处,它们都有共享和隔离,而线程和共享库内的线程私有数据第一个“多对多”的关系,而多对多关系的描述和实现会比较相对复杂,这一点可以参考一些复杂数据库以及之前博客中曾经简单讨论过的cgroup实现机制。
从对程序员的印象上来讲,两个都相对比较高级一些,因为平时很多程序不需要考虑这些问题,也就是说,可能是相对“高级”一些的内容。当然这里说复杂,可能很多人不以为然。这就是一个所谓“深入简出”的问题,简单的hello world程序,可以引申出很多问题,例如用户态printf缓冲区如何管理(考虑一些超长字符串)、多线程下如何保证打印的粒度互斥(一个printf是否中间夹杂其它printf的内容)、当格式化中有些参数异常时打印结果(考虑格式化“%#30.2”这个打印结果)、printf不定长参数个数如何实现(可能以后参数的入栈顺序)、printf如何进入系统调用、不同文件对相同字节流的不同行为(考虑串口对彩色支持)、hello world是如何显示在屏幕上的等问题。所以同样是一个简单的问题,可以引申出很多东西,这就是不应该看一个东西执行一下就说“精通”之类。我看之前的博客,觉得内容也是比较简单,这都是一个渐进的过程:吾生也有涯,而知也无涯。何况技术这东西,你汪深处挖挖总是有的。
二、延迟绑定
这个就是所谓“懒惰思想”(lazy)在程序设计中的有一个体现,另一个体现就是经典的内存管理。其实这里的“lazy”并不是我们通常所说的懒惰,而是一种坦然和自信。因为别人交给你的任务,比可以随时快速的答应下来,而当别人真正需要的时候,可以快速的交互,而懒惰则是消极的拒绝,不答应做,也做不下来。做技术到最后,也不是说把所有的东西全部学完,而是可以很多东西不学,但是当真正用的时候可以快速理解和运用。
这里的延迟绑定就是对于一个模块使用的另一个so文件中的符号,它可以在这个符号第一次被使用的时候完成真正的重定位而不是在程序运行的开始就匆匆忙忙把所有的符号动态链接完成(当然也可以通过LD_BIND_NOW来要求它在加载so是就一次性完成动态重定位)。作为一个直接推论,如果这个重定位位置上的代码从来没有被真正执行过,那么这个动态重定位永远也不会发生。这样的另一个好处就是可以将重定位的时间分摊到运行的各个阶段,从而提高程序刚启动起来之后的交互感受。
三、测试代码
//测试代码使用到的文件
[tsecer@Harry SoDemo]$ ls
main.c Makefile miniso.c
//使用动态库内容的主函数
[tsecer@Harry SoDemo]$ cat main.c
extern int bay,baz;
extern int foo(int);
extern int bar(int);
int main()
{
return foo(bay) + bar(baz);
}
//简单Makefile
[tsecer@Harry SoDemo]$ cat Makefile
main.exe:miniso.so
gcc main.c -fPIC -L. -lminiso -o main.exe -g
miniso.so:
gcc -fPIC miniso.c -shared -o libminiso.so
clean:
rm -f *.so *.exe *.o
//该文件用来生成so文件。
[tsecer@Harry SoDemo]$ cat miniso.c
/*
* miniso source
*/
int bay = 0x44444444;
int baz = 0x22222222;
/* multipy 2*/
int foo(int arg)
{
return arg<<1;
}
/*divide 2*/
int bar(int arg )
{
return arg>>1;
}
[tsecer@Harry SoDemo]$
四、main函数代码分析
(gdb) disas main
Dump of assembler code for function main:
0x080484f4 <main+0>: push %ebp
0x080484f5 <main+1>: mov %esp,%ebp
0x080484f7 <main+3>: and $0xfffffff0,%esp
0x080484fa <main+6>: push %esi
0x080484fb <main+7>: push %ebx
0x080484fc <main+8>: sub $0x18,%esp
0x080484ff <main+11>: call 0x8048538 <__i686.get_pc_thunk.bx> 该函数执行完成之后,ebx寄存器中存放本进程的GOT表位置,这一点可以通过之后的readelf输出确认。
0x08048504 <main+16>: add $0x1274,%ebx
0x0804850a <main+22>: mov -0x8(%ebx),%eax bay的地址为 ebx -8,
0x08048510 <main+28>: mov (%eax),%eax
0x08048512 <main+30>: mov %eax,(%esp)
0x08048515 <main+33>: call 0x8048428 <foo@plt>
0x0804851a <main+38>: mov %eax,%esi
0x0804851c <main+40>: mov -0x4(%ebx),%eax baz的地址为ebx -4
0x08048522 <main+46>: mov (%eax),%eax
0x08048524 <main+48>: mov %eax,(%esp)
0x08048527 <main+51>: call 0x80483f8 <bar@plt>
0x0804852c <main+56>: lea (%esi,%eax,1),%eax
0x0804852f <main+59>: add $0x18,%esp
0x08048532 <main+62>: pop %ebx
0x08048533 <main+63>: pop %esi
0x08048534 <main+64>: mov %ebp,%esp
0x08048536 <main+66>: pop %ebp
0x08048537 <main+67>: ret
End of assembler dump.
(gdb) disas 0x8048428
Dump of assembler code for function foo@plt:
0x08048428 <foo@plt+0>: jmp *0x8049790 跳转地址为
0x0804842e <foo@plt+6>: push $0x18
0x08048433 <foo@plt+11>: jmp 0x80483e8
End of assembler dump.
(gdb) disas 0x80483f8
Dump of assembler code for function bar@plt:
0x080483f8 <bar@plt+0>: jmp *0x8049784
0x080483fe <bar@plt+6>: push $0x0
0x08048403 <bar@plt+11>: jmp 0x80483e8 两个plt都跳转到了同一个地址0x80483e8,但是这个地址并没有位于代码段。
End of assembler dump.
(gdb)
1、0x80483e8 地址意义
使用objdump -D main.exe命令,可以看到其中该地址处的指令为:
080483e8 <bar@plt-0x10>:
80483e8: ff 35 7c 97 04 08 pushl 0x804977c
80483ee: ff 25 80 97 04 08 jmp *0x8049780 这里两条语句使用了两个编译时确定地址,并且它们的地址是连续的。
80483f4: 00 00 add %al,(%eax)
2、0x804977c 地址的意义
调试器中
(gdb) x 0x8049780
0x8049780 <_GLOBAL_OFFSET_TABLE_+8>: 0x001fc850
(gdb) disas 0x001fc850
Dump of assembler code for function _dl_runtime_resolve:
……
这说明0x8049780处存放了_dl_runtime_resolve函数的实现,注意,这个函数位于ld-linux.so,也就是它的位置是程序运行起来之后才能确定的。
3、这个_dl_runtime_resolve地址由谁何时填充
看可执行文件的动态节中PLTGOT节中内容
[tsecer@Harry SoDemo]$ readelf -a main.exe
……
Dynamic section at offset 0x69c contains 21 entries:
Tag Type Name/Value
……
0x00000003 (PLTGOT) 0x8049778 这个PLTGOT从0x8049778 开始和上面的连续地址最为接近。
……
0x00000000 (NULL) 0x0
动态链接器相关代码glibc-2.7\sysdeps\i386\dl-machine.h
static inline int __attribute__ ((unused, always_inline))
elf_machine_runtime_setup (struct link_map *l, int lazy, int profile)
{
Elf32_Addr *got;
extern void _dl_runtime_resolve (Elf32_Word) attribute_hidden;
extern void _dl_runtime_profile (Elf32_Word) attribute_hidden;
if (l->l_info[DT_JMPREL] && lazy)
{
/* The GOT entries for functions in the PLT have not yet been filled
in. Their initial contents will arrange when called to push an
offset into the .rel.plt section, push _GLOBAL_OFFSET_TABLE_[1],
and then jump to _GLOBAL_OFFSET_TABLE[2]. */
got = (Elf32_Addr *) D_PTR (l, l_info[DT_PLTGOT]);
/* If a library is prelinked but we have to relocate anyway,
we have to be able to undo the prelinking of .got.plt.
The prelinker saved us here address of .plt + 0x16. */
if (got[1])
{
l->l_mach.plt = got[1] + l->l_addr;
l->l_mach.gotplt = (Elf32_Addr) &got[3];
}
got[1] = (Elf32_Addr) l; /* Identify this shared object. */
/* The got[2] entry contains the address of a function which gets
called to get the address of a so far unresolved function and
jump to it. The profiling extension of the dynamic linker allows
to intercept the calls to collect information. In this case we
don't store the address in the GOT so that all future calls also
end in this function. */
if (__builtin_expect (profile, 0))
{
got[2] = (Elf32_Addr) &_dl_runtime_profile;
if (GLRO(dl_profile) != NULL
&& _dl_name_match_p (GLRO(dl_profile), l))
/* This is the object we are looking for. Say that we really
want profiling and the timers are started. */
GL(dl_profile_map) = l;
}
else
/* This function will get called to fix up the GOT entry indicated by
the offset on the stack, and then jump to the resolved address. */
got[2] = (Elf32_Addr) &_dl_runtime_resolve;
}
return lazy;
}
这意味着每个PIC文件中,它的PLTGOT标签说明了一个特殊的got地址,该地址开始第一个地址意义未知,第二项为标识该文件的link_map对象地址,第三个为动态链接时确定的_dl_runtime_resolve函数地址,而这个函数是完成惰性链接的真正执行者。
结合上面的例子
0x00000003 (PLTGOT) 0x8049778
该地址为代码中got的的值,然后got[2] = 0x8049778 + 2*4=0x8049780,而这个地址也就是
080483e8 <bar@plt-0x10>:
80483e8: ff 35 7c 97 04 08 pushl 0x804977c
80483ee: ff 25 80 97 04 08 jmp *0x8049780
中使用的跳转地址。
这个调用关系为:_dl_relocate_object--->>>ELF_DYNAMIC_RELOCATE--->>>elf_machine_runtime_setup
五、外部数据动态链接实现
这里的外部数据访问和外部函数调用是不同的,外部数据访问必须在so加载之后马上完成重定位,它不能使用惰性链接,所谓惰性链接只能对函数调用实现。因为数据可以通过指令直接访问,动态连接器没有位置拦截对于这个数据的使用,而对于外部函数的调用链接器可以方便的定义自己的适配函数。
1、glibc实现代码
ELF_DYNAMIC_RELOCATE---->>>ELF_DYNAMIC_DO_REL
# define _ELF_DYNAMIC_DO_RELOC(RELOC, reloc, map, do_lazy, test_rel) \
do { \
struct { ElfW(Addr) start, size; int lazy; } ranges[2]; \
ranges[0].lazy = 0; \对于数据访问,始终不会使用lazy方式。
ranges[0].size = ranges[1].size = 0; \
ranges[0].start = 0; \
\
if ((map)->l_info[DT_##RELOC]) \
{ \
ranges[0].start = D_PTR ((map), l_info[DT_##RELOC]); \
ranges[0].size = (map)->l_info[DT_##RELOC##SZ]->d_un.d_val; \
} \
if ((map)->l_info[DT_PLTREL] \
&& (!test_rel || (map)->l_info[DT_PLTREL]->d_un.d_val == DT_##RELOC)) \
{ \
ElfW(Addr) start = D_PTR ((map), l_info[DT_JMPREL]); \
\
if (! ELF_DURING_STARTUP \
&& ((do_lazy) \
/* This test does not only detect whether the relocation \
sections are in the right order, it also checks whether \
there is a DT_REL/DT_RELA section. */ \
|| ranges[0].start + ranges[0].size != start)) \
{ \
ranges[1].start = start; \
ranges[1].size = (map)->l_info[DT_PLTRELSZ]->d_un.d_val; \
ranges[1].lazy = (do_lazy); \对于PLT类型动态重定位项,根据参数确定是否进行惰性链接。
} \
else \
{ \
/* Combine processing the sections. */ \
assert (ranges[0].start + ranges[0].size == start); \
ranges[0].size += (map)->l_info[DT_PLTRELSZ]->d_un.d_val; \
} \
} \
\
if (ELF_DURING_STARTUP) \
elf_dynamic_do_##reloc ((map), ranges[0].start, ranges[0].size, 0); \
else \
{ \
int ranges_index; \
for (ranges_index = 0; ranges_index < 2; ++ranges_index) \
elf_dynamic_do_##reloc ((map), \
ranges[ranges_index].start, \
ranges[ranges_index].size, \
ranges[ranges_index].lazy); \
} \
} while (0)
2、以测试程序说明相关内容
对于我们测试的main.exe,其相关动态节内容为
0x00000002 (PLTRELSZ) 32 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048398
0x00000011 (REL) 0x8048380
0x00000012 (RELSZ) 24 (bytes)
0x00000013 (RELENT) 8 (bytes)
这里看到,其中的DT_REL从0x8048380开始,总共占用DT_RELSZ=24 字节,所以结束位置为0x8048398,而这个位置也就是DT_JMPREL的起始位置,而plt的大小为DT_PLTRELSZ=32字节,共32/DT_RELENT=24/8=4项。
其它对应输出
Relocation section '.rel.dyn' at offset 0x380 contains 3 entries:
Offset Info Type Sym.Value Sym. Name
0804976c 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
08049770 00000606 R_386_GLOB_DAT 00000000 bay
08049774 00000706 R_386_GLOB_DAT 00000000 baz 数据重定位共三项。
Relocation section '.rel.plt' at offset 0x398 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
08049784 00000107 R_386_JUMP_SLOT 00000000 bar
08049788 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
0804978c 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main
08049790 00000507 R_386_JUMP_SLOT 00000000 foo 代码重定位共四项。
六、todo
ldd的如何实现,
动态库的ELF header中 entry意义
so文件中初始化函数(init)何时调用,如何定义和配置
动态链接器直接加载文件 ld-linux.so prog args
动态链接器bootstrap启动
共享库文件单独运行时地址如何确定,内核如何处理