Colin’s Blog

A C++ Programmer

链接 Note 1

链接的笔记

本文部分信息来自网络

.bss段不占用存储空间

为什么要有.bss段?因为已经初始化的全局变量,需要在目标文件里面占用空间来存储它们被初始化的值。例如int a=3,b=2;我们需要存下3和2. 但是如果是没有初始化的全局变量,只需要记录一下长度就会好了。例如两个int,记录一些.bss段的长度为8字节即可。

那么,这个长度总要占地方的吧?

.bss段占据的大小存放在ELF文件格式中的段表(Section Table)中,段表存放了各个段的各种信息,比如段的名字、段的类型、段在elf文件中的偏移、段的大小等信息。同时符号存放在符号表.symtab中。

.bss不占据实际的磁盘空间,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化。

程序为什么要分成数据段和代码段

数据和指令被映射到两个虚拟内存区域,数据段对进程来说可读写,代码段是只读,这样可以防止程序的指令被有意无意的改写。

有利于提高程序局部性,现代CPU缓存一般被设计成数据缓存和指令缓存分离,分开对CPU缓存命中率有好处。

代码段是可以共享的,数据段是私有的,当运行多个程序的副本时,只需要保存一份代码段部分。

链接器通过什么进行的链接

链接的接口是符号,在链接中,将函数和变量统称为符号,函数名和变量名统称为符号名。链接过程的本质就是把多个不同的目标文件之间相互“粘”到一起,像玩具积木一样各有凹凸部分,有固定的规则可以拼成一个整体。

可以将符号看作是链接中的粘合剂,整个链接过程基于符号才可以正确完成,符号有很多类型,主要有局部符号和外部符号,局部符号只在编译单元内部可见,对于链接过程没有作用,在目标文件中引用的全局符号,却没有在本目标文件中被定义的叫做外部符号,以及定义在本目标文件中的可以被其它目标文件引用的全局符号,在链接过程中发挥重要作用。

为什么需要extern “C”

C语言函数和变量的符号名基本就是函数名字变量名字,不同模块如果有相同的函数或变量名字就会产生符号冲突无法链接成功的问题,所以C++引入了命名空间来解决这种符号冲突问题。同时为了支持函数重载C++也会根据函数名字以及命名空间以及参数类型生成特殊的符号名称。

由于C语言和C++的符号修饰方式不同,C语言和C++的目标文件在链接时可能会报错说找不到符号,所以为了C++和C兼容,引入了extern “C”,当引用某个C语言的函数时加extern “C"告诉编译器对此函数使用C语言的方式来链接,如果C++的函数用extern “C"声明,则此函数的符号就是按C语言方式生成的。

以memset函数举例,C语言中以C语言方式来链接,但是在C++中以C++方式来链接就会找不到这个memset的符号,所以需要使用extern “C"方式来声明这个函数,为了兼容C和C++,可以使用宏来判断,用条件宏判断当前是不是C++代码,如果是C++代码则extern “C”。

1#ifdef __cplusplus
2extern "C" {
3#endif
4
5void *memset(void *, int, size_t);
6
7#ifdef __cplusplus
8}
9#endif

强符号和弱符号

我们经常编程中遇到的multiple definition of ‘xxx’,指的是多个目标中有相同名字的全局符号的定义,产生了冲突,这种符号的定义指的是强符号。有强符号自然就有弱符号,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。attribute((weak))可以定义弱符号。

1extern int ext;
2
3int weak; // 弱符号
4int strong = 1; // 强符号
5__attribute__((weak)) int weak2 = 2; // 弱符号
6
7int main() {
8    return 0;
9}

链接器规则:

不允许强符号被多次定义,多次定义就会multiple definition of ‘xxx’ 一个符号在一个目标文件中是强符号,在其它目标文件中是弱符号,选择强符号 一个符号在所有目标文件中都是弱符号,选择占用空间最大的符号,int类型和double类型选double类型

强引用和弱引用

一般引用了某个函数符号,而这个函数在任何地方都没有被定义,则会报错error: undefined reference to ‘xxx’,这种符号引用称为强引用。与此对应的则有弱引用,链接器对强引用弱引用的处理过程几乎一样,只是对于未定义的弱引用,链接器不会报错,而是默认其是一个特殊的值。

 1#include <cstdio>
 2
 3__attribute__((weak)) void foo();
 4
 5int main()
 6{
 7    printf("%d\n", &foo); // 0
 8    foo();
 9    return 0;
10}

这里foo的地址是0

则可以改为

1__attribute__ ((weak)) void foo();
2
3int main() {
4    if (foo) {
5        foo();
6    }
7    return 0;
8}

这种强引用弱引用对于库来说十分有用,库中的弱引用可以被用户定义的强引用所覆盖,这样程序就可以使用自定义版本的库函数,可以将引用定义为弱引用,如果去掉了某个功能,也可以正常连接接,想增加相应功能还可以直接增加强引用,方便程序的裁剪和组合。

例如再加一个文件

1void foo()
2{
3    printf("foo2\n");
4}

这个时候就可以g++ 1.cpp 2.cpp从而得到一个可以输出foo2的可执行文件。

弱符号的出现主要是为了解决宏条件编译问题的,宏条件编译对于长期维护的代码是个灾难。

程序首先执行的代码并不是main开始的代码,也就是程序的真正入口不是main函数,而是运行库的入口函数;运行库会 先把main函数需要的参数、环境变量等准备好;然后将标准输入输出文件描述符打开,这样才能保证main函数开始就可以使用printf;然后把堆初始化,这样才能保证程序可以自由地执行malloc、new等;还有就是把全局变量初始化、全局构造函数执行完;做完这么多工作之后运行库就执行回调函数main,这时候程序才开始进入人们常说的函数入口。程序的运行环境组成是:程序本身逻辑代码、运行库、系统内核、内存;内存分为用户空间和内核空间,只要内核才使用内核空间,其它的包括运行库都是使用用户空间;程序运行空间分为栈空间和堆空间,函数运行的环境就是栈空间,栈空间都是有固定大小的,一般是2M,地址增加方向是向低地址扩张;堆空间比较大,使用也很灵活,这个堆空间一般都是运行库在帮你管理,堆分配算中中最简单的就是空闲链表算法;程序运行完之后,运行库还要帮你把所有的后事处理掉,释放堆空间、关闭所有打开的文件描述符、释放所有的进程资源等,这就是进程关闭内存泄漏的那些空间能够得到回收的原因;从程序的整个过程可以看书,main函数只不过是运行库的一个回调函数,不是真正的函数入口,当然我们也可以自己写一个运行库,这样就可以直接运行在系统内核之上了,运行库主要部分就是标准c接口的实现,听上去并不复杂。

动态和静态的比较

动态链接库有两个设计目标,也是它的优势:

动态链接。例如MySQL支持MyISAM和InodeDB等不同的存储引擎,你也可以为它添加新的存储引擎,那么这个存储引擎就只能编译成动态链接库的形式。因为MySQL释出(release)的时候,还不知道有会有谁为它写新的存储引擎,所以也不知道应该链接那个静态库。当然,MySQL有源码,你可以重新编译一下,把你的新存储引擎链接进去,但是像Oracle这种没有源码的程序,就只能用动态链接库解决这个问题了。

共享,节约内存。例如libc.so提供了基础的C语言函数和系统调用接口,每个应用程序都会用到,如果每个进程都用静态库,那么printf这个函数的代码在内存中会有许多份,每个进程都有一个副本。使用动态链接库就可以避免这个问题,同一个动态链接库在多个不同进程之间的代码是共享的,不会占用多余的内存空间。数据不是共享的,每个进程都有自己的独有数据。

缺点1、当系统中多个应用程序都用了一个动态链接库,但是要求的版本不同,这时动态链接库之间就会相互干扰。2、性能开销。动态链接库为了做到“共享代码,但是不共享数据”,引入了不小的开销,调用动态链接库中的函数,需要好几次间接内存访问才能走到函数入口,全局数据也是。

静态链接的优缺点:

优点:代码装载速度快,执行速度略比动态链接库快;只需保证在开发者的计算机中有正确的.lib文件,在以二进制形式发布程序时不需考虑在用户的计算机上.lib文件是否存在及版本问题。

缺点:

使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费。

静态库对程序的更新、部署和发布会带来麻烦。如果静态库liba.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。

动态链接的优缺点:

优点:生成的可执行文件较静态链接生成的可执行文件小;适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试;不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;

缺点:使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息;速度比静态链接慢;

为什么要进行动态链接?为了解决静态链接浪费空间和更新困难的缺点。

动态链接的方式?装载时重定位和地址无关代码技术。

地址无关代码技术原理?通过GOT段实现间接跳转。

延迟加载技术原理?对外部函数符号通过PLT段实现延迟绑定及间接跳转。

如何进行显式运行时链接?通过<dlfcn.h>头文件中的四个函数。