程序运行之静态链接一

前面介绍了单个.o文件的格式以及里面的内容。那么如果我们有多个目标文件,如何将它们链接成一个可执行的文件呢。多个目标文件就涉及到了链接。我们首先介绍静态链接。

有如下2个文件,test.c/test1.c

test.c

#include<stdlib.h>

#include<stdio.h>

extern int shared;

int main(){

int a=100;

swap(&a,&shared);

}

test1.c

#include <stdio.h>  

int shared=1;

void swap(int *a,int *b){

int temp;

temp=*a;

*a=*b;

*b=temp;

}

首先我们通过gcc分别得到2个文件的.o文件。 gcc -c test.c test1.c

从代码中可以看到,test1.c总共定义了两个全局符号,一个是变量shared,一个是函数swaptest.c里面引用到了test1.c中的sharedswap。接下里的工作就是要把test.otest1.o这两个目标文件链接在一起形成一个可执行的文件。

前面介绍过每个目标文件都有代码段,数据段等等。那么多个目标文件链接在一起的话,这些段该如何组合呢。

一个最简单的方法就是叠加,如下图所示,在最终的目标文件中,把所有的目标文件都依次叠加。这样做的确很简单明了。但是会造成一个问题,在有很多输入文件的情况下,输出文件将会有很多零散的段。比如一个规模稍大的工程就有好几百个文件。而且每个文件都有.data,.text.bss段。那么最后输出的文件将会有成百上千个零散的段。这样非常浪费空间,而且每个段都必须要用一定的地址和空间对齐要求。比如X86的硬件,段的装载地址和空间的对齐单位是页,也就是4096字节。如果一个段的长度是1个字节。在内存中就要占用4096字节。多个这样的段将会极大的浪费内存。

那么第二种方法就是相似段合并了。我们把目标文件的.data,.text,.bss段分别整合在一起。如下图所示。

这种链接方法内部分为两个步骤:

第一步: 空间与地址分配

扫描所有的输入目标文件,获得它们的各个段的长度,属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来。统一放到一个全局符号表。这一步中,链接器能够所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。

第二步:符号解析与重定位 

使用上面收集到的所有信息,读取输入文件中段的数据,重定位消息,并且进行符合解析与重定位,调整代码中的地址等

比如我们将test.otest1.o链接起来生成一个可执行的文件

gcc test.o test1.o -o ab

首先来看test.o的文件

root@zhf-maple:/home/zhf/c_prj# objdump -h test.o

test.o:     文件格式 elf64-x86-64

节:

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

                  ALLOC

objdump -h test1.o

test1.o:     文件格式 elf64-x86-64

节:

Idx Name          Size      VMA               LMA               File off  Algn

  0 .text         0000002d  0000000000000000  0000000000000000  00000040  2**0

                  CONTENTS, ALLOC, LOAD, READONLY, CODE

  1 .data         00000004  0000000000000000  0000000000000000  00000070  2**2

                  CONTENTS, ALLOC, LOAD, DATA

  2 .bss          00000000  0000000000000000  0000000000000000  00000074  2**0

                  ALLOC

objdump -h ab

ab:     文件格式 elf64-x86-64

节:

Idx Name          Size      VMA               LMA               File off  Algn

                  CONTENTS, ALLOC, LOAD, READONLY, CODE

 13 .text         00000202  0000000000000560  0000000000000560  00000560  2**4

 22 .data         00000014  0000000000201000  0000000000201000  00001000  2**3

                  CONTENTS, ALLOC, LOAD, DATA

 23 .bss          00000004  0000000000201014  0000000000201014  00001014  2**0

                  ALLOC

 24 .comment      00000023  0000000000000000  0000000000000000  00001014  2**0

                  CONTENTS, READONLY

从上面的结果可以看到,在链接之前,VMA的地址都是0,这是因为虚拟空间还没有分配。等到链接之后,可执行文件ab中的各个段都被分配到了相应的虚拟地址。在ab.text段被分配到了0000000000000560,大小为0x202字节。.data段从地址0000000000201000开始。大小为14字节。

完成空间和地址的分配步骤以后,链接器就进入了符号解析与重定位的步骤。这也是静态链接的核心。在分析符号解析和重定位之前。我们首先看下test.o里面是怎么使用这两个外部符号的。

objdump -d test.o

test.o:     文件格式 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 

上面2个标红加粗的部分分别是引用sharedswap的位置。对于shared的应用是一个lea指令,前面3个字节是指令码,后面4个字节是shared的地址。对于swap的应用是callq指令。前面1个字节是指令码,后面4个字节是swap的调用地址。可以看到sharedswap的地址都是0.这是因为它们定义在其他目标文件中,所以编译器就暂时把地址0看做是shared的地址

再来看下ab.out中的命令

objdump -d ab

000000000000066a <main>:

 66a: 55                    push   %rbp

 66b: 48 89 e5              mov    %rsp,%rbp

 66e: 48 83 ec 10           sub    $0x10,%rsp

 672: 64 48 8b 04 25 28 00  mov    %fs:0x28,%rax

 679: 00 00 

 67b: 48 89 45 f8           mov    %rax,-0x8(%rbp)

 67f: 31 c0                 xor    %eax,%eax

 681: c7 45 f4 64 00 00 00  movl   $0x64,-0xc(%rbp)

 688: 48 8d 45 f4           lea    -0xc(%rbp),%rax

 68c:48 8d 35 7d 09 20 00 lea    0x20097d(%rip),%rsi        # 201010 <shared>

 693: 48 89 c7              mov    %rax,%rdi

 696: b8 00 00 00 00        mov    $0x0,%eax

 69b:e8 1b 00 00 00       callq  6bb <swap>

 6a0: b8 00 00 00 00        mov    $0x0,%eax

 6a5: 48 8b 55 f8           mov    -0x8(%rbp),%rdx

 6a9: 64 48 33 14 25 28 00  xor    %fs:0x28,%rdx

 6b0: 00 00 

 6b2: 74 05                 je     6b9 <main+0x4f>

 6b4: e8 87 fe ff ff        callq  540 <__stack_chk_fail@plt>

 6b9: c9                    leaveq 

 6ba: c3                    retq 

可以看到main函数中sharedswap都已经被修正到正确的位置。经过修正以后,sharedswap的位置分别是0x20097d6bb

那么链接器是如何知道那些指令是需要被调整的呢。在ELF文件中, 有一个叫重定位表的结构专门用来保存这些与重定位相关的信息。我们使用objdump来查看目标文件的重定位表。

root@zhf-maple:/home/zhf/c_prj# objdump -r test.o

test.o:     文件格式 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] 表示这个重定位是代码段的重定位表。0x250x32分别是lea指令和callq指令的地址部分。具体的地址寻址方式需要查看对应的指令手册,这里就不再介绍

原文地址:https://www.cnblogs.com/zhanghongfeng/p/9104137.html