回顾 链接操作的步骤
1.确定符号的引用关系 (符号解析)
2.合并.o文件(同节合并)
3.确定每个符号的地址(确定地址)
4.在指令中填入新地址 (2.3.4属于重定位)(修改引用)
符号解析(符号绑定)
说白了就是把每个模块引用的符号与某个目标模块中的定义符号建立关联
-程序中有定义和引用的符号
1 | void swap(){} /*定义符号swap*/ |
-编辑器把定义的符号放在一个符号表当中
1.符号表是一个结构数组,在.symtab节中
2.每个表项包含符号名,长度与位置信息
-链接器把符号的引用存放在重定位节中(.rel.text与rel.data)
-链接器把每个符号的引用都与一个确定的符号定义建立关联
重定位
1.把多个代码段与数据段合并成一个单独的代码段和数据段
2.计算每个定义的符号在虚拟地址空间中的绝对地址
3.把可执行文件中符号引用处的地址修改为重定位后的地址信息
(局部变量temp分配在栈中,不会在过程外被引用,因此不属于符号定义)
链接符号的类型
1.Global Symbols (模块内部定义的全局符号)
eg: 全局变量 指在本模块定义,但是在外部模块可以使用的
2.External Symbols (外部定义的外部符号)
eg: 函数原型的函数名,函数中的带extern的变量名 外部模块定义,在本模块可以使用
3.Local Symbols (本模块定义并且引用的局部符号)
eg: 带static的变量名 分配在静态数据区,并不是局部变量!局部变量是分配在栈中的
本模块定义,只能在本模块使用
类型举例:
1 | int buf[2] = {1,2}; |
1 | extern int buf[]; |
全局符号: main.c中的buf,swap.c中的swap(),bufp0;
外部符号 swap.c中的buf[],main.c中的swap();
局部符号 swap.c中的局部符号
我们说符号的定义,实质上就是分配了存储空间。
函数名符号指代码所在区
变量名指静态数据区
定义符号的值就是目标所在的首地址
全局符号的强,弱
函数名以及初始化后的变量是强全局符号
未初始化变量是弱全局符号
上面函数 main.c 的swap(),swap.c的 buf[],是弱符号,其余的全局符号都是强符号
多重定义符号的解析规则
1.强符号不能多次定义
2.一个符号被定义为一次强符号和多次弱符号,则按强定义为准
3.多个弱符号定义,则任选其中一个
例子
FLD1指把1.0放在ST(0)处,ST(0)是x87浮点处理器的一个栈
FSTPL &d L指double对应的双精度,存放8个字节
相当于d变为浮点数表示之后,把x所在的位置给冲掉了,导致x会被改变
(注意小端方式,意味着位数小的放在地址小的位置)
静态连接
多个可重定位目标模块 + 静态库(标准库,自定义库等)
(.o文件) + (.a文件,其中包含多个.o模块)
静态库(.a archive files)
1.把所有相关的目标模块(.o)打包为一个单独的库文件(.a),称为静态库文件,也成为存档文件
2.在构建可执行文件的时候,只需要指定库文件名,连接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来,链接到可执行文件中。
eg: 在函数中使用printf函数,程序员不需要在文件中包括printf的代码,链接器会自动在静态库查找定义的符号来解析printf符号,及实现直接调用。
静态库的创建
ar 能把指定的.o文件打包生成静态库文件(.a文件)
eg: ar rs libc.a atoi.o printf.o random.o
自定义库文件
gcc -c p1.c p2.c
ar rcs mylib.a p1.o p2.o
然后如果你写了一个main.c,想要调用静态库中的函数
gcc -c main.c
gcc -static -o myproc main.o ./mylib.a
使用静态库的过程
-按照命令行给出的顺序扫描.o,.a文件
-扫描期间把当前未解析的引用记录到一个列表U中
-每遇到一个新的.o与.a模块,试图用其来解析UI中的符号
-扫描到最后,如果U不为空,则发生错误
总结出来的技巧: 把静态库都放到后面去
共享库
但如果我们只是想用库中的某一函数,却仍然要把所有内容都链接进去,会造成文件中静态库的大量重复。
动态链接可以在首次载入的时候就执行,由ld-linux.so完成,这样所有的程序可以共享同一个库,而不用进行封装。
符号重定位之后,得到两个文件:将要合并的*.o文件的集合以及定义符号的集合
重定位
第一步 合并相同的节
把集合E中的所有目标模块相同的节合并成新节
例如把所有.text节合并作为可执行文件中的.text节
第二步 对集合D中的定义符号进行重定位(确定地址)
即确定新节中所有定义符号在虚拟地址空间中的地址
例如为函数确定首地址,进而确定每条指令的地址,为变量确定首地址
完成这一步之后,每条指令和每个变量的地址都可以确定
第三步 对引用符号进行重定位
修改.text和.data节中对每个符号的引用(地址)
需要用到.o文件中的.rel_data 和.rel_text中保存的重定位信息
什么是重定位信息?
例如 把add B 这一句翻译为机器代码,add翻译为操作码05 B为一个引用,一开始不知道是什么数值,于是写入一个00000000初始位置,同时生成一个重定位条目,提示后面这个位置的值要被修改。
又例如jmp 后面接的是L0,则在.rel_text节中生成重定位信息,但一开始还没走到L0指令,还不知道L0的定义是什么,于是填了一个随机地址FCFFFFFF,在后续当中修改这个地址,让他指向真正的L0地址
左下角 offset 0x1 代表从第一个字节开始重定位(下方框是.text节,起始第0个字节为05),两个数字(一共八位)一个字节,那么02就是第五个字节,FC就是第六个字节。
左下角symbol 表示重定位的符号为B
左下角 type 重定位类型为绝对地址
重定位操作举例
E中有main.o 和 swap.o两个模块! D中有所有定义的符号!
在main.o与swap.o的重定位条目中有重定位信息,反映了符号引用位置(offset),绑定的定义符号名,重定位类型)
读取重定位条目: readelf -r main.o
PC相对地址重定位
0: 55 表示main从.text最初的地方开始
3: and 指令的 0xfffffff0 是为了让栈顶指针esp为16字节的倍数
swap函数调用: 6: e8 fc ff ff ff 注意x86是小端法存储,因此
因此立即数为ff ff ff fc 即-4
由于 fc ff ff ff 不是swap函数的首地址,所以会生成一个重定位信息1
7:R_386_PC32 SWAP
表示在偏移量为7的位置,即fc开始的位置,按相对地址方式进行重定位ie
数据的存储方式:
小端方式,第一个数据为 00 00 00 01
大端方式,第二个数据为 00 00 00 02
相对地址的重定位方式
假定:
main占 0x12B,因此swap初始地址为
-0x8048380+0x12=0x8048392
但由于不是4的倍数,因此在四字节边界对齐的情况下,应该为0x8048394
call 指令最后的机器代码应该为多少?
call指令运行机理:把call指令的下一条指令压栈,然后跳转到swap函数的首地址
转移目标地址 = PC+偏移地址 PC=0x8040380+0x07-init
注意当执行到某一条指令的时候,PC为该条指令下一条指令的地址噢,所以PC
应该为0x08048380+b = 0x0804838b
重定位值就是偏移地址
所以重定位值 = 转移目标地址 - PC = 0X9
CALL 机器代码就是如上图结果所示
绝对地址重定位
绝对地址重定位意思就是把buf的地址直接填到00 00 00 00
这两个段都从可执行文件装入,占了4kb
具体过程
buf在运行时候的存储地址ADDR(buf)=0x8049620
重定位之后,bufp0地址及内容变成什么? bufp0紧接在buf后,故地址为0x8049620+8 = 0x8049628
符号重定位
重定位结果:
09 00 00 00 含义是当前PC(0x804838b加上一个立即数偏移量9)得到要跳转到的目标地址
为什么是不是从8048392开始呢?因为函数地址要求4边界对齐
可执行文件的存储器映像
左边是存储在物理磁盘当中的,而右边是存储在内存当中的