在Linux中动态库的确给程序带来了良好的扩充性,并减少了内存的使用量,但这是有代价的。例如:
#include <stdio.h> int main(int argc, char *argv[]) { printf(“hello\n”); return 0; }我们知道printf是在glibc中定义的,如果不适用动态库,而是将glibc静态链接到进程中的话,那么printf函数的地址在编译时就是已知的了,使用很简单的依据地址转移,就可以进行函数调用。
可是如果采用动态库的话,在程序编译阶段,编译器就无法得知printf的函数地址,因为动态库加载的内存地址时随机的。那么对于动态库的情况,针对printf是如何寻址的呢?
在程序运行时,当调用printf的时候,程序会将处理权交给linker,由其负责在执行文件以及其连接的动态库中查找printf的函数地址。由于linker不知道printf具体是在哪个动态库,所以它将在整个执行文件和动态库的范围内查找。更糟糕的是在C++程序中,符号的命名是类名+函数名,这导致在做字符串比较时,往往直到字符串的结尾才能获得结果。
这也就导致了在进程启动过程中,符号查找占据了一大部分时间。在Linux的KDE进程启动中,符号查找甚至占据了进程启动80%的时间。
因此就针对上述的情况,有以下优化解决方案:
1、减少导出符号的数量
在动态库编译和生成时,默认所有的函数和全局变量都是导出的。而实际上有很多函数只是动态库内部使用,通过去掉那些动态库中不必要的导出符号,从而减少动态库在做链接时所查找的符号数量,可以加快动态库链接的速度。
可以使用ld的ld --retain-symbols-file --version-script两个选项实现。写一个导出符号文件,如 symbol 指定你只导出的函数,如 func1。使用 ld 的--retain-symbols-file 参数可以在 static section 里取消 func1 以外的所有函数。这时你用 readelf 看编译好后的 .so 文件 static section 里没有了,使用 nm 看 .so文件它无法查出导出函数。但这并不完全。因为在 dynamic section 里还是会看到所有符号被导出。如果想在 dynsym section 里也不让他导出的话,需要再编写一个 script 文件,指定 global 与 local 在 global 中指定你要导出的函数,简单的格式如下:
VERSION{
VER_1.0{
global: 导出函数名;
local: *;
};
}
再在 ld 时用 --version-script 选项来 load 你的文件。都完事后再使用 readelf 观察static 与 dynamic section 发现只导出了你指定的函数名即符号。
例:
ld -shared --retain-symbols-file 符号文件 --version-script 脚本文件 -o 动态库文件。so filename
2、减少符号的长度
3、使用prelink
在这里另外在提一个问题,很有趣的东西。
gcc -fvisibility=hidden 只在链接时传入的.c文件起作用,对.o文件不其作用;
比如test.c test1.c,使用以下命令:
gcc -shared -fvisibility=hidden -otest.so test.c test1.c
和命令
gcc -c test.c test1.c
gcc -shared -fvisibility=hidden -otest.so test.o test1.o
生成的test.so中的对应可见性是不一样的,使用“readelf -s test.so”查看发现:
第一个达到预期目的,即将两个.c文件中的functions设为HIDDEN,
而第2个则不行,-fvisibility=hidden不起作用;
再用gcc -shared -fvisibility=hidden -o test.so test.o test1.c
生成的so,则可发现test1.c中的函数为HIDDEN,但test.o中的函数仍为DEFAULT;