计算机系统基础-学习记录11

链接(续)

  上回提到,链接器linker主要做的有两件事:符号解析和重定位。其中符号解析已经详细提及,接下来是重定位

重定位

  重定位:重新赋予运行的地址

  重定位的目标:将多个重定位文件融合在一起(代码与代码,数据与数据),给一个融合后的运行地址的值

  两步操作:
  1、对节和符号进行重新定位
  2、重定位节中的符号引用

  在重定位后进行融合,以生成可执行目标文件。融合结束后,每一个引用的位置都发生了变化。因此,此时应该找到新的地方的地址。由汇编器产生重定位的索引

重定位条目

  ELF重定位条目的格式如下:

typedef struct {
    long offset; /* Offset of the reference to relocate */
    long type:32, /* Relocation type */
    	symbol:32; /* Symbol table index */
    long addend; /* Constant part of relocation expression */
} Elf64_Rela;

  其中:
  offset:偏移地址。在这个节里面的偏移地址
  type:类型。采用什么类型进行重定位(?)。在ELF的定义中,重定位类型有32种,在这里只关注其中两种:一种是R_X86_64_PC32,表示重定位一个使用32位PC相对地址的引用;而另一种则是R_X86_64_32,表示重定位一个使用32位绝对地址的引用。
  symbol:符号。与符号表中的符号对应
  addend:常量

  对于下述存放在两个程序文件中的函数:

// 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;
}

  在一开始的时候,对main.c进行编译,得到main.o文件,反汇编后得到:

0000000000000000 <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 $0x0,%edi # %edi = &array
    				a: R_X86_64_32 array # Relocation entry
    e: e8 00 00 00 00 callq 13 <main+0x13> # sum()
    				f: R_X86_64_PC32 sum-0x4 # Relocation entry
    13: 48 83 c4 08 add $0x8,%rsp
    17: c3 retq

  注意到在引用全局符号array和sum的后一行,都单独加了一行看不懂是啥意思的内容。这是重定位条目,这些重定位条目告诉链接器对sum 的引用要使用32位PC 相对地址进行重定位,而对array 的引用要使用32位绝对地址进行重定位。现以此为例,指出这两种重定位类型的细节:

重定位PC相对引用

  在上例中,对sum是进行了重定位PC相对引用的,相应的重定位条目r由4个字段组成:

r.offset = 0xf
r.symbol = sum
r.type = R_X86_64_PC32
r.addend = -4

  这4个字段将会告诉链接器,让链接器修改开始于偏移量0xf处的32位PC相对引用,从而指向sum。假设此时链接器已经确定了:Addr(s)=Addr(.text)=0x4004d0,Addr(r.symbol)=Addr(sum)=0x4004e8,则链接器会先计算出引用的运行时地址:refaddr=Addr(s)+r.offset=0x4004df,然后更新该引用,使其在运行时指向sum程序:*refptr=(unsigned)(Addr(r.symbol)+r.addend-refaddr)=(unsigned)(0x5)

  因此,原来的main对应汇编的call指令:

e: e8 00 00 00 00 callq 13 <main+0x13> # sum()

  就变成了:

4004de: e8 05 00 00 00 callq 4004e8 <sum> # sum()

重定位绝对引用

  对于例中的array而言,对应的占位符条目r包括4个字段:

r.offset = 0xa
r.symbol = array
r.type = R_X86_64_32
r.addend = 0

  这4个字段告诉链接器要修改从偏移量0xa开始的绝对引用,使得在运行时指向array的第一个字节。假定链接器已经确定:Addr(r.symbol)=Addr(array)=0x601018。则链接器修改引用:*refptr=(unsigned)(Addr(r.symbol)+r.addend)=(unsigned)(0x601018)

  因此,原来的汇编指令:

9: bf 00 00 00 00 mov $0x0,%edi # %edi = &array

  变更为:

4004d9: bf 18 10 60 00 mov $0x601018,%edi # %edi = &array

  将上述两种重定位放在一起,就得到了最终的汇编:

00000000004004d0 <main>:
    4004d0: 48 83 ec 08 sub $0x8,%rsp
    4004d4: be 02 00 00 00 mov $0x2,%esi
    4004d9: bf 18 10 60 00 mov $0x601018,%edi # %edi = &array
    4004de: e8 05 00 00 00 callq 4004e8 <sum> # sum()
    4004e3: 48 83 c4 08 add $0x8,%rsp
    4004e7: c3 retq
00000000004004e8 <sum>:
    4004e8: b8 00 00 00 00 mov $0x0,%eax
    4004ed: ba 00 00 00 00 mov $0x0,%edx
    4004f2: eb 09 jmp 4004fd <sum+0x15>
    4004f4: 48 63 ca movslq %edx,%rcx
    4004f7: 03 04 8f add (%rdi,%rcx,4),%eax
    4004fa: 83 c2 01 add $0x1,%edx
    4004fd: 39 f2 cmp %esi,%edx
    4004ff: 7c f3 jl 4004f4 <sum+0xc>
    400501: f3 c3 repz retq

使用静态库

  静态库是编译系统将所有相关的目标模块打包成为的一个单独的文件,在Linux系统中以.a后缀出现,而在Windows系统则是以.lib后缀出现

与静态库连接

  在gcc中,要创建几个函数的一个静态库,需要用到AR工具。例如,现在要在Linux系统下打包a.c和b.c这两个模块,则可以在命令行输入:

gcc -c a.c b.c
ar rcs libvector.a a.o b.o

  这样就可以生成静态库libvector.a。随后,对于需要调用这个静态库的main.c文件:

gcc -c main.c
gcc -static -o prog2c main.o -L -lvector

  即可完成与静态库libvector.a的连接(不是很懂为啥可以拿-lvector作为libvector.a的缩写)

链接器如何使用静态库解析引用

  链接器维护三个集合E、U、D:
  E:可重定位目标文件的集合,这个集合中的文件会被合并起来形成可执行文件
  U:未解析符号集合
  D:已经找到定义的符号集合

  下面这段话是直接从书上复制上来的(课件上写的不太一样,当场做的笔记记的又不全,而官网上的课件暂时也没找到这个,只能先复制书上的说法了,泪目):

  对于命令行上的每个输入文件f, 链接器会判断f是一个目标文件还是一个存档文件:
  -如果f是一个目标文件,那么链接器把f添加到E, 修改U和D来反映f中的符号定义和引用,并继续下一个输入文件
  -如果f是一个存档文件,那么链接器就尝试匹配U中未解析的符号和由存档文件成员定义的符号。如果某个存档文件成员m, 定义了一个符号来解析U中的一个引用,那么就将m加到E中,并且链接器修改U和D来反映m中的符号定义和引用。对存档文件中所有的成员目标文件都依次进行这个过程,直到U和D都不再发生变化。此时,任何不包含在E中的成员目标文件都简单地被丢弃,而链接器将继续处理下一个输入文件
  如果当链接器完成对命令行上输入文件的扫描后, U是非空的,那么链接器就会输出一个错误并终止。否则,它会合并和重定位E中的目标文件,构建输出的可执行文件

  关于连接静态库的一般准则,是把它们放在命令行的结尾。例如,当foo.c调用libx.a和libz.a中的函数,而这两个库又调用liby.a中的函数时,就可以使用下述指令:

gcc foo.c libx.a libz.a liby.a

  注意到liby.a被放在了最后,被调用者始终在调用者的后面

  如果有相互调用的关系,则库应在命令行上重复出现。例如,foo.c调用libx.a,libx.a调用liby.a,而liby.a又调用libx.a,这时就可以:

gcc foo.c libx.a liby.a libx.a
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值