链接装载与库 第4章 静态链接

静态链接

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

4.1 空间与地址分配

对于多个输入文件,链接器如何将它们各个段合并到输出文件?必须要为这些段分配空间。

4.1.1 按序叠加

也就是将各个目标文件依次合并。
缺点:
由于空间对齐而产生可观的浪费。

4.1.2 相似段合并

将相同的段合并到一起,并为合并后的段分配空间。

现在的链接器基本都采用相似段合并的方式。整个链接过程分两步:1. 空间与地址分配 2.符号解析与重定位。

  • 空间与地址分配 扫描所有输入目标文件,获得它们各个段的长度,属性和位置,并且将他们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系(映射关系由链接器保存?)。同时将所有的符号定义和符号引用(为什么需要符号引用)收集起来,统一放到一个全局符号表中。
  • **符号解析与重定位 ** 读取输入文件中段的数据,重定位信息,进行符号解析与重定位,调整代码中的地址。
root@debian:~/compileLinkLoad# objdump -h a.o

a.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .group        00000008  00000000  00000000  00000034  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  1 .text         0000004a  00000000  00000000  0000003c  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  2 .data         00000000  00000000  00000000  00000086  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000000  00000000  00000000  00000086  2**0
                  ALLOC
  4 .text.__x86.get_pc_thunk.ax 00000004  00000000  00000000  00000086  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  5 .comment      0000002e  00000000  00000000  0000008a  2**0
                  CONTENTS, READONLY
  6 .note.GNU-stack 00000000  00000000  00000000  000000b8  2**0
                  CONTENTS, READONLY
  7 .eh_frame     00000060  00000000  00000000  000000b8  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
root@debian:~/compileLinkLoad# objdump -h b.o

b.o:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .group        00000008  00000000  00000000  00000034  2**2
                  CONTENTS, READONLY, EXCLUDE, GROUP, LINK_ONCE_DISCARD
  1 .text         00000043  00000000  00000000  0000003c  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  2 .data         00000004  00000000  00000000  00000080  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .bss          00000000  00000000  00000000  00000084  2**0
                  ALLOC
  4 .text.__x86.get_pc_thunk.ax 00000004  00000000  00000000  00000084  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  5 .comment      0000002e  00000000  00000000  00000088  2**0
                  CONTENTS, READONLY
  6 .note.GNU-stack 00000000  00000000  00000000  000000b6  2**0
                  CONTENTS, READONLY
  7 .eh_frame     0000004c  00000000  00000000  000000b8  2**2
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

root@debian:~/compileLinkLoad# objdump -h ab

ab:     file format elf32-i386

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000091  08048094  08048094  00000094  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000080  08048128  08048128  00000128  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .got.plt      0000000c  0804a000  0804a000  00001000  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  3 .data         00000004  0804a00c  0804a00c  0000100c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  4 .comment      0000002d  00000000  00000000  00001010  2**0
                  CONTENTS, READONLY

VMA表示虚拟地址,LMA表示加载地址,一般这两个值相等。在有些嵌入式系统中不相同。
(具体理解有待补充)

4.1.3 符号地址的确定

经过前面的空间分配之后,每个输入文件的每个段在输出文件的虚拟地址都已经确定。而每个符号相对于段的偏移也是确定的,所以每个符号在输出文件的虚拟地址也就确定了。

符号解析与重定位

4.2.1 重定位

将源文件a.c编译成a.o的时候,并不知道shared和main地址。那么a.o中,是怎么保存这两个变量的地址的呢?

root@debian:~/compileLinkLoad# objdump -d a.o

a.o:     file format elf32-i386


Disassembly of section .text:

00000000 <main>:
   0:   8d 4c 24 04             lea    0x4(%esp),%ecx
   4:   83 e4 f0                and    $0xfffffff0,%esp
   7:   ff 71 fc                pushl  -0x4(%ecx)
   a:   55                      push   %ebp
   b:   89 e5                   mov    %esp,%ebp
   d:   53                      push   %ebx
   e:   51                      push   %ecx
   f:   83 ec 10                sub    $0x10,%esp
  12:   e8 fc ff ff ff          call   13 <main+0x13>
  17:   05 01 00 00 00          add    $0x1,%eax
  1c:   c7 45 f4 64 00 00 00    movl   $0x64,-0xc(%ebp)
  23:   83 ec 08                sub    $0x8,%esp
  26:   8b 90 00 00 00 00       mov    0x0(%eax),%edx
  2c:   52                      push   %edx
  2d:   8d 55 f4                lea    -0xc(%ebp),%edx
  30:   52                      push   %edx
  31:   89 c3                   mov    %eax,%ebx
  33:   e8 fc ff ff ff          call   34 <main+0x34>
  38:   83 c4 10                add    $0x10,%esp
  3b:   b8 00 00 00 00          mov    $0x0,%eax
  40:   8d 65 f8                lea    -0x8(%ebp),%esp
  43:   59                      pop    %ecx
  44:   5b                      pop    %ebx
  45:   5d                      pop    %ebp
  46:   8d 61 fc                lea    -0x4(%ecx),%esp
  49:   c3                      ret

Disassembly of section .text.__x86.get_pc_thunk.ax:

00000000 <__x86.get_pc_thunk.ax>:
   0:   8b 04 24                mov    (%esp),%eax
   3:   c3                      ret

可以看出,偏移为0x27的位置,shared变量的地址为0。偏移为0x34的位置,swap函数的地址为0xfffffffc(小端)。

链接的时候,这两个地址会被修改为正确的值:

root@debian:~/compileLinkLoad# objdump -d ab

ab:     file format elf32-i386


Disassembly of section .text:

08048094 <main>:
 8048094:       8d 4c 24 04             lea    0x4(%esp),%ecx
 8048098:       83 e4 f0                and    $0xfffffff0,%esp
 804809b:       ff 71 fc                pushl  -0x4(%ecx)
 804809e:       55                      push   %ebp
 804809f:       89 e5                   mov    %esp,%ebp
 80480a1:       53                      push   %ebx
 80480a2:       51                      push   %ecx
 80480a3:       83 ec 10                sub    $0x10,%esp
 80480a6:       e8 33 00 00 00          call   80480de <__x86.get_pc_thunk.ax>
 80480ab:       05 55 1f 00 00          add    $0x1f55,%eax
 80480b0:       c7 45 f4 64 00 00 00    movl   $0x64,-0xc(%ebp)
 80480b7:       83 ec 08                sub    $0x8,%esp
 80480ba:       c7 c2 0c a0 04 08       mov    $0x804a00c,%edx
 80480c0:       52                      push   %edx
 80480c1:       8d 55 f4                lea    -0xc(%ebp),%edx
 80480c4:       52                      push   %edx
 80480c5:       89 c3                   mov    %eax,%ebx
 80480c7:       e8 16 00 00 00          call   80480e2 <swap>
 80480cc:       83 c4 10                add    $0x10,%esp
 80480cf:       b8 00 00 00 00          mov    $0x0,%eax
 80480d4:       8d 65 f8                lea    -0x8(%ebp),%esp
 80480d7:       59                      pop    %ecx
 80480d8:       5b                      pop    %ebx
 80480d9:       5d                      pop    %ebp
 80480da:       8d 61 fc                lea    -0x4(%ecx),%esp
 80480dd:       c3                      ret
....

shared变量地址为0x804a00c,函数地址为0x00000016(近址相对位移)

4.2.2 重定位表

重定位表保存与重定位相关的信息,供链接器在重定位的时候使用。

root@debian:~/compileLinkLoad# objdump -r a.o

a.o:     file format elf32-i386

RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000013 R_386_PC32        __x86.get_pc_thunk.ax
00000018 R_386_GOTPC       _GLOBAL_OFFSET_TABLE_
00000028 R_386_GOT32X      shared
00000034 R_386_PLT32       swap


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET   TYPE              VALUE
00000020 R_386_PC32        .text
00000054 R_386_PC32        .text.__x86.get_pc_thunk.ax

每个重定位表是elf文件的一个段,是一个Elf32_Rel结构的数组。

typedef struct
{
  Elf32_Addr    r_offset;               /* Address */
  Elf32_Word    r_info;                 /* Relocation type and symbol index */
} Elf32_Rel;

r_offset 所要修正的位置的第一个字节相对于段起始的偏移
r_info 重定位入口的类型和符号。低8位表示类型。高24位表示重定位入口的符号在符号表中的下标。

4.2.3 符号解析

当链接器需要对某个符号的引用进行重定位时,链接器就会去查的由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
如果某个符号无法在全局符号表中找到,链接器就会报符号未定义错误。

### 4.2.4 指令修正方式
不同的处理器寻址方式千差万别。对于32位的x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有2种:
绝对近址32位寻址
相对近址32位寻址
R_386_32 1 绝对寻址修正 S + A
R_386_PC32 2 相对寻址修正 S+A-P
A = 被保存在修正位置的值
P = 被修正的位置
S = 符号的实际地址

4.3 common块

对于c/c++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。非全局变量没有强弱符号的概念。
针对强弱符号,链接器会按如下规则处理与选择被多次定义的全局符号:

  • 不允许强符号多次定义。
  • 强符号出现一次,弱符号出现一次或多次,选择强符号
  • 都是弱符号,选择占用空间最大的一个

强引用:如果找不到对应的符号,链接器就会报错
弱引用:如果找不到对应的符号,链接器不会报错

为什么在目标文件中,编译不直接把未初始化的全局变量也当做未初始化的局部静态变量一样处理,为它在bss段分配空间,而是标记为一个common类型的变量。
这是因为未初始化的全局变量是一个弱符号,经过链接之后,最终占用的空间是未知的,所以无法放到BSS段。但是链接完成之后,最终还是放到了BSS段

4.4 c++相关问题

4.5 静态库链接

一个静态库可以简单的看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。一般使用ar将多个目标文件压缩到一起,并进行编号和索引,以便于查找和检索。
用ar工具查看静态库包含哪些文件:

ar -t  /usr/lib/i386-linux-gnu/libc.a

ld链接器要做的事情,就是在静态库中寻找所有需要的符号以及它们所在的目标文件,将这些目标文件解压出来,链接在一起成为可执行文件。

gcc -static --verbose -fno-builtin hello.c
-static 表示静态链接
–verbose 显示详细过程
-fno-builtin 关闭内置函数优化选项
可以看出整个过程分为3步:

  1. 编译
  2. 汇编
  3. 链接

链接器在链接静态库的时候是以目标文件为单位的,所以一个目标文件只包含一个函数有助于减小生成文件的大小。

4.6 链接过程控制

一些特殊的程序,如操作系统内核,BIOS,可能需要指定输出文件的各个段的邪气地址,段的名称,段存放的顺序等。默认的链接规则无法满足需求。

4.6.1 链接控制脚本

在使用ld链接器的时候,没有指定链接脚本时会使用默认的链接脚本,存放到/usr/lib/ldscripts/目录下。
指定脚本语法:

ld -T link.script

4.6.2 最小的程序“hello world”

  • 不使用c语言库
  • 不使用main作为入口(貌似只是为了炫技)
/*TinyHelloWorld.c*/
char* str="Hello world!\n";

void print()
{
	asm("movl $13,%%edx \n\t"
		"movl %0,%%ecx \n\t"
		"movl $0,%%ebx \n\t"
		"movl $4,%%eax \n\t"
		"int $0x80     \n\t"
		::"r"(str):"edx","ecx","ebx");
}

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

void nomain()
{
	print();
	exit();
}

编译链接:

gcc -c -fno-builtin TinyHelloWorld.c
ld -static -e nomain -o TinyHelloWorld TinyHelloWorld.o

生成的TinyHelloWorld居然有5000个字节(作者书上写的是924)。而且有很多的段,显然还不够小。这个时候该轮到链接器脚本上场了。

4.6.3 使用ld链接脚本

/*TinyHelloWorld.lds*/
ENTRY(nomain)

SECTIONS
{
  .= 0x08048000 + SIZEOF_HEADERS;
  tinytext : { *(.text) *(.data) *(.rodata)}
  /DISCARD/ :{ *(.comment) }
}

重新链接:

ld -static -T TinyHelloWorld.lds -o TinyHelloWorld TinyHelloWorld.o

删除这些多余的段之后,大小直接变为了1160字节,果然厉害。

4.6.4 ld链接脚本语法简介

略。将来有需要再细学

4.7 BFD库

现代的硬件和软件平台各类繁多,导致编译器和链接器很难处理不同平台之间的目标文件。

BFD(binary file descriptor library)是一个GNU项目,致力于规则一种统一的接口来处理不同目标文件格式之间的差异。
现在GCC,ld,GDB及binutils的其他工具都通过BFD库来处理目标文件,而不直接操作目标文件。从而将编译器,链接器本身同具体的目标文件格式隔离开来。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值