程序编译链接(四)-- 静态链接

/* a.c */                                       
extern int shared;                             

void exit()
{
    asm( "movl $42, %ebx \n\t"
         "movl $1, %eax \n\t"
         "int $0x80 \n\t");
}

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

/* b.c */
int shared = 1;

void swap(int *a, int *b)
{
    *a ^= *b ^= *a ^= *b;
}
gcc -g -fno-stack-protector  -c -m32 a.c -o a.o  
gcc -g -fno-stack-protector  -c -m32 b.c -o b.o 
ld -static -m elf_i386 -e main a.o b.o -o ab

对于链接器来说,整个链接的过程,就是将几个输入目标文件加工后合并成一个输出文件。
那么对于多个输入的文件,链接器是如何将他们的段合并到输出文件 。有以下2种方法:

按序叠加

按序叠加的做法很简单,就是直接将各个目标文件依次合并,但是这样做法非常浪费空间。因为每个段都有一定的地址和空间对齐要求,比如对于X86平台,段的装载地址和空间的对齐要求是页,也就是4096字节。那么就是说如果一个段的长度只有1个字节,它要在内存中占用一个段,这样会造成内存空间的大量碎片。

相似段合并

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


现在的链接器空间分配基本上采用上述第二种方案。使用这种方法的链接器一般都采用两步链接的方法:

  • 第一步:空间与地址分配
    扫描所有的输入目标文件,获得它们的各个段的长度,属性和位置,并且将输入目标文件中的符号表中所有符号定义和符号引用收集起来,统一放到一个全局符号表中。

  • 第二步:符号解析与重定位
    使用上面第一步中收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址。这一步是链接的核心,特别是重定位过程。

$ objdump -h a.o  # a.o是可重定位文件
image.png
$ objdump -h b.o  # b.o是可重定位文件
image.png
$ objdump -h ab  # ab是可执行文件
image.png
空间与地址分配

从上面看出,链接器已经按照前面介绍的空间分配方法进行分配,这时候各段在链接后的虚拟地址已经确定。


image.png

当前面一步完成之后,链接器开始计算各个符号的虚拟地址,因为各个符号在段内的相对位置是固定的,所以这时mainsharedswap地址已经确定了。


符号解析与重定位

在完成空间和地址的分配之后,每个段在链接后的虚拟地址就已经确定了。
这时链接器就进入了符号解析与重定位的步骤,这也是静态链接的核心内容。

重定位
$ objdump -d a.o
  0:    55                      push   %ebp
  1:    89 e5                   mov    %esp,%ebp
  3:    83 e4 f0                and    $0xfffffff0,%esp
  6:    83 ec 20                sub    $0x20,%esp
  9:    c7 44 24 1c 64 00 00    movl   $0x64,0x1c(%esp)
  10:   00 
  11:   c7 44 24 04 00 00 00    movl   $0x0,0x4(%esp)
  18:   00 
  19:   8d 44 24 1c             lea    0x1c(%esp),%eax
  1d:   89 04 24                mov    %eax,(%esp)
  20:   e8 fc ff ff ff          call   21 
  25:   c9                      leave  
  26:   c3                      ret 

c7 44 24 04 00 00 00 0000 00 00 00shared的地址。
因为当a.c被编译为目标文件时,编译器并不知道sharedswap的地址。因为,它们定义在其他文件中, 所以编译器暂时把地址0看作是shared的地址。

e8 fc ff ff ffff ff ffswap函数的地址。
e8 是近址相对位移调用指令,后面4个字节就是被调用函数的相对于调用指令的下一条指令的偏移量。 0xFFFFFFFC(小端法已转换),它是-4的补码。因为下一条指令的地址是0x25,所以 0x25 - 4 = 0x21,
所以call指令的实际调用地址是0x21,但是0x21并不是存放swap的函数地址。

编译器把这两条指令的暂时用两个假地址代替,把真正的地址计算工作留给了链接器。

$ objdump -d ab  # ab是可执行文件
  08048094 
: 8048094: 55 push %ebp 8048095: 89 e5 mov %esp,%ebp 8048097: 83 e4 f0 and $0xfffffff0,%esp 804809a: 83 ec 20 sub $0x20,%esp 804809d: c7 44 24 1c 64 00 00 movl $0x64,0x1c(%esp) 80480a4: 00 80480a5: c7 44 24 04 54 91 04 movl $0x8049154,0x4(%esp) 80480ac: 08 80480ad: 8d 44 24 1c lea 0x1c(%esp),%eax 80480b1: 89 04 24 mov %eax,(%esp) 80480b4: e8 02 00 00 00 call 80480bb 80480b9: c9 leave 80480ba: c3 ret 080480bb : 80480bb: 55 push %ebp ......

从链接后的结果来看,shared地址已被更新为0x08049154,swap的地址等于0x80480b9 + 0x2 = 0x80480bb。

重定位表

那么链接器是怎么知道哪些指令要被调整的呢?在可重定位的ELF文件中,有一个重定位表的结构专门用来保存这些与重定位相关的信息。对于每一个要被重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是ELF文件中的一个段。比如代码段".txt"有要被重定位的地方,那么就又一个相应的叫".rel.text"的段保存了代码段的重定位表。

objdump -r a.o  # -r 显示重定位信息
image.png

对于32位的Intel x86系列处理器来说,重定位表的结构很简单,它是一个Elf32_Rel结构的数组,每个数组元素对应一个重定位入口。

typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
} Elf32_Rel;
  • r_offset
    重定位入口的偏移,对于可重定位文件来说,这个值是该重定位入口所要修正的位置的第一个字节相对于段起始的偏移,

  • r_info

指令修正方式

32位平台x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有两种:

  • 绝对近32位寻址
  • 相对近32位寻址

这两种重定位方式修正指令方式每个被修正的位置的长度都为32位,即4字节


COMMON块

强符号和弱符号

经常在编程中碰到一种情况叫做符号重定义,在什么情况下会报这那个错误呢?

对于 C/C++ 来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。

注意强弱符号都是通过定义而言的,不是针对符号引用。
比如下面代码中:weakweak2是弱符号,strongmain 是强符号。

extern int ext;

int week;
int strong = 1;
__attribute__((week)) week2 = 2;

int main()
{
    return 0;
}
  • 规则一:不允许强符号被多次定义,有多个强符号被多次定义,则链接器报符号重定义的错误。
  • 规则二:如果一个符号在某个目标文件中是强符号,其他文件中都是弱符号,选择强符号。
  • 规则三:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
COMMON 块

你可能感兴趣的:(程序编译链接(四)-- 静态链接)