编译链接的基础概念

标准库和编译器

标准库

标准库和编译器并不是绑定的,

  • Clang 可以用 libstdc++ 或 MSVC STL
  • GCC 可以被配置使用 libc++

在 Linux 系统中,Clang 默认用的就是 libstdc++。需要为 Clang 指定 --stdlib=libc++ 选项,才能使用。

“如果你不知道一个人是用的什么标准库,那么你可以猜他用的是 libstdc++。即使他的编译器是 Clang,他用的大概率依然是 libstdc++。”

标准库所属官方常应用于开源地址
libstdc++GCCLinuxlibstdc++
libc++ClangMacOSlibc++
MSVC STLMSVCWindowsMSVC STL

编译器

概念:编译器是将源代码 ( .cpp ) 编译成可执行程序 ( .exe ) 的工具。

常见编译器:GCC、Clang、MSVC

总览

编译器标准库平台平台补充优点缺点
GCClibstdc++Linux 和 MacOS存在windows移植版本 MinGW-w64有着大量好用的扩展功能
Clanglibc++跨平台 (Linux、MacOS、Windows…)苹果魔改Apple Clang性能优化激进(性能高,易发现未定义行为)对C++ 新标准特性支持相对较慢
MSVCMSVC STLWindows 限定魔改了 Clang 的MSVC clang-cl 优化能力差(未定义行为不容易产生 Bug 得过且过)
Intel C++ compiler特别擅长做性能优化新特性支持特别慢

分类

GCC

gcc——GNU Compiler Collection (GNU编译器套件),是多种GNU编译器的集合,包含可以编译内核的C编译器,也可以包含可以编译Linux系统上C/C++代码的其它编译器。

如果你不知道一个人是用的什么编译器,可以猜他用的是 GCC

  • 适用的操作系统:Unix 类系统
    • Linux 和 MacOS 等 可用
    • 不支持Windows:但存在MinGW-w64这样的 GCC Windows 移植版
  • 有着大量好用的扩展功能,例如
    • pbds (基于策略的数据结构)
    • 各种 _attribute_ ,各种_builtin_ 系列函数
    • 随着新标准的出台,很多原本属于 GCC的功能都成了标准的一部分,例如
      • _ attribute _ ((warn_unused))变成了标准的 [[nodiscard]]
      • _builtin_clz 变成了std::countl_zero
      • __VA_OPT_ 名字都没变
clang

Clang 优点

  • 跨平台的编译器,

    • Clang 支持包括Linux、MacOS、Windows等大多数主流平台

    • Apple Clang 苹果公司魔改版本,基本上只有苹果的开发者会用

      • 只在 MacOS 系统上可用
      • 支持 Objective-C 和 Swift 语言
      • 较官方 Clang 落后一些,很多新特性都没有跟进
    • clang-cl ,在 Clang 上魔改出的 MSVC 兼容模式

      • 兼顾 Clang 特性的同时,支持了 MSVC 的一些特性(例如 __declspec
      • 可以编译适用 MSVC 特性的代码,
  • 支持了很大一部分 GCC特性和部分 MSVC 特性。

  • Clang 的性能优化比较激进

    • 有助于性能提升
    • Clang 可能对未定义行为优化出匪夷所思的结果,擅于对未定义行为的debug

Clang 缺点

  • 对一些 C++ 新标准特性支持相对较慢,没有 GCC 和 MSVC 那么上心。

    例如 C++20 早已允许 lambda 表达式捕获 结构化绑定structural-binding 变

    量,而 Clang 至今还没有支持,尽管 Clang 已经支持了很多其他 C++2 0

    特性。

LLVM重要性

  • Clang所属的 LLVM 项目更是编译器领域的中流砥柱,

    • 支持 C、C++、Objective-C、Fortran
    • Rust 和 Swift 等语言也是基于 LLVM 后端编译的
    • 很多显卡厂商的OpenGL 驱动也是基于 LLVM 实现编译的。
  • Clang 身兼数职

    • 不仅可以编译还支持静态分析。

    • 许多 IDE 常见的语言服务协议 (LSP)就是基于 Clang 的服务版——Clangd 实现的

      (例如按 Ctrl点击 跳转到函数定义,就是 IDE 通过调用 Clangd 的LSP 接口实现)。

MSVC

MSVC Windows 限定的编译器

  • 提供了很多 MSVC 特有的扩展。

clang-cl ,在 Clang 上魔改出的 MSVC 兼容模式

  • VS2022 IDE 中集成了 clang-cl

  • 兼顾 Clang 特性的同时,支持了 MSVC 的一些特性(例如 __declspec )可以编译适用 MSVC 特性的代码。

MSVC 的优化能力差

  • 比GCC 和 Clang 都差

    例如 MSVC 几乎总是假定所有指针 aliasing,这意味着当遇到很多指针操作的循环时,几乎没法做循环矢量化。

  • 使得未定义行为不容易产生 Bug

    导致一些只用MSVC 的人不知道某些写法是未定义行为。

Intel C++ compiler

英特尔开发的 C++ 编译器,由于是硬件厂商开发的,特别擅长做性能优化。但由于更新较慢 基本没有上新特性,也没什么人在用了。

下载&编译

Linux下载高版本的gcc与g++并编译

sudo apt-get install g++
sudo apt-get install libgmp-dev
sudo apt-get install libmpfr-dev
sudo apt-get install libmpc-dev

mkdir ~/gcc & cd ~/gcc
# 从清华源下载资源 https://mirror.tuna.tsinghua.edu.cn/gnu/gcc/
wget  https://mirror.tuna.tsinghua.edu.cn/gnu/gcc/gcc-14.2.0/gcc-14.2.0.tar.gz
tar -zxvf gcc-14.2.0.tar.gz -C ./
cd gcc-14.2.0

# 下载所需依赖包
./contrib/download_prerequisites
#可能会报错: error: Cannot verify integrity of possibly corrupted file xxx
#是网络原因,到 http://gcc.gnu.org/pub/gcc/infrastructure/ 下载对应压缩包放在当前gcc-14.2.0目录下即可

# 指定编译产物路径,生成的Makefile文件
mkdir -p /home/pwd/gcc14/gcc14Make
./configure --prefix=/home/pwd/gcc14/gcc14Make --enable-languages=c,c++ --enable-checking=release --disable-multilib
# 编译
make -j8
# 安装
make install
# 环境变量
sudo vim ~/.bashrc
source ~/.bashrc

# 之前指定了编译产物路径
/home/pwd/gcc14/gcc14Make
cd /home/pwd/gcc14/gcc14Make
cp -ri * /usr/bin

或者

# 
# 最后 sudo vim ~/.bashrc 加入如下代码
PATH=/home/pwd/CppTest/gcc14Make/bin:$PATH
LD_LIBRARY_PATH=/home/pwd/gcc14/gcc14Make/lib:$LD_LIBRARY_PATH
LD_LIBRARY_PATH=/home/pwd/gcc14/gcc14Make/lib64:$LD_LIBRARY_PATH
LD_LIBRARY_PATH=/home/pwd/gcc14/gcc14Make/libxec:$LD_LIBRARY_PATH
LD_LIBRARY_PATH=/home/pwd/gcc14/gcc14Make/include:$INCLUDE
# 执行source ~/.bashrc

# 软连接以及管理g++版本
# 优先级数字越大,越优先
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 10
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-7 10

sudo update-alternatives --install /usr/bin/gcc gcc /home/wjl/work/gcc112/bin/gcc 20
sudo update-alternatives --install /usr/bin/g++ g++ /home/wjl/work/gcc112/bin/g++ 20

编译器选项

编译器选项是用来控制编译器的行为的。不同的编译器有不同的选项,语法有微妙的不同,但大致功效相同。

其中 Clang 和 GCC 的编译器选项有很大交集。而 MSVC 基本自成一派。

  • Clang 和 GCC 的选项是 -xxx 的形式
  • MSVC 的选项是 /xxx 的形式。

优化等级

编译器进行优化时,会把标准规定的未定义行为作为共识。 因此在高优化等级下,未定义行为容易导致诡异的bug,从而提早暴露。release模式:写东西大量输出时,就建议使用。及早的暴露未定义行为 至于debug提供的工具只是辅助,哪怕没有它你也应该知道自己在干什么(debug时的诡异行为总要好过release后的问题,对吧?)

优化等级 作为一个编译器选项

  • Clang 和 GCC: -O1 -O2 -O3 -Ofast -Os -Oz -Og

  • 忽略MSVC,没用过。

编译速度从上至下越来越慢。

优化等级概述作用一般情况下的用途
-O0不进行任何优化,编译速度最快(默认选项直接复刻你的代码。未定义行为不容易产生诡异的结果开发人员内部调试阶段
-Og-O0 基础上做简单的优化,尽可能保留更多调试信息。所有优化都不会涉及到未定义行为,插入了调试信息最终的可执行文件会变得很大。同上,快速调试
-O1最基本的优化,影响诸如 gdb 等调试(丢失函数的行号信息)删除一些简单的死代码(编译器检测到的不可抵达代码),去掉没有用的变量,用寄存器代替部分变量。
-O2更强的优化(删除未使用的代码块把一些循环展开,把一些函数内联 减少函数调用,把简单的数组操作用更快的指令替代等
-Os-O2 基础上,专门优化代码大小,性能被当作次要需求禁止会导致可执行文件变大的优化。关闭诸如循环展开、内联等优化,尽可能减小可执行文件的尺寸节省内存的嵌入式系统开发
-O3激进的优化(如果有未定义行为,可能会导致一些 Bug)复杂的循环用 SIMD 矢量化优化,复杂的数组操作用更快的指令替代最终成品发布阶段
-Ofast-O3 基础上,对浮点数的运算进行更深层次优化导致一些浮点数计算结果不准确科学计算领域的终极性能优化

C++ 标准

# 要编译一个 C++2 0 源码文件,分别用 GCC、Clang、MSVC
g++ -std=c++20 -c main.cpp -o main
clang++ -std=c++20 -c main.cpp -o main
cl.exe /std:c++20 /c main.cpp

指定要选用的 C++ 标准。

# Clang 和 GCC
-std=c++98	-std=c++03	-std=c++11
-std=c++14	-std=c++17	-std=c++20
-std=c++23
# MSVC
/std:c++98	/std:c++11	/std:c++14
/std:c++17	/std:c++20	/std:c++latest

其它…

还有很多常见的编译器选项,下一章节我们还会挑选一部分基础的编译命令进行介绍。

gcc [options] infile
-E	对源文件进行 预处理,输出.i文件;
-S	对源文件进行 预处理、编译,输出.s文件;
-c	对源文件进行 预处理、编译、汇编,输出.o文件;

-o	重定位输入文件位置;
-I	包含头文件路径,如 g++ -Iopencv/include/;
-L	包含库文件路径,如 g++ -Lopencv/lib/ ;
-l	链接库文件,如链接lib.so:g++ -llib;
-shared	编译.so库;
-fPIC	生成位置无法码;
-Wall	对代码有问题的地方发出警告;
-g	在目标文件中嵌入调试信息,便于gdb调试;

编译命令

编译过程

general

一个.c源文件编译的过程:

​ 源文件–>预处理–>编译cc1–>汇编器as–>链接器ld–>可执行文件PE/ELF

main.c–>main.i–>main.s–>main.o–>main.o–>main

源文件
预处理
编译cc1
汇编器as
链接器ld
可执行文件PE/ELF
main.c
main.i
main.s
main.o
main.o
main
.ii .i
.asm .s
.obj .o
C Code
Pre-Processor
Compiler
Assembler
Linker
Executable File .exe
source code .c&.cpp&.h
Include Header Files & expand macros
Generate Assembly Code
Generate Machine Cod
Linking Static Library .lib&.a
Executable Machine Code

预处理 .i

把源文件 .c文件进行预编译后,生成相应的.i文件。

文件转换 .c --> .i .ii 经过预编译后的.i文件

  • 不包含任何宏定义,所有的宏都已经被展开

    当无法判断宏定义是否正确或头文件包含是否正确使,可以查看预编译后的文件来确定问题。

  • #include包含的文件已经被插入

预处理过程主要处理那些源代码中以#开始的预处理指令,主要处理规则如下∶

    • 将所有的#define删除,
    • 处理所有条件编译指令,如#if#ifdef等;
    • 展开所有的宏定义;
  • 处理#include预处理指令,

    • 将被包含的文件插入到该预处理指令的位置。

    • 该过程递归进行,

    • 被包含的文件可能还包含其他文件。

  • 注释

    • 删除所有的注释///**/
  • 添加行号和文件标识

    • #1"main.c"
    • 以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号信息;
  • 保留所有的#pragma编译器指令,因为编译器需要使用它们;

编译 .s

把预处理完的.i文件生成相应的汇编代码文件.s

  • 进行一系列词法分析、语法分析、语义分析及优化

  • 这个过程是整个程序构建的核心部分,也是最复杂的部分之一。

汇编 .o

汇编器将汇编代码.s转变成机器可以执行的命令,

  • 汇编相对于编译过程比较简单

    根据汇编指令和机器指令的对照表进行翻译,每一个汇编语句几乎都对应一条机器指令。

  • 产物.o的内容为机器码,不能以文本形式方便的呈现。

  • 可以利用objdump-S file查看源码反汇编。

链接 可执行文件

为什么需要链接

一个程序往往由多个模块组成,这就必然涉及到以下两个环节

  1. 每个模块独立的编译

    在此之前,编译器在预处理、编译、汇编的过程中,将源代码文件(模块)编译成一个个未链接的目标文件(二进制文件)。到此为止但是编译器仍然还不知道其中的符号的地址。

  2. 由链接器最终将进行链接将各个目标文件组装起来。例如:

    • 确定定义在其他模块的全局变量和函数在最终运行时的绝对地址
    • 解决符号依赖,库依赖关系,
    • 确定 各个模块中所有引用的符号的地址

第二步,这个拼接的过程就是链接,即模块间的符号引用的过程,解决了各模块之间的通信问题。

相关概念
符号

符号表示一个地址,可能是

  • 一段子程序(函数)的起始地址
  • 一个变量的起始地址。

符号的概念,确保了通过符号可以访问到正确的地址。例如,把函数foo 的函数地址赋予符号 foo后,对应的地址会被修正。

符号(函数/变量)如果定义在

  • 静态目标模块中,则

    • 在链接时,链接器对所有绝对地址的引用进行重定位
  • 动态目标模块中,则

    • 在链接时,将这个符号的应用标记为动态链接的符号,不进行地址重定位
    • 在加载可执行文件进行装载时, 再对所有绝对地址的引用/对符号地址进行重定位 —— 基址重置

jmp foo 语句的含义是直接的跳转到该函数处执行代码。

不管foo 之前或是之后插入多少的代码,导致foo 地址的变化,汇编器在每次汇编程序的时候会重新的计算foo 这个符号的地址,然后所有引用到foo 这个地址的指令修正到正确的指令地址。

重定位

重定位就是把程序的逻辑地址空间变换成内存中的实际物理地址空间的过程。是实现多道程序在内存中同时运行的基础。

重定位有两种,分别是动态重定位与静态重定位。

  1. 静态重定位∶

    • 完成时机:在程序装入内存的过程中(程序开始运行前)完成,以后不再改变

    • 即在生成可执行文件/可重定位文件的同时,已完成地址的静态定位

  2. 动态重定位

    • 完成时机:CPU每次访问内存时由动态地址变换机构(硬件)自动进行把相对地址转换为绝对地址。这个过程在程序装入内存之后。
    • 动态重定位需要软件和硬件相互配合完成。可执行文件/共享目标文件的矛盾需要外部环境解决
有关gcc参数
# lib.so 就是让链接器知晓 某些符号 为动态符号的输入文件。
gcc -fPIC -shared -o lib.so lib.c

-share 参数:动态目标模块 装载时重定位

-fPIC 参数:地址无关代码

为了让动态重定位时,解决动态模块中绝对地址引用——程序模块中共享的指令部分不需要随着装载地址的改变而改变。

  • 解决方案:地址无关代码

    • 指令中那些需要被修改的部分,被分离出来,跟数据部分放在一起。
    • 从而使得指令部分可以保持不变,数据部分在每个进程中拥有一个副本,即地址无关代码 (PIC position independent code)
  • 缺点:指令部分仍然无法在多个进程之间共享

PLT 延迟绑定

  • 在程序运行过程中,可能很多程序中的函数在程序运行完毕后,都不会被用到。如果一开始就把所有函数都链接好,是一种资源的浪费。
  • 所以ELF 采用了一种延迟绑定的做法 lazy binding 即当函数第一次被调用时候才进行绑定。

静/动态库

将模块生成的目标文件,进行打包,就形成了库。库即目标文件有序的组织,方便进行快速的查找。

# lib.so 就是让链接器知晓 某些符号 为动态符号的输入文件。
gcc -fPIC -shared -o lib.so lib.c

-share 参数:动态目标模块 装载时重定位,往往用于生成动态库。

默认生成静态库。

动态库

编译期:动态库只在生成的可执行文件中生成**插桩 函数**

加载可执行文件的装载时:总库通过地址回填,在动态库中寻找被调用的函数/变量。

  • 读取指定目录中的动态库文件 .dll .a,加载到内存中空闲的位置

    查找路径 Windows:可执行文件同目录,其次是环境变量%PATH%。Linux:ELF格式可执行文件的RPATH,其次是/usr/lib等

  • 替换相应的“插桩”指向的地址为加载后的地址,这个过程称为重定向。函数被调用就会跳转到动态加载的地址去

优点:

  • 开发过程中各个模块更加的独立,耦合度更小
  • 便于不同的开发者之间独立的进行开发和测试。

命名规则 libname.so.x.y.z

  • 主版本号x:表示库的重大升级,不同的版本号之间的库是不兼容的。
  • 次版本号y:表示库的增量升级,即增加一些新的接口符号,且保持原来的符号不变。在主版本号相同的情况下,高的次版本号的库向下兼容低的次版本号的库。
  • 发布版本z:表示库的一些错误的修正,性能的改进等,并不填加任何的新接口,也不对接口进行更改。

共享库的主版本号和次版本号决定了一个共享库的接口. linux中,普遍采用一种SO-NAME 的命名机制来记录共享库的依赖关系。每个的共享库都有一个SO-NAME 即为共享库的文件名去掉次版本号和发布版本号。

软链接

  • SO-NAME 为名字的软连接会指向目录中主版本号相同,次版本和发布版本号最新的共享库。
  • 系统会为每个共享库在它的共享目录创建一个跟SO-NAME 相同并且只想它的软连接。

静态库

静态库在编译期完成静态链接,本质一组目标文件的集合

  • 编译期间:链接阶段把静态库中的代码copy到了总库。

    • 将所有目标文件.o文件经过压缩打包合并成一个.so或.out文件

    • 处理所有.o文件节区在目标文件中的布局

  • 加载可执行文件的装载时:总库完全独立于静态库,有了总库后不再需要静态库。

相当于直接把代码插入到生成的可执行文件中,会导致体积变大,但是只需要一个文件即可运行。

  • 通常使用 ar 压缩程序将这些目标文件压缩到一起并且对其进行编号和索引。

**缺点:**浪费内存和磁盘空间,模块更新困难。

命令行执行

在 windows 操作系统下,编译工具用集成开发环境 vc6.0

在 Linux 操作系统下没有很好的集成环境让我们用,用的编译器是 gcc

程序序的编译分为四个阶段: 由.c 到可执行程序

1,预编译、2,编译、3,汇编、4,链接

4个阶段 分步骤

gcc [options] infile
# 预处理 预编译 
# # -E 对源文件进行 预处理,输出.i文件;
gcc -E hello.c hello.h -o hello.i
gcc -E *.c -o hello.i	# 预处理所有.c
# 编译
# # -S	对源文件进行 预处理、编译,输出.s文件;
gcc -S hello.i -o hello.s
gcc -S *.i -o hello.s	# 编译所有.i
# 汇编
# # -c	对源文件进行 预处理、编译、汇编,输出.o文件;
# 直到.o才是机器指令,此前还是文本文件, 因此.i .s文件还可以-c输出为 .o
gcc -c hello.s -o hello.o
gcc -c *.s -o hello.o	# 汇编所有.s
# 链接
# # -o	重定位输入文件位置;
gcc hello.o –o hello
gcc *.o –o hello		# 链接所有.o

一步到位

简而言之

# MSVC
cl.exe /c main.cpp

# GCC
g + -c main.cpp -o main

# Clang
# # Windows
clang++ .exe -c main.cpp -o main.exe
# # Linux / MacOS
clang++ -c main.cpp -o main

更花里胡哨一点:

# # 默认命名: a.out可执行文件
gcc 依赖文件	  # 默认会生成一个名为 a.out 的可执行文件 
gcc hello.c	# 运行程序./a.out

# # 自定义可执行文件名
gcc 依赖文件 -o 指定目标文件
gcc hello.c hello.h -o hello	# 运行程序 ./hello

# # 批量指定某类型文件
gcc *.c	-o target.out	# 编译所有.c文件	

目标文件分类

目标文件格式:

  • win:PE (Portable Executable)
  • linux :ELF(Executable Linkable Format)

ELF 文件标准把系统中采用ELF 格式的文件分为四类。

核心转储文件

当进程意外终止时,系统可以将以下内容转储到核心转储文件 (core dump file)

  • 该进程的地址空间的内容
  • 终止时的其他信息

使用file 命令可进行查看。

可执行文件

可执行文件(executable file)

  • linux 下没有后缀,win 下的exe。
  • 包含了可以直接执行的程序,代表即为ELF 可执行文件,一般没有扩展名。

可重定位文件

可重定位文件(reloactable file)

  • 包含代码和数据

    • 静态链接库是可重定位文件的一种
    • linux 的 .o 文件。win 下的 xx.obj
  • 可被链接为

    • 可执行文件(executable file)
    • 或 共享目标文件 (动态链接库)
    • 啥都可以单独做。

共享目标文件

共享目标文件(shared object file)

  • 需要 与 其它文件结合,才能可执行/成为新的目标文件

    • 动态链接库是可重定位文件的一种

    • linux 中的xx.so,win下的xx.dll

  • 包含了代码和数据,

    • 连接器:与其他的**可重定位文件(静态链接库)**链接,产生新的目标文件。

    • 动态链接器:与可执行文件结合,作为进程影响的一部分来运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值