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

2020-08-26  本文已影响0人  wayyyy
/* 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 <main+0x21>
  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 <main>:
  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 <swap>
  80480b9:  c9                      leave  
  80480ba:  c3                      ret
080480bb <swap>:
 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;

指令修正方式

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

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


COMMON块

强符号和弱符号

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

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

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

extern int ext;

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

int main()
{
    return 0;
}
COMMON 块

上一篇下一篇

猜你喜欢

热点阅读