By Zheng-pu Peng
在涉及到动态链接的开发中,未定义的符号引用和符号重定义的错误都由编译器帮你发现,处理起来相对简单,然而其中隐形的符号覆盖导致(全局符号介 入)的问题却难以让人察觉,本来你想调用A模块的符号,而链接器却关联到B模块的同名符号。本文简述静态库和动态库符号查找方法,重点分析全局符号介入现 象及如何控制符号的导入导出,另外也对比了动态库全局符号和局部符号的引用效率。
内容 [隐藏]
1 1 ELF文件格式2 2 链接基本过程2.1 2.1 Merge:2.2 2.2 Symbol Resolve:2.3 2.3 Relocation:3 3 动态库之全局符号介入3.1 CASE1:全局符号介入概念3.2 CASE2:.dynsym的重要性3.3 CASE3:app -l和dlopen加载同一个库4 4 动态库符号引用效率分析4.1 4.1 Static / 匿名namespace: 局部化4.2 4.2 __attribute__((visibility))4.3 4.3 使用export maps5 5 总结1 ELF文件格式ELF(ExecutableandLinkableFormat)是可用于二进制文件、可执行文件、目标代码、共享库和核心转存的标准文件格式。
readelf -S a.out (readelf -a a.out 可以读出所有信息),下面给出的section是删除与符号解析和重定位无关section后的结果。
关键section分析:
[35]symtab => [4]dynsym => [8]rela.dyn + [9][]rela.ptl <本节末给出一段代码和相应的ELF内容>
依 次可以看成后面是前面的子集。symtab在静态链接后就没多大用了,可以strip掉。dynsym记录了导入导出符号信息,也是动态链接符号查找的起 点。rela.dyn 和 rela.ptl 记录着哪些符号需要进行重定位,重定位的结果会刷新got和got.plt section。这里需要强调的是,动态库中定义的全局变量/函数,符号会放入dynsym表项中,而对这些符号的引用会生成重定位表项(变量=>rela.dyn, 函数=>rela.plt)。如果是局部变量/函数,则不会放入,采用相对寻址即可。函数相对寻址很常见,变量相对寻址是与体系结构相关的,x86_64支持该特性。
The dynsym is a smaller version of the symtab that only contains global symbols. The information found in the dynsym is therefore also found in the symtab, while the reverse is not necessarily true. You are almost certainly wondering why we complicate the world with two symbol tables. Won’t one table do? Yes, it would, but at the cost of using more memory than necessary in the running process. [Reference]
[36].strtab是辅助[35].symtab,记录着符号表中用到的字符串,一般就是函数名,变量名和文件名等。同样,[5].dynstr是辅助[4].dynsym的。
[11].plt [21].got [22].got.plt
这 三个section都与动态链接的重定位有关,.plt可以看作代码段,而.got/.got.plt可以看作数据段。动态库中定义的全局符号或者引用其 他库的符号,都会在这些section做相应记录。简单来说,动态库中对全局函数的访问,都会先跳转到.plt表项,然后plt中的代码段,该代码段从 got.plt取函数地址,执行跳转。而对全局变量的访问,都会先访问got表项,其中记录着真正的变量地址。重定位时都会修改这些表项记录的地址。
[20] dynamic
默认情况记录着-l**链接的动态库和一些初始化和结束函数等。 readelf -d a.out可读出。
readelf -d a.out
Dynamic section at offset 0xaa0 contains 22 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libdl.so.2]
0x0000000000000001 (NEEDED) Shared library: [libc1.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x400640
0x000000000000000d (FINI) 0x400928
注 意:之前谈到静态库时,有抛弃无用o文件的做法,但动态库在默认情况下, 只要gcc -l关联了一个动态库,无论是否会用到,都会将这种关联信息写入dynamic section 的NEEDED表项中,并在程序初始化时加载该动态库到程序进程的地址空间。当然,GCC/ld也提供了选项(-Wl,–as-needed)来只将那些 用到的动态库信息写入到NEEDED表项,对于没有用到的动态库果断放弃。
为了对全局符号和局部符号的动态符号解析和重定位有个直观的认识,先给出如下代码:
//libc2.c
#include
static
int
var_local = 0;
int
var_global =0 ;
static
void
pixman_local()
{}
void
pixman()
{}
void
cairo()
{
pixman();
pixman_local();
printf
(
"%d\n"
,var_global);
printf
(
"%d\n"
,var_local);
}
gcc -shared -fPIC libc2.c -g -o libc2.so,编译后,readelf -a libc2.so,请仔细观察symtab, dynsym, rela.dyn rela.ptl对全局/局部符号的处理
Relocation section
'.rela.dyn'
at offset 0x580 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000100a60 001900000006 R_X86_64_GLOB_DAT 0000000000100aac var_global + 0
Relocation section
'.rela.plt'
at offset 0x610 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000100a80 000d00000007 R_X86_64_JUMP_SLO 0000000000000752 pixman + 0
000000100a88 001000000007 R_X86_64_JUMP_SLO 0000000000000000
printf
+ 0
Symbol table
'.dynsym'
contains 26 entries:
Num: Value Size Type Bind Vis Ndx Name
13: 0000000000000752 6 FUNC GLOBAL DEFAULT 10 pixman
15: 0000000000000758 75 FUNC GLOBAL DEFAULT 10 cairo
16: 0000000000000000 160 FUNC GLOBAL DEFAULT UND
printf
@GLIBC_2.2.5 (2)
25: 0000000000100aac 4 OBJECT GLOBAL DEFAULT 22 var_global
Symbol table
'.symtab'
contains 67 entries:
Num: Value Size Type Bind Vis Ndx Name
50: 000000000000074c 6 FUNC LOCAL DEFAULT 10 pixman_local
51: 0000000000100ab0 4 OBJECT LOCAL DEFAULT 22 var_local
54: 0000000000000752 6 FUNC GLOBAL DEFAULT 10 pixman
56: 0000000000000758 75 FUNC GLOBAL DEFAULT 10 cairo
57: 0000000000000000 160 FUNC GLOBAL DEFAULT UND
printf
@@GLIBC_2.2.5
66: 0000000000100aac 4 OBJECT GLOBAL DEFAULT 22 var_global
2 链接基本过程将.o, .so, .a文件链接生成动态库或主程序(App)时,链接时会遵循以下过程,而.o文件合并成.a静态库,只是打包而已,不涉及符号解析和重定位:
Merge => Symbol Resolve => Relocation
2.1 Merge:生 成单个.o文件时,其相对地址都是从0开始计算的。Merge过程可以说是同名节(data section,text section等)合并构成同样名称的section,进一步具有相同读/写/执行权限/可加载的section会merge到一个segment中。 section是供链接器使用,而segment是加载器使用。.o文件肯定会merge到生成文件中,在此很容易出现重定义的错误。而静态库是.o文件 的集合,只按需选择其中部分.o文件进行merge. 动态库(-l**形式的)只做关联,从不merge到生成文件中。
在merge之前,每个库都有各自的符号表(Symbol Table),在merge之后,生成文件中只有一份符号表,其中有定义的/未定义的符号(变量,函数名),文件名,节名等。
静态库符号查找方法(merge过程):
采用一次遍历的算法,从主程序开始,其符号构成{已定义,未定义} = {D,U},从左往右搜索待链接的库,如果含有之前U集合中的定义项,则把该o文件merg沈阳网站建设e到生成文件,并把该符号从U中移到D集合,并添加本o文件中 其他符号到{D, U},其他无关的o文件被抛弃(绝不回头再次查找的),继续依次搜索。搜索完成后,如果生成的是主程序(app),若集合U不为空,则会报错,若生成的是 动态库,若集合U不为空,一般情况是不报错的,因为动态库是容许有未定义符号的,除非你通过链接选项来禁止该功能。
由于采用一次遍历,所以如果A库引用B库的符号,则B库应该放在A库的右边。换而言之,越基础的库放到越后面。当然gcc/ld提供了选项来指定某些静态库进行循环查找来解决循环依赖问题。如:
gcc -Wl,–start-group -lmy_lib -lyour_lib -lhis_lib -Wl,–end-group
举例:gcc main.c ./lib1.a ./lib2.a (或gcc main.c -L. -l1 -l2)
主 程序中含有的未定义和定义的符号:U={cairo},D= {mian}, 首先查找lib1.a中的.o文件,发现c1.o可以满足要求,遂将c1.o merge到主程序并修正全局符号表为U={pixman,frame} ,D={main,cairo}。继续在lib1.a中查看其他.o文件,发现c2.o满足要求,将其merge后U={frame}, D={main, cairo pixman, nouse},而c3.o没有用到则抛弃它。继续搜索lib2.a,发现p1.o可以解决U中未定义的符号,将其merge,而后U为空集,完成静态链 接。需注意在静态库内部诸多.o可以构成循环查找,但链接时指定的库正如前文所言,其顺序是一次遍历。此外,如果编译指令为gcc main.c c1.o c2.o c3.o p1.o,则c3.o即使没有用到也会merge到app/so中,这个就是之前提到的o文件肯定会merge。
2.2 Symbol Resolve:凡 是引用的符号都要进行符号解析,即找出定义它的地址,并将其的地址加入到全局符号表,方便后续的重定位。全局符号表更多的是链接时动态维护的一张表,与 ELF中的符号表并不一样。静态库在merge时就确定了符号定义的唯一性,在编译之后的链接阶段就立马进行符号解析和重定位,而动态库在编译链接时只是 做关联,只有在运行时才能确定符号的地址,所以其符号解析和重定位推迟到运行时进行。
动态库符号查找方法:
例如:gcc main.c -ldl -L. -lc1 -lc2, 如上图所示。假定libc1.so要用到printf,那么按如下顺序查找:
· 主程序a.out的.dynsym;
· 按广度优先遍历依次查找-l**关联的库。至于主程序(App)关联了哪些有效的so,可以通过readelf –d a.out 的NEEDED项看出;
通过设置export LD_DEBUG=symbols,然后运行主程序a.out, 可以看到符号查找的详细过程:
24484: symbol=printf; lookup in file=./a.out
24484: symbol=printf; lookup in file=/lib64/libdl.so.2
24484: symbol=printf; lookup in file=libc1.so
24484: symbol=printf; lookup in file=libc2.so
24484: symbol=printf; lookup in file=/lib64/tls/libc.so.6
在 符号引用的地方,生成代码时并不知道符号的地址,就会以一种假地址来占位。重定位就是将这些假地址占位的地方替换成,符号解析过程找到的真地址。那怎么知 道那些地方是假地址占位呢,要替换的真地址又怎么得到呢?重定位表就是做这个事的。重定位的每个表项记录着需要重定位的地方和重定位所需真地址。重定位简 而言之,将需要重定位地方的假地址改写为关联的真地址。下面会有详细的剖析。
小结:
无论是静态库的merge还是动态库的符号查找,都对库的顺序极为敏感;
3 动态库之全局符号介入动态库的加载分为隐式加载(程序初始化)和显示加载(dlopen)两种,本节将分析同时存在两者的符号查找机制及如何处理全局符号介入。
首先给出基本的分析模型:
//libp1.c
#include
void
pixman()
{
printf
(
"pixman in libp1\n"
);
}
//libc1.c
#include
void
pixman();
void
cairo()
{
printf
(
"cairo1\n"
);
pixman();
}
//libp2.c
#include
void
pixman()
{
printf
(
"pixman in libp2\n"
);
}
//libc2.c
#include
void
pixman();
void
cairo()
{
printf
(
"cairo2\n"
);
pixman();
}
//main.c
#include
#include
void
cairo();
void
pixman()
{
printf
(
"pixman in main\n"
);
}
int
main()
{
pixman();
void
* handle=NULL;
void
(*callfun)();
handle=dlopen(
"/home/zpeng/test/so_test_paper/libc2.so"
,RTLD_LAZY);
callfun = (
void
(*)())dlsym(handle,
"cairo"
);
callfun();
dlclose(handle);
return
0;
}
编译指令如下:
1 gcc -shared -fPIC libp1.c -o libp1.so
2 gcc -shared -fPIC libc1.c -L./ -o libc1.so
3 gcc -c libp2.c -fPIC -olibp2.o
4 rm libp2.a
5 ar -rs libp2.a libp2.o
6 gcc -shared -fPIC libc2.c -L. -lp2 -g -o libc2.so
7 gcc main.c -ldl -L. -lc1 -g
编译条件:
如上图和代码所示,编译后运行a.out
运行结果:
pixman in main
cairo2
pixman in main
结果分析:
很明显,libc2.so中调用的pixman没有选择本地的pixman(libp2.c定义),而是选择了main.c中的pixman。这意味着本地符号被主程序中的符号覆盖了,也称之为全局符号介入。 原因:还记得之前讲过,动态库符号查找的顺序吗?其起点是主程序的.dynsym,即使是dlopen打开的库也不例外。但也请注意,dlsym()查找 符号是从dlopen打开库为起点哦,不信,你可以把dlsym(*,”pixman””),调用的结果是 “pixman in libp2”。
同样的,如果去除主程序的pixman定义,其运行结果会依赖第7条编译指令-l**添加库的顺序。
如何避免这种全局符号介入:
方 法1 更改dlopen内部符号查找的起点,dlopen(“/home/zpeng/test/so_test_paper /libc2.so”,RTLD_LAZY|RTLD_DEEPBIND);RTLD_DEEPBIND的含义就是首先搜索本地符号定义,然后再搜索主程 序的.dynsym。
方法2 在编译动态库时,加入-Wl,-Bsymbolic。它含义是动态库的符号在链接时绑定到本地定义,而不依赖运行时的符号查找。显而易见,这种效率比 RTLD_DEEPBIND效率高,加不加链接选项-Bsymbolic,其ELF文件内容见附录,看完文章后面部分就明白为什么了。
>–Wl.-Bsymbolic
When creating a shared library, bind references to global symbols to the definition within the shared library, if any. Normally, it is possible for a program linked against a shared library to override the definition within the shared library. This option is only meaningful on ELF platforms which support shared libraries.
编译条件:
更改编译指令,gcc main.c -ldl -g
运行结果:
pixman in main
cairo2
pixman in libp2
结果分析:
这个结果看着蛮正常的,那么为什么去掉-lc1会产生这样的效果呢?因为没有-lc1 ,无法使主程序的pixman被导到.dynsym中,查不到该符号,继而最终查到本地的pixman。
同样的逻辑,但凡使得主程序中的pixman导出到.dynsym都会覆盖掉libp2.c中定义。
-rdynamic,-Wl,–export-dynamic和-Wl,–version-script都可以做到。前两者含义一样,最后一种需要单独写一个控制脚本,具体用法参考相关文档。
>–export-dynamic
When creating a dynamically linked executable, add all symbols to the dynamic symbol table. The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time. If you do not use this option, the dynamic symbol table will normally contain only those symbols which are referenced by some dynamic object mentioned in the link.
这里还涉及到一问题,主程序如果要引用dlopen打开库的符号,该怎么办?
dlopen(“/home/zpeng /test/so_test_paper/libc2.so”,RTLD_LAZY|RTLD_GLOBAL),RTLD_GLOBAL的含义就是将 dlopen打开的动态库符号添加到全局的.dynsym,以便主程序和其他的动态库能用。默认情况是RTLD_LOCAL,即dlopen打开的库会形 成一个局部的查找系统。
编译条件A:
main.c修改dloepn(*,|RTLD_DEEPBIND)
修改编译指令,gcc main.c -ldl -L. -lc1
运行结果A:
pixman in main
cairo2
pixman in libp2
编译条件B:
main.c修改dloepn(*,|RTLD_DEEPBIND)
修改编译指令,gcc main.c -ldl -L. -lc1 -lc2
运行结果B:
pixman in main
cairo2
pixman in main
结果分析:
两组对比,-lc2后可以看出dlopen的参数RTLD_DEEPBIND没有起作用。原因在于一个动态库只能被加载一次,libc2.so在程序启动初期即加载了,后面的隐式加载没有起作用。
strace ./a.out 结果:
open(
"./libc2.so"
, O_RDONLY) = 3
read(3,
"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\6\0\0\0"
..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=8017, ...}) = 0
getcwd(
"/home/zpeng/test/so_test_paper"
, 128) = 31
mmap(NULL, 1051192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x2a95686000
mprotect(0x2a95687000, 1047096, PROT_NONE) = 0
mmap(0x2a95786000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0) = 0x2a95786000
close(3) = 0
open(
"/home/zpeng/test/so_test_paper/libc2.so"
, O_RDONLY) = 3
read(3,
"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0p\6\0\0\0"
..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=8017, ...}) = 0
close(3)
strace还可以查看动态库查找路径和加载顺序(广度优先)。
小结:
虽然静态库也存在全局符号介入,但情况较为简单,在此重点说明了动态库全局符号介入。默认的动态库符号查找模型:
1. 在动态可执行文件中.dynsym中查找符号(dlopen打开的库也不例外);
2. 查找LD_PRELOAD项
3. 在进程初始化期间提供的so中查找(readelf -d 的NEEDED项),执行的是广度优先;
4. 接下来,运行时链接程序会在通dlopen获取的目标文件及其依赖项中查找(只对dlopen打开的库有效);
GCC/ld提供了一些选项以便控制符号查找路径,下图做了个小结。
总而言之,一方面来讲,主程序与dlopen打开的库之间有道默认的屏障,如果要引用对方的符号,需增加相应的命令选项。另一方面来讲,动态库的符号极容易被主程序或沈阳做网站其他排在前面的动态库符号覆盖,可增加编译选项来强制引用本地符号。
4 动态库符号引用效率分析4.1 Static / 匿名namespace: 局部化如果引用本地定义的变量或函数,尽可能给定义添加static或匿名namespace,不仅可以避免重定义的错误,还可以加快动态库变量引用和函数调用的速度。因为局部化后偏移地址在编译时链接就可知,减少got,got.plt跳转指令。下面从代码层面给予说明。
3
void
static
pixman_local()
4 {
5
printf
(
"pixman_local in libc2\n"
);
6 }
7
int
var_global = 1;
8
static
int
var_local = 0;
9
void
pixman();
11
void
cairo()
12 {
13
printf
(
"cairo2\n"
);
14 pixman();
15 pixman_local();
16
printf
(
"%d\n"
,var_global);
17
printf
(
"%d\n"
,var_local);
18 }
Dump of assembler code
for
function cairo:
0x0000002a95688763 : push %rbp
0x0000002a95688764 : mov %rsp,%rbp
0x0000002a95688767 : lea 207(%rip),%rdi # 0x2a9568883d
0x0000002a9568876e : mov $0x0,%eax
0x0000002a95688773 : callq 0x2a95688690
0x0000002a95688778 : mov $0x0,%eax
0x0000002a9568877d : callq 0x2a95688680
0x0000002a95688782 : mov $0x0,%eax
0x0000002a95688787 : callq 0x2a9568874c
0x0000002a9568878c : mov 1049389(%rip),%rax # 0x2a95788ac0
//var_global ,1049389(%rip)为该变量存放在GOT表项中的真实地址
0x0000002a95688793 : mov (%rax),%esi
//var_global 间接取值,(%rax)表示取%rax指向的值
0x0000002a95688795 : lea 169(%rip),%rdi # 0x2a95688845
//printf 第一个参数
0x0000002a9568879c : mov $0x0,%eax
//printf 返回值用
0x0000002a956887a1 : callq 0x2a95688690
//printf("%d\n",var_global);
0x0000002a956887a6 : mov 1049444(%rip),%esi # 0x2a95788b10
// var_local 直接取值
0x0000002a956887ac : lea 146(%rip),%rdi # 0x2a95688845
0x0000002a956887b3 : mov $0x0,%eax
0x0000002a956887b8 : callq 0x2a95688690
0x0000002a956887bd : leaveq
0x0000002a956887be : retq
全局/局部变量对比分析:
如果是全局变量,会产生一个到got的表项的引用,而该表项才真正存放变量的地址。关于全局变量很容易出汇编上看到,多出一条指令:mov (%rax), %esi。
全局/局部函数调用开销分析:
如果是全局的函数,就会产生一个到plt的调用,而plt段又会调用.got.plt中保存的函数地址跳转过去执行,注意由于现在动态库一般采用延迟绑定 技术(PLT),在首次跳转到got.plt表项时,需要进行运行时符号解析,得出真正的要调用函数的地址,并填入到got.plt表项。
将上述函数调用
callq 0x2a95688680
callq 0x2a9568874c
再次展开,可见全局函数要执行PLT相关过程,而局部函数直接调用展开。
(gdb) disas 0x2a95688680
Dump of assembler code
for
function pixman@plt:
0x0000002a95688680 : jmpq *1049690(%rip) # 0x2a95788ae0 [pixman]
0x0000002a95688686 : pushq $0x0 # 在rela.plt中的索引位置
0x0000002a9568868b : jmpq 0x2a95688670 # 跳转到plt-0x10的位置,
0x0000002a95688690 : jmpq *1049682(%rip) # 0x2a95788ae8 [
printf
]
0x0000002a95688696 : pushq $0x1
0x0000002a9568869b : jmpq 0x2a95688670
End of assembler dump.
(gdb) disas 0x2a9568874c
Dump of assembler code
for
function pixman_local: #直接跳转到函数pixman_local处开始执行
0x0000002a9568874c : push %rbp
0x0000002a9568874d : mov %rsp,%rbp
0x0000002a95688750 : lea 207(%rip),%rdi # 0x2a95688826
0x0000002a95688757 : mov $0x0,%eax
0x0000002a9568875c : callq 0x2a95688690
0x0000002a95688761 : leaveq
0x0000002a95688762 : retq
End of assembler dump.
为了更加清楚的表述动态重定位的过程,请看下图:
首先需要说明的是,plt类似于text section, 反汇编时有指令,而got/got.plt类似于data section,可读可写并不执行,在此处相当于一个查询字典。假定text段要调用bar(),则跳转到.plt段对应的表项bar@plt,该表项第一条指令就是跳转到从got.plt中查询到的函数地址,在首次进行重定位时,该地址是个fake地址(指向bar@plt 的第二项指令“Fake”),将符号索引号push进堆栈,接着跳转到一个plt的公共头,并将模块id也push到堆栈,然后跳转到符号解析函数查找出 函数的真正地址(“Real:”),并将该地址填入到got.plt对应的表项。在首次完成解析后,后面调用该函数时,在bar@plt的首条指令即可查 询到正确的地址,进行跳转。这里也给出 .plt section静态反汇编的图,objdump -j.plt -S libc2.so:
4.2 __attribute__((visibility))int __attribute__((visibility (“hidden”))) last;
void __attribute__((visibility (“default”))) next () {}
class __attribute__((visibility(“hidden”))) foo{…}; (gcc4.0)
在编译时:-fvisibility=hidden, 默认改为隐藏属性。它与static的区别在于,它的边界范围是动态库,而static是文件,但两者都能做如上所述的优化(消除 got,got.plt)。需注意,-fvisibility=hidden须在编译源码时传入,否则不会起作用。
c的方式如下所示,C++考虑到名字改编,如果需要使用export map会更加复杂些。
-Wl,–version-script=test.ver
test.ver:
VER_1.0{
global: cairo;
local: pixman;
};
–version-script 支持生成动态库,而对主程序没有符号控制作用。
小结:
static/ 匿名空间/export map改变的符号表中的“Bind”:global/local,而__attribute__((visibility))改变的是 “visibility”:HIDDEN/DEFAULT..。然而两者在调用优化上结果是相同的。
在这里也解释之前遗留的小问题,-Wl,-Bsymbolic的作用。由附录可见,加了该选项后pixman没有出项在rela.plt中,说明该符号不需要重定位,对pixman的引用会转为相对寻址,带来效率的提升。
5 总结事实上关于链接和加载更多是个有关ABI(Application Binary Interface)的话题,不同的体系结构有不同规定,本文的例子均在x86_64上测试所得。
要想了解更多这方面的信息可以阅读附录给的参考书和X86_64 ABI标准文档。
总而言之:在链接阶段,你想要的并不一定是你所得:即符号有覆盖,全局和局部符号效率有差异。
附录1 with/without -Wl,-Bsymbolic 编译动态库
------without -Wl,-Bsymbolic--------------------------------
gcc -shared -fPIC libc2.c -L . -lp2 -o libc2.so
Relocation section
'.rela.dyn'
at offset 0x578 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000100a68 000000000008 R_X86_64_RELATIVE 0000000000100a68
000000100a70 000000000008 R_X86_64_RELATIVE 0000000000100890
000000100a78 000000000008 R_X86_64_RELATIVE 00000000000007d6
000000100a20 001200000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0
000000100a28 001800000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000100a30 001900000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section
'.rela.plt'
at offset 0x608 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000100a50 000d00000007 R_X86_64_JUMP_SLO 000000000000074c pixman + 0
000000100a58 001000000007 R_X86_64_JUMP_SLO 0000000000000000
printf
+ 0
000000100a60 001200000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
------with -Wl,-Bsymbolic--------------------------------
gcc -shared -fPIC libc2.c -Wl,-Bsymbolic -L . -lp2 -o libc2.so
Relocation section
'.rela.dyn'
at offset 0x578 contains 6 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000100a40 000000000008 R_X86_64_RELATIVE 0000000000100a40
000000100a48 000000000008 R_X86_64_RELATIVE 0000000000100860
000000100a50 000000000008 R_X86_64_RELATIVE 00000000000007a6
000000100a00 001200000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize + 0
000000100a08 001800000006 R_X86_64_GLOB_DAT 0000000000000000 _Jv_RegisterClasses + 0
000000100a10 001900000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
Relocation section
'.rela.plt'
at offset 0x608 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000100a30 001000000007 R_X86_64_JUMP_SLO 0000000000000000
printf
+ 0
000000100a38 001200000007 R_X86_64_JUMP_SLO 0000000000000000 __cxa_finalize + 0
附录2 参考资料
<<程序员的自我修养—链接、装载与库>>
man5/elf.5.html
http://www.akkadia.org/drepper/dsohowto.pdf
http://www.x86-64.org/documentation/abi.pdf