本文探讨了Linux系统中动态链接库机制,包括全局符号介入、延迟绑定及地址无关代码等概念,通过实例分析动态链接过程,并解释了如何控制全局变量和函数的装载顺序,以及动态和静态链接的重定向区别。同时,介绍了几个关键概念如ELF、PIC、GOT、PLT等,并提供了附录部分供读者查阅。
探讨了Linux系统中动态链接库机制,包括全局符号介入、延迟绑定及地址无关代码等概念,解释了如何控制全局变量和函数的装载顺序,以及动态和静态链接的重定向区别。
提供了附录部分供读者查阅,包括动态链接器的使用、常见命令、参考文档等。
↓推荐关注↓
本文将深入探讨Linux系统中的动态链接库机制,这其中包括但不限于全局符号介入、延迟绑定以及地址无关代码等内容。
在软件开发过程中,动态库链接问题时常出现,这可能导致符号冲突,从而引起程序运行异常或崩溃。为深入理解动态链接机制及其工作原理,我重温了《程序员的自我修养》,并通过实践演示与反汇编分析,了解了动态链接的过程。
本文将深入探讨Linux系统中的动态链接库机制,这其中包括但不限于全局符号介入(Global Symbol Interposition)、延迟绑定(Lazy Binding)以及地址无关代码(Position-Independent Code, PIC)等内容。通过对上述概念和技术细节的讨论,希望能够提供一个更加清晰的认知框架,从而揭示符号冲突背后隐藏的本质原因。这样一来,在实际软件开发过程中遇到类似问题时,开发者们便能更加游刃有余地采取措施进行预防或解决,确保程序稳定运行的同时提升整体质量与用户体验。
为便于读者查阅,本文中提及的一些基本概念,例如ELF、PIC、GOT、PLT、常用的section等,被归纳整理于附录部分。
我们将通过一个简单的 C 语言程序,逐步探讨动态链接库在模块内部及模块间的运行机制,其中涉及变量和函数之间的交互过程。同时,我们将使用 -fPIC 选项,以确保生成位置无关代码。
#include
static int a;
extern int b;
int c = 3;
extern void ext();
static void inner() {}
void bar() {
a = 1;
b = 2;
c = 4;
}
void foo() {
inner();
bar();
ext();
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
int b = 1;
void ext() {
b = 3;
}
int main() {
foo();
return 0;
}
gcc -shared -fPIC -o libpic.so pic.c -g
gcc -o main main.c -L. -lpic
在此代码示例中,使用 -fPIC 编译选项可以生成位置无关的代码,适用于创建共享库。代码中包含了多个场景:
不同类型的变量:
我们都知道动态链接库需要能够在多个进程之间共享同一段代码。为了实现这一点,代码必须是位置无关的,从而可以在加载时按需被链接到不同的地址,编译时添加编译选项-fPIC 可以生成地址无关代码,那这些函数和变量运行时,如何做到呢?接下来将逐步分析动态链接的过程。
例子中 foo 函数实现中有两个函数调用:静态函数 inner()和非静态函数 bar(),反汇编后结果。
Disassembly of section .plt:
0000000000000670 :
670: ff 35 92 09 20 00 push QWORD PTR [rip+0x200992] # 201008 <_global_offset_table_>
676: ff 25 94 09 20 00 jmp QWORD PTR [rip+0x200994] # 201010 <_global_offset_table_>
67c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
0000000000000680 :
680: ff 25 92 09 20 00 jmp QWORD PTR [rip+0x200992] # 201018 <_global_offset_table_>
686: 68 00 00 00 00 push 0x0
68b: e9 e0 ff ff ff jmp 670 <_init>
...
00000000000007e8 :
foo():
00000000000007e2 :
inner():
/mnt/share/demo1/pic.c:12
static void inner() {}
7e2: 55 push rbp
7e3: 48 89 e5 mov rbp,rsp
7e6: 5d pop rbp
7e7: c3 ret
...
/mnt/share/demo1/pic.c:15
inner();
7ec: b8 00 00 00 00 mov eax,0x0
7f1: e8 ec ff ff ff call 7e2
/mnt/share/demo1/pic.c:16
bar();
7f6: b8 00 00 00 00 mov eax,0x0
7fb: e8 80 fe ff ff call 680
2.1.1 静态函数调用:inner()函数调用
和静态编译重定位相似,这里更简单,具体如下:
7f1: e8 ec ff ff ff call 7e2
结论:静态函数调用很简单,通过相对地址偏移就可以跳转。
2.1.2 全局函数调用:bar()函数调用
首次调用
7fb: e8 80 fe ff ff call 680
objdump -s libpic.so
Contents of section .got:
200fc8 00000000 00000000 00000000 00000000 ................
200fd8 00000000 00000000 00000000 00000000 ................
200fe8 00000000 00000000 00000000 00000000 ................
200ff8 00000000 00000000 ........
Contents of section .got.plt:
201000 080e2000 00000000 00000000 00000000 .. .............
201010 00000000 00000000 86060000 00000000 ................
201020 96060000 00000000 a6060000 00000000 ................
201030 b6060000 00000000 c6060000 00000000 ................
-
发现这个地址在.got.plt section,0x00000686, 该地址存的地址为
0000000000000680 :
680: ff 25 92 09 20 00 jmp QWORD PTR [rip+0x200992] # 201018 <_global_offset_table_>
686: 68 00 00 00 00 push 0x0
68b: e9 e0 ff ff ff jmp 670 <_init>
那上面一系列地址跳转是在干什么?用一个示意图表示 bar 首次地址重定位过程(橙色是调用入口,蓝色是运行的指令,紫色代表修正的地址)。
_dl_runtime_resolve()函数实现不展开,该函数的入参为入栈的符号索引 index 和库 ID,解析过程会依赖.dynamic、.rela.plt 等 section 信息,解析后重定向地址后填入地址0x201018 。可以查看下.rela.plt 段内容有什么。
[root@docker-desktop demo1]# readelf -r libpic.so
Relocation section '.rela.dyn' at offset 0x4e8 contains 10 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200de8 000000000008 R_X86_64_RELATIVE 780
000000200df0 000000000008 R_X86_64_RELATIVE 740
000000200e00 000000000008 R_X86_64_RELATIVE 200e00
000000200fc8 000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fd0 000300000006 R_X86_64_GLOB_DAT 0000000000000000 b + 0
000000200fd8 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200fe0 000e00000006 R_X86_64_GLOB_DAT 0000000000201040 c + 0
000000200fe8 000700000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000200ff0 000800000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8 000900000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0
Relocation section '.rela.plt' at offset 0x5d8 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000201018 000b00000007 R_X86_64_JUMP_SLO 00000000000007b8 bar + 0
000000201020 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0
000000201028 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000201030 000600000007 R_X86_64_JUMP_SLO 0000000000000000 ext + 0
000000201038 000900000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
.rela.plt是 ELF 文件中包含了函数跳转槽重定位信息。具体代表含义:
-
Offset - 表示在内存中的偏移地址,即在 GOT 中重定位项的地址。
-
Info - 包含两个部分:符号的索引和重定位类型。在这种情况下,重定位类型是 R_X86_64_JUMP_SLOT,用于处理函数调用的跳转。
-
Type - 描述了重定位的类型,这里是 R_X86_64_JUMP_SLOT,用于通过懒加载解析符号的PLT入口。其他类型还有很多,常见的还有
-
Sym. Value - 是符号在它本身定义模块内的值。在重定位发生之前,符号可能还没有最终的运行时地址。对于本地符号(比如 bar 函数),这里通常是它们在当前模块中的偏移地址。对于外部符号(比如 printf),在重定位前这里通常是 0,表示地址还未确定。
-
Sym. Name + Addend - 显示了符号的名称以及添加量。添加量在这里是 0,因为我们正在查看 .rela 格式的重定位项,添加量已经包含在每个重定位项中。
在运行时,动态链接器会依据这些重定位项进行地址解析工作。例如,当程序第一次调用 printf 时,控制流首先跳转到 printf 在 PLT 中的对应项,PLT 中会有一段存根代码触发动态链接器,动态链接器解析出 printf 的真实地址并更新 GOT 中对应的地址。
第二次调用
运行后地址重定位后,第二次调用就会简单很多,如下图所示:
使用 GDB 调试运行后,单步调试地址重定向.got.plt 段内容(基地址为:0x7F7A97F75000)。
201000 080e2000 00000000 00000000 00000000 .. .............
(gdb) x/16a 0x7f7a98176000
0x7f7a98176000: 0x200e08 0x7f7a983976a8
0x7f7a98176010: 0x7f7a9818d890 <_dl_runtime_resolve_xsave> 0x7f7a97f75686 6>
0x7f7a98176020: 0x7f7a97f75696 6> 0x7f7a97f756a6 <__gmon_start__ class="code-snippet__number">6>
0x7f7a98176030: 0x7f7a97f756b6 6> 0x7f7a97f756c6 <__cxa_finalize class="code-snippet__number">6>
0x7f7a98176040 : 0x3 0x0
0x7f7a98176050: 0x31303220352e382e 0x5228203332363035
0x7f7a98176060: 0x3420746148206465 0x2936332d352e382e
0x7f7a98176070: 0x20000002c00 0x8000000
.got.plt 中 bar 地址 = 0x201018 + 0x7F7A97F75000(基地址) = 0x7F7A98176018,0x7F7A98176018 内容为0x7f7a97f75686
,和上图的相对地址偏移相同,重定向后结果如下
(gdb) x/16a 0x7f7a98176000
0x7f7a98176000: 0x200e08 0x7f7a983976a8
0x7f7a98176010: 0x7f7a9818d890 <_dl_runtime_resolve_xsave> 0x7f7a97f757b8
0x7f7a98176020: 0x7f7a97f75696 6> 0x7f7a97f756a6 <__gmon_start__ class="code-snippet__number">6>
0x7f7a98176030: 0x7f7a97f756b6 6> 0x7f7a97f756c6 <__cxa_finalize class="code-snippet__number">6>
0x7f7a98176040 : 0x3 0x0
0x7f7a98176050: 0x31303220352e382e 0x5228203332363035
0x7f7a98176060: 0x3420746148206465 0x2936332d352e382e
0x7f7a98176070: 0x20000002c00 0x8000000
0x7f7a97f757b8 为代码段,0x7f7a97f757b8 - 0x7F7A97F75000(基地址)= 0x7B8,该偏移在.text 的 bar 入口地址,也对应起来了。
抽象一下,如下示意图:
通过上图指令跳转得出,.plt,利用.got.plt 可写权限,在程序运行时,修正.got.plt 对应函数指向的.text (不可写)地址,从而实现了地址无关代码。
该过程还隐藏了一个知识点,
延迟绑定(lazy binding)
。动态链接器在运行时完成,若已一开始执行,要加载完所有的符号的话,想必会减慢程序的启动速度,影响性能。所以当函数第一次被用到时再进行绑定,如果没有用就不绑定,这样可以大大加快程序启动速度。本例子中的 bar 也是在调用时才进行重定向,不调用不进行地址重定向绑定,即实现了延迟绑定效果。
是不是外部函数重定向一定在 .rela.plt?
不是,如果是PIC 编译,会在.rela.plt;如果不是PIC 编译,会在.rela.dyn 出现。
原因:开启 PIC 调用指令会指向 PLT 中的一个条目,需要.rela.plt section 配合实现 Lazy Binding,.rela.dyn 段用于动态链接器在加载时将符号绑定到其运行时地址的重定位条目。它包含了不特定于PLT条目的其他动态重定位信息,.rela.plt 主要针对PLT进行重定位,用于动态链接时解析函数地址,实现惰性绑定,而 .rela.dyn 用于更广泛的动态重定位需求。
疑问?
这两个问题先不着急回答,我们接着看模块间函数调用。
例子中是 foo() 对 ext()函数的调用,查看汇编,发现和模块内函数调用方式一模一样。汇编指令如下:
/mnt/share/demo1/pic.c:17
ext();
800: b8 00 00 00 00 mov eax,0x0
805: e8 a6 fe ff ff call 6b0
那现在回答上一节的第一个问题,模块内和模块间全局函数调用没有区别,为什么呢?
先回忆下加载过程,动态链接器完成自举后,会将可执行文件和链接器本身的符号表都合并到一个符号表中,该符号表叫做全局符号表(Global Symbol Table)。
当一个符号需要被加入全局符号表时,如果相同的符号已经存在,则后加入的符号被忽略,这种规则叫做全局符号介入。
由于全局符号介入规则,若上一节的模块内部函数调用 bar() 直接采用相对地址调用话,可能会被其他模块的同名函数符号覆盖,那相对地址就是无法准确找到正确的函数地址,故模块内和模块外的函数调用,都需要通过.got.plt 重定位方法间接调用。
那上一节第二个问题答案也显而易见,静态函数不涉及全局符号介入问题,可以通过模块内部相对地址跳转就可以。这样调用的寻址速度也比全局函数的寻址速度快。
为了更深入理解全局符号介入,我们再举个例子。
#include
void a() {
printf("a1.c\n");
}
#include
void a() {
printf("a2.c\n");
}
void a();
void b1() {
a();
}
void a();
void b2() {
a();
}
#include
void b1();
void b2
();
int main() {
b1();
b2();
return 0;
}
[root@docker-desktop priority]
[root@docker-desktop priority]
[root@docker-desktop priority]
[root@docker-desktop priority]
[root@docker-desktop priority]
a1.so (0x0000004001c2a000)
libstdc++.so.6 => /usr/local/gcc-5.4.0/lib64/libstdc++.so.6 (0x0000004001e2c000)
libm.so.6 => /lib64/libm.so.6 (0x00000040021ad000)
libgcc_s.so.1 => /usr/local/gcc-5.4.0/lib64/libgcc_s.so.1 (0x00000040024b0000)
libc.so.6 => /lib64/libc.so.6 (0x00000040026c7000)
/lib64/ld-linux-x86-64.so.2 (0x0000004000000000)
[root@docker-desktop priority]
a2.so (0x0000004001c2a000)
libstdc++.so.6 => /usr/local/gcc-5.4.0/lib64/libstdc++.so.6 (0x0000004001e2c000)
libm.so.6 => /lib64/libm.so.6 (0x00000040021ad000)
libgcc_s.so.1 => /usr/local/gcc-5.4.0/lib64/libgcc_s.so.1 (0x00000040024b0000)
libc.so.6 => /lib64/libc.so.6 (0x00000040026c7000)
/lib64/ld-linux-x86-64.so.2 (0x0000004000000000)
[root@docker-desktop priority]
[root@docker-desktop priority]
a1.c
a1.c
在上述例子中,虽然 b1.so 和 b2.so 中都调用了 a() 函数,但由于 main 程序首先链接了 b1.so,导致 a() 的实现使用了 a1.so 中的定义。因此,无论 b2.so 如何变化,main 程序中调用的都始终是 a1.so 的实现。这种现象强调了在动态链接库中符号的解析顺序及如何影响最终的执行结果,开发者在设计接口时需谨慎考虑符号的命名和库的加载顺序,以避免潜在的符号冲突和不确定性。
例子中的静态变量 a 、外部全局变量 b、 内部全局变量 c,看下反汇编后结果:
void bar() {
7b8: 55 push rbp
7b9: 48 89 e5 mov rbp,rsp
/mnt/share/demo1/pic.c:7
a = 1;
7bc: c7 05 82 08 20 00 01 mov DWORD PTR [rip+0x200882],0x1 # 201048 <__tmc_end__>
7c3: 00 00 00
/mnt/share/demo1/pic.c:8
b = 2;
7c6: 48 8b 05 03 08 20 00 mov rax,QWORD PTR [rip+0x200803] # 200fd0 <_dynamic>
7cd: c7 00 02 00 00 00 mov DWORD PTR [rax],0x2
/mnt/share/demo1/pic.c:9
c = 4;
7d3: 48 8b 05 06 08 20 00 mov rax,QWORD PTR [rip+0x200806] # 200fe0 <_dynamic>
7da: c7 00 04 00 00 00 mov DWORD PTR [rax],0x4
/mnt/share/demo1/pic.c:10
}
Idx Name Size VMA LMA File off Algn
CONTENTS, ALLOC, LOAD, DATA
20 .got 00000038 0000000000200fc8 0000000000200fc8 00000fc8 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .got.plt 00000040 0000000000201000 0000000000201000 00001000 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .data 00000004 0000000000201040 0000000000201040 00001040 2**2
CONTENTS, ALLOC, LOAD, DATA
23 .bss 0000000c 0000000000201044 0000000000201044 00001044 2**2
ALLOC
static int a; # 201048 <__tmc_end__> ==> .bss
extern int b; # 200fd0 <_dynamic> ==> .got
int c; # 200fe0 <_dynamic> ==> .got
结合上面了解的函数调用,变量调用跳转类似,static 变量的访问直接通过偏移量完成,这种方式更高效,因为 static 变量的作用域限制在同一个编译单元,所以它们的地址可以在编译时确定(相对于 rip)。而非 static 变量(包括定义在当前模块的全局变量和 extern 变量)可能被其他模块引用或修改,其地址需要在运行时通过动态链接器解析,对于全局和 extern 变量,共享库使用基于 rip 的寻址加上 运行时重定位.got 段中地址,以确保位置无关。
全局变量的地址不存在延迟绑定,因为通常会在加载时解析,并通过全局偏移表(Global Offset Table, GOT)来访问,而不是延迟到首次使用时。因此,把它们的地址解析延迟将不会带来明显的优势,而且会在运行时增加额外的性能负担。
如果把 bar 和变量 c 使用__attribute__((visibility("hidden")))隐藏的符号,那函数调用跳转会有什么变化?
#include
static int a;
extern int b;
__attribute__((visibility("hidden"))) int c = 3;
extern void ext();
void bar()
__attribute__((visibility("hidden")));
void bar() {
a = 1;
b = 2;
c = 4;
}
static void inner() {}
void foo() {
inner();
bar();
ext();
printf("a = %d, b = %d, c = %d\n", a, b, c);
}
反汇编后结果
[root@docker-desktop demo1]# objdump -d -M intel -S -l libpic_hidden.so
Disassembly of section .text:
...
0000000000000738 :
bar():
/mnt/share/demo1/pic_hidden.c:7
static int a;
extern int b;
__attribute__((visibility("hidden"))) int c = 3;
extern void ext();
void bar() __attribute__((visibility("hidden")));
void bar() {
738: 55 push rbp
739: 48 89 e5 mov rbp,rsp
/mnt/share/demo1/pic_hidden.c:8
a = 1;
73c: c7 05 fa 08 20 00 01 mov DWORD PTR [rip+0x2008fa],0x1 # 201040 <__tmc_end__>
743: 00 00 00
/mnt/share/demo1/pic_hidden.c:9
b = 2;
746: 48 8b 05 8b 08 20 00 mov rax,QWORD PTR [rip+0x20088b] # 200fd8 <_dynamic>
74d: c7 00 02 00 00 00 mov DWORD PTR [rax],0x2
/mnt/share/demo1/pic_hidden.c:10
c = 4;
753: c7 05 db 08 20 00 04 mov DWORD PTR [rip+0x2008db],0x4 # 201038
75a: 00 00 00
...
/mnt/share/demo1/pic_hidden.c:17
bar();
773: b8 00 00 00 00 mov eax,0x0
778: e8 bb ff ff ff call 738
[root@docker-desktop demo1]# readelf -S libpic_hidden.so
There are 34 section headers, starting at offset 0x1470:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
......
[23] .data PROGBITS 0000000000201038 00001038
0000000000000004 0000000000000000 WA 0 0 4
查看.rela.plt section
[root@docker-desktop demo1]# readelf -r libpic_hidden.so
Relocation section '.rela.dyn' at offset 0x4a8 contains 9 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000200df0 000000000008 R_X86_64_RELATIVE 700
000000200df8 000000000008 R_X86_64_RELATIVE 6c0
000000200e08 000000000008 R_X86_64_RELATIVE 200e08
000000200fd0 000200000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000200fd8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 b + 0
000000200fe0 000500000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000200fe8 000700000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000200ff0 000800000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000200ff8 000900000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0
Relocation section '.rela.plt' at offset 0x580 contains 4 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000201018 000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf + 0
000000201020 000500000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
000000201028 000600000007 R_X86_64_JUMP_SLO 0000000000000000 ext + 0
000000201030 000900000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
.rela.plt 中已经没有 bar(),.rela.dyn中没有变量 c ,所以隐藏后,bar() 不需要重定位,变量 c也不需要间接跳转。隐藏的符号 bar() 和 c 也不会出现在动态链接库的动态符号表(.dynsym)中,因此它们在链接时不可见于其他共享对象或者可执行文件,所以隐藏符号
不存在全局符号介入
的场景。
-
如何区分一个 DSO 是否为 PIC
readelf -d xxx.so | grep TEXTREL
如果没有输出,则动态库是使用 PIC 生成的。文本重定位(TEXTREL)意味着代码部分(.text section)需要修改以引用正确的地址,在非PIC的代码中,会存在基于绝对地址的引用,这就需要在加载时进行修改,从而使得代码能够正确运行,这个过程就是文本重定位。
2. 如何区分一个静态库是否为 PIC
ar -t xxx.a
readelf -r xxx.o
你需要检查输出中是否有基于绝对地址的重定位类型比如 R_X86_64_GOTPCREL 或其他类似的不是专为 PIC 代码的重定位类型。
3. 假设静态编译库编译不使用-fPIC,动态库编译使用-fPIC,是否 ok?
不行。实测静态库 a.a 不使用-fPIC,动态库 b.so 使用-fPIC,可执行程序 main 链接两个库会编译失败。报错日志如下:
g++ -c nopic_common.c -o nopic_common.o
ar rcs libnopic_common.a nopic_common.o
g++ -shared -o libnopic.so pic.c -L. -lnopic_common -fPIC
/usr/bin/ld: ./libnopic_common.a(nopic_common.o): relocation R_X86_64_PC32 against symbol `b' can not be used when making a shared object; recompile with -fPIC
/usr/bin/ld: final link failed: Bad value
collect2: error: ld returned 1 exit status
nopic_common.o 对象文件是没有使用 -fPIC 编译的,因此包含以 PC 相对的方式(R_X86_64_PC32 relocation type)引用全局变量 b。这种类型的重定位不兼容于动态库的创建,因为它要求代码必须在特定地址执行,而动态库加载的地址在运行时是未知的,甚至每次运行都可能不同。即静态库的代码假定某些数据或函数存在于固定地址,而该地址已经被其他代码或库占用,则可能会导致链接错误或运行时错误。
要修复这个错误,你需要重新编译 nopic_common.o,将其中的代码编译为位置无关代码(PIC)。
4. 为什么动态库编译时不默认采用PIC:
-
历史原因:
历史惯性,较早的编译器版本中没有将生成PIC作为默认选项。
-
选项传递的问题:
-fPIC
是编译器的选项,是在源代码编译阶段决定的,而
-shared
是链接器的选项, 是在不同阶段,所以无法通过-shared自动启用
-fPIC
。
-
性能:
虽然PIC对于共享库的高效运行是很重要的,但在某些情况下PIC代码也可能稍微慢于非PIC代码,因为它需要使用间接地址引用全局变量和函数。这种性能影响一般是很小的,但在对性能要求非常高的应用程序中,这可能是一个因素。
-
编译器和构建系统设计:
编译器和构建系统往往允许开发者根据项目需求选择是否生成PIC。允许灵活配置使开发者能够根据具体的使用场景和需求,选择最合适的编译选项。
|
静态链接
|
动态链接
|
阶段
|
编译链接阶段
|
装载运行阶段
|
执行控制权
|
控制权直接交给可执行文件
|
控制权限交给动态链接器,映射完成后再交给可执行文件
|
运行寻址速度
|
速度快
|
由于间接跳转,比静态链接慢约 1%~5%,使用 lazy binding 改善
|
重定位表名
|
.rela.text 代码段重定位表
.rela.data 数据段重定位表
|
.rela.plt 代码段重定位表
.rela.dyn 数据段重定位表
|
上面主要介绍了动态装载过程,在初始化和反初始化的时候,特别需要关注全局变量和函数的构造与析构顺序。这些过程直接影响到模块间的依赖关系和对象之间的交互。因此,我们需要了解如何通过使用特定的属性来控制这些顺序,以确保程序的稳定性和预期行为。特别是在多模块动态库的环境中,合理安排初始化和反初始化的顺序,是避免运行时错误和崩溃的重要措施。
对于跨共享库的全局变量,其初始化顺序受这些共享库之间的依赖关系影响。如果共享库 A 依赖于共享库 B,那么 B 的初始化代码将会在 A 的初始化代码之前执行,因此 B 中的全局变量会在 A 中的全局变量之前被初始化。
再来看一下《第一章 2 模块间函数调用》例子中,通过LD_DEBUG=files ./main命令看链接顺序和初始化顺序。
[root@docker-desktop]# LD_DEBUG=files ./main
112: find library=b1.so [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64/tls/i686:/usr/local/gcc-5.4.0/lib64/tls:/usr/local/gcc-5.4.0/lib64/i686:/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/tls/i686/b1.so
112: trying file=/usr/local/gcc-5.4.0/lib64/tls/b1.so
112: trying file=/usr/local/gcc-5.4.0/lib64/i686/b1.so
112: trying file=/usr/local/gcc-5.4.0/lib64/b1.so
112: trying file=tls/i686/b1.so
112: trying file=tls/b1.so
112: trying file=i686/b1.so
112: trying file=b1.so
112:
112: find library=b2.so [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/b2.so
112: trying file=tls/i686/b2.so
112: trying file=tls/b2.so
112: trying file=i686/b2.so
112: trying file=b2.so
112:
112: find library=libstdc++.so.6 [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/libstdc++.so.6
112:
112: find library=libm.so.6 [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/libm.so.6
112: trying file=tls/i686/libm.so.6
112: trying file=tls/libm.so.6
112: trying file=i686/libm.so.6
112: trying file=libm.so.6
112: search cache=/etc/ld.so.cache
112: trying file=/lib64/libm.so.6
112:
112: find library=libgcc_s.so.1 [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/libgcc_s.so.1
112:
112: find library=libc.so.6 [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/libc.so.6
112: trying file=tls/i686/libc.so.6
112: trying file=tls/libc.so.6
112: trying file=i686/libc.so.6
112: trying file=libc.so.6
112: search cache=/etc/ld.so.cache
112: trying file=/lib64/libc.so.6
112:
112: find library=a1.so [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/a1.so
112: trying file=tls/i686/a1.so
112: trying file=tls/a1.so
112: trying file=i686/a1.so
112: trying file=a1.so
112:
112: find library=a2.so [0]; searching
112: search path=/usr/local/gcc-5.4.0/lib64:tls/i686:tls:i686: (LD_LIBRARY_PATH)
112: trying file=/usr/local/gcc-5.4.0/lib64/a2.so
112: trying file=tls/i686/a2.so
112: trying file=tls/a2.so
112: trying file=i686/a2.so
112: trying file=a2.so
112:
112:
112: calling init: /lib64/libc.so.6
112:
112:
112: calling init: /lib64/libm.so.6
112:
112:
112: calling init: /usr/local/gcc-5.4.0/lib64/libgcc_s.so.1
112:
112:
112: calling init: /usr/local/gcc-5.4.0/lib64/libstdc++.so.6
112:
112:
112: calling init: a2.so
112:
112:
112: calling init: a1.so
112:
112:
112: calling init: b2.so
112:
112:
112: calling init: b1.so
112:
112:
112: initialize program: ./main
112:
112:
112: transferring control: ./main
112:
a1.c
a1.c
......
从日志中可以看到,动态库的加载顺序如下:b1.so,b2.so,a1.so,a2.so,这些库根据依赖关系进行加载,使用 find library 语句可以看到它们被搜索并找到成功的路径。
初始化的顺序则是:a2.so,a1.so,b2.so,b1.so
这个顺序展示了在执行 main 函数之前,各个库的构造函数是如何被调用的。从中可以看出,动态库的初始化是按照依赖顺序进行的,即一个库的初始化会在它所依赖的库都初始化完成后进行。
__attribute__((__init_priority__(PRIORITY)))是GCC提供的一个特性,用于对一个全局变量或函数的初始化优先级进行控制。只能用于全局或静态对象的声明。它改变了对象构造函数的调用顺序,其作用是在程序启动时(即 main() 函数执行之前)确保不同对象的构造函数按照指定的优先级顺序调用。PRIORITY 必须是一个介于 101 和 65535 之间的整数,其中 101 是最高优先级(最先初始化),65535 是最低优先级(最后初始化)。