高级C/C++编译技术之读书笔记(三)之动态库设计

                                                                                    

  最近有幸阅读了《高级C/C++编译技术》深受启发,该书深刻浅出地讲解了构建过程(编译、连接)中的各类细节,从多个角度展现了程序与库文件或代码的集成方法,提出了面向代码复用和系统集成的软件架构设计方法,以及系统开发过程当中疑难问题的解决方案。
  如下将回头记录下其中的关键要点,以便后面查阅。

本节思惟导图

1. 关于-fPIC编译器选项

1.1 -fPIC表明什么

  “PIC”是位置无关代码(Position-independent Code)的缩写,说到位置无关代码,咱们会立马想到加载重定位,加载重定位将动态库加载到进程内存空间中,可是只有第一次加载这个动态库的进程可使用它,当其它进程须要加载同一动态库的时候,除了将动态库的完整副本加载到自身内存空间之外,别无他法,当更多的进程须要加载某一特定的动态库时,内存中会存在更多的副本。这种限制的根本缘由在于加载过程设计的缺陷,在将动态库加载到进程中以前,装载器须要修改动态库的代码段,使得在加载该库的进程中,动态库的全部符号是有意义的,即使这种方法能够知足基本的运行需求,但其致使的最终结果是因为动态库代码的修改是不可逆的,所以其它进程难以直接复用这个已加载的动态库。linux

  为了解决加载重定位的缺陷,从新设计加载机制,避免将加载的动态库代码段绑定到第一个加载该动态库的进程中,提出了PIC机制,使得多个进程能够无缝映射到已加载动态库的内存映射中windows

1.2 必定要使用-fPIC编译器选项来建立吗?

  在32位体系架构中,咱们不须要使用-fPIC编译器选项,若是没有指定该选项,编译出来的动态库就会遵循旧式的装载时重定位机制架构

  在64位体系结构中,简单地忽略-fPIC编译器选项就会致使连接错误,要修正连接错误,一种方法是向编译器传递-fPIC选项,另外一种方法是向编译器传递-mcmodel=large选项函数

1.3 只有在编译动态库时才会使用-fPIC编译选项吗?可否在静态连接库的状况下使用?

  在32位体系结构中,编译静态库时是否使用-fPIC选项是无所谓的,这样会对编译生成的代码结构产生必定的影响,可是对于静态库的连接和运行时行为的影响是微乎其微的spa

  在64为体系结构中,状况会变得更加有意思:架构设计

  (1)若是静态库是连接到可执行文件中,那么编译时能够指定也能够不指定,设计

  (2)若是静态库连接到的是动态库,那么必须使用-fPIC选项编译调试

2. C++引发的连接问题

2.1 C++使用了更加复杂的符号命名规则

  为了惟一地标识函数,链接器在为函数入口点创建符号的时候,必须使用某种方法来包含函数的从属信息和输入参数信息,连接器的设计为了知足这种更加复杂的需求,最终产生了“名称修饰”这种技术。名称修饰是将函数名、函数的从属信息、函数的参数列表进行组合,最后生成符号名称的过程。code

  为了统一,当咱们但愿避免名称修饰时,必须使用一个特殊的关键字来告知链接器不要修饰符号名称对象

#ifdef __cplusplus
extern "C"
{
#endif // __cplusplus
void fun1(void);
void fun2(void);
void fun3(void);
void printMessage(void);

#ifdef __cplusplus
}
#endif // __cplusplus

2.2 静态初始化顺序问题

  C语言中的一项遗留特性:连接器能够处理很简单的初始化变量,不管是简单数据类型仍是结构体,链接器只须要在数据段中保留存储空间,并将初始值写入该位置便可,在C语言领域,变量初始化的顺序一般不是很重要,关键在于变量的初始化其实在程序启动时候就完成了。

  可是在C++中,数据类型每每是一个对象,对象的初始化是在运行时经过对象构造函数来完成的,为了初始化C++对象,链接器须要作更多的工做,为了帮助链接器完成其任务,编译器将特定文件须要使用的全部构造器的列表嵌入到目标文件中,并将相关信息存放在特定的目标文件段中,在链接时,链接器会检查全部的目标文件,并将其中的构造函数列表合并成完整的列表,以备运行时执行,说了这么多,总的一句话是C++对象的初始化,加剧了编译器和链接器的负担,因为链接器依然不够智能,在大多数状况下,程序在加载时会引发很是严重的崩溃,并且是在任何调试器可以捕捉到以前

  发生这种状况是由于初始化的对象依赖于另一些须要在器以前初始化的对象,而且没有任何规则能够指定静态对象的初始化顺序,咱们将这类问题一般称为静态初始化顺序问题。

解决方案:

(1)为_init()函数提供自定义实现,这是一个在动态库加载时会被当即调用的标准函数,在该函数中能够经过静态成员函数初始化对象,以经过构造函数强制初始化,所以,也能够为_fini()函数提供自定义实现

(2)调用一个自定义函数去访问特定对象,而不是直接访问该函数会包含C++类的一个静态实例,并返回其引用

2.3 模版

这涉及到编译器的设计问题:

(1)编译器能够保证生成全部的模版特殊化代码,并为每一个特殊化版本建立一个弱符号

(2)链接器在连接结束以前都不包含模版特殊化的机器码是想爱你,但其他全部的连接任务都完成后,链接器会检查代码,肯定到底须要哪些特殊化版本,并调用C++编译器建立所需的模版特殊化,最后,将机器码插入可执行文件中

3. 控制动态库符号的可见性

  在Linux中全部动态库链接器符号默认都是外部可见的,任未尝试连接这些动态库的用户均可以访问这些符号

  在windows中,DLL连接符号默认都是外部不可见的

3.1 导出linux动态库符号

(1)方法一

  经过向编译器传递编译选项-fvisibility=hidden就能够将全部的动态库符号置为对外不可见,默承认见

(2)方法二

  __attibute__((visibility("<default | hidden>")))

  经过在函数前面使用编译器属性修饰,能够指示连接器容许或禁止对外部提供该符号

(3)方法三

  #pragma visibility push(hidden)

  #pragma visibility pop

3.2 导出windows动态连接库符号

  __descspec(dllexport)

4. 动态库连接模式

  (1)加载时动态连接

  (2)运行时动态连接

目的 Linux版本 Windows版本
加载库 dlopen() LoadLibrary()
查找符号 dlsym() GetProcAddress()
卸载库 dlclose() FreeLibrary()
错误报告 dlerror() GetLastError()

示例伪代码:

handle = do_load_library("<library path>", optional_flags);
if(NULL == handle)
{
    report_error();  
}

pRunction = (function_type)do_find_library_symbol(handle);
if(NULL == pFunction)
{
    report_error();
    unload_libray();
    handle = NULL;
    return;
}

pFunction(function arguments);

do_unload_library(handle);
handle = NULL;