cmake文件中SHARED和MODULE
在cmake中可以使用add_library函数生成静态库(STATIC)、动态库(SHARED)和模块库(MODULE)三种。在MacOS上,对应生成的文件类型分别为:.a .dylib .so
动态库链接和重定位的过程
- 1、加载库:操作系统根据库的路径将库加载到内存中,并将库中的符号地址保存在一个全局的符号表中。加载库时,操作系统会执行一些初始化工作,如分配内存空间、建立虚拟地址映射等。
- 2、程序链接:程序运行时,操作系统会将程序中引用的库中的符号地址替换成实际的地址。这个过程称为链接。链接的过程包括两个步骤:符号查找和符号重定位。
- 3、符号查找:当程序引用某个库中的符号时,操作系统首先在全局符号表中查找该符号的地址。如果找到了该符号,操作系统会将该符号的地址保存在程序中的重定位表中。
- 4、符号重定位:当程序运行时,重定位表中保存的地址需要进行重定位。操作系统会将重定位表中的地址加上库的基地址,得到最终的符号地址。这个过程称为符号重定位。
- 5、重定位完成:完成符号地址的计算和修改后,程序即可正常运行。
以上操作通常由操作系统的动态链接器完成。在Linux系统中动态链接器默认为ld.so,而在MacOS上则是dyld(Dynamic Linker)
.dylib文件和.so文件的区别
- 1、文件格式:.dylib文件是Mach-O格式的动态链接库文件,而.so文件是ELF格式的动态链接库文件。(Mach-O格式是macOS系统中使用的可执行文件格式,而ELF格式则是Unix/Linux系统中使用的可执行文件格式)
- 2、版本控制:.dylib文件支持使用版本控制符号来管理和控制库的版本,在.dylib文件中,每个符号都可以包含一个版本号,而.so文件则不支持。这意味着.dylib文件可以在不破坏二进制兼容性的情况下更新库的版本,而.so文件则需要在更新版本时重新编译和链接程序。
例如,假设我们有一个.dylib文件名为libtest.dylib,它的当前版本号为1.0,兼容版本号为1.0.0,
我们可以使用以下命令编译程序并链接到libtest.dylib文件,并指定需要使用的符号版本:
gcc -o myprog myprog.c -L/path/to/lib -ltest -Wl,-current_version,1.0,-compatibility_version,1.0.0
- 3、符号绑定:.dylib文件支持懒惰绑定和弱符号绑定。懒惰绑定是指在需要使用符号时才进行符号绑定,从而提高了程序的启动速度和内存使用效率。而弱符号绑定是指在符号未定义时也不会引发链接错误,而是将其绑定到一个默认值上。这些特性可以使得.dylib文件的使用更为灵活和高效。(下面有例子详细说明该特性)
- 4、延迟绑定:.dylib文件支持延迟绑定符号,也就是在程序运行时才进行符号绑定。延迟绑定可以减少程序启动时间和内存使用,特别是对于大型程序和复杂的.dylib文件,效果更为明显。在.dylib文件中,可以使用LD_BIND_NOW环境变量或dyld命令行选项来控制符号的绑定时间。
库中符号的可见性(以.dylib举例)
例子:
// 计算一个数平方根的简单程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "TutorialConfig.h"__attribute__((visibility("hidden")))
int global_var = 4;__attribute__((visibility("hidden")))
int test_func(const char* str)
{if (strlen(str) > 0)printf("In test_func, args: %s", str);return 0;
} int main (int argc, char *argv[]) {if (argc < 2) {printf("%s Version %d.%d\n", argv[0],Tutorial_VERSION_MAJOR,Tutorial_VERSION_MINOR);printf("Usage: %s number\n",argv[0]);return 1;}double inputValue = atof(argv[1]) * global_var;test_func("haha");double outputValue = sqrt(inputValue);printf("The square root of %g is %g\n", inputValue, outputValue);return 0;
}
编译生成libTutorial.dylib后使用"nm libTutorial.dylib"命令查看其符号如下:
0000000000003db8 t __Z9test_funcPKc
0000000000008018 d __dyld_privateU _atof
0000000000008020 d _global_var
0000000000003e08 T _mainU _printfU _strlenU dyld_stub_binder
可以看到__Z9test_funcPKc,因为nm命令显示的符号通常是经过链接器处理后的符号,也称为外部符号(External Symbol),而原始符号(Raw Symbol)是在编译器生成目标文件时产生的符号。链接器在将目标文件链接成可执行文件或共享库时,会将原始符号转换为外部符号,并进行符号分辨和符号解析等操作。
所以我们可以使用 objdump -t 或者 c++filt 查看原始符号,这里我使用 nm libTutorial.dylib | c++filt 查看的结果如下:
0000000000003db8 t test_func(char const*)
0000000000008018 d __dyld_privateU _atof
0000000000008020 d _global_var
0000000000003e08 T _mainU _printfU _strlenU dyld_stub_binder
上面的一些符号“t T d U” 和 __dyld_private、dyld_stub_binder 我解释一下:
U类型符号:U表示未定义(Undefined),即该符号在当前模块中未定义,但在其他模块中定义了。通常情况下,链接器会在链接时将未定义的符号与其他模块中定义的符号进行匹配。如果找不到匹配的符号,则会报链接错误。
d类型符号:d表示已定义的数据段(Defined data)。这种类型的符号表示当前模块中定义了一个全局变量或静态变量。
D类型符号:D表示已定义的数据段(Defined data)。与d类型符号类似,但这种类型的符号表示当前模块中定义了一个公共变量或公共静态变量。
t类型符号:t表示已定义的文本段(Defined text)。这种类型的符号表示当前模块中定义了一个函数或静态函数。
T类型符号:T表示已定义的文本段(Defined text)。与t类型符号类似,但这种类型的符号表示当前模块中定义了一个公共函数或公共静态函数。
r类型符号:r表示只读数据段(Read-only data)。这种类型的符号表示当前模块中定义了一个只读全局变量或静态变量。
R类型符号:R表示只读数据段(Read-only data)。与r类型符号类似,但这种类型的符号表示当前模块中定义了一个只读公共变量或静态变量。
__dyld_private是动态链接器(dyld)的私有符号。
dyld_stub_binder是一个占位符(stub),该占位符指向动态链接库中函数的符号。当程序运行时,动态链接器会将占位符替换为实际函数的地址,这个过程称为符号绑定(Symbol Binding),这个就是前文谈到的dylib文件和so文件的区别。dyld_stub_binder变量就是在动态链接器中实现符号绑定的函数之一。
在代码中全局变量global_var和函数test_func之前,加上了编译器修饰符__attribute__((visibility(“hidden”)))。将符号的可见性设置为"hidden",表示该符号只能在当前编译单元内部使用,不能被其他编译单元引用,所以在nm查看符号的时候global_var前是d而不是D,test_func前是t而不是T。
当然,我们也可以通过visibility控制要导出哪些符号:
"hidden":将符号的可见性设置为"hidden",表示该符号只能在当前编译单元内部使用,不能被其他编译单元引用。
"internal":将符号的可见性设置为"internal",表示该符号可以被其他编译单元引用,但是只能在当前库中使用,不能被库外的程序使用。
"protected":将符号的可见性设置为"protected",表示该符号可以被其他编译单元引用,并且可以被库外的程序使用,但是只有在动态链接库中才能访问该符号。
"default":将符号的可见性设置为"default",表示该符号可以被其他编译单元引用,并且可以被库外的程序使用。这是默认的可见性级别。
"external":将符号的可见性设置为"external",表示该符号可以被其他编译单元引用,并且可以被库外的程序使用。和"default"可见性级别相比,"external"可见性级别会将符号的可见性增强,使得符号可以被其他库中的符号引用。
需要注意的是,attribute((visibility()))修饰符只在支持可见性控制的编译器中有效,例如GCC、Clang等。在其他编译器中可能不支持该修饰符。
总结
在cmake中,虽然SHARED和MODULE都能生成动态库,但是区别还是挺大的,一定要彻底搞清楚这两者的区别吖~。于细微之处见知著,于无声处听惊雷。