C++的编译链接

1、前置条件

# 操作系统版本
cat /proc/version 
Linux version 3.10.0-1160.95.1.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (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个阶段:预处理/预编译、编译、汇编、链接

2、预处理/预编译

第一步的编译过程使用如下命令(-E 表示只进行编译)

g++ -E main.cpp -o main.i

这是预编译之后的main.i文件

# 1 "main.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 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 "<built-in>"  表示接下来的内容是编译器内置的内容。
# 1 "<command-line>"  表示接下来的内容是从命令行传递给编译器的参数。
# 1 "/usr/include/stdc-predef.h" 1 3 4  表示引用了标准库预定义的宏的头文件。
# 1 "<command-line>" 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<iostream>,然后进行预编译,可以看到生产的对应文件都有两三万行代码

预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include"“#define”等,主要处理规则如下:

  • 将所有的“#define”删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
  • 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  • 删除所有的注释“//”和“/**/”。
  • 添加行号和文件名标识,比如#2“hello.c”2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
  • 保留所有的#pragma编译器指令,因为编译器须要使用它们。

经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

2、编译

编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析、汇总所有的符号及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。

这里所谓汇总所有的符号,就是对应的函数名

使用以下命令对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语言不支持函数重载的原因

3、汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了。

使用以下命令对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表示符号表,我们将函数和变量统称为符号,每一个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他几种不常用到的符号。我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:

  • 定义在本目标文件的全局符号,可以被其他目标文件引用。
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号
  • 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。比如 main.o 里面的“.text”、".data”等。
  • 局部符号,这类符号只在编译单元内部可见。
  • 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。

注意,这里的局部符号不是局部变量,而是定义在局部的静态变量
举一个简单的例子:

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重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。

5、链接

整个链接过程主要分为两步。

第一步 空间与地址分配:扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。

这里合并段的含义是合并相似的段,将所有输入文件的“.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++语言来说,编译器默认函数和初始化了的全局变最为强符号,未初始化的全局变量为弱符号。

针对强弱符号的概念,链接器就会按如下规则处理与选择被多次定义的全局符号:

  • 规则1: 不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号)。如果有多个强符号定义,则链接器报符号重复定义错误。
  • 规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
  • 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。比如目标文件A定义全局变量global为int型,占4个字节;目标文件B定义global为double型,占8个字节,那么目标文件A和B链接后,符号global占8个字节(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)。

弱引用和强引用
对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议(绑定),如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用。与之相对应还有一种弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
编译链接 C++ OpenCV 项目通常有两种方式,一种是使用 CMakeLists.txt 文件进行编译,另一种是使用 makefile 文件进行编译。 使用 CMake 进行编译链接的步骤如下: 1. 在项目根目录下创建一个 CMakeLists.txt 文件,并添加必要的配置: ``` cmake_minimum_required(VERSION 3.12) project(your_project_name) set(CMAKE_CXX_STANDARD 11) # 设置 OpenCV 路径 set(OpenCV_DIR /path/to/opencv) # 寻找 OpenCV 库 find_package(OpenCV REQUIRED) # 添加头文件路径 include_directories(${OpenCV_INCLUDE_DIRS}) # 添加源文件 add_executable(your_executable_name main.cpp) # 链接 OpenCV 库 target_link_libraries(your_executable_name ${OpenCV_LIBS}) ``` 2. 在项目的 source 目录下创建一个 CMakeLists.txt 文件,并添加需要编译的源文件: ``` add_library(your_library_name SHARED your_source_files.cpp) ``` 3. 创建一个 build 目录,在终端中进入该目录: ``` mkdir build cd build ``` 4. 运行 CMake 生成 Makefile: ``` cmake .. ``` 5. 使用 make 命令进行编译链接: ``` make ``` 6. 最终会在 build/source/lib 目录下生成一个 libyour_library_name.so 的动态库,这就是可供他人调用的动态库。 使用 makefile 进行编译链接的步骤如下: 1. 在项目根目录下创建一个 makefile 文件,并添加必要的配置: ``` CXX = g++ CXXFLAGS = -Wall -std=c++11 LDFLAGS = -L/path/to/opencv/libs LDLIBS = -lopencv_core -lopencv_imgproc -lopencv_highgui your_executable_name: main.o $(CXX) $(LDFLAGS) -o $@ $^ $(LDLIBS) main.o: main.cpp $(CXX) $(CXXFLAGS) -c -o $@ $^ ``` 2. 在项目的 include 目录下放置项目的头文件。 3. 在项目的 source 目录下放置项目的源文件。 4. 在终端中进入项目根目录,并运行命令 make。 5. 最终会在项目根目录生成一个名为 your_executable_name 的可执行文件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值