目录
- 一、动态连接库的原理
- 二、编译和链接
- 三、栈堆的增长方向
- 四、extern和static的区别
- 五、内存字节对齐
一、动态连接库的原理
.data 数据段;.code 代码段
全局偏移表(GOT,Global Offset Table)引入.data数据段。
查看GOT
readelf -S ./math.so
在动态链接库加载的时候GOT表项会被修改为真正的代码段地址:
动态链接库的.code代码段到GOT可以相对寻址(上图中的黄色箭头——查表过程)。结果动态链接库的.code代码段可以在内存中被所有进程共享,通过代码段的地址相对寻址GOT表项,查表后再确定其他动态库的代码段地址。
.data数据段中的GOT表在每个进程中保留一份副本。
地址无关代码(Position Independent Code):
动态链接库.so加载内存后.code代码段为所有进程共享,但.data数据段中开辟一个GOT,GOT及.data在每个进程中保留一份独立的副本。通过共享的.code相对于.data寻址查找GOT表项,从而定位(重定位)其他.code的方式。
以PIC方式进行的动态连接,动态库不需要做任何的修改,被加载到任意内存地址都能够正常运行。
对代码段函数的重定位,与其在程序一开始就对所有函数重定位,不如将这个过程推迟到函数第一次被调用的时候。
于是我们再次调用函数的时候就会直接跳转到动态库中真正的函数实现。
二、编译和链接
Linux下的目标文件是ELF(Executable and Linkable Format)格式——可执行文件的通用格式。Windows下的目标文件是PE(Portable Executable)格式。
readelf -h main.o
readelf -S main.o
objdump -s -d main.o
objdump -r main.o
其中.text就是前边的.code代码区,.data就是数据区。
三、栈堆的增长方向
四、extern和static的区别
编译的时候,头文件.h是不编译的(一般里边只有声明),编译的基本单元是.c或者.cpp文件(.c和.cpp都是编译单元)。编译预处理#include其实就是把.h中的声明拷贝到编译单元中。
- extern
默认外部连接。定义的变量可以被其他文件使用。当编译单元中使用的变量在另一个文件中定义时,为了使得编译通过需要使用extern告诉编译器变量在另外的文件中定义,并需要启用对变量的外部链接。
外部函数也类似。 - static
默认内部连接。定义的变量只能在当前文件内访问。静态函数也类似。
五、内存字节对齐
内存地址的寻址存在字节对齐,即寻址的地址值是某个数的整数倍(比如),如果不足的就补齐。
可以使用预编译指令
#pragma pack(show)
以warning的形式显示出#pragma pack(n)中的n。
内存对齐的规则:
- 对于基本数据类型,它的地址只要求是它长度的整数倍。
- 对于自定义数据类型,比如结构体,对齐规则如下
- 数组成员。第一个数组成员应该放在offset为0的地方,以后每个数组成员应该放在offset为min(当前成员的大小,#pragma pack(n)中的n)整数倍的地方开始。比如int 在32位机器上为4字节,#pragma pack(2),那么从2的倍数的地方开始存储。
- 结构体总大小(sizeof的值),必须是min(结构体内部最大成员,#pragma pack(n)中的n)的整数倍,不足要补齐。
- 如果一个结构体B中嵌套一个结构体A,还是以最大成员类型的大小对齐,但是结构体A的起点为A内部最大成员的整数倍的地方。struct B里边嵌套struct A,A里边有char,int,double,则A应该从8的整数倍开始存储。结构体A中的成员的对齐规则仍满足规则1和2。
字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
- 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;例如上面第二个结构体变量的地址空间。
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。