linux 库与连接

本文介绍了Linux中静态库和动态库的概念,动态库分为加载时连接和运行时连接。静态库内容会被复制到可执行文件,而动态库则在运行时动态加载。动态库具有节省硬盘空间和内存、提供更强动态性的优点,但也可能导致可执行文件因找不到库或不兼容而出现问题。配置动态库路径需设置LD_LIBRARY_PATH,ld.so.preload允许预加载动态库以拦截其他库或系统调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

库分为两种: 静态库和动态库.

对于静态库, 它的连接是静态连接, 其内容将复制到可执行文件.

对于动态库, 它的连接是动态连接, 其内容没有嵌入可执行文件, 细分为两种: 加载时连接和运行时连接.

本文用到的三个文件:

// 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, 再启动实例就正常了.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值