静态链接
/* 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步:
- 编译
- 汇编
- 链接
链接器在链接静态库的时候是以目标文件为单位的,所以一个目标文件只包含一个函数有助于减小生成文件的大小。
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库来处理目标文件,而不直接操作目标文件。从而将编译器,链接器本身同具体的目标文件格式隔离开来。