库分为两种: 静态库和动态库.
对于静态库, 它的连接是静态连接, 其内容将复制到可执行文件.
对于动态库, 它的连接是动态连接, 其内容没有嵌入可执行文件, 细分为两种: 加载时连接和运行时连接.
本文用到的三个文件:
// shared_library.h
void foo(void);
// shared_library.c
#include "stdio.h"
void foo(void) {
printf("Hello, World!\n");
}
// shared_library_example.c
#include "shared_library.h"
int main(void) {
foo();
}
加载时连接
先看 CSAPP 的例子:
gcc -shared -fPIC -o libvector.so addvec.c multvec.c
gcc -o p2 main2.c ./libvector.so
连接时, ld 并未将动态库的代码和数据复制到可执行文件, 加载程序时, execve会使用动态连接器ld-linux.so将动态库的代码和数据加载到进程的虚拟内存.
再来看看我们的例子:
$ gcc -shared -fPIC -o libshared.so shared_library.c
$ gcc shared_library_example.c ./libshared.so
$ ./a.out
Hello, World!
改变动态库, 重新编译:
// shared_library.c
#include "stdio.h"
void foo(void) {
printf("hello, world!\n");
}
$ gcc -shared -fPIC -o libshared.so shared_library.c
$ ./a.out
hello, world!
动态库改变了, 在不重新编译shared_library_example.c 的情况下, shared_library_example.c 的输出改变了, 这就是动态库相比静态库的动态性.
运行时连接
// shared_library_example2.c
#include "dlfcn.h"
int main(void) {
void *handle = dlopen("libshared.so", RTLD_LAZY);
void (*foo)(void) = dlsym(handle, "foo");
foo();
dlclose(handle);
return 0;
}
$ gcc shared_library_example2.c -ldl
$ ./a.out
hello, world!
dlopen必须与 dlclose 配套, 不然会产生内存泄露
// shared_library_example2.c
#include "dlfcn.h"
int main(void) {
void *handle = dlopen("libshared.so", RTLD_LAZY);
void (*foo)(void) = dlsym(handle, "foo");
foo();
handle = dlopen("libshared.so", RTLD_LAZY);
dlclose(handle);
return 0;
}
$ gcc shared_library_example2.c -ldl
$ valgrind ./a.out
==15910== Memcheck, a memory error detector
==15910== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==15910== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==15910== Command: ./a.out
==15910==
hello, world!
==15910==
==15910== HEAP SUMMARY:
==15910== in use at exit: 1,435 bytes in 4 blocks
==15910== total heap usage: 8 allocs, 4 frees, 4,827 bytes allocated
==15910==
==15910== LEAK SUMMARY:
==15910== definitely lost: 0 bytes in 0 blocks
==15910== indirectly lost: 0 bytes in 0 blocks
==15910== possibly lost: 0 bytes in 0 blocks
==15910== still reachable: 1,435 bytes in 4 blocks
==15910== suppressed: 0 bytes in 0 blocks
==15910== Rerun with --leak-check=full to see details of leaked memory
==15910==
==15910== For lists of detected and suppressed errors, rerun with: -s
==15910== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
静态库和动态库的优缺点
因为静态连接把所有依赖内嵌到可执行文件中, 所以可执行文件一般都比较大, 并且可执行文件的行为是确定的. 而动态连接让可执行文件依赖于外部的动态库, 可执行文件比较小, 然而外部的动态库可能被删除, 可能升级, 可执行文件可能找不到动态库, 可能不兼容升级的动态库.
运行时连接比加载时连接有更强的动态性. 加载时连接必然将动态库的所有代码和数据加入进程的虚拟内存, 而运行时连接可以实现按需加载, 不需要就完全不加载.
因为静态库会复制到每个用到它的可执行文件中, 而动态库整个系统只有一份, 所以动态库的硬盘用量更小.
根据mmap文档, 动态库的代码和只读数据只映射到物理内存一次, 而每个用到静态库的可执行文件都会把静态库映射到物理内存, 所以动态库的内存用量肯定大大小于静态库, 并且 page faults 次数也大大小于静态库, 性能会有所提高.
配置
在 Ubuntu 中, 连接动态库时不会去当前目录找动态库, 所以要设置LD_LIBRARY_PATH: export LD_LIBRARY_PATH="."
ld.so.preload
关于ld.so.preload, 先看一个例子:
// foo.h
void foo(void);
// foo1.c
#include "stdio.h"
void foo(void) {
printf("I am foo1\n");
}
// foo2.c
#include "stdio.h"
void foo(void) {
printf("I am foo2\n");
}
// test.c
#include "foo.h"
int main(void) {
foo();
return 0;
}
$ gcc -shared -fPIC -o libfoo1.so foo1.c
$ gcc -shared -fPIC -o libfoo2.so foo2.c
$ gcc test.c libfoo2.so
$ ldd a.out
linux-vdso.so.1 (0x00007fffabc7b000)
libfoo2.so => ./libfoo2.so (0x00007f5a3b839000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5a3b608000)
/lib64/ld-linux-x86-64.so.2 (0x00007f5a3b845000)
$ ./a.out
I am foo2
$ echo "/tmp/libfoo1.so" | sudo tee -a /etc/ld.so.preload
/tmp/libfoo1.so
$ ldd a.out
linux-vdso.so.1 (0x00007ffd4835c000)
/tmp/libfoo1.so (0x00007f7ba1dde000)
libfoo2.so => ./libfoo2.so (0x00007f7ba1dd9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7ba1ba8000)
/lib64/ld-linux-x86-64.so.2 (0x00007f7ba1dea000)
$ ./a.out
I am foo1
/etc/ld.so.preload添加了/tmp/libfoo1.so之后, 动态连接器先加载了libfoo1.so, 在libfoo1.so中找到了 foo 函数的定义, 也就是说, 定义在/etc/ld.so.preload中的动态库是最先加载的, 可以拦截其他动态库, 这个例子中, libfoo1.so拦截了 libfoo2.so
/etc/ld.so.preload甚至可以拦截libc.so和系统调用. 我定义了和 open 系统调用相同签名的函数, 函数什么都不做, 只是返回-1, 即打开文件永远失败, 我把它打包成动态库加入 aws 云主机的/etc/ld.so.preload, 结果 less, vim, ls 命令全都失败了. 登出之后就再也无法登入aws, 感觉自己要重装系统了, 幸好急中生智, 关掉实例, 起了一个新的实例, 把前一个实例的系统分区硬盘挂上去, 删掉/etc/ld.so.preload, 再启动实例就正常了.