如果读过我之前的文章就会知道,程序构建大概需要经历四个过程:预处理、编译、汇编、链接,这里主要介绍链接这一过程。
链接链的是什么?
链接链的就是目标文件,什么是目标文件?目标文件就是源代码编译后但未进行链接的那些中间文件,如Linux下的.o,它和可执行文件的内容和结构很相似,格式几乎是一样的,可以看成是同一种类型的文件,Linux下统称为ELF文件,这里介绍下ELF文件标准:
- 可重定位文件:Linux中的.o,这类文件包含代码和数据,可被链接成可执行文件或共享目标文件,例如静态链接库。
- 可执行文件:可以直接执行的文件,如/bin/bash文件。
- 共享目标文件:Linux中的.so,包含代码和数据,一种是链接器可以使用这种文件和其它的可重定位文件和共享目标文件链接,另一种是动态链接器可以将几个这种共享目标文件和可执行文件结合,作为进程映像的一部分来执行。
- core dump文件:进程意外终止时,系统可以将该进程的地址空间的内容和其它信息存到coredump文件用于调试,如gdb。
我们可以使用command file来查看文件的格式:
file test.o; file /bin/bash;
file test.o
test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
目标文件的构成
目标文件主要分为文件头、代码段、数据段和其它。
-
文件头:描述整个文件的文件属性(文件是否可执行、是静态链接还是动态链接、入口地址、目标硬件、目标操作系统等信息),还包括段表,用来描述文件中各个段的数组,描述文件中各个段在文件中的偏移位置和段属性。
-
代码段:程序源代码编译后的机器指令。
-
数据段:数据段分为.data段和.bss段。
-
.data段内容:已经初始化的全局变量和局部静态变量
-
.bss段内容:未初始化的全局变量和局部静态变量,.bss段只是为未初始化的全局变量和局部静态变量预留位置,本身没有内容,不占用空间。
-
除了代码段和数据段,还有.rodata段、.comment、字符串表、符号表和堆栈提示段等等,还可以自定义段。
.bss段不占用存储空间?
看下面代码:
#include <stdio.h>
int a[1000];
int b[1000] = {1};
int main() {
printf("程序喵\n");
return 0;
}
我们查看下文件大小和各个段大小:
$ gcc testlink.c -o test
$ ls -l test
-rwxrwxrwx 1 wzq wzq 12368 May 30 08:48 test
$ size test
text data bss dec hex filename
1512 4616 4032 10160 27b0 test
再看这段初始化的代码:
#include <stdio.h>
int a[1000] = {1};
int b[1000] = {1};
int main() {
printf("程序喵\n");
return 0;
}
再查看下文件大小和各个段大小:
$ gcc testlink.c -o test
$ ls -l test
-rwxrwxrwx 1 wzq wzq 16368 May 30 08:49 test
$ size test
text data bss dec hex filename
1512 8616 8 10136 2798 test
可以看到仅仅是做了一次初始化,文件大小就从12368变成了16368,正好是初始化了的那a[1000]的大小,这4000字节从.bss段移动到了.data段,程序大小增加了,这里可以看出.bss段不占据磁盘空间。
既然.bss段不占据空间,那它的大小和符号存在哪呢?
-
.bss段占据的大小存放在ELF文件格式中的段表(Section Table)中,段表存放了各个段的各种信息,比如段的名字、段的类型、段在elf文件中的偏移、段的大小等信息。同时符号存放在符号表.symtab中。
-
.bss不占据实际的磁盘空间,只在段表中记录大小,在符号表中记录符号。当文件加载运行时,才分配空间以及初始化。
其实程序里还有好多系统保留段,还可以自定义段,将某个变量放在自定义段,如下:
__attribute__((section("Custom"))) int global = 1;
可以使用一些工具查看ELF文件头以及各个段的内容:
gcc -c testlink.c -o test.o
readelf -h test.o
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 8768 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 64 (字节)
节头数量: 13
字符串表索引节头: 12
查看段表的方法:
使用objdump查看ELF文件中包含的关键的段:
objdump -h test.o
test.o: 文件格式 elf64-x86-64
节:
Idx Name Size VMA LMA File off Algn
0 .text 00000017 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00001f40 0000000000000000 0000000000000000 00000060 2**5
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00001fa0 2**0
ALLOC
3 .rodata 0000000a 0000000000000000 0000000000000000 00001fa0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002a 0000000000000000 0000000000000000 00001faa 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00001fd4 2**0
CONTENTS, READONLY
6 .eh_frame 00000038 0000000000000000 0000000000000000 00001fd8 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
readelf -S test.o
readelf -S test.o
There are 13 section headers, starting at offset 0x2240:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000017 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00002190
0000000000000030 0000000000000018 I 10 1 8
[ 3] .data PROGBITS 0000000000000000 00000060
0000000000001f40 0000000000000000 WA 0 0 32
[ 4] .bss NOBITS 0000000000000000 00001fa0
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 00001fa0
000000000000000a 0000000000000000 A 0 0 1
[ 6] .comment PROGBITS 0000000000000000 00001faa
000000000000002a 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 00001fd4
0000000000000000 0000000000000000 0 0 1
[ 8] .eh_frame PROGBITS 0000000000000000 00001fd8
0000000000000038 0000000000000000 A 0 0 8
[ 9] .rela.eh_frame RELA 0000000000000000 000021c0
0000000000000018 0000000000000018 I 10 8 8
[10] .symtab SYMTAB 0000000000000000 00002010
0000000000000150 0000000000000018 11 9 8
[11] .strtab STRTAB 0000000000000000 00002160
0000000000000030 0000000000000000 0 0 1
[12] .shstrtab STRTAB 0000000000000000 000021d8
0000000000000061 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
objdump只能查看关键的段,而readelf可以查看所有段。
其中,.rela.text是针对.text段的重定位表,链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位,即代码段和数据段那些对绝对地址的引用的位置,这些重定位的信息都会放在.rela.text中,.rel开头的都是用于重定位。
-
LINK表示符号表的下标,INFO表示它作用于哪个段,值是相应段的下标。
-
字符串表(.strtab):保存普通字符串,比如符号名字。
-
段表字符串表(.shstrtab):保存段表中用到的字符串,比如段名。
-
ELF文件头和段表都有各自的结构体,这里不列举,只需要知道它里面存储的是什么东西就好。
程序为什么要分成数据段和代码段
-
数据和指令被映射到两个虚拟内存区域,数据段对进程来说可读写,代码段是只读,这样可以防止程序的指令被有意无意的改写。
-
有利于提高程序局部性,现代CPU缓存一般被设计成数据缓存和指令缓存分离,分开对CPU缓存命中率有好处。
-
代码段是可以共享的,数据段是私有的,当运行多个程序的副本时,只需要保存一份代码段部分。
经典语录:真正了不起的程序员对自己程序的每一个字节都了如指掌。
链接器通过什么进行的链接
链接的接口是符号,在链接中,将函数和变量统称为符号,函数名和变量名统称为符号名。链接过程的本质就是把多个不同的目标文件之间相互“粘”到一起,像玩具积木一样各有凹凸部分,有固定的规则可以拼成一个整体。
可以将符号看作是链接中的粘合剂,整个链接过程基于符号才可以正确完成,符号有很多类型,主要有局部符号和外部符号,局部符号只在编译单元内部可见,对于链接过程没有作用,在目标文件中引用的全局符号,却没有在本目标文件中被定义的叫做外部符号,以及定义在本目标文件中的可以被其它目标文件引用的全局符号,在链接过程中发挥重要作用。
可以使用一些命令来查看符号信息:
command nm:
nm test.o
0000000000000000 D a
0000000000000fa0 D b
U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
U puts
objdump -t test.o
test.o: 文件格式 elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 testlink.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000fa0 a
0000000000000fa0 g O .data 0000000000000fa0 b
0000000000000000 g F .text 0000000000000017 main
0000000000000000 *UND* 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000 *UND* 0000000000000000 puts
command readelf:
readelf -s test.o
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS testlink.c
2: 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 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 4000 OBJECT GLOBAL DEFAULT 3 a
10: 0000000000000fa0 4000 OBJECT GLOBAL DEFAULT 3 b
11: 0000000000000000 23 FUNC GLOBAL DEFAULT 1 main
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
有些符号在程序中并没有被定义,但是可以直接声明并且引用的符号称为特殊符号,这些符号其实是定义在ld链接器脚本中的,如下面代码中的符号:
#include <stdio.h>
extern char __executable_start[];
extern char etext[], _etext[], __etext[];
extern char edata[], _edata[];
extern char end[], _end[];
int main() {
printf("Executable Start %X \n", __executable_start);
printf("Text End %X %X %X \n", etext, _etext, __etext);
printf("Data End %X %X \n", edata, _edata);
printf("Executable End %X %X \n", end, _end);
return 0;
}
输出:
$ ./a.out
Executable Start 68800000
Text End 6880075D 6880075D 6880075D
Data End 68A01010 68A01010
Executable End 68A01018 68A01018
为什么需要extern “C”
C语言函数和变量的符号名基本就是函数名字变量名字,不同模块如果有相同的函数或变量名字就会产生符号冲突无法链接成功的问题,所以C++引入了命名空间来解决这种符号冲突问题。同时为了支持函数重载C++也会根据函数名字以及命名空间以及参数类型生成特殊的符号名称。
由于C语言和C++的符号修饰方式不同,C语言和C++的目标文件在链接时可能会报错说找不到符号,所以为了C++和C兼容,引入了extern “C”,当引用某个C语言的函数时加extern “C”告诉编译器对此函数使用C语言的方式来链接,如果C++的函数用extern “C”声明,则此函数的符号就是按C语言方式生成的。
以memset函数举例,C语言中以C语言方式来链接,但是在C++中以C++方式来链接就会找不到这个memset的符号,所以需要使用extern “C”方式来声明这个函数,为了兼容C和C++,可以使用宏来判断,用条件宏判断当前是不是C++代码,如果是C++代码则extern “C”。
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
这种技巧几乎在所有的系统头文件中都会被用到。
强符号和弱符号
我们经常编程中遇到的multiple definition of ‘xxx’,指的是多个目标中有相同名字的全局符号的定义,产生了冲突,这种符号的定义指的是强符号。有强符号自然就有弱符号,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。attribute((weak))可以定义弱符号。
extern int ext;
int weak; // 弱符号
int strong = 1; // 强符号
__attribute__((weak)) int weak2 = 2; // 弱符号
int main() {
return 0;
}
链接器规则:
- 不允许强符号被多次定义,多次定义就会multiple definition of ‘xxx’
- 一个符号在一个目标文件中是强符号,在其它目标文件中是弱符号,选择强符号
- 一个符号在所有目标文件中都是弱符号,选择占用空间最大的符号,int类型和double类型选double类型
强引用和弱引用
一般引用了某个函数符号,而这个函数在任何地方都没有被定义,则会报错error: undefined reference to ‘xxx’,这种符号引用称为强引用。与此对应的则有弱引用,链接器对强引用弱引用的处理过程几乎一样,只是对于未定义的弱引用,链接器不会报错,而是默认其是一个特殊的值。
今天的文章谈谈程序链接及分段那些事分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/57449.html