什么是静态链接

案例探索

a.c

extern int shared;

int main()
{
    int a = 100;
    swap(&a, &shared);
}

b.c

int shared = 1;

void swap(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}
gcc -c a.c b.c

我们得到了a.ob.o文件。

  • b.c总共定义了两个全局符号,变量shared,函数swap
  • a.c总共定义了一个全局符号,函数main
  • 模块a.c引用了b.c里面的swapshared

我们需要把a.ob.o两个目标文件链接在一起并最终形成一个可执行文件

空间与地址分配

在有很多输入文件情况下,输出文件将会有很多零散的段。这会浪费空间,毕竟这设计到需要空间对齐的要求。那么必然会有空余出来的,用于凑整的空间被浪费掉。

可执行文件中的代码段和数据段都是由输入文件中合并而来的,那么链接器是如何将它们的各个段合并到输出文件中的?

一个更实际的方法是将相同性质的段合并到一起。链接器将相似的段合并到一起,比如将输入文件所有的.text段合并到输出文件的.text段。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X4LHYQ7h-1650725766092)(https://syz-picture.oss-cn-shenzhen.aliyuncs.com/D:%5CPrograme%20Files(x86)]%5CPicGoimage-20220131160006444.png)

之前提到,.bss段在目标文件和可执行文件中并不占用文件空间,但是它在装载时占用地址空间(进程的虚拟地址)。所以链接器在合并各个段时,也将.bss段合并,并分配虚拟空间。

链接器为目标文件分配地址和空间,这里地址和空间有两个含义

  • 在输出中的可执行文件的空间
  • 在装载后的虚拟地址中的虚拟地址空间(主要关注这个)

一般链接过程分两步

  • 第一步:空间与地址分配
    • 扫描所有输入目标文件,获得各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步,链接器可以获得所有输入目标文件中的段长度,并将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系
  • 第二步:符号解析与重定位(核心)
    • 使用上面第一步中收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址等。

使用objdump查看链接前后地址的分配(VMA

objdump -h a.o
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000051  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  00000091  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000091  2**0
objdump -h b.o
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004b  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000090  2**0
                  ALLOC

我们发现在未进行链接时,a.ob.o文件的虚拟地址空间都为0,因为虚拟地址空间还未被分配,所以都默认为0。而且分配的时候,默认是从0x08048000开始分配的,给0也代表这个值不可用。

objdump -h ab
Idx Name          Size      VMA               LMA               File off  Algn
  0 .interp       0000001c  0000000000000238  0000000000000238  00000238  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  1 .note.ABI-tag 00000020  0000000000000254  0000000000000254  00000254  2**2
 ...
 13 .text         00000222  0000000000000560  0000000000000560  00000560  2**4

链接后,VMA开始有值,说明各个段都被分配了相应的虚拟地址空间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uT01S4vN-1650725766093)(https://syz-picture.oss-cn-shenzhen.aliyuncs.com/D:%5CPrograme%20Files(x86)]%5CPicGoimage-20220131161551546.png)

符号地址的确定

在链接器第一步扫描和空间分配阶段,输入文件中的各个段在链接后的虚拟地址就已经确定了。比如.text段的起始地址为0x8048094

当前一步完成后,链接器开始计算各个符号的虚拟地址。比如a.omain函数相对于a.o的代码段的偏移为X,则经链接合并以后,a.o的代码段位于虚拟地址0x8048094,那么main的地址为0x8048094+ X

符号解析与重定位

完成空间和地址的分配后,链接器就进入了符号解析与重定位步骤。

编译器在将a.c编译成指令时,它是如何访问shared变量如何调用swap函数的?

objdump -d a.o

a.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   48 83 ec 10             sub    $0x10,%rsp
   8:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
   f:   00 00 
  11:   48 89 45 f8             mov    %rax,-0x8(%rbp)
  15:   31 c0                   xor    %eax,%eax
  17:   c7 45 f4 64 00 00 00    movl   $0x64,-0xc(%rbp)
  1e:   48 8d 45 f4             lea    -0xc(%rbp),%rax
  22:   48 8d 35 00 00 00 00    lea    0x0(%rip),%rsi        # 29 <main+0x29>
  29:   48 89 c7                mov    %rax,%rdi
  2c:   b8 00 00 00 00          mov    $0x0,%eax
  31:   e8 00 00 00 00          callq  36 <main+0x36>
  36:   b8 00 00 00 00          mov    $0x0,%eax
  3b:   48 8b 55 f8             mov    -0x8(%rbp),%rdx
  3f:   64 48 33 14 25 28 00    xor    %fs:0x28,%rdx
  46:   00 00 
  48:   74 05                   je     4f <main+0x4f>
  4a:   e8 00 00 00 00          callq  4f <main+0x4f>
  4f:   c9                      leaveq 
  50:   c3                      retq   

当源代码a.c在被编译成目标文件是,编译器并不知道sharedswap的地址,因为他们定义在b.c中,所以编译器暂时将他们的地址看作0

而经历链接后,地址已经被确认

重定位表

那么链接器是怎么知道哪些指令要被调整呢?

之前在ELF文件中,有一个叫做重定位表的存在,这个结构是专门保存这些与重定位相关的信息。每个要被重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是ELF文件中的一个段。

使用objdump命令查看重定位表

objdump -r a.o
a.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE              VALUE 
0000000000000025 R_X86_64_PC32     shared-0x0000000000000004
0000000000000032 R_X86_64_PLT32    swap-0x0000000000000004
  • 这个命令可以查看所有要被重定位的地方,每个被重定位的地方叫一个重定位入口。
  • RELOCATION RECORDS FOR [.text]:代表.text段的重定位表
  • 我们看到有两个重定位入口,重定位入口的偏移表示该入口在要被重定位的段中的位置。(正好对应上了反汇编指令中的mov指令和call指令)

符号解析

重定位的过程中,每个重定位的入口就是对一个符号的引用,那么当链接器要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址。这时候链接器就会去查找由所有目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。

readelf -s a.o
9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND shared
11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
readelf -s ab
62: 00000000000006bb    75 FUNC    GLOBAL DEFAULT   14 swap
65: 0000000000201010     4 OBJECT  GLOBAL DEFAULT   23 shared

可以看到,经过链接过后,UND改变,说明在全局符号表中找到了。

COMMON块

如果一个弱符号定义存在于多个文件中,且它们的类型又不同,那么链接器如何选择

链接器遇到的三种情况

  • 两个或两个以上强符号类型不一致(无需处理,链接器会报符号多重定义错误)
  • 有一个强符号,其他都是弱符号,出现类型不一致
  • 两个或两个以上弱符号类型不一致

现在编译器和链接器都支持一种叫做COMMON块的机制。在早期Fortran没有动态分配空间的机制的时候,程序员必须实现声明他所需要的临时使用空间的大小,Fortran把这种空间叫COMMON块,当不同的目标文件需要的COMMON块大小不一致时,选择最大的那一块。

而现代链接器处理不同类型的弱符号就是采用COMMON块的处理方法,选择占据空间最大的那一个

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值