# 操作系统版本
cat /proc/version
Linux version 3.10.0-1160.95.1.el7.x86_64 ([email protected]) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC) ) #1 SMP Mon Jul 24 13:59:37 UTC 2023
# gcc版本
gcc -v
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
# g++ 版本
g++ -v
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
add.h
#pragma once
void add(int a, double b);
void add(double a, int b);
add.cpp
#include "add.h"
void add(int a, double b){}
void add(double a, int b){}
main.cpp
#include "add.h"
//宏定义
#define A 10
int main()
{
double b = 1.1;
add(A, b);
add(b, A);
return 0;
}
需要确保这三个文件在同一目录下
C++的编译和链接分为4个阶段:预处理/预编译、编译、汇编、链接
第一步的编译过程使用如下命令(-E 表示只进行编译)
g++ -E main.cpp -o main.i
这是预编译之后的main.i文件
# 1 "main.cpp"
# 1 ""
# 1 ""
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "" 2
# 1 "main.cpp"
# 1 "add.h" 1
void add(int a, double b);
void add(double a, int b);
# 2 "main.cpp" 2
int main()
{
double b = 1.1;
add(10, b);
add(b, 10);
return 0;
}
通过这个代码,可以看见原本定义在add.h中的函数声明,出现了在这里。对应的注释也被删除,对应的A也被替换成了10,而#define A 10 这行代码也消失了
对应的相关注释:
# 1 "main.cpp" 标志着文件开始,显示了当前处理的源文件是 main.cpp。
# 1 "" 表示接下来的内容是编译器内置的内容。
# 1 "" 表示接下来的内容是从命令行传递给编译器的参数。
# 1 "/usr/include/stdc-predef.h" 1 3 4 表示引用了标准库预定义的宏的头文件。
# 1 "" 2 表示继续处理命令行参数。
# 1 "main.cpp" 表示恢复到处理的源文件为 main.cpp。
# 1 "add.h" 1 表示引用了头文件 add.h。后续就展示了add.h 文件的内容。
void add(int a, double b);
void add(double a, int b);
# 2 "main.cpp" 2 表示恢复到处理 main.cpp 文件的第二行
int main()
{
double b = 1.1;
add(10, b);
add(b, 10);
return 0;
}
这还是一个非常简单的程序,因为它不包含任何对应的库文件,并且add.cpp中的实现为空,可以自行去包含一个任意一个库文件,例如#include
预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include"“#define”等,主要处理规则如下:
经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析、汇总所有的符号及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。
这里所谓汇总所有的符号,就是对应的函数名
使用以下命令对add.cpp进行处理
g++ -E add.cpp -o add.i
g++ -S add.i -o add.s
生成的内容如下:
.file "add.cpp"
.text
.globl _Z3addid
.type _Z3addid, @function
_Z3addid:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movsd %xmm0, -16(%rbp)
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size _Z3addid, .-_Z3addid
.globl _Z3adddi
.type _Z3adddi, @function
_Z3adddi:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movsd %xmm0, -8(%rbp)
movl %edi, -12(%rbp)
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size _Z3adddi, .-_Z3adddi
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits
其中例如movl、popq这些都是汇编指令,而以%开头的,例如%rbp,%edi这些都是寄存器
.LFB0: 标记着函数的起始点。在这之后通常会包含一系列指令,用于函数的实际执行
LFE0:标记着函数的结束点。在这之后通常会包含一些清理工作,例如出栈操作,并且可能有返回指令,将控制权返回到调用者
这些标签的命名通常是由汇编器生成的本地标签,以确保在程序中不会有冲突。 .LFB0, .LFB1, 等等是按序生成的标签,对应不同的函数
注意看C++是对函数名进行了修饰的
_Z表示前缀,3表示函数名的长度为3,add则表示函数名,i表示参数类型为int,d表示参数类型为double,因此组成了_Z3addid,对应函数void add(int a, double b),而_Z3adddi对应函数void add(double a, int b),这就是C++为什么支持函数重载,并且对返回值没有影响的原因。
总的来说,C++支持函数重载,因为对函数名进行了相应的修饰,修饰的规则就跟函数的参数类型,参数的个数,参数的顺序有关,跟返回值没有关系
此时也可以重写一个add2.c文件,内容如下:
void add(int a, double b){}
进行预编译+编译
gcc -E add2.c -o add2.i
gcc -S add2.i -o add2.s
生成内容如下:
.file "add2.c"
.text
.globl add
.type add, @function
add:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp)
movsd %xmm0, -16(%rbp)
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size add, .-add
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
.section .note.GNU-stack,"",@progbits
确实没有对函数名进行任何修饰,这就是为什么C语言不支持函数重载的原因
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。
使用以下命令对add.s文件处理
g++ -c add.s -o add.o
生成的add.o文件就是二进制,可重定位的目标文件了,也就是对应的机器指令,无法查看
一个.cpp就对应一个.o文件
目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和 Linux下的.o),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用种格式存储。从广义上看,目标文件与可执行文件的格式其实几乎是一样的,所以我们可以广义地将目标文件与可执行文件看成是一种类型的文件,在Windows下,我们可以统称它们为PE-COFF文件格式。在Linux下,我们可以将它们统称为ELF文件。
ELF的结构如下:
这些是一些常见的段
.text(代码段):用于保存程序中的代码片段(指令),计算机在执行程序时,CPU就会从这里面取出指令再执行
.data(数据段):保存的是那些已经初始化了的全局静态变量和局部静态变量
.rodata(只读数据段/常量区):存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。单独设立“.rodata”段有很多好处,不光是在语义上支持了C++的const关键字,而且操作系统在加载的时候可以将“.rodata”段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理,保证了程序的安全性
.bss段:存放的是未初始化的全局变量和局部静态变量,如果一个变量初始化为0,那么它也会被放在.bss段中
请看以下代码:
static int x1 = 0;
static int x2 = 1;
int main()
{
return 0;
}
x1和x2会被放在什么段中呢?
x1会被放在.bss 中,x2会被放在.data中。为什么一个在.bss 段,一个在.data段?因为xl为0,可以认为是未初始化的,因为未初始化的都是O,所以被优化掉了可以放在.bss,这样可以节省磁盘空间,因为.bss不占磁盘空间。另外一个变量x2初始化值为1,是初始化
除了.text、.data、.bss 这3个最常用的段之外,ELF文件也有可能包含其他的段,用来保存与程序相关的其他信息
每个目标文件(可执行文件)除了都包含这些段之外,还有自己的堆和栈
ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表〈Section Header Table),该表描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性
可以使用命令objectdump -h add.o查看对应的段信息
[root@fl test]# objdump -h add.o
add.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000001c 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 0000005c 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 0000005c 2**0
ALLOC
3 .comment 0000002e 0000000000000000 0000000000000000 0000005c 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000008a 2**0
CONTENTS, READONLY
5 .eh_frame 00000058 0000000000000000 0000000000000000 00000090 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
其中 .comment表示注释信息段,.note.GNU-stack表示堆栈提示段
在异常处理过程中,需要知道如何展开函数调用的栈帧,以及在栈上保存的寄存器状态。.eh_frame段包含了这些信息,使得异常处理程序能够有效地回溯到调用栈的正确位置
实际上,objdump -h 命令只是把ELF文件中关键的段显示了出来,而忽略了其他的辅助性的段,比如:符号表、字符串表、段名字符串表、重定位表等。我们可以使用readelf 工具来查看ELF文件的段,它显示出来的结果才是真正的段表结构:
readelf -S main.o,这里查看main.o,而非add.o
[root@fl test]# readelf -S main.o
There are 12 section headers, starting at offset 0x2d0:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000004b 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000228
0000000000000030 0000000000000018 I 9 1 8
[ 3] .data PROGBITS 0000000000000000 0000008b
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 0000008b
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .comment PROGBITS 0000000000000000 0000008b
000000000000002e 0000000000000001 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 0000000000000000 000000b9
0000000000000000 0000000000000000 0 0 1
[ 7] .eh_frame PROGBITS 0000000000000000 000000c0
0000000000000038 0000000000000000 A 0 0 8
[ 8] .rela.eh_frame RELA 0000000000000000 00000258
0000000000000018 0000000000000018 I 9 7 8
[ 9] .symtab SYMTAB 0000000000000000 000000f8
0000000000000108 0000000000000018 10 8 8
[10] .strtab STRTAB 0000000000000000 00000200
0000000000000021 0000000000000000 0 0 1
[11] .shstrtab STRTAB 0000000000000000 00000270
0000000000000059 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)
.symtab表示符号表,我们将函数和变量统称为符号,每一个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他几种不常用到的符号。我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:
注意,这里的局部符号不是局部变量,而是定义在局部的静态变量
举一个简单的例子:
static int x1 = 1;
static int x2 = 2;
const int x3 = 3;
int x4 = 4;
int func(int a, int b)
{
return a + b;
}
int main()
{
static int x5 = 5;
int x6 = 6;
const int x7 = 7;
const char* p = "hello fl";
return 0;
}
将其汇编成test.o文件,再查看test.o的符号表
[root@fl test]# g++ -c test.cpp -o test.o
[root@fl test]# readelf -s test.o
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 4 OBJECT LOCAL DEFAULT 3 _ZL2x1
6: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 _ZL2x2
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 000000000000000c 4 OBJECT LOCAL DEFAULT 5 _ZL2x3
9: 000000000000000c 4 OBJECT LOCAL DEFAULT 3 _ZZ4mainE2x5
10: 0000000000000000 0 SECTION LOCAL DEFAULT 7
11: 0000000000000000 0 SECTION LOCAL DEFAULT 8
12: 0000000000000000 0 SECTION LOCAL DEFAULT 6
13: 0000000000000008 4 OBJECT GLOBAL DEFAULT 3 x4
14: 0000000000000000 20 FUNC GLOBAL DEFAULT 1 _Z4funcii
15: 0000000000000014 33 FUNC GLOBAL DEFAULT 1 main
通过对比可以看出,只有静态变量或者全局变量才会放在符号表中,而像x6、x7和p这样的局部变量,都没在符号表中,它们都是在栈上保存的
.strtab表示字符串表,用来保存普通的字符串,比如符号的名字
.shstrtab表示段表字符串表,用来保存段表中用到的字符串,最常见的就是段名
.rela.text表示重定位表,链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。比如 main.o 中的“.rel.text”就是针对“.text”段的重定位表,因为“.text”段中至少有两个绝对地址的引用,那就是对两个“add”函数的调用;而“.data”段则没有对绝对地址的引用,它只包含了一个常量,所以 main.o中没有针对“.data”段的重定位表“.rel.data”。
.symtab符号表
使用命令readelf -h add.o查看ELF文件头信息
[root@fl test]# readelf -h main.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 720 (bytes into file)
Flags: 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: 12
Section header string table index: 11
从上面输出的结果可以看到,ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABl版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。
整个链接过程主要分为两步。
第一步 空间与地址分配:扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
这里合并段的含义是合并相似的段,将所有输入文件的“.text”合并到输出文件的“.text”段,接着是“.data”段、“.bss”段等
第二步符号解析与重定位:使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。
其实重定位过程也伴随着符号的解析过程,每个目标文件都可能定义一些符号,也可能引用到定义在其他目标文件的符号。重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器须要对某个符号的引用进行重定位时,它就要确定这个符号的目标地址。这时候链接器就会去查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号后进行重定位。
查看main.o的符号
[root@fl test]# readelf -s main.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
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 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 75 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z3addid
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _Z3adddi
可以发现符号 _Z3addid 和 _Z3adddi 都是未定义的(UND),因此此时还没链接add.o,虽然在main.cpp中调用了两个add函数,但它们都是在add.cpp中实现的,在main.cpp根本找不到
两main.o和add.o链接在一起,生成main可执行文件
[root@fl test]# g++ main.o add.o -o main
[root@fl test]# readelf -s main
Symbol table '.dynsym' contains 3 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 65 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000400238 0 SECTION LOCAL DEFAULT 1
2: 0000000000400254 0 SECTION LOCAL DEFAULT 2
3: 0000000000400274 0 SECTION LOCAL DEFAULT 3
4: 0000000000400298 0 SECTION LOCAL DEFAULT 4
5: 00000000004002b8 0 SECTION LOCAL DEFAULT 5
6: 0000000000400300 0 SECTION LOCAL DEFAULT 6
7: 0000000000400360 0 SECTION LOCAL DEFAULT 7
8: 0000000000400368 0 SECTION LOCAL DEFAULT 8
9: 0000000000400388 0 SECTION LOCAL DEFAULT 9
10: 00000000004003a0 0 SECTION LOCAL DEFAULT 10
11: 00000000004003d0 0 SECTION LOCAL DEFAULT 11
12: 00000000004003f0 0 SECTION LOCAL DEFAULT 12
13: 0000000000400420 0 SECTION LOCAL DEFAULT 13
14: 00000000004005f4 0 SECTION LOCAL DEFAULT 14
15: 0000000000400600 0 SECTION LOCAL DEFAULT 15
16: 0000000000400610 0 SECTION LOCAL DEFAULT 16
17: 0000000000400658 0 SECTION LOCAL DEFAULT 17
18: 0000000000600de0 0 SECTION LOCAL DEFAULT 18
19: 0000000000600de8 0 SECTION LOCAL DEFAULT 19
20: 0000000000600df0 0 SECTION LOCAL DEFAULT 20
21: 0000000000600df8 0 SECTION LOCAL DEFAULT 21
22: 0000000000600ff8 0 SECTION LOCAL DEFAULT 22
23: 0000000000601000 0 SECTION LOCAL DEFAULT 23
24: 0000000000601028 0 SECTION LOCAL DEFAULT 24
25: 000000000060102c 0 SECTION LOCAL DEFAULT 25
26: 0000000000000000 0 SECTION LOCAL DEFAULT 26
27: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
28: 0000000000600df0 0 OBJECT LOCAL DEFAULT 20 __JCR_LIST__
29: 0000000000400450 0 FUNC LOCAL DEFAULT 13 deregister_tm_clones
30: 0000000000400480 0 FUNC LOCAL DEFAULT 13 register_tm_clones
31: 00000000004004c0 0 FUNC LOCAL DEFAULT 13 __do_global_dtors_aux
32: 000000000060102c 1 OBJECT LOCAL DEFAULT 25 completed.6355
33: 0000000000600de8 0 OBJECT LOCAL DEFAULT 19 __do_global_dtors_aux_fin
34: 00000000004004e0 0 FUNC LOCAL DEFAULT 13 frame_dummy
35: 0000000000600de0 0 OBJECT LOCAL DEFAULT 18 __frame_dummy_init_array_
36: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.cpp
37: 0000000000000000 0 FILE LOCAL DEFAULT ABS add.cpp
38: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
39: 0000000000400788 0 OBJECT LOCAL DEFAULT 17 __FRAME_END__
40: 0000000000600df0 0 OBJECT LOCAL DEFAULT 20 __JCR_END__
41: 0000000000000000 0 FILE LOCAL DEFAULT ABS
42: 0000000000400610 0 NOTYPE LOCAL DEFAULT 16 __GNU_EH_FRAME_HDR
43: 0000000000601000 0 OBJECT LOCAL DEFAULT 23 _GLOBAL_OFFSET_TABLE_
44: 0000000000600de8 0 NOTYPE LOCAL DEFAULT 18 __init_array_end
45: 0000000000600de0 0 NOTYPE LOCAL DEFAULT 18 __init_array_start
46: 0000000000600df8 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
47: 0000000000601028 0 NOTYPE WEAK DEFAULT 24 data_start
48: 00000000004005f0 2 FUNC GLOBAL DEFAULT 13 __libc_csu_fini
49: 0000000000400420 0 FUNC GLOBAL DEFAULT 13 _start
50: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
51: 0000000000400566 14 FUNC GLOBAL DEFAULT 13 _Z3adddi
52: 00000000004005f4 0 FUNC GLOBAL DEFAULT 14 _fini
53: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
54: 0000000000400600 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
55: 0000000000601028 0 NOTYPE GLOBAL DEFAULT 24 __data_start
56: 0000000000400558 14 FUNC GLOBAL DEFAULT 13 _Z3addid
57: 0000000000601030 0 OBJECT GLOBAL HIDDEN 24 __TMC_END__
58: 0000000000400608 0 OBJECT GLOBAL HIDDEN 15 __dso_handle
59: 0000000000400580 101 FUNC GLOBAL DEFAULT 13 __libc_csu_init
60: 000000000060102c 0 NOTYPE GLOBAL DEFAULT 25 __bss_start
61: 0000000000601030 0 NOTYPE GLOBAL DEFAULT 25 _end
62: 000000000060102c 0 NOTYPE GLOBAL DEFAULT 24 _edata
63: 000000000040050d 75 FUNC GLOBAL DEFAULT 13 main
64: 00000000004003d0 0 FUNC GLOBAL DEFAULT 11 _init
此时发现符号 _Z3addid 和 _Z3adddi 已经能找到了,这就是符号解析。
这也是我们平时在编写程序的时候最常碰到的问题之一,就是链接时符号未定义。导致这个问题的原因很多,最常见的一般都是链接时缺少了某个库,或者输入目标文件路径不正确或符号的声明与定义不一样。
链接器是如何知道哪些符号需要重定位呢?
在前面的ELF文件中,已经谈到有一个叫重定位表的结构专门用来保存这些与重定位相关的信息,我们在前面介绍ELF文件结构时已经提到过了重定位表,它在ELF文件中往往是一个或多个段。
查看main.o的重定位表
[root@fl test]# objdump -r main.o
main.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000029 R_X86_64_PC32 _Z3addid-0x0000000000000004
0000000000000040 R_X86_64_PC32 _Z3adddi-0x0000000000000004
里面就记录着 _Z3addid 和 _Z3adddi 在链接后需要进行重定位
为什么需要重定位?
举个例子,在main.cpp中,调用了两个add函数,但这个两个add函数却定义在add.cpp中,所以对于main.cpp来说,这两个add函数,也就是 _Z3addid 和 _Z3adddi 是两个外部符号,当编译器在将main.cpp编译成指令后(main.o),它会为这两个函数分配一个假地址。当将main.o和add.o进行链接后,通过查看重定位表,知道符号 _Z3addid 和 _Z3adddi 的地址需要调整后,再去全局的符号表中查找对应的符号,每个符号都对应着一个值,这个值就是该符号(函数)的地址,从而就能进行正常的函数调用
链接过程中的强符号于弱符号
我们经常在编程中碰到一种情况叫符号重复定义。多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。比如我们在目标文件A和目标文件B都定义了一个全局整形变量global,并将它们都初始化,那么链接器将A和B进行链接时会报错
[root@fl test]# gcc main.cpp add.cpp -o main2
/tmp/cchHrya2.o:(.data+0x0): multiple definition of `global'
/tmp/ccrpPNPD.o:(.data+0x0): first defined here
这种符号的定义可以被称为强符号。有些符号的定义可以被称为弱符号。对于C/C++语言来说,编译器默认函数和初始化了的全局变最为强符号,未初始化的全局变量为弱符号。
针对强弱符号的概念,链接器就会按如下规则处理与选择被多次定义的全局符号:
弱引用和强引用
对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议(绑定),如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用。与之相对应还有一种弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。