一、链接文件
通过前面的文章,可以了解在Linux下的ELF文件的大概的分类。而且也知道了,链接主要是链接的目标文件。那么目标文件是指什么呢?这个很好理解,就是编译源文件后中间的文件,包括.o(可重定位目标文件)、.so(共享目标文件)、可执行目标文件和转储文件。其中前两个是常用的链接目标文件,可能大多数的开发者一看就明白了。后两个它只是目标文件,但一般不会被链接。
二、目标文件的组成
在前面的链接流程中已经对链接的目标文件的具体内容进行过说明。此处再次总结一下,目标文件的组成主要包括:
- 文件头
文件头是对整个文件的一主属性的描述或者说是元数据的集合。包括执行类型、入口地址、目标平台等等。最重要是要有段表,这是链接的最重要的部分 - 代码段
.text节,存放程序编译后的机器指令即cpu可直接执行的二进制代码。也就是开发者编写的代码中的函数、流程控制等语句 - 数据段
.data节,主要是已初始化的全局变量和局部静态变量;.bss节,未初始化的全局变量和局部静态变量,本身不占用空间。它的符号和相关信息存储在符号表和段表中 - 其它
当然,除了上面的还有几个段,如.rodata、.strtab(字符串表)和shstrtab(段表字符串表)段等。有兴趣的可以查找一下相关的资料
三、段查看
为了能够更好的展现相关的段的查看过程,下面给出一个包含两个编译单元的例程:
//main.cpp
extern int getID();
#include <iostream>
char buf[100] = {0};
char getData(int id) {
if (id < 100 && id >= 0) {
return buf[id];
}
return '0';
}
void init() {
for (int i = 0; i < 100; i++) {
buf[i] = i;
}
}
int main() {
int id = 0;
init();
id = getID();
std::cout << "get random id:" << id << std::endl;
char b = getData(id);
std::cout << "cur id char:" << b << std::endl;
return 0;
}
//myrandom.cpp
#include <stdlib.h>
#include <time.h>
int getID() {
srandom((unsigned int)time(NULL));
return random() % 100;
}
这里将使用常见工具如objdump和readelf等进行相关的属性和段节的查看。可以使用下面的命令进行目标文件的编译:
#编译中间文件
g++ -c main.cpp myrandom.cpp #生成中间目标文件main.o myrandom.o
#编译可执行文件
g++ -o main main.cpp myrandom.cpp #生成可执行文件main
可以使用下面的命令查看相关的节大小:
size main
text data bss dec hex filename
2784 672 392 3848 f08 main
可以使用readelf的不同参数来查看具体的信息,查看文件头信息(readelf -h )、查看节头表(readelf -S)、查看程序头表(readelf -l )、查看符号表 (readelf -s)、查看动态符号表(readelf --dyn-syms)、查看重定位信息(readelf -r)、查看文件依赖(readelf -d)等等,下面给出一个查看文件头的例子:
readelf main.o -h
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 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: 1880 (bytes into file)
标志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
objdump可以提供类似的功能,它的功能非常强大也更通用,但在查看段信息上,则更关注于关键的段。下面的命令只查看代码段:
objdump -d -j .text main
main: 文件格式 elf64-x86-64
Disassembly of section .text:
0000000000001120 <_start>:
1120: f3 0f 1e fa endbr64
1124: 31 ed xor %ebp,%ebp
1126: 49 89 d1 mov %rdx,%r9
1129: 5e pop %rsi
112a: 48 89 e2 mov %rsp,%rdx
112d: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
1131: 50 push %rax
1132: 54 push %rsp
1133: 45 31 c0 xor %r8d,%r8d
1136: 31 c9 xor %ecx,%ecx
1138: 48 8d 3d 2d 01 00 00 lea 0x12d(%rip),%rdi # 126c <main>
113f: ff 15 9b 2e 00 00 call *0x2e9b(%rip) # 3fe0 <__libc_start_main@GLIBC_2.34>
1145: f4 hlt
1146: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
114d: 00 00 00
0000000000001150 <deregister_tm_clones>:
1150: 48 8d 3d b9 2e 00 00 lea 0x2eb9(%rip),%rdi # 4010 <__TMC_END__>
1157: 48 8d 05 b2 2e 00 00 lea 0x2eb2(%rip),%rax # 4010 <__TMC_END__>
115e: 48 39 f8 cmp %rdi,%rax
以下省略
当然,也可以直接使用更多的辅助参数来让objdump命令显示的内容更丰富。
四、链接说明
链接器处理链接,离不开.rela.text。它是ELF文件格式中专门用于代码重定位的一个节(Section),是.text 节的重定位表,存储着链接阶段需要修正的地址信息。它是元数据节区,是编译器生成的机器码与链接器之间的“桥梁”,确保代码中对未知地址的引用能在链接阶段被正确修正。
之所以在编译后将编译后的代码划分成多个段,最重要的就是权限控制和可重用,并在运行时有效的利用局部性原理。一般来说,代码的功能一般是可重用的,比如一个程序可以启动多个进程,但数据往往是不能共享的,每个进程必须有自己的数据区。其它的段亦是如此。
-
链接的方式
链接目标文件,用江湖黑话来说,得有一个抓手。用编程的角度看就是要有个handle,而在链接中,则称为符号,其实就是一个ID。它代表着一系列的变量和函数以及其它相关标记。链接的过程就是把这些符号理顺清晰,把不同目标文件的调用逻辑、入口地址等有机的统一到一个整体中。
在链接的符号中,可以像编程语言中有一样有外部符号和局部符号。而在链接中起主要作用的就是外部符号,它本质也是变量、函数等的处理的过程。这也是为什么在链接是出现问题总是报哪个函数或变量未定义之类的原因。
要想查看符号信息,就离不开刚刚说明的两个命令以及以前查看库依赖和改名机制的nm命令等。可以通过它们来查看符号表和相关的符号信息,如下的例子:nm main 00000000000021b0 r __abi_tag 0000000000004040 B __bss_start 0000000000004160 B buf 0000000000004150 b completed.0 w __cxa_finalize@GLIBC_2.2.5 0000000000004000 D __data_start 0000000000004000 W data_start 0000000000001150 t deregister_tm_clones 00000000000011c0 t __do_global_dtors_aux 0000000000003d78 d __do_global_dtors_aux_fini_array_entry #或才使用 objdump -t main main: 文件格式 elf64-x86-64 SYMBOL TABLE: 0000000000000000 l df *ABS* 0000000000000000 Scrt1.o 00000000000021b0 l O .note.ABI-tag 0000000000000020 __abi_tag 0000000000000000 l df *ABS* 0000000000000000 crtstuff.c 0000000000001150 l F .text 0000000000000000 deregister_tm_clones 0000000000001180 l F .text 0000000000000000 register_tm_clones 00000000000011c0 l F .text 0000000000000000 __do_global_dtors_aux 0000000000004150 l O .bss 0000000000000001 completed.0另外在实际的应用中,还有一类特殊符号即可以在程序中直接声明引用但并没有定义的符号。它一般是直接定义在链接器脚本中的。这里面有一个典型的例子,就是c/c++混合编程中的extern “C”。它的目的变是为了解决c++的更名机制导致的符号的改变。
-
强符号和弱符号
在C/C++的编译链接过程中,Strong Symbol)和Weak Symbol是链接器处理全局变量和函数定义时的两种不同属性。它们主要用来处理多目标文件中存在同名符号问题,即出现这种情况时,链接器应该如何进行选择来决定最终的定义。它是在链接过程中出现“重复定义”的重要底层机制。
强符号是指程序中明确且唯一的定义,如果无法找到则会报错。它一般是指已初始化的全局变量和函数定义。弱符号一般是指备选或默认的定义,如果链接器发现有强符号的实现,则优先使用发现的强符号。它一般是指未初始化的全局变量和显式声明的弱符号(如__attribute__((weak)))。
在实际的链接过程中,不允许出现强符号的重复定义,强符号和弱符号同时发现使用强符号,两人个均为弱符号则任意选择一个(一般选择占用空间大的符号或第一个符号),都未发现报链接错误。 -
强引用和弱引用
符号的目的是为了使用,也就是引用。这就会产生强引用和弱引用。它和强弱符号机制有着密切的联系。对于函数、变量等符号的引用,就是一种强引用。它如果无法找到,则会报“未找到引用”错误。而如果显式的指定了是一个弱引用,则即使找不到这个符号,同样也会链接通过。只不过在运行时会报异常错误。弱引用的应用主要是在库的工程开发中,可以通过弱引用机制来实现自定义库的显式接口覆盖。不过这对于大多数的开发者可能很难遇到这咱情况,了解一下即可
五、总结
在掌握了程序的运行和链接流程,并再次分析了链接过程中的相关细节后。对一个程序的生命周期应该有了一个更清晰完整的认知。正如军事名言所讲:知己知彼,百战不殆。想写好代码,就要先明白它的流程内部的运转。这才是根本。

2151

被折叠的 条评论
为什么被折叠?



