一个动态链接加载的例子如下
假设有p1.c 和 p2.C 都引用了 foo.c程序
foo.h内容如下
#ifndef LIB_H
#define LIB_H
void foobar(int i);
#endif
foo.c内容如下
#include <stdio.h>
void foo(int i) {
printf("printf from lib.so %d\n", i);
sleep(-1);
}
p1.c 内容如下
#include "foo.h"
int main() {
foo(1);
return 0;
}
p2.c 内容如下
#include "foo.h"
int main() {
foo(2);
return 0;
}
编译命令如下
//将foo.c编译成动态链接库
gcc -fPIC -shared -o libfoo.so foo.c
//编译p1和p2
gcc -o p1 p1.c ./libfoo.so
gcc -o p2 p2.c ./libfoo.so
运行p1
./p1
printf from lib.so 1
修改foo.c程序
#include <stdio.h>
void foo(int i) {
printf("printf from lib.so %d\n", i);
printf("hehe --> %d\n", i);
sleep(-1);
}
再编译一遍foo.c 生成 libfoo.so文件,不用再编译p1和p2,直接p1结果为
printf from lib.so 1
hehe --> 1
执行p1 , cat /proc/[PID]/maps 结果如下
00400000-00401000 r-xp 00000000 fd:01 1063464 /root/c/book_link/7/p1
00600000-00601000 r--p 00000000 fd:01 1063464 /root/c/book_link/7/p1
00601000-00602000 rw-p 00001000 fd:01 1063464 /root/c/book_link/7/p1
7f5422887000-7f5422a3f000 r-xp 00000000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f5422a3f000-7f5422c3f000 ---p 001b8000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f5422c3f000-7f5422c43000 r--p 001b8000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f5422c43000-7f5422c45000 rw-p 001bc000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f5422c45000-7f5422c4a000 rw-p 00000000 00:00 0
7f5422c4a000-7f5422c4b000 r-xp 00000000 fd:01 1063461 /root/c/book_link/7/libfoo.so
7f5422c4b000-7f5422e4a000 ---p 00001000 fd:01 1063461 /root/c/book_link/7/libfoo.so
7f5422e4a000-7f5422e4b000 r--p 00000000 fd:01 1063461 /root/c/book_link/7/libfoo.so
7f5422e4b000-7f5422e4c000 rw-p 00001000 fd:01 1063461 /root/c/book_link/7/libfoo.so
7f5422e4c000-7f5422e6d000 r-xp 00000000 fd:01 1049801 /usr/lib64/ld-2.17.so
7f5423061000-7f5423064000 rw-p 00000000 00:00 0
7f542306b000-7f542306d000 rw-p 00000000 00:00 0
7f542306d000-7f542306e000 r--p 00021000 fd:01 1049801 /usr/lib64/ld-2.17.so
7f542306e000-7f542306f000 rw-p 00022000 fd:01 1049801 /usr/lib64/ld-2.17.so
7f542306f000-7f5423070000 rw-p 00000000 00:00 0
7fff36862000-7fff36883000 rw-p 00000000 00:00 0 [stack]
7fff369a2000-7fff369a4000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
这里出现了p1程序, libc-2.17.so
还有 ld-2.17.so,这个是c语言下的动态连接器,负责将动态链接库加载到内存中
通过readelf -l 查看动态链接库,其内容跟普通的可执行文件,或者重定位文件是差不多的
假设动态链接文件 foo.so是 0x1234开头的,而内存中0x1234已经被占用了,0x8234是空闲的,于是就将foo.so重定位0x8234这个位置,而foo.so中程序的函数的相对调用地址是不变的,所以将0x1234+0x7000 变成0x8234就可以了
用下面这个命令 不带 -fPIC 就是装载时重定位方式
gcc -shared -o libfoo.so foo.c
/usr/bin/ld: /tmp/ccd9Axtv.o: relocation R_X86_64_32 against `.rodata' can not be used when making a shared object; recompile with -fPIC
/tmp/ccd9Axtv.o: error adding symbols: Bad value
collect2: error: ld returned 1 exit status
gcc默认生成的是不支持装载时重定位方式
使用的是地址无关的方式,把需要重定位的部分单独拿出来,跟数据部分放到一起,所以上面执行的那条命令就报错了
地址无关的重定位方式有 4 种方式
1.模块内部的函数调用,跳转等
2.模块内部的数据访问,比如定义的全局变量,静态变量
3.模块外部的函数调用,跳转等
4.模块外部的数据访问,比如其他的模块中定义的全局变量
编译方式
gcc -fPIC -shared -o pic.so pic.c
static int a;
extern int b;
extern void ext();
void bar() {
a = 1;
b = 2;
}
void foo() {
bar();
ext();
}
objdump -d pic.so反编译结果如下
Disassembly of section .init:
00000000000005e0 <_init>:
5e0: 48 83 ec 08 sub $0x8,%rsp
5e4: 48 8b 05 f5 09 20 00 mov 0x2009f5(%rip),%rax # 200fe0 <_DYNAMIC+0x1d0>
5eb: 48 85 c0 test %rax,%rax
5ee: 74 05 je 5f5 <_init+0x15>
5f0: e8 2b 00 00 00 callq 620 <__gmon_start__@plt>
5f5: 48 83 c4 08 add $0x8,%rsp
5f9: c3 retq
Disassembly of section .plt:
0000000000000600 <bar@plt-0x10>:
600: ff 35 02 0a 20 00 pushq 0x200a02(%rip) # 201008 <_GLOBAL_OFFSET_TABLE_+0x8>
606: ff 25 04 0a 20 00 jmpq *0x200a04(%rip) # 201010 <_GLOBAL_OFFSET_TABLE_+0x10>
60c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000000610 <bar@plt>:
610: ff 25 02 0a 20 00 jmpq *0x200a02(%rip) # 201018 <_GLOBAL_OFFSET_TABLE_+0x18>
616: 68 00 00 00 00 pushq $0x0
61b: e9 e0 ff ff ff jmpq 600 <_init+0x20>
0000000000000620 <__gmon_start__@plt>:
620: ff 25 fa 09 20 00 jmpq *0x2009fa(%rip) # 201020 <_GLOBAL_OFFSET_TABLE_+0x20>
626: 68 01 00 00 00 pushq $0x1
62b: e9 d0 ff ff ff jmpq 600 <_init+0x20>
0000000000000630 <ext@plt>:
630: ff 25 f2 09 20 00 jmpq *0x2009f2(%rip) # 201028 <_GLOBAL_OFFSET_TABLE_+0x28>
636: 68 02 00 00 00 pushq $0x2
63b: e9 c0 ff ff ff jmpq 600 <_init+0x20>
0000000000000640 <__cxa_finalize@plt>:
640: ff 25 ea 09 20 00 jmpq *0x2009ea(%rip) # 201030 <_GLOBAL_OFFSET_TABLE_+0x30>
646: 68 03 00 00 00 pushq $0x3
64b: e9 b0 ff ff ff jmpq 600 <_init+0x20>
Disassembly of section .text:
0000000000000735 <bar>:
735: 55 push %rbp
736: 48 89 e5 mov %rsp,%rbp
739: c7 05 f9 08 20 00 01 movl $0x1,0x2008f9(%rip) # 20103c <a>
740: 00 00 00
743: 48 8b 05 8e 08 20 00 mov 0x20088e(%rip),%rax # 200fd8 <_DYNAMIC+0x1c8>
74a: c7 00 02 00 00 00 movl $0x2,(%rax)
750: 5d pop %rbp
751: c3 retq
0000000000000752 <foo>:
752: 55 push %rbp
753: 48 89 e5 mov %rsp,%rbp
756: b8 00 00 00 00 mov $0x0,%eax
75b: e8 b0 fe ff ff callq 610 <bar@plt>
760: b8 00 00 00 00 mov $0x0,%eax
765: e8 c6 fe ff ff callq 630 <ext@plt>
76a: 5d pop %rbp
76b: c3 retq
调用bar(),ext()都是先 call一个中间函数
这个中间函数是跳转到 _GLOBAL_OFFSET_TABLE_ + 一个偏移量
通过这个全局偏移量表最终选择到内部函数bar,或者外部函数ext
bar()函数的内部通过相当地址找到变量a,通过_DYNAMIC + 偏移量的方式找到变量b
通过objdunp -h pic.so,查看各段的信息如下
Sections:
Idx Name Size VMA LMA File off Algn
。。。。。
19 .got 00000030 0000000000200fd0 0000000000200fd0 00000fd0 2**3
CONTENTS, ALLOC, LOAD, DATA
。。。。。
通过objdump -R pic.so 查看重定位表,重定位表就是 0x200fd0开头的,b的位置是0x200fd8
pic.so: file format elf64-x86-64
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000200df0 R_X86_64_RELATIVE *ABS*+0x0000000000000700
0000000000200df8 R_X86_64_RELATIVE *ABS*+0x00000000000006c0
0000000000200e08 R_X86_64_RELATIVE *ABS*+0x0000000000200e08
0000000000200fd0 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000200fd8 R_X86_64_GLOB_DAT b
0000000000200fe0 R_X86_64_GLOB_DAT __gmon_start__
0000000000200fe8 R_X86_64_GLOB_DAT _Jv_RegisterClasses
0000000000200ff0 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000200ff8 R_X86_64_GLOB_DAT __cxa_finalize
0000000000201018 R_X86_64_JUMP_SLOT bar
0000000000201020 R_X86_64_JUMP_SLOT __gmon_start__
0000000000201028 R_X86_64_JUMP_SLOT ext
0000000000201030 R_X86_64_JUMP_SLOT __cxa_finalize
指定跳转调用 | 数据访问 | |
模块内部 | 相对跳转和调用 | 相对地址访问 |
模块外部 | 间接跳转和调用 GOT | 间接访问 GOT |
进程A和进程B都调用了共享库libfoo.so
进程A改变了libfoo.so中变量X的值,对于进程B来说没影响,因为两个进程用的是不同的副本
对于进程A内部的两个线程a1和a2来说就有影响,因为进程内部的线程共享一个副本
也可以将A进程和B进程共享一个数据,这个叫共享数据段,用来做进程间通讯
而A进程内部的线程a1和a2都可以有自己的副本,这是线程私有存储
延迟加载
动态链接比静态链接慢
1.是多了一步函数调用,main->got全局选择函数->bar函数
2.静态链接是已经整合好成一个文件了,动态链接是在运行启时候把相关函数加载进来
延迟加载就是在使用的时候才进行链接,避免不必要的链接,一般动态链接比静态慢5%左右
ELF使用PLT procedure linkage table方法来实现
程序的调用逻辑是 foo() -> bar(),foo的反汇编如下
0x0000000000000752 <+0>: push %rbp
0x0000000000000753 <+1>: mov %rsp,%rbp
0x0000000000000756 <+4>: mov $0x0,%eax
0x000000000000075b <+9>: callq 0x610 <bar@plt>
0x0000000000000760 <+14>: mov $0x0,%eax
0x0000000000000765 <+19>: callq 0x630 <ext@plt>
0x000000000000076a <+24>: pop %rbp
0x000000000000076b <+25>: retq
foo函数是先调用bar@plt函数的,调用关系图如下
1.jmpq *0x200a02(%rip) 先调用重定位表中bar函数
2.重定位表中的bar函数位置是空的,最终会调用回来,也就是bar@plt的第二个指定
3.jmpq 600,调用一个全局函数,这个函数会确定bar的地址
pushq $0x0可能是类似传递参数的意思,got_func会重新重定位表中bar函数的地址
4.bar@plt再次调用bar函数的时候去重定位表中bar就有正确地址了,于是可以直接调用了
pic.so的重定位表内容如下:
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000200df0 R_X86_64_RELATIVE *ABS*+0x0000000000000700
0000000000200df8 R_X86_64_RELATIVE *ABS*+0x00000000000006c0
0000000000200e08 R_X86_64_RELATIVE *ABS*+0x0000000000200e08
0000000000200fd0 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000200fd8 R_X86_64_GLOB_DAT b
0000000000200fe0 R_X86_64_GLOB_DAT __gmon_start__
0000000000200fe8 R_X86_64_GLOB_DAT _Jv_RegisterClasses
0000000000200ff0 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000200ff8 R_X86_64_GLOB_DAT __cxa_finalize
0000000000201018 R_X86_64_JUMP_SLOT bar
0000000000201020 R_X86_64_JUMP_SLOT __gmon_start__
0000000000201028 R_X86_64_JUMP_SLOT ext
0000000000201030 R_X86_64_JUMP_SLOT __cxa_finalize
真实的ELF文件中有 got section和 got.plt section,前者是保存全局变量的引用,后者保存全局函数的引用
动态连接的程序在启动时,操作系统会先启动一个动态连接器
ELF文件中包含的 .interp 段中有ld.so的信息
通过objdump -s a.out 反编译的结果
Contents of section .interp:
400238 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
400248 7838362d 36342e73 6f2e3200 x86-64.so.2.
动态链接文件中有一个 .dynamic的段,这个段中包含了依赖哪些共享对象,动态链接符号表位置,动态链接重定位表的位置,共享对象初始代码位置,代码开始位置,代码结束位置,有点类似ELF的头文件信息
通过命令 readelf -d pic.so查看
Dynamic section at offset 0xe10 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x5e0
0x000000000000000d (FINI) 0x76c
0x0000000000000019 (INIT_ARRAY) 0x200df0
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x200df8
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x1f0
0x0000000000000005 (STRTAB) 0x3b0
0x0000000000000006 (SYMTAB) 0x230
0x000000000000000a (STRSZ) 177 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000003 (PLTGOT) 0x201000
0x0000000000000002 (PLTRELSZ) 96 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x580
0x0000000000000007 (RELA) 0x4a8
0x0000000000000008 (RELASZ) 216 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x488
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x462
0x000000006ffffff9 (RELACOUNT) 3
0x0000000000000000 (NULL) 0x0
通过 ldd pic.so查看 这个共享库依赖哪些其他库,linux-vdso.so是跟系统内核有关的
linux-vdso.so.1 => (0x00007ffdb249b000)
libc.so.6 => /lib64/libc.so.6 (0x00007f8bc7e5c000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8bc842a000)
通过 readelf -r libfoo.so 查看重定位表
Relocation section '.rela.dyn' at offset 0x490 contains 8 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200df8 000000000008 R_X86_64_RELATIVE 6d0
000000200e00 000000000008 R_X86_64_RELATIVE 690
000000200e10 000000000008 R_X86_64_RELATIVE 200e10
000000200fd8 000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fe0 000400000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200fe8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000200ff0 000600000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8 000800000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0
Relocation section '.rela.plt' at offset 0x550 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000201018 000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0
000000201020 000400000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000201028 000700000007 R_X86_64_JUMP_SLO 0000000000000000 sleep + 0
000000201030 000800000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
libfoo.so中包含了plt相关的函数表的段
[21] .got PROGBITS 0000000000200fd8 00000fd8
0000000000000028 0000000000000008 WA 0 0 8
[22] .got.plt PROGBITS 0000000000201000 00001000
0000000000000038 0000000000000008 WA 0 0 8
got.plt的前三项是被系统占据的,第四项是_gmon_start_,第五项是printf,第六项是sleep,当链接器确定了printf的函数地址时,就会重写got.plt表中的printf地址,也就是0x000015d8这一栏
动态链接启动是依靠 动态链接器,这个链接器会自己初始化自己
由于使用地址无关的方式编译共享对象,模块内部的函数调用都是采用plt方式,所以动态连接器内部不能调用全局变量,也不能调用其他函数
动态连接器完成重定位之后会初始化共享对象的 .init段,当程序退出时执行 .finit段
一个例子,演示引用多个共享对象,共享对象之间函数名有冲突的情况
//cat a1.c
#include <stdio.h>
void a() {
printf("a1.c \n");
}
//cat a2.c
#include <stdio.h>
void a() {
printf("a2.c\n ");
}
//cat b1.c
void a();
void b1() {
a();
}
//cat b2.c
void a();
void b2() {
a();
}
//cat main.c
#include <stdio.h>
void b1();
void b2();
int main() {
b1();
b2();
sleep(-1);
return 0;
}
编译这些文件
gcc -fPIC -shared a1.c -o a1.so
gcc -fPIC -shared a2.c -o a2.so
gcc -fPIC -shared b1.c a1.so -o b1.so
gcc -fPIC -shared b2.c a2.so -o b2.so
gcc main.c b1.so b2.so -o main -Xlinker
ldd b1.so
linux-vdso.so.1 => (0x00007ffcaede8000)
a1.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f9f23a58000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9f24027000)
ldd b2.so
linux-vdso.so.1 => (0x00007fffd7fcc000)
a2.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f6f7bbc1000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6f7c190000)
运行mian程序之后, cat /proc/[PID]/maps 结果
00400000-00401000 r-xp 00000000 fd:01 1063486 /root/c/book_link/7/conflect/main
00600000-00601000 r--p 00000000 fd:01 1063486 /root/c/book_link/7/conflect/main
00601000-00602000 rw-p 00001000 fd:01 1063486 /root/c/book_link/7/conflect/main
7f74b25b2000-7f74b25b3000 r-xp 00000000 fd:01 1063483 /root/c/book_link/7/conflect/a2.so
7f74b25b3000-7f74b27b2000 ---p 00001000 fd:01 1063483 /root/c/book_link/7/conflect/a2.so
7f74b27b2000-7f74b27b3000 r--p 00000000 fd:01 1063483 /root/c/book_link/7/conflect/a2.so
7f74b27b3000-7f74b27b4000 rw-p 00001000 fd:01 1063483 /root/c/book_link/7/conflect/a2.so
7f74b27b4000-7f74b27b5000 r-xp 00000000 fd:01 1063478 /root/c/book_link/7/conflect/a1.so
7f74b27b5000-7f74b29b4000 ---p 00001000 fd:01 1063478 /root/c/book_link/7/conflect/a1.so
7f74b29b4000-7f74b29b5000 r--p 00000000 fd:01 1063478 /root/c/book_link/7/conflect/a1.so
7f74b29b5000-7f74b29b6000 rw-p 00001000 fd:01 1063478 /root/c/book_link/7/conflect/a1.so
7f74b29b6000-7f74b2b6e000 r-xp 00000000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f74b2b6e000-7f74b2d6e000 ---p 001b8000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f74b2d6e000-7f74b2d72000 r--p 001b8000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f74b2d72000-7f74b2d74000 rw-p 001bc000 fd:01 1049812 /usr/lib64/libc-2.17.so
7f74b2d74000-7f74b2d79000 rw-p 00000000 00:00 0
7f74b2d79000-7f74b2d7a000 r-xp 00000000 fd:01 1063485 /root/c/book_link/7/conflect/b2.so
7f74b2d7a000-7f74b2f79000 ---p 00001000 fd:01 1063485 /root/c/book_link/7/conflect/b2.so
7f74b2f79000-7f74b2f7a000 r--p 00000000 fd:01 1063485 /root/c/book_link/7/conflect/b2.so
7f74b2f7a000-7f74b2f7b000 rw-p 00001000 fd:01 1063485 /root/c/book_link/7/conflect/b2.so
7f74b2f7b000-7f74b2f7c000 r-xp 00000000 fd:01 1063484 /root/c/book_link/7/conflect/b1.so
7f74b2f7c000-7f74b317b000 ---p 00001000 fd:01 1063484 /root/c/book_link/7/conflect/b1.so
7f74b317b000-7f74b317c000 r--p 00000000 fd:01 1063484 /root/c/book_link/7/conflect/b1.so
7f74b317c000-7f74b317d000 rw-p 00001000 fd:01 1063484 /root/c/book_link/7/conflect/b1.so
7f74b317d000-7f74b319e000 r-xp 00000000 fd:01 1049801 /usr/lib64/ld-2.17.so
7f74b3391000-7f74b3394000 rw-p 00000000 00:00 0
7f74b339b000-7f74b339e000 rw-p 00000000 00:00 0
7f74b339e000-7f74b339f000 r--p 00021000 fd:01 1049801 /usr/lib64/ld-2.17.so
7f74b339f000-7f74b33a0000 rw-p 00022000 fd:01 1049801 /usr/lib64/ld-2.17.so
7f74b33a0000-7f74b33a1000 rw-p 00000000 00:00 0
7ffdae24c000-7ffdae26d000 rw-p 00000000 00:00 0 [stack]
7ffdae2c1000-7ffdae2c3000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
这里能看到,链接器将 a1.so,a2.so, b1.so, b2.so都加载进来了
当符号名冲突的时候,链接器则规定先加载进来的有效,于是就打印出两次 a1.c
像 LD_PRELOAD 这样的方式引入自定义的动态库时,就可以覆盖掉系统的函数
/lib64/ld-2.17.so 这个文件就是链接器,而这个程序本身也是一个可执行程序
dlopen,dlsym, dlclose,dlerror 函数例子
#include <stdio.h>
#include <dlfcn.h>
typedef void (*func)(int);
int main(int argc, char **argv) {
void *handle;
char *error;
handle = dlopen("libfoo.so", RTLD_LAZY);
if (!handle) {
fprintf (stderr, "%s ", dlerror());
exit(1);
}
func func_ = dlsym(handle, "foo");
if ((error = dlerror()) != NULL) {
fprintf (stderr, "%s ", error);
exit(1);
}
func_(10);
dlclose(handle);
return 0;
}
编译时需要加上 -ldl,否则会出错
gcc -o dd dlopen.c -ldl
另外设置LD_LIBRARY_PATH,把libfoo.so的所在的路径加上,否则会运行时会报找不到动态库的错误