兰州北京网站建设,品牌营销网站建设,企业管理咨询是干什么的,免费杂志排版软件1、空间与地址的分配 现在的链接器空间分配的策略基本上都采用 “相似段合并” 的方式。通过将所有相同类型的 section 合并到一起#xff0c;例如将所有输入目标文件的 .text 合并#xff08;按顺序合并#xff09;到输出文件的 .text 节中#xff1b;然后#xff0c;链接…1、空间与地址的分配 现在的链接器空间分配的策略基本上都采用 “相似段合并” 的方式。通过将所有相同类型的 section 合并到一起例如将所有输入目标文件的 .text 合并按顺序合并到输出文件的 .text 节中然后链接器根据运行平台中进程虚拟地址空间的划分规则为所有输入目标文件中定义的节和符号分配运行时内存地址完成之后程序中的每条指令和符号都有唯一的运行时内存地址了。链接器的空间分配示意如下 使用这种方法的链接器都采用两步链接的方法。 第一步空间与地址分配。扫描所有的输入目标文件获取她们各个段的长度、属性和位置并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来统一放到一个全局符号表中。这一步链接器将能够获得所有输入目标文件的段长度并且将他们合并计算出输出文件中各个段合并后的长度与位置并建立映射关系。 第二步符号解析与重定位。使用上一步收集的所有信息读取输入文件中段的数据、重定位信息并且进行符号解析与重定位、调试代码中的地址等。 下面我们以a.c、b.c两个文件为例进行讲解。首先这两个源文件的编译和手动链接就很麻烦耐下心来先把这两个源文件的编译和链接搞清楚。
/* a.c */
extern int shared;
extern void swap(int *a, int *b);int main()
{int a 100;swap(a, shared);
}/* b.c */
int shared 1;void swap(int *a, int *b)
{*a^*b^*a^*b;
}gcc编译然后ld去链接。链接过程中会报未定义引用错误。
liangliang-virtual-machine:~/cfp$ gcc -c a.c b.c
liangliang-virtual-machine:~/cfp$ ld -e main a.o b.o -o ab
a.o: In function main:
a.c:(.text0x44): undefined reference to __stack_chk_fail实际上我们手动调用 ld 链接器链接其实没有链接什么库文件所以自然会报这个未定义引用最简单的办法就是把这个默认的栈相关的检查给关了解决办法不是在链接过程中而是在编译时加参数“-fno-stack-protector”强制 gcc 不进行栈检查。
liangliang-virtual-machine:~/cfp$ gcc -c -fno-stack-protector a.c b.c
liangliang-virtual-machine:~/cfp$ ld -e main a.o b.o -o ab
liangliang-virtual-machine:~/cfp$ ./ab
Segmentation fault (core dumped)然而解决了未定义引用的问题又蹦出来一个段错误——Segmentation fault。 编程语言一般都需要语言库语言库的一个重要作用就是实现对操作系统 API 的封装我们平时跑个小程序啥的本质上是把代码编译成可执行文件然后以一个进程的方式运行该可执行文件。 而对于一个进程的开始和结束其实就是依靠操作系统提供的 API 来实现的而如何调用操作系统的API呢就是通过刚刚提到的语言库了在我的测试环境下C的语言库就是大名鼎鼎的GNU搞的Glibc。 简单地说平时用到的那种 main 函数形式的程序其实是依靠 Glibc 来实现的那种程序真正的入口并非 main 函数而是该库里面的_start 函数由库负责初始化后调用 main 函数来执行程序的主体部分。 但是我们现在搞的这段代码我并不想依赖 Glibc是想尽量啥都不依赖。所以我们写这个 main 函数本质上没啥特殊的跟平时我们常见的 main 函数不一样了平时 main 函数那么特殊还是因为在依赖GlibcGlibc指定入口就是 main 函数所以它才特殊现在我们不用 Glibc 了入口其实是链接时候自己指定的也就是上面出现过的 ld 的命令里 “-e main” 那个选项所以这时候main函数其实不特殊了。 说回刚才的话题既然不用 Glibc 了那原来 main 函数结束后 Glibc 帮我们结束进程的活也没人干了呀所以刚才运行的时候进程一直没结束一直在往后边地址上跑结果跑到不该去的地方就自然就 Segmentation fault。明白了原理其实解决方法也不难的Glibc也不是神仙它其实是通过调用操作系统的API “EXIT”实现的中断进程我们现在只要自己搞一个函数实现同样的功能就好了以下是加上这个功能的代码
extern int shared;
void swap(int *a, int *b);#define x86_64#ifdef x86_64
void exit()
{asm(movq $66,%rdi \n\tmovq $60,%rax \n\tsyscall \n\t);
}
#else
void exit()
{asm(movl $42,%ebx \n\tmovl $1,%eax \n\tint $0x80 \n\t);
}
#endifint main()
{int a 100;swap(a, shared);exit();
}这里面会涉及到内联汇编的知识感兴趣的朋友可以自己学习学习x86_64位和x86_32的汇编操作不一样这一点要注意上面代码中也有体现。下面给出相关链接可以看一看。 系统调用约定
添加了exit退出函数后编译会报warning。这是因为 exit( ) 是C语言内置函数有对应的__builtin_exit()内建函数。内建函数是编译器针对特定平台对代码所作的优化会使代码的运行效率有很大提高。所以不加“-fno-builtin”参数会默认把 “exit” 识别为内置函数所以会报warning。解决的方法是加上“-fno-builtin”参数默认不使用内建函数。
liangliang-virtual-machine:~/cfp$ gcc -c -fno-stack-protector a.c b.c
a.c: In function ‘exit’:
a.c:5:6: warning: number of arguments doesn’t match built-in prototypevoid exit()^编译、链接如下发现终于正确了。
liangliang-virtual-machine:~/cfp$ gcc -c -fno-builtin -fno-stack-protector a.c b.c
liangliang-virtual-machine:~/cfp$ ld -e main a.o b.o -o ab
liangliang-virtual-machine:~/cfp$ ./abTips这里关于内建函数和内置函数拓展一下
内置函数分为两类 一类与标准c库函数对应例如strcpy()有对应的__builtin_strcpy()内建函数另一类是对库函数的拓展。 且所有的内置函数都是 inline 的 从这里我们其实也可以知道所谓的内建 其实可以理解为内置的一种~~内建/内置函数英文翻译都是 built-in function gcc官方文档里面有一段话大意是对于内置函数如果能对代码进行优化gcc会优化代码如果不能优化往往就是直接调用同名的标准库函数。上边为什么会报错因为调用了 exit 对应的内建函数去优化代码 1.1 符号地址的确定 到此为止终于把这个两个源文件的编译和链接讲清楚了下面我们进入正题。
liangliang-virtual-machine:~/cfp$ objdump -h a.oa.o: file format elf64-x86-64Sections:
Idx Name Size VMA LMA File off Algn0 .text 00000048 0000000000000000 0000000000000000 00000040 2**0CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE1 .data 00000000 0000000000000000 0000000000000000 00000088 2**0CONTENTS, ALLOC, LOAD, DATA2 .bss 00000000 0000000000000000 0000000000000000 00000088 2**0ALLOC
...
liangliang-virtual-machine:~/cfp$ objdump -h b.ob.o: file format elf64-x86-64Sections:
Idx Name Size VMA LMA File off Algn0 .text 0000004b 0000000000000000 0000000000000000 00000040 2**0CONTENTS, ALLOC, LOAD, READONLY, CODE1 .data 00000004 0000000000000000 0000000000000000 0000008c 2**2CONTENTS, ALLOC, LOAD, DATA2 .bss 00000000 0000000000000000 0000000000000000 00000090 2**0ALLOC3 .comment 00000036 0000000000000000 0000000000000000 00000090 2**0CONTENTS, READONLY
...
liangliang-virtual-machine:~/cfp$ objdump -h abab: file format elf64-x86-64Sections:
Idx Name Size VMA LMA File off Algn0 .text 00000093 00000000004000e8 00000000004000e8 000000e8 2**0CONTENTS, ALLOC, LOAD, READONLY, CODE1 .eh_frame 00000078 0000000000400180 0000000000400180 00000180 2**3CONTENTS, ALLOC, LOAD, READONLY, DATA2 .data 00000004 00000000006001f8 00000000006001f8 000001f8 2**2CONTENTS, ALLOC, LOAD, DATA
...其中 VMA 表示 virtual memory address 即虚拟地址LMA 表示 load memory address即加载地址。我们暂且不关心文件偏移File off。我们看到在链接之前所有的 VMA 都是0因为虚拟空间还没有分配。链接之后可执行文件 ab 中的各个段都被分配到了虚拟地址VMA不为0。从上述分析中我们也可以看出“a.o”和“b.o”的代码段被先后叠加起来合并成 “ab” 的一个.text段加起来长度刚好是0x93。数据段也是如此。但是为什么给 ab 文件的 text 段分配到0x00000000004000e8地址呢后面会有具体分析。 当空间分配阶段结束后链接器按照前面介绍的空间分配方法进行分配这时候输入文件中的各个段在链接后的虚拟地址就已经确定了。 当前面一步完成之后链接器就开始计算各个符号的虚拟地址了。因为各个符号在段内的相对位置是固定的所以这时候其实“main”、“shared”和“swap”的地址也已经是确定的了只不过链接器需要给每个符号加上一个偏移使它们能够调整到正确的虚拟地址。
liangliang-virtual-machine:~/cfp$ objdump -d a.oa.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 exit:0: 55 push %rbp1: 48 89 e5 mov %rsp,%rbp... 0000000000000017 main:17: 55 push %rbp18: 48 89 e5 mov %rsp,%rbp...liangliang-virtual-machine:~/cfp$ objdump -d b.ob.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 swap:0: 55 push %rbp1: 48 89 e5 mov %rsp,%rbp...例如我们可以用 objdump 查看目标文件 a.o我们可以看到exit 函数相对于.text 段的偏移为0x0而main函数相对于 .text 段的偏移为0x17。而我们知道最终可执行文件中的 .text 段的虚拟地址是 0x4000e8objdump -h ab。链接器根据最终可执行文件中的 .text 段的虚拟地址就可以计算出main、exit函数的虚拟地址。 swap函数位于b.o中通过反汇编b.o我们可以看到 swap 函数相对 .text 段的偏移为0。而我们知道链接器是按序、采用相似段合并的方法分配空间的所以最终可执行文件的 .text 段中a.o的 .text 段在b.o的 .text 段上面。事实也是如此我们可以通过反汇编可执行文件ab看到。所以swap函数的虚拟地址 可执行文件中 .text 段的虚拟地址 a.o 中 .text 段大小。shared变量的虚拟地址是一样的计算方法。
符号类型虚拟地址main函数0x4000e8 0x17 4000ffswap函数0x4000e8 0x48 400130exit函数0x4000e8 0x0shared变量0x6001f8 0x0 其实到这这些个符号的地址就已经确定了。下面就是要根据这些符号地址去修正相应的符号无效地址。
2、符号解析与重定位
2.1 重定位
反汇编查看a.o
liangliang-virtual-machine:~/cfp$ objdump -d a.oa.o: file format elf64-x86-64Disassembly of section .text:0000000000000000 exit:0: 55 push %rbp1: 48 89 e5 mov %rsp,%rbp4: 48 c7 c7 42 00 00 00 mov $0x42,%rdib: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax12: 0f 05 syscall 14: 90 nop15: 5d pop %rbp16: c3 retq 0000000000000017 main:17: 55 push %rbp18: 48 89 e5 mov %rsp,%rbp1b: 48 83 ec 10 sub $0x10,%rsp1f: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)26: 48 8d 45 fc lea -0x4(%rbp),%rax2a: be 00 00 00 00 mov $0x0,%esi2f: 48 89 c7 mov %rax,%rdi32: e8 00 00 00 00 callq 37 main0x2037: b8 00 00 00 00 mov $0x0,%eax3c: e8 00 00 00 00 callq 41 main0x2a41: b8 00 00 00 00 mov $0x0,%eax46: c9 leaveq 47: c3 retq 在完成空间和地址分配步骤以后链接器就进入了符号解析与重定位步骤这也是静态链接的核心内容。 我们可以很清楚的看到 “a.o” 的反汇编中偏移为 0x2a 的 move 指令。 这里的share的地址为什么是0呢因为当源代码“a.c”被编译成目标文件 a.o 时编译器并不知道shared的地址。所以编译器就暂时把地址0看作“shared”的地址。 另一个偏移为 0x32 的指令也是如此。这条指令表示对 swap 函数的调用。偏移为 0x3c 的指令表示对 exit 函数的调用 这里call指令是一条近地址相对位移调用指令后面四个字节就是被调用的函数相对于调用指令的下一条指令的偏移量。 编译器无法确定的符号的地址就由链接器来修正符号的地址。我们反汇编可执行文件ab可以看到里面的符号地址已经被修正了。
liangliang-virtual-machine:~/cfp$ objdump -d abab: file format elf64-x86-64Disassembly of section .text:00000000004000e8 exit:4000e8: 55 push %rbp4000e9: 48 89 e5 mov %rsp,%rbp4000ec: 48 c7 c7 42 00 00 00 mov $0x42,%rdi4000f3: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax4000fa: 0f 05 syscall 4000fc: 90 nop4000fd: 5d pop %rbp4000fe: c3 retq 00000000004000ff main:4000ff: 55 push %rbp400100: 48 89 e5 mov %rsp,%rbp400103: 48 83 ec 10 sub $0x10,%rsp400107: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)40010e: 48 8d 45 fc lea -0x4(%rbp),%rax400112: be f8 01 60 00 mov $0x6001f8,%esi400117: 48 89 c7 mov %rax,%rdi40011a: e8 11 00 00 00 callq 400130 swap40011f: b8 00 00 00 00 mov $0x0,%eax400124: e8 bf ff ff ff callq 4000e8 exit400129: b8 00 00 00 00 mov $0x0,%eax40012e: c9 leaveq 40012f: c3 retq 0000000000400130 swap:400130: 55 push %rbp400131: 48 89 e5 mov %rsp,%rbp400134: 48 89 7d f8 mov %rdi,-0x8(%rbp)400138: 48 89 75 f0 mov %rsi,-0x10(%rbp)40013c: 48 8b 45 f8 mov -0x8(%rbp),%rax400140: 8b 10 mov (%rax),%edx400142: 48 8b 45 f0 mov -0x10(%rbp),%rax400146: 8b 00 mov (%rax),%eax400148: 31 c2 xor %eax,%edx40014a: 48 8b 45 f8 mov -0x8(%rbp),%rax40014e: 89 10 mov %edx,(%rax)400150: 48 8b 45 f8 mov -0x8(%rbp),%rax400154: 8b 10 mov (%rax),%edx400156: 48 8b 45 f0 mov -0x10(%rbp),%rax40015a: 8b 00 mov (%rax),%eax40015c: 31 c2 xor %eax,%edx40015e: 48 8b 45 f0 mov -0x10(%rbp),%rax400162: 89 10 mov %edx,(%rax)400164: 48 8b 45 f0 mov -0x10(%rbp),%rax400168: 8b 10 mov (%rax),%edx40016a: 48 8b 45 f8 mov -0x8(%rbp),%rax40016e: 8b 00 mov (%rax),%eax400170: 31 c2 xor %eax,%edx400172: 48 8b 45 f8 mov -0x8(%rbp),%rax400176: 89 10 mov %edx,(%rax)400178: 90 nop400179: 5d pop %rbp40017a: c3 retq 我们通过前面的空间与地址分配可以得知链接器在完成地址和空间分配之后就已经可以确定所有符号的虚拟地址了那么链接器就可以根据符号的地址对每个需要重定位的指令进行地址修正。我们用 objdump 来反汇编输出程序“ab”的代码段可以看到main函数的三个重定位入口都已经被修正到正确的位置了 400112: be f8 01 60 00 mov $0x6001f8,%esi...40011a: e8 11 00 00 00 callq 400130 swap...400124: e8 bf ff ff ff callq 4000e8 exitshared的地址为 0x6001f8这个很好理解因为 shared 变量的地址的确是 0x6001f8。
be f8 01 60 00 那么 swap 函数的虚拟地址为什么是 0x11 呢这个和 call 指令有关
40011a: e8 11 00 00 000x400130 40011f 调用指令的下一条指令地址 0x11偏移。这里 call 指令是一条近地址相对位移调用指令。后面四个字节就是被调用的函数相对于调用指令的下一条指令的偏移量。
2.2 重定位表 链接器是怎么知道哪些指令是要被调整的呢这些指令的哪些部分要被调整怎么调整比如上面例子中的 “mov” 指令和“call” 指令的调整方式就有所不同。事实上在ELF文件中有一个叫重定位表Relocation Table的结构专门用来保存这些与重定位相关的信息我们在前面介绍ELF文件结构时已经提到过重定位表它在ELF文件中往往是一个或多个段。 对于可重定位的ELF文件来说它必须包含重定位表用来描述如何修改相应的节的内容。对于每个要被重定位的ELF段都有一个对应的重定位表。如果 .text 段需要被重定位则会有一个相对应叫 .rel.text 的段保存了代码节的重定位表如果 .data 段需要被重定位则会有一个相对应的 .rel.tdata 的段保存了数据节的重定位表。
我们可以使用objdump工具来查看目标文件中的重定位表
liangliang-virtual-machine:~/cfp$ objdump -r a.oa.o: file format elf64-x86-64RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000002b R_X86_64_32 shared
0000000000000033 R_X86_64_PC32 swap-0x0000000000000004
000000000000003d R_X86_64_PC32 exit-0x0000000000000004
...我们可以看到每个要被重定位的地方是一个重定位入口Relocation Entry。利用数据结构成员包含的信息即可完成重定位。 重定位入口的偏移Offset 表示该入口在要被重定位的段中的位置这里的0x2b是目标文件 a.o 代码段中 shared 指令地址部分的地址。0x33和0x3d是目标文件 a.o 代码段中 swap、exit 指令地址部分的地址。简单来说就是需要修正的地方的地址。
typedef struct {Elf32_Addr r_offset;Elf32_Word r_info;
} Elf32_Rel;typedef struct {Elf64_Addr r_offset;Elf64_Xword r_info;
} Elf64_Rel;r_offset此成员指定应用重定位操作的位置。不同的目标文件对于此成员的解释会稍有不同。对于可重定位文件该值表示节偏移。重定位段说明如何修改文件中的其他节。重定位偏移会在第二节中指定一个存储单元。对于可执行文件或共享目标文件该值表示受重定位影响的存储单元的虚拟地址。此信息使重定位项对于运行时链接程序更为有用。虽然为了使相关程序可以更有效地访问不同目标文件的成员的解释会发生变化但重定位类型的含义保持相同r_info重定位入口的类型和符号。这个成员的低8位表示重定位入口类型高24位表示重定位入口的符号在符号表的下标。 因为各种处理器的指令格式不一样所以重定位所修正的指令地址格式也不一样。每种处理器都有自己的一套重定位入口类型。对于可执行文件和共享目标文件来说它们的重定位入口是动态链接类型的请参考”动态链接“一章
2.3 符号解析 通过前面指令重定位的介绍我们可以更加深层次地理解为什么缺少符号的定义会导致链接错误。其实重定位过程也伴随着符号解析过程每个目标文件都可能定义一些符号也可能引用到定义在其他目标文件的符号。重定位的过程中每个重定位入口都是对一个符号的引用那么当链接器需要对某个符号重定位时它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表找到相应的符号后进行重定位。 例如我们查看a.o的符号表
liangliang-virtual-machine:~/cfp$ readelf -s a.oSymbol table .symtab contains 12 entries:Num: Value Size Type Bind Vis Ndx Name0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 6 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 5 8: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 exit9: 0000000000000017 49 FUNC GLOBAL DEFAULT 1 main10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swapGLOBAL类型的符号除了main函数、exit函数是定义在代码段之外其他两个“shared”和“swap”都是“UND”。这种未定义的符号都是因为该目标文件中有关它们的重定位项所以在链接器扫描完所有输入目标文件之后所有这些未定义的符号都应该能够在全局符号表中找到否则链接器就报符号未定义错误。
2.4 指令修正方式
这一块我们暂不作讲解感兴趣的可以自己 goole 一下
3、COMMON块 正如前面提到的由于弱符号机制允许同一个符号的定义存在于多个文件中所以可能会出现一个问题如果一个弱符号定义在多个目标文件中而它们的类型又不相同那怎么办到底实际被用到的是哪个
多个符号定义类型不一致的几种情况如下
两个或两个以上强符号类型不一致有一个强符号其他都是弱符号出现类型不一致两个或两个以上弱符号类型不一致。 对于上述三种情况第一种情况是无需额外处理的因为多个强符号定义本身就是非法的链接器会报符号多重定义错误链接器要处理的就是后两种情况 现在的编译器和链接器都支持一种叫 COMMON 块的机制这种机制最早来源于 Fortran细节不多说了自己去了解。主要想表达的是当不同的目标文件需要的 COMMON 块空间大小不一致时以最大的那块为准。 现代的链接器在处理弱符号的时候采用的就是与 COMMON 块一样的机制。我们的样例 SimpleSection.c 中符号 global_uninit_var 就是一个弱符号我们看一下它的相关信息 ...12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM golbal_uninit_var...它的类型是 SHN_COMMON size 为 4。假设我们在另外一个文件中也定义了 global_uninit_var 变量。且未初始化它的类型为 double占 8 个字节情况会怎样 事实上按照 COMMON 类型的链接规则最终链接后输出文件中 global_uninit_var 的大小以输入文件中最大的那个为准即 global_uninit_var 所占的空间为 8 字节。我们可以验证一下在另一个文件定义 double global_uninit_var然后编译和本章的样例进行链接然后 readelf -s 查看一下输出文件的符号表找到 global_uninit_var 的相关信息如下 72: 0000000000601048 8 OBJECT GLOBAL DEFAULT 26 golbal_uninit_var当然 COMMON 类型的链接规则是针对符号都是弱符号的情况如果其中有一个符号为强符号那么最终输出结果中的符号所占空间与强符号相同。值得注意的是如果链接过程中有弱符号大小大于强符号那么 ld 链接器会报如下警告
/usr/bin/ld: Warning: alignment 4 of symbol golbal_uninit_var in /tmp/ccWbmtFc.o is smaller than 8 in /tmp/ccZVSY46.o这种使用 COMMON 块的方法实际上是一种类似“黑客”的取巧办法直接导致需要 COMMON 机制的原因是编译器和链接器允许不同类型的弱符号存在。但本质原因还是链接器不支持符号类型即链接器无法判断各个符号的类型是否一致但是在每个输出文件的符号表中都有 COMMON 块相应符号大小每个输出文件编译的时候就生成了链接器在所有需要链接的文件中对相同符号名选择大小最大的为准 。 通过上面对链接器处理多个弱符号的过程我们可以知道编译成目标文件时如果包含了弱符号未初始化的全局变量就是典型的弱符号那么该弱符号最终所占的空间大小在此时是未知的因为有可能其他编译单元中该符号所占的空间与本编译单元该符号占的空间大小不一致。所以编译器此时无法为该符号在 BSS 段分配空间因为所需空间的大小未知。但是链接器在链接过程中就可以确定弱符号的大小了它可以在最终输出文件的 BSS 段为其分配空间。所以未初始化全局变量最终是被放在 BSS 段的。 GCC的“-fno-common”也允许我们把所有未初始化的全局变量不以 COMMON 块的形式处理或者使用“__attribute__”扩展: int global _attribute_((nocommon))一旦一个未初始化的全局变量不是以 COMMON 块的形式存在那么它就相当于一个强符号如果其他目标文件中还有一个同一个变量的强符号定义链接时就会发生符号重定义错误。
4、静态库链接
4.1 静态库的链接 其实一个静态库可以简单地看成一组目标文件地集合即很多目标文件经过压缩打包后形成地一个文件。比如我们在Linux中最常用地C语言静态库libc.a它是glibc项目的一部分。 我们知道在一个C语言的运行库中包含了很多跟系统功能相关的代码比如输入输出、文件操作、时间日期、内存管理等。glibc本身是用C语言开发的它由成千上百个C语言源代码文件组成也就是说编译完成以后有相同数量的目标文件比如输入输出printf.o、scanf.o文件操作有fread.o、fwrite.o等。把这些零散的目标文件直接提供给库的使用者很大程序上会造成文件传输、管理和组织方面的不便于是通常人们使用 “ar” 压缩程序将这些目标文件压缩到一起并且对其进行编号和索引以便于查找和检索就形成了libc.a这个静态库文件。我们可以使用 “ar” 工具来查看这个文件包含了那些目标文件
liangliang-virtual-machine:/usr/lib/x86_64-linux-gnu$ ar -t libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
dso_handle.o
errno.o
errno-loc.o
iconv_open.o
iconv.o
iconv_close.o
gconv_open.o
...使用objdump 和 grep 工具来查看libc.a的符号可以发现如下结果
liangliang-virtual-machine:/usr/lib/x86_64-linux-gnu$ objdump -t libc.a | grep -w printf
reg-printf.o: file format elf64-x86-64
printf-prs.o: file format elf64-x86-64
printf.o: file format elf64-x86-64
0000000000000000 g F .text 000000000000009e printf
printf-parsemb.o: file format elf64-x86-64
printf-parsewc.o: file format elf64-x86-64可以看到“printf” 函数被定义在 “printf.o” 的目标文件中。这里我们似乎找到了最终的机制那就是“Hello World”程序编译出来的目标文件只要和 libc.a 里面的 “printf.o” 链接在一起最后就可以形成一个可用的可执行文件了。虽然很接近最后的答案了但是还是会有问题。当具体去尝试的时候我们会发现还是会报找不到其他符号的错误。这是因为“printf.o” 又依赖其它的一些目标文件… 可以看到如果单靠人工来完成链接一个“Hello World”小程序都这么费劲那么一个大的工程就可想而知了。幸好 ld 链接器会处理着一切繁琐的事务自动寻找所有需要的符号及他们所在的目标文件将这些目标文件从“libc.a”中“解压”出来最终将他们链接在一起成为一个可执行文件。 现在Linux系统上的库比我们想象的要复杂许多。当我们编译链接一个普通C程序的时候不仅要用到C语言的libc.a而且还有其它一些辅助性的目标文件和库。
--verbose打印出编译链接时的详细信息-static默认情况下gcc采用动态链接的方式连接第三方库,比如指定-lpng链接程序就会去找libpng.so。指定了 -static 这个选项gcc在链接时对项目所有的依赖库都尝试去搜索名为libname.a的静态库文件完成静态连接如果找不到就报错。
liangliang-virtual-machine:~/cfp$ gcc -static --verbose -fno-builtin Hello.c
Using built-in specs.
COLLECT_GCCgcc
COLLECT_LTO_WRAPPER/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversionUbuntu 5.4.0-6ubuntu1~16.04.12 --with-bugurlfile:///usr/share/doc/gcc-5/README.Bugs --enable-languagesc,ada,c,java,go,d,fortran,objc,obj-c --prefix/usr --program-suffix-5 --enable-shared --enable-linker-build-id --libexecdir/usr/lib --without-included-gettext --enable-threadsposix --libdir/usr/lib --enable-nls --with-sysroot/ --enable-clocalegnu --enable-libstdcxx-debug --enable-libstdcxx-timeyes --with-default-libstdcxx-abinew --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awtgtk --enable-gtk-cairo --with-java-home/usr/lib/jvm/java-1.5.0-gcj-5-amd64/jre --enable-java-home --with-jvm-root-dir/usr/lib/jvm/java-1.5.0-gcj-5-amd64 --with-jvm-jar-dir/usr/lib/jvm-exports/java-1.5.0-gcj-5-amd64 --with-arch-directoryamd64 --with-ecj-jar/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32i686 --with-abim64 --with-multilib-listm32,m64,mx32 --enable-multilib --with-tunegeneric --enable-checkingrelease --buildx86_64-linux-gnu --hostx86_64-linux-gnu --targetx86_64-linux-gnu
Thread model: posix
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.12)
COLLECT_GCC_OPTIONS-static -v -fno-builtin -mtunegeneric -marchx86-64
/usr/lib/gcc/x86_64-linux-gnu/5/cc1 -quiet -v -imultiarch x86_64-linux-gnu Hello.c -quiet -dumpbase Hello.c -mtunegeneric -marchx86-64 -auxbase Hello -version -fno-builtin -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccbev2gR.s
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand100 --param ggc-min-heapsize131072
ignoring nonexistent directory /usr/local/include/x86_64-linux-gnu
ignoring nonexistent directory /usr/lib/gcc/x86_64-linux-gnu/5/../../../../x86_64-linux-gnu/include
#include ... search starts here:
#include ... search starts here:/usr/lib/gcc/x86_64-linux-gnu/5/include/usr/local/include/usr/lib/gcc/x86_64-linux-gnu/5/include-fixed/usr/include/x86_64-linux-gnu/usr/include
End of search list.
GNU C11 (Ubuntu 5.4.0-6ubuntu1~16.04.12) version 5.4.0 20160609 (x86_64-linux-gnu)compiled by GNU C version 5.4.0 20160609, GMP version 6.1.0, MPFR version 3.1.4, MPC version 1.0.3
GGC heuristics: --param ggc-min-expand100 --param ggc-min-heapsize131072
Compiler executable checksum: 8087146d2ee737d238113fb57fabb1f2
COLLECT_GCC_OPTIONS-static -v -fno-builtin -mtunegeneric -marchx86-64as -v --64 -o /tmp/ccXqEnrN.o /tmp/ccbev2gR.s
GNU assembler version 2.26.1 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.26.1
COMPILER_PATH/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/
LIBRARY_PATH/usr/lib/gcc/x86_64-linux-gnu/5/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/5/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS-static -v -fno-builtin -mtunegeneric -marchx86-64/usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt-fresolution/tmp/ccx6TzCJ.res -plugin-opt-pass-through-lgcc -plugin-opt-pass-through-lgcc_eh -plugin-opt-pass-through-lc --sysroot/ --build-id -m elf_x86_64 --hash-stylegnu --as-needed -static -z relro /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/ccXqEnrN.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o
下面我们讲解下这里面的三个关键步骤。 第一步是调用 cc1 程序这个程序实际上就是GCC的C语言编译器它将“Hello.c”编译成一个临时的汇编文件“/ccbev2gR.s”。
/usr/lib/gcc/x86_64-linux-gnu/5/cc1 -quiet -v -imultiarch x86_64-linux-gnu Hello.c -quiet -dumpbase
Hello.c -mtunegeneric -marchx86-64 -auxbase Hello -version -fno-builtin
-fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccbev2gR.s然后调用 as 程序as 程序是GNU的汇编器它将“/tmp/ccbev2gR.s”汇编成临时目标文件“/tmp/ccXqEnrN.o”。这个“/tmp/ccXqEnrN.o”实际上就是前面的“Hello.o”。
as -v --64 -o /tmp/ccXqEnrN.o /tmp/ccbev2gR.s接着最关键的是最后一部GCC调用 collect2 程序来完成最后的链接。实际上 collect2 可以看作是 ld 链接器的一个包装他会调用 ld 链接器来完成对目标文件的链接然后再对链接结果进行一些处理主要是收集所有与程序初始化相关的信息并且构造初始化的结构。在后面我们会介绍程序的初始化结构相关的内容还会再介绍 collect2 程序。 /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/5/liblto_plugin.so -plugin-opt/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt-fresolution/tmp/ccx6TzCJ.res -plugin-opt-pass-through-lgcc -plugin-opt-pass-through-lgcc_eh -plugin-opt-pass-through-lc --sysroot/ --build-id -m elf_x86_64 --hash-stylegnu --as-needed -static -z relro /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o
-L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib
-L/lib/x86_64-linux-gnu -L/lib/../lib
-L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib
-L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/ccXqEnrN.o
--start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o
/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o可以看到最后一步中至少有下列几个库和目标文件被链接入了最终可执行文件 crt1.o 、crti.o、crtbeginT.o、libgcc.a、libgcc_eh.a、libc.a、crtend.o、crtn.o
4.2 关于GCC
GCC官网 GCC, the GNU Compiler Collection。GCC的官网的开头一段话 The GNU Compiler Collection includes front ends for C, C, Objective-C, Fortran, Ada, Go, and D, as well as libraries for these languages (libstdc,…). GCC was originally written as the compiler for the GNU operating system. The GNU system was developed to be 100% free software, free in the sense that it respects the user’s freedom. 翻译过来就是 GNU Compiler Collection 包括 C、C、Objective-C、Fortran、Ada、Go 和 D 的前端以及这些语言的库libstdc…。 GCC 最初是作为 GNU 操作系统的编译器编写的。 GNU 系统被开发为 100% 自由软件自由是因为它尊重用户的自由。 我们可以看到GCC实际上是一个编译器套装里面包含了各种的编译器以及编译器自带的一些库。以gcc为例我们最常使用的就是 gcc Hello.c -o Hello。短短这句话其实包含了很多内容比较重要的就是编译、汇编、链接三个步骤就像前面静态库链接里面所讲的。
编译器C语言通常用的是 cc1C用的是 cc1 pluse汇编器常见的就是 as链接器ld、colletc2colletc2 会间接调用 ld 我们可以把 gcc 理解成一个总的调度程序它按照需求去调用cpp/cc1/as/collect2/ld等程序cpp是预处理完成对应四个过程。为什么要一个总的调度程序 通过前面的讲解知道虽然我们能够自己调用cpp/cc1/as/collect2/ld来完成四个过程得到最后的可执行文件但是如果我们都手动去执行这4个过程就非常麻烦。有了gcc后可以调用gcc一次性快速完成四个过程gcc会自动调用cpp/cc1/as/collect2/ld来完成。一次性完成时中间产生的.i/.s/.o都是临时文件编译后会被自动删除这些文件我们并不关心。使用gcc这个总调度程序一次性完成所有过程时编译速度非常快用起来非常方便。
Tips Windows下我们常见的是MinGW编译器。 Windows下的MinGW全称Minimalist GNU For Windows是个精简的Windows平台C/C、ADA及Fortran编译器相比Cygwin而言体积要小很多使用较为方便。MinGW提供了一套完整的开源编译工具集以适合Windows平台应用开发且不依赖任何第三方C运行时库。 MinGW包括 一套集成编译器包括C、C、ADA语言和Fortran语言编译器 用于生成Windows二进制文件的GNU工具的编译器、链接器和档案管理器 用于Windows平台安装和部署MinGW和MSYS的命令行安装器mingw-get 用于命令行安装器的GUI打包器mingw-get-inst MinGW 可以理解为GCC在windows平台下的实现。但是MinGW使用Windows中的C运行库因此用MinGW开发的程序不需要额外的第三方DLL支持就可以直接在Windows下运行而且也不一定必须遵从GPL许可证这同时造成了MinGW开发的程序只能使用Win32API和跨平台的第三方库而缺少POSIX支持大多数GNU软件无法在不修改源代码的情况下用MinGW编译。
5、链接过程控制 整个链接过程中除了需要确定链接的目标文件、静态库之外还需要确定链接的规则和结果。大部分情况下链接器使用默认的链接规则对目标文件进行链接的。但对于一些特殊要求的程序比如内核、BIOS或者boot loader、嵌入式程序等往往受限于一些特殊的条件如需要指定输出文件的各个段虚拟地址、段的名称、段存放的顺序。其实除了上面这些信息之外链接过程可能还需要确定是否在可执行文件中保留调试信息、输出文件格式动态链接库还是可执行文件、是否导出某些符号以供调试器、程序本身或者其他程序使用等。这些都是可控制的。
5.1 链接控制脚本 前面我们在使用 ld 链接器的时候没有指定链接脚本其实 ld 在用户没有指定链接脚本的时候会使用默认链接脚本。我们可以使用下面命令行来查看 ld 默认链接脚本
liangjieliangjie-virtual-machine:~/Desktop$ ld -verbose链接器一般提供多种方法来控制整个链接过程以产生用户所需要的文件。一般有以下这三种方法
使用命令行来给链接器指定参数比如使用 ld 的-o、-e等参数就是这种方法将链接指令存放到目标文件里面编译器经常会通过这种方法向链接器传递指令。比如VISUAL C编译器会把链接参数放在 PE 目标文件的.drectve段来传递参数使用链接控制脚本这种方法最为灵活、最为强大
5.2 链接脚本详解 这里就偷个懒关于链接脚本如何使用等内容读者可以自行查阅相关资料。链接脚本可单独作为一个章节后面讲解。
6、 BFD 库 不同的平台可能有不同的目标文件格式即使同一个格式比如 ELF 在不同的软硬件平台都有着不同的变种。种种差异导致编译器和链接器很难处理不同平台之间的目标文件特别是对于像 GCC 和 binutils 这种跨平台的工具来说最好有一种统一的接口来处理这些不同格式之间的差异。 Binary File Descriptor library。BFD库是一个 GNU 项目它的目标是希望通过一个统一的接口来处理不同的目标文件。BFD项目本身是 binutils 项目的一个子项目。BFD 把目标文件抽象成一个统一模型比如在这个抽象模型中最开始有一个描述整个目标文件总体信息的“文件头”就跟我们实际的 ELF 文件一样文件头后面是一系列的一段每个段都有名字、属性和段的内容同时还抽象了符号表、定位表、字符串表等类似的概念使得 BFD 库的程序只要通过这个抽象的目标文件模型就可以实现操作所有 BFD 支持目标文件格式。 现在 GCC 更具体是GAS GNU Assembler、链接器ld、调试器 GDB 及 binutils 的其他工具都通过 BFD 库来处理目标文件而不是直接操作目标文件。这样做的最大好处就是将编译器和链接器同具体的文件格式隔离开来。一旦我们需要支持一种新的目标文件格式只须在 BFD 库中添加一种格式就可以了而不需要修改编译器和链接器。
7、静态链接的顺序问题 链接器使用直接读取可重定位目标文件和静态库的方法完成链接工作。符号解析阶段链接器会从左到右按照它们在编译器驱动程序命令行上出现顺序来扫描可重定位目标文件和静态库文件。在扫描的整个周期中链接器会维护一个可重定位目标文件的集合(集合E中的文件最终会被合并起来形成可执行文件)。一个未解析的符号表U (即集合E中的目标文件中引用了符号但集合E中所有目标文件都未定义该符号) 以及已定义的符号集合D (即集合E中的所有目标文件中定义的符号集合) 。在扫描初始阶段集合E、U和D都是空的。 对于命令行上的每个输入文件 f 链接器会判断 f 是目标文件还是静态库。
如果 f 是目标文件那么链接器把 f 添加到E将文件 f 中的符号定义和引用该引用在D中没有定义分别添加到集合 D 和集合 U 中并继续下一个输入文件。如果 f 是静态库文件那么链接器就尝试匹配 U 中未解析的符号和静态库成员定义的符号。静态库匹配顺序就是静态库的打包顺序可使用 nm 指令查看静态库顺序。如果静态库文件中某个目标文件成员 m定义了一个符号来解析 U 中的一个引用那么就将目标文件 m 加入到 E 中并且将 m 中的符号定义和引用 该引用在集合D中不存在 分别添加到集合 D 和集合 E 中。对于静态库文件中所有的成员目标文件都依次进行这个过程并且循环多次直到集合 U 和 D 都不再发生变化。此时静态库文件中任何不包含在 E 中的成员目标文件都会被直接丢弃而链接器将继续处理下一个目标文件或库文件。 如果当链接器完成对命令行上输入文件的扫描后U是非空的那么链接器将会输出一个错误并终止。否则它会合并和重定位E中的目标文件构建输出的可执行文件。 由上可知GCC链接时对依赖库/目标文件的顺序是敏感的是因为在解析过程中先被解析的文件中定义的所有符号如果没有被集合E中的目标文件引用到 (即使一个符号被引用也会添加到E中) 该文件会被直接丢弃而在后续文件中即使再有该符号的引用但已经找不到该符号的定义了所以链接器会报错。解决这一问题的方法一般是调整好依赖顺序或者将所有的库文件打包成一个单独的库文件。 针对链接顺序的问题gcc 也给出了两个可能的解决方法。 GCC传递给链接器 ld 可以同时使用 Xlinker 和 Wl 两种命令这两个命令都可以正确传递给 ld 作为使用 使用 --whole-archive。链接参数“–whole-archive”来告诉链接器将后面库中所有符号都链接进来也就是不会丢弃库里面的任何目标文件参数“-no-whole-archive”则是重置以避免后面库的所有符号被链接进来。使用 -Xlinker。Xlinker主要是解决静态库循环依赖问题。链接器在处理 “-(” 和 “-)” 之间的静态库时是会重复查找这些静态库的所以就解决了静态库查找顺序问题。不过这种方式比人工提供了链接顺序的方式效率会低很多。例如 liba.a 依赖 libb.a同时libb.a 依赖 liba.a
gcc main.cpp -Xlinker -( libb.a liba.a -Xlinker -)在linux下我们可以使用 man ld 来查看 ld 链接器的一些参数含义 -( archives -)--start-group archives --end-groupThe archives should be a list of archive files. They may be eitherexplicit file names, or -l options.The specified archives are searched repeatedly until no newundefined references are created. Normally, an archive is searchedonly once in the order that it is specified on the command line.If a symbol in that archive is needed to resolve an undefinedsymbol referred to by an object in an archive that appears later onthe command line, the linker would not be able to resolve thatreference. By grouping the archives, they all be searchedrepeatedly until all possible references are resolved.Using this option has a significant performance cost. It is bestto use it only when there are unavoidable circular referencesbetween two or more archives.