深入理解计算机系统第七章知识点总结(上)+https://developer.aliyun.com/article/1416624
符号
- 全局符号
- 外部符号
- 本文件引用外部文件的变量
- 局部符号
static
是局部符号
编译器如何解析多重定义的全局符号
强弱符号的概念
- 函数和已初始化的全局变量是强符号
- 未初始化的全局变量是弱符号
处理多重定义的符号名
- 规则一:不允许有多个同名的强符号
- 规则二:如果有一个强符号与多个若符号同名,那么选择强符号
- 规则三:如果有多个弱符号同名,那么从这些弱符号中任意选择一个
对于这样一种情况:如果重复定义的符号是不同类型时,往往会破坏其他符号的内存
// foo1.cpp #include<stdio.h> void f(); int x = 15212; int y = 15213; int main() { f(); printf("x = 0x%x y = 0x%x \n", x, y); return 0; } // foo2.cpp double x; void f(){ x = -0.0; }
- 在
foo1
中x
和y
的地址是连续的,被定义被int
,占4
个字节,但是在bar
中,x
是double
类型,占8
个字节,在bar
中对x
赋值会影响y
的值
静态库与静态链接
静态库:可以将多个相关的目标模块打包成一个单独的文件,称为静态库
- 通过静态库,相关的函数可以被编译为独立的目标模块,然后封装成一个单独的静态库文件
- 静态库可以用作链接器的输入。链接器在构造可执行文件时,从静态库中复制被应用程序引用的目标模块,其他未用到的模块则不会复制
- 静态库的实例:
// main2.c #include <stdio.h> #include "vector.h" int x[2] = {1, 2}; int y[2] = {3, 4}; int z[2]; int main() { addvec(x, y, z, 2); printf("z = [%d %d]\n", z[0], z[1]); return 0; } // vector.h 其中的函数定义都在打包后的静态库中 void addvec(int *x, int *y, int *z, int n); void multvec(int *x, int *y, int *z, int n); int getcount();
构造和使用一个静态库
> gcc -c addvec.c multvec.c # 将想要打包的函数定义变成可重定位目标文件 > ar rcs libvector.a addvec.o mutvec.o # 打包成静态库 > gcc -c main2.c > gcc -static -o prog main2.o ./libvector.a # 与静态库链接(使用静态库)
- 总结:
- 静态库就是各种可重定位文件的集合
- 静态链接链接一个静态库的时候会按需链接
静态库的解析过程
- 当在命令行输入以下命令来让程序使用静态库时
gcc -static -o prog2c main2.o ./libvector.a
链接器从左到右按照他们在编译器驱动程序命令行上出现的顺序来扫描可重定位目标文件和存档文件,这会导致一些问题,最后来说。 - 在链接器的扫描过程中,链接器维护三个集合
- 可重定位目标文件的集合
E
(这个集合中的文件会被合并起来形成可执行文件) - 未解析的符号集合
U
(就是引用了但是没有定义,链接器会把他当做在其他文件定义) - 在前面的输入文件以及定义的符号集合
D
- 模拟解析过程
- 第一个扫描的文件是
main2.o
,观察源程序可知,main.o
会被放进集合E
中,U
中会增加main2.o
中调用的addvec
和printf
,D
中会增加已定义的函数main
和变量x、y、z
- 第二个扫描的文件是
libvector.a
,根据其后缀,可知其为一个静态库,此时链接器就会尝试在U
集合中寻找是否有与静态库同名的变量或函数,由于U
中有addvec
,故匹配,删除U
集合中的addvec
,将addvec.o
放入E
集合中,然后将addvec.o
中定义的符号放进符号D
中 libvector.a
中包含的所有目标文件要执行上述操作- 任何不包含在集合
E
中的成员目标文件都被简单的丢弃
- 未定义的原因
- 如果在最后一个目标文件读取完成之后,
U
集合不为空,则会产生未定义的情况。
- 该算法的缺陷
- 由于链接器是按照顺序从前往后的,故如果此指令
gcc -static -o prog2c main2.o ./libvector.a
中main
与静态库调换顺序,当扫描静态库时U
集合并没有元素,故main扫描后U
中符号无法消除就会产生未定义的情况
- 互相依赖的命令
- 当
foo.c
调用libx.a
,libx.a
调用liby.a
,然后liby.a
又调用libx.a
:gcc -static foo.c libx.a liby.a libx.a
重定位
重定位节(section
)和重定位符号定义
- 对于
main.o
和sum.o
,他们相同类型的section
会被合并为一个新的section
- 观察
main.o
和sum.o
的符号表,他们的.text section
都是从0开始的 - 在
64
位linux
系统中,ELF
可执行文件默认从地址0x400000
处开始分配,人们
重定位节中的符号引用
- 本步骤是确定那些调用外部函数的目的地址(就是指令编码后面的的字节,汇编器把他们都填充成0,由链接器来赋值)
- 链接器要依赖可重定位条目的数据结构来决定目的地址的值
- 当编译器遇到最终位置不确定的符号引用时,他就产生一个重定位条目
typedef struct{ long offset; //需要被修改的引用的节偏移(即该符号引用距离所在节的初始位置的偏移)。 long type:32, //重定位类型,不同的重定位类型会用不同的方式来修改引用 symbol:32; //指向的符号,比如sum long addend; //一个有符号常数,一些类型的重定位要使用它对被修改引用的值做偏移调整 pc相对寻址中默认是-4 绝对寻址默认是 0 }
- 关于
type
字段,只学习两种即可,
R_X86_64_PC32
:PC
相对地址R_X86_64_32
:绝对地址
- 给出重定位之后的汇编代码,解释
当执行到call
指令时,PC
为0x4004e3
,目标地址就是PC
地址加0x00000005
,这个5
就是链接器通过重定位条目所计算出来的
如何计算的?
// 对于这样两个函数 // main.c int sum(int *a, int n); int array[2] = {1, 2}; int main(){ int val = sum(array, 2); return val; } // sum.c int sum(int * a, int n){ int i, s = 0; for(i = 0; i < n; i++){ s += a[i]; } return s; } // 已经确定重定位后的.text节和sum函数的绝对地址分别为 0x4004d0 和 0x4004e8 ` // 重定位之前的汇编代码(在合成之前main.o或者sum.o的.text起始地址都是0) 000000000000<main>: 0: 48 83 ec 08 sub $0x8, %rsp 4: be 02 00 00 00 mov $0x2, %esi 9: bf 00 00 00 00 moV $0×0, %edi e: e8 00 00 00 00 callq 13 <main+13> 13: 48 83 c4 08 add $0x8, %rsp 17: c3 retq // call后的目标位置值为f,表示与所在节的初始位置的偏移值为f,所以计算 目标位置的值为 sum - addend - (main + offset) 0x4004e8 - 4 - (0x4004d0 + f) = 0x5 // 所以 e: e8 00 00 00 00 callq 13 <main+13> // 变为 e: e8 05 00 00 00 callq 4004e8 <sum>
可执行目标文件
可执行目标文件是一个二进制文件
- 可执行目标文件的格式与可重定位目标文件的格式类似
ELF
头部描述文件的总体格式,还包括程序的入口点也就是当程序运行时要执行的第一条地址.init
节定义了一个小函数,叫做_init
,程序的初始化代码会调用它
加载可执行目标文件
- 任何
Linux
程序都可以通过调用execve
函数来调用加载器。加载器将可执行文件的代码和数据从磁盘复制到内存中,然后通过跳转到程序的第一条指令或入口来运行该程序。这个将程序复制到内存并运行的过程叫做加载 - 每个
Linux
程序都有一个运行时内存映像,内存映像如图所示
- 在
Linux x86-64
系统中,代码段总是从地址0x400000
处开始后面是数据段, - 运行时堆在数据段之后,通过调用
malloc
库往上增长 - 堆后的区域是为共享模块保留的
- 用户栈总是从最大的合法用户地址(248 - 1)开始,向小内存地址增长。
- 内核就是操作系统驻留在内存的部分
- 注意:
- 为了简洁,将代码段与数据段挨在了一起,事实上,
.data
段是有对齐要求的。所以代码段和数据段之间是有间隙的。 - 同时,在分配栈、共享库和堆段运行时地址时,链接器还会使用地址空间布局随机化。但是他们的相对位置不会变
加载运行过程细节
- 当加载器运行时,在程序头部表的引导下,加载器将可执行文件的片复制到代码段和数据段。
- 然后,加载器跳转到程序的入口点(
_start
函数的地址),这个函数是在系统目标文件ctrl.o
中定义的 - start函数调用系统启动函数
_ _libc_start_main
,该函数定义在libc.so
中。 - 上一函数初始化执行环境,调用用户层的main函数,再由
_ _libc_start_main
处理main
函数的返回值,并且它在需要的时候返回给内核
共享库
是一种特殊的可重定位目标~文件
构造一个共享库
> gcc -shared -fpic -o libvector.so addvec.c mulvec.c
-fpic
:告诉编译器生成与位置无关的代码
使用共享库
> gcc -o prog main.c ./libvector.so
解释共享库工作原理
- 当创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。
- 没有任何
libvector.so
的代码和数据节真的被复制到可执行文件prog
中,-> 链接器复制了一些重定位和符号表信息,他们使得运行时可以解析对libvector.so
中代码和数据的引用 - 当可执行程序
prog
被加载运行时,加载器会发现prog
中存在一个名为.interp
的section
,这个section
包含了动态链接器的路径名,这个动态链接器本身也是一个共享目标文件 - 接下来,加载器会将这个动态链接器加载到内存中运行,然后由动态链接器执行重定位代码和数据的工作
- 重定位
libc.so
的文本和数据到某个内存段 - 重定位
libvecor.so
的文本和数据到另一个内存段 - 重定位
prog
中所有由libc.so
和libvector.so
定义的符号的引用
- 之后共享库的位置就固定了,并且在程序执行的过程中都不会改变