笔记:关于链接库那点事儿

2021年7月31日
根据《程序员的自我修养-链接、装载与库(潘爱民著)》整理

1)总线。

北桥:高速设备(内存,缓存,CPU,PCIbridge)。南桥:低速设备(磁盘,USB,键盘,鼠标)。

2)内存。

增加中间曾层,内存映射,地址隔离,分段,分页。

3)线程。

程序执行最小单元:线程ID,指令指针,寄存器集合,堆栈。
线程三种状态:运行,就绪,等待

Linux Task:

  • fork:复制当前进程,只产生本任务的镜像
  • exec:使用新的可执行镜像覆盖当前的镜像
  • clone:创建子进程并从指定位置开始执行(产生新线程)
if ((pid=fork()) == 0) {
// 子进程
} else {
// 主进程
}

4)线程同步

信号量(任意线程获取和释放)
互斥锁(谁拥有谁释放)
临界区(进程内同步代码块)
条件变量(唤醒等待的线程)

5)线程过度优化

a. 阻止编译器为了提高速度缓存变量到寄存器:volatile x;
b. 阻止编译器调整操作 volatile 变量的指令次序 (volatile 无法阻止CPU动态调整执行顺序)

x = 0;
Thread1 Thread2
lock(); lock();
x++; x++;
unlock(); unlock();

CPU 乱序执行的问题:volatile 无法阻止CPU动态调整执行顺序:

x = y = 0;
Thread1 Thread2
r1 = y; y = 1;
x = 1; r2 = x;

采用 barrier() 指令 (lwsync),阻止 CPU 将 barrier 之前的指令交换到 barrier 之后。

6)多线程内部情况

用户线程:内核线程

  • 1:1
    CreateThread
    clone(thread_fun, thread_stack, CLONE_VM, 0);
    默认模型。真并发。数量限制,开销大
  • N:1 线程切换快,无限的线程数量。性能提高不显著。
  • M:N

7)编译和链接

a.预编译(展开全部的宏,处理全部预编译指令,删除注释):$ gcc -E

hello.c -o hello.i

b. 编译(cc 语法分析,构建,生成汇编代码):

$ gcc -S hello.i -o hello.S

直接:

$ gcc -S hello.c -o hello.S

c. 汇编(as 汇编代码转为机器指令产生目标文件):

$ as hello.S -o hello.o

直接:

$ gcc -c hello.c -o hello.o

d. 链接(ld relocation 符号解析重定位,给程序地址打补丁,使其指向正确的地址。):

$ ld /path/to/a.o /path/to/b.o -L/path/to/lib -lm

8)目标文件(可执行文件)格式COFF

Linux ELF,Windows PE-COFF。格式查看:

$ file foobar.o

文件头 0x00000000
.text 段 代码段 机器代码指令
.data 段 数据段 已初始化的全局变量和局部静态变量
.bss 段 未初始化的全局变量和局部静态变量(0)。预留位置,没有内容,不占空间。
内容查看:$ objdump -h foobar.o
分析代码段:$ objdump -s -d foobar.o
内容查看:$ readelf -h foobar.o
段表结构:$ readelf -S foobar.o

extern “C” {
// C++ 编译器把这里的代码当作 C 语言代码处理
}

9)可执行文件(映像文件:进程)装载

每个程序有自己的虚拟地址空间。常用部分放内存,不常用部分放磁盘。在操作系统(动态装载,页映射)看来:进程最关键的特征是拥有独立的虚拟地址空间:
a. 创建独立的虚拟地址空间
b. 读取可执行文件头,建立虚拟地址空间和可执行文件的映射关系
c. 将CPU的指令寄存器设置成可执行文件的入口地址,运行。缺页中断。加载执行

10)动态链接

静态链接缺点:空间浪费,更新困难。链接时重定位:Link Time Relocation
Linux 动态共享对象(DSO)。Windows DLL。位置无关代码(-fPIC),装载时重定位:Load Time Reloc (Win基址重置Rebasing)。

$ gcc -fPIC -shared -o Lib.so Lib.c
$ reade;f -l Lib.so

查看DSO是否为 PIC(无输出则为PIC):

$ readelf -d foo.so |grep TEXTREL

TEXTREL(重定位表地址)

11)运行时加载

dlopen(sopathfile, flag)

  • sopathfile: ‘/’开头绝对路径直接加载。
  • 相对路径加载(搜索)次序:
    i. LD_LIBRARY_PATH 指定的一系列目录
    ii. 查找 /etc/ld.so.cache 里面指定的共享库路径
    iii. /lib, /usr/lib

sopathfile: 0. 返回全局符号表句柄。全局符号表:程序可执行文件及已经装载的所有共享模块和通过dlopen(file, RTLD_GLOBAL) 方式已经打开的符号。

dlsym(handle, symbol)

返回函数地址,变量地址,常量的值(配合char * dlerror()==NULL 成功)。

dlclose(handle)

卸载。

/* test.c */
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, chat *argv[]) {
void * hdl;
double (*func)(double);
char *error;
hdl =dlopen(argv[1], RTLD_NOW);
if (!hdl) exit(1);
func=dlsym(hdl, “sin”);
if ((error = dlerror()) != NULL) {
dlclose(hdl);
exit(-1);
}
func(3.1416/2);
dlclose(hdl);
return 0;
}

编译运行:

$ gcc -o test test.c -ldl
$ ./test /lib/libm-2.6.1.so

12)Linux共享库组织

ABI 兼容。命令规则:libname.so.X.Y.Z

X – 主版本号:重大升级。2.y.z不兼容1.y.z
Y – 次版本:增量(兼容)升级。1.4.z兼容1.3.z,1.2.z,1.1.z,1.0.z
Z – 发布版本:不添加任何接口。只是 BUG FIX。

共享库名字:libfoo.so.2 (必须保留主版本号。libfoo.so.1和libfoo.so.2是完全不同的共享库! )

libfoo.so.2 -> libfoo.so.2.6.1
$ readelf -d Lib.so

共享库的链接名XXX(自动查找最新的XXX):libfoo.so.2.6.1 = libXXX.so.2.6.1
编译路径查找:-L共享库路径

当共享库同时存在动态和静态版本时(如libc.a, libc.so.1.2.3), $ ld -static -lc查找静态库(libc.a),$ ld -Bdynamic -lc (默认)查找动态库libc.so.X。

此版本号交会。当只存在低于要求的次版本号共享库时。
Linux共享库符号版本机制。

/lib:系统运行必不可少最关键的库。
/usr/lib:非系统关键,但是可能开发时会用到。
/usr/local/lib:第3方程序库安装位置。

13)共享库查找

优先级:LD_PRELOAD->LD_LIBRARY_PATH->ld.so.cache->/usr/lib->/lib

a.永久添加共享库:
/etc/ld.so.conf 添加搜索路径,运行 ldconfig 调整 SO-NAME和更新 /etc/ld.so.cache。ld.so.cache为针对查找共享库特殊设计的文件结构。

b.临时添加共享库:
环境变量 LD_LIBRARY_PATH=冒号分隔的一系列路径

$ LD_LIBRARY_PATH=/path1:/path2:/path3 /opt/myapp/test

或者:

$ /lib/ld-linux.so.2 -library-path /path1:/path2:/path3 /opt/myapp/test

LD_PRELOAD(/etc/ld.so.preload):里面指定的共享库和目标文件全部都会加载。
LD_DEBUG:打印整个装载过程: LD_DEBUG=files ./HelloWorld.out

查看依赖:

$ ldd /path/to/your.so.2

14)共享库创建

libfoo1.c libfoo2.c 创建libfoo.so.1.2.3
单步:

$ gcc -shared -fPIC -Wl,soname,libfoo.so.1 -o libfoo.so.1.2.3 \

libfoo1.c libfoo2.c -lbar1 -lbar2

多步:

$ gcc -c -g -Wall -o libfoo1.o libfoo1.c
$ gcc -c -g -Wall -o libfoo1.o libfoo2.c
$ ld -shared -soname libfoo.so.1 -o libfoo.so.1.2.3 libfoo1.o libfoo2.o -lbar1 -lbar2

去掉调试(-S)和符号(-s)信息(binutils):

$ strip libfoo.so

或:

$ gcc -Wl,-s 或 -Wl,-S

15)共享库的构造函数和析构函数(仅GCC)

attribute((constructor)) libfoo_init(…); main() 之前执行或 dlopen返回前执行
attribute((destructor)) libfoo_fini(…); exit() 时执行或 dlclose返回前执行

构造优先级(执行次序):
attribute((constructor(100))) libfoo_init1()
attribute((constructor(200))) libfoo_init2()
attribute((constructor(300))) libfoo_init3()
析构优先级(执行次序):
attribute((destructor(300))) libfoo_fini3()
attribute((destructor(200))) libfoo_fini2()
attribute((destructor(100))) libfoo_fini1()

16)Windows 上的共享库

windows 上的函数族和Linux映射:

LoadLibrary    -----   dlopen
GetProcAddress ----- dlsym
FreeLibrary ----- dlclose
GetLastError ---- dlerror

如果跨平台,统一开发Linux的API,需要包装Win32 API。下面的开源库提供了Win32 API的封装版本 :

​dlfcn-win32​

如果用 VS 开发,程序可以静态链接 libdl.lib,如果使用 mingw 编译,可以静态链接到 libdl.a。(Linux上通过 -ldl 动态链接到Linux自身的 libdl.so)

下面提供示例代码演示如何动态绑定动态库api:

// myapp.c
#if defined (_WIN32) || defined(__MINGW__)
// VC 静态链接到 libdl.lib, MingW(GCC) 静态链接到 libdl.a
# include <dl/dlfcn.h>
#else
// Linux 动态链接: -ldl
# include <dlfcn.h>
#endif

#define dlsym_find_chked(symbolholder, dlhandle, returntype, symbolname, ...) do { \
char * _error_str_tmp; \
void * _sym_addr_tmp = dlsym(dlhandle, #symbolname); \
if (!_sym_addr_tmp || (_error_str_tmp = dlerror()) != NULL) { \
printf("failed on dlsym: %s", #symbolname); \
exit(-1); \
} \
(symbolholder)._##symbolname = (returntype (*)(__VA_ARGS__)) _sym_addr_tmp; \
} while(0)

struct libcairoapi_t
{
int (*_cairo_version)(void);
const char* (*_cairo_version_string)(void);
cairo_t * (*_cairo_create)(cairo_surface_t *);
cairo_t * (*_cairo_reference)(cairo_t *);
void (*_cairo_destroy)(cairo_t *);
...
} cairoapi = {0};

void load_cairo_dll()
{
void *cairolib= dlopen("libcairo-2.dll", RTLD_LAZY);
if (! cairolib) {
printf("failed to load: libcairo-2.dll. error=%d\n", dlerror());
return -1;
}

dlsym_find_chked(cairoapi, cairolib, int, cairo_version, void);
dlsym_find_chked(cairoapi, cairolib, const char*, cairo_version_string, void);
dlsym_find_chked(cairoapi, cairolib, cairo_t *, cairo_create, cairo_surface_t *);
dlsym_find_chked(cairoapi, cairolib, cairo_t *, cairo_reference, cairo_t *);
dlsym_find_chked(cairoapi, cairolib, void, cairo_destroy, cairo_t *);
...

// call cairo api:
printf("cairo version=%s\n", cairoapi._cairo_version_string());

//...

dlclose(cairolib);
}