C++编译知识笔记(一)——基本知识

2023-11-09

作为一个C++工程师,编译可以说是日常工作内容中最常见的一个部分了,但实际情况是,这是一个很容易开发人员忽略的部分,绝大部分开发者对于这个环节并不了解,在日常迭代的过程中似乎也影响不大,但在引入一些新的依赖,或者要编译一个开源项目的时候,如果对编译不了解的话,碰到一些编译时和运行时的奇奇怪怪的问题往往会让人崩溃,因此还是有必要系统地学习一下,所以开一个新坑来聊一聊编译,也当作自己的学习笔记,本篇是个开头,主要是一些基本但有非常重要的知识。

一、编译的基本步骤

我们平常所说的编译一般指从源码到可执行文件或者库的过程,在实际的工程应用中,我们往往会使用诸如CMake、bazel等高级构建工具来进行编译相关操作,底层实际上是调用gcc等编译器的相关功能,这个过程其实包含了预处理、编译、汇编、链接四个阶段,以下以《一个程序员的自我修养》里用到的一个linux平台的简单源码用gcc进行编译为实例介绍这几个阶段。

//hello.cpp
#include <stdio.h>
int main() {
    printf("Hello World\n");
    return 0;
}

1.1 预处理阶段

(1)将#include 的头文件展开:#include本质上是代码的拷贝动作,所有inlclude的头文件的内容在展开阶段均会被直接复制过来。
(2)将#define语句指定的值转换为变量。
(3)将宏定义转换为具体代码。
(4)根据#if #elif 和#endif指定的位置包含或排除特定部分的代码。

对应的典型gcc命令如下:

$ gcc -E hello.cpp -o hello.i

1.2 编译阶段

编译阶段则是基于预处理阶段得到的.i文件进行一系列的词法分析、语法分析、语义分析和相关优化后生成汇编文件。

对应的典型gcc命令如下:

$ gcc -S hello.i -o hello.s

1.3 汇编阶段

该阶段则是将汇编文件转换为机器可以执行的指令,也就是机器码,示例程序在汇编阶段会生成目标文件hello.o,这个目标文件从结构上来讲,已经是编译后的可执行文件格式,只是在经过链接之前,缺少一些外部调用的依赖等,还不能直接运行。

对应的gcc命令如下:

$ gcc -c hello.s -o hello.o

或者使用linux内建的as命令:

$ as hello.s -o hello.o

1.4 链接阶段

链接由链接器完成,在linux下面通常是ld,在实际能运行的程序中,即便简单如上述示例程序,要想在操作系统上运行起来,实际上还是需要有其他依赖,也就是需要依赖自身代码之外的函数、变量等,简单来说,链接就是将这些依赖连接到用户程序中需要用到的地方。比如示例hello.cpp文件中调用了printf函数,目标文件hello.o中引用的printf符号还未解析,同时也缺少系统运行库libc和相关的启动文件,只有这些都链接在一起,才能产出最终的可执行程序,本质上来说链接就是将多个不同的目标文件连接到一起,而链接的直接依据就是符号,下面会详细介绍下符号的概念。

以上就是编译的基本阶段,这里再多说两句,我们说到要执行一个编译操作,相应的产出无非是三种,可执行文件,静态链接库或动态链接库。
1.可执行文件:可以直接执行,如果链接了动态库需要结合动态库一起运行。
2.静态链接库.a:静态库本质上是各种.o文件的打包,我们平常编译生成静态库实际上就是将源码编译成.o之后打包得到一个.a,供其他模块编译使用。
3.动态链接库.so:各目标文件链接处理得到的库,供其他模块编译和运行使用。

二、核心常用基本概念

2.1 .o目标文件

.o目标文件是汇编阶段过后的产物,在gcc编译中,一个cpp文件对应一个编译单元,即对应一个.o目标文件,在linux下,.o是ELF格式的,这里不展开说明目标文件的具体格式,但我们需要知道,目标文件里面除了包含了编译后的机器指令代码、数据,还包括及符号表等链接需要的信息,它从格式上已经非常接近可执行文件,只需要再进行链接处理即可成为最终的可执行文件或者库。

2.2 符号

看过编译相关资料就会发现,符号这个名词会经常出现,它也是链接过程中的核心元素,可以说链接就是根据符号来的。在链接中,目标文件之间的连接实际上是目标文件直接对地址的引用,也就是对函数和变量的地址的引用,以上面的示例程序为例,我们生成的hello.o就引用了其他.o文件中定义的printf,除了函数,变量也是类似,每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数的混淆,编译器有一套生成规则,具体的可以用nm命令去查看符号。在链接中,我们将函数和变量统称为符号(Symbol),函数名或者变量名就是符号名(Symbol Name)。
每个目标文件都有相应的符号表,记录了目标文件所用到的所有的符号,每个定义的符号由一个对应的值,叫做符号值,对于变量和函数来说,符号值就是他们的地址。

2.3 静态链接库

前面讲到了,一个源码文件会生成一个目标文件,一个程序往往会有很多的依赖,比如系统调用相关的,以C语言运行库glibc为例,其中包含了输入输出、文件操作、时间管理、内存管理等内容。glibc由上千个c语言源文件组成,编译后会有相同数量的目标文件。如果把这些零散的文件直接提供给使用者,会造成文件传输、组织和管理的不便。把这些目标文件压缩在一起,对其进行编号和索引,以便查找和检索,方便使用,这便形成了libc.a,因此静态库是.o目标文件的集合,供编译时按需静态链接使用,编译完毕后不再需要相关文件。

2.4 动态链接库

.o目标文件经过了链接处理便成为了动态库,linux下一般为so文件,从操作系统的角度而言,动态库包含了指令和数据,从文件结构上更接近于应用程序,给动态库增加入口函数,动态库也可以像应用程序一样正常运行。生成动态库需要指定-shared和-fPIC 参数,前者是指定生成动态库,后者是生成地址无关代码因此可以动态加载,以后单独聊一聊,这里有个概念就行。动态链接库在程序运行的时候才会被真正加载和使用,编译期主要是起到了一个检查的作用,因此编译和运行的时候都需要相关so文件。

三、链接和加载

前面介绍了链接的基本概念,而链接的对象包括源码文件生成的目标文件,静态库和动态库,针对不同类型的链接对象,所发生的链接行为是不一样的,

3.1 .o文件和静态库的链接

链接.o文件和静态库本质上是一样的,因为静态库只是对o文件的简单打包
一个源码文件中引用其他文件(cpp生成的.o,.a或.so)中定义的函数、变量,在链接阶段需要定位这些外部的函数、变量。对静态库或.o文件,链接器会将.o目标文件中用到的外部符号其所在的.o文件一并保存到最终编译产出中。静态库是按需链接,链接的粒度是.o文件,并不是指定的.a文件,只有存在被引用符号的.o 文件才会被写入最终的编译产出。

3.2 动态库的链接

对so动态库而言,在编译阶段链接器并不会执行真正的链接动作,只是检查代码中所需的符号能否在动态库中找到,找不到就报错,找到便将so文件的元信息记录到编译产出中,直到所有的符号都被找到并被记录到了编译产出中,编译阶段的链接相关工作就算完成了,真正意义上的链接在程序运行时由动态加载器来加载相关so完成。

四、编译的关键参数—各种路径

4.1 头文件搜索路径

作为一个c/c++语言的coder,#include "xxx.h"恐怕是最熟悉的语句之一了,但背后编译器寻找头文件的细节大部分人其实并不清楚,只知道凭着感觉写能work就行,而且我们会看到,有的include会带路径,有的就是一个文件名,有的时候不同依赖的同名头文件还会发生冲突,因此理解编译器是如何搜索头文件是十分重要的,也就是要弄清楚编译器的头文件搜索路径,编译器会搜索所有的路径去找include的头文件。
头文件可以分为两种,一种是标准库的头文件,比如stdio.h,编译器的默认搜索路径会包含这些文件存放的路径,直接inlclude就行,可以通过cc1plus –v查看包含哪些路径,这里不展开说明。还有一种就是自己编写或者依赖的第三方的头文件,这个时候就需要添加额外的头文件搜索路径了,对于gcc,通过-I参数来指定,比如

gcc -c –Imodule1/a/include –Imodel2 -o src/hello.o src/hello.cpp

这个语句中,我们制定了两个额外的相对搜索路径,module1/a/include和model2/b/include,编译器在查找头文件时,会按搜索路径加上include语句里的内容去查找,比如,如果有以下include代码:

#include "foo.h"
#include "b/include/bob.h"

分别对应的搜索路径如下:

#include "foo.h"
module1/a/include/foo.h
model2/foo.h

#include " b/include/bob.h"
module1/a/include/b/include/bob.h 
model2/b/include/bob.h

gcc会按搜索路径的先后顺序遍历查找,找到第一个存在的文件路径便停止查找,因此如果有同名的头文件,搜索路径比较杂并且include语句没有包含路径信息的话有可能会出现找错的情况,一个比较好的方法是搜索路径不要太深入,在include语句里保留部分目录信息,这样不但不容易找错开发者自己看着也更直观。

4.2 编译时库文件搜索路径

对于依赖了库文件的编译,自然也需要根据路径去找库文件,我们知道有静态库和动态库两种,无论哪种库都需要在编译阶段用到。
和头文件搜索路径类似,gcc默认的库文件搜索路径下存放了C++标准库,操作系统为了方便大家使用,也会将相关的库文件放到默认的搜索路径下,许多开源软件也会将库文件的安装目录默认设置为/lib或/usr/lib,这样不管在程序编译阶段还是在程序运行阶段,gcc都可以找到需要的库文件,无需再额外指定搜索路径。
对于不在默认的库文件搜索路径下的库文件,则需要手动指定,gcc通过-L指定库文件搜索路径,根据-l指定库文件
比如:

gcc -o hello hello.cpp -L /usr/local/libtest/lib -ltest

4.3 运行时库文件搜索路径

对于静态库,编译阶段用完后就了事了,库中所需的所有符号对应的.o文件都被写入到应用程序中,程序在运行时不需要进行额外的链接动作。但动态库在运行时也是需要的,我们经常也在运行时碰到一些动态库找不到或者版本不一致之类的问题,因此很有必要了解动态库在运行时的搜索方式,

简单来说,linux动态库搜索路径的先后顺序可以认为依次为:
(1)LD_RELOAD
(2)RPATH
(3)LD_LIBRARY_PATH

4.3.1 LD_PRELOAD环境变量

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),通过它可以定义在程序运行前优先加载的动态链接库,也就是它指定的有最高的搜索优先级。该方案一般仅用于极为特殊的情况,例如测试与诊断紧急补丁。
设置LD_PRELOAD环境变量如下:

export LD_PRELOAD=/home/work/test/libs/libtest.so:$LD_PRELOAD
4.3.2 RPATH

RPATH是一个链接选项,指定RPATH是比较推荐的一种指定动态库搜索路径的一种方式,具体到实现上又有以下两种方式:
1.在编译命令中使用-R参数在编译阶段指定,该命令会将RPATH直接写入应用程序,编译阶段指定不会对其他程序产生影响,可以减少服务运维的难度和成本,应该优先考虑此方式,如下:

$ gcc -Wl,-R/home/work/test/libs -ltest

2.指定LD_RUN_PATH环境变量,该环境变量既能影响编译时搜索路径的写入,也能动态影响程序启动时动态库的搜索。
由于设置LD_RUN_PATH会影响整个shell session的环境,避免对其他程序启动产生影响的典型用法:
(1)采用独立的启动脚本,在里面修改LD_RUN_PATH环境变量。
(2)启动前修改,启动后还原,如下:

#!/bin/bash
OLD_LD_RUN_PATH=$LD_RUN_PATH
export LD_RUN_PATH=/home/work/test/libs:$LD_RUN_PATH
./hello
export LD_RUN_PATH=$OLD_LD_RUN_PATH

对于写入程序中的RPATH可以用readelf命令来查看

$ readelf -d curl | grep RPATH
4.3.3 LD_LIBRARY_PATH环境变量

LD_LIBRARY_PATH 估计大家都不陌生,平常在一些找不到so的库的处理方法的文章中也经常能看到,在没有设置LD_RUN_PATH的时候,LD_LIBRARY_PATH路径的优先级是最高的,设置方法和LD_RUN_PATH类似,但是只会影响运行时的动态库搜索,这里不再赘述。

【参考】
程序员的自我修养
高级C/C++编译技术

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++编译知识笔记(一)——基本知识 的相关文章

  • c和java语言中的换行符

    现在行分隔符取决于系统 但在 C 程序中我使用 n 作为行分隔符 无论我在 Windows 还是 Linux 中运行它都可以正常工作 为什么 在java中 我们必须使用 n 因为它与系统相关 那么为什么我们在c中使用 n 作为新行 而不管我
  • 如何在 C++ 中的文件末尾添加数据?

    我已按照网上的说明进行操作 此代码应该将输入添加到文件 数据库 的末尾 但当我检查时 数据会覆盖现有数据 请帮忙 这是我的代码 int main string name string address string handphone cou
  • 如何读取扩展文件属性/文件元数据

    因此 我按照教程使用 ASP net core 将文件 上传 到本地路径 这是代码 public IActionResult About IList
  • C++ 中本地类中的静态成员变量?

    我知道我们不能宣布static本地类中的成员变量 但其原因尚不清楚 那么请问有人可以解释一下吗 另外 为什么我们不能访问非static函数内部定义的变量 内部已经定义了局部类 直接在局部类成员函数中 在下面给出的代码中 int main i
  • 如何在 C# 中从 UNIX 纪元时间转换并考虑夏令时?

    我有一个从 unix 纪元时间转换为 NET DateTime 值的函数 public static DateTime FromUnixEpochTime double unixTime DateTime d new DateTime 19
  • 如何为 C 分配的 numpy 数组注册析构函数?

    我想在 C C 中为 numpy 数组分配数字 并将它们作为 numpy 数组传递给 python 我可以做的PyArray SimpleNewFromData http docs scipy org doc numpy reference
  • 在 C# 中循环遍历文件文件夹的最简单方法是什么?

    我尝试编写一个程序 使用包含相关文件路径的配置文件来导航本地文件系统 我的问题是 在 C 中执行文件 I O 这将是从桌面应用程序到服务器并返回 和文件系统导航时使用的最佳实践是什么 我知道如何谷歌 并且找到了几种解决方案 但我想知道各种功
  • 将 Excel 导入到 Datagridview

    我使用此代码打开 Excel 文件并将其保存在 DataGridView 中 string name Items string constr Provider Microsoft Jet OLEDB 4 0 Data Source Dial
  • 未定义的行为或误报

    我 基本上 在野外遇到过以下情况 x x 5 显然 它可以在早期版本的 gcc 下编译干净 在 gcc 4 5 1 下生成警告 据我所知 警告是由 Wsequence point 生成的 所以我的问题是 这是否违反了标准中关于在序列点之间操
  • 在一个字节中存储 4 个不同的值

    我有一个任务要做 但我不知道从哪里开始 我不期待也绝对不想要代码中的答案 我想要一些关于该怎么做的指导 因为我感到有点失落 将变量打包和解包到一个字节中 您需要在一个字节中存储 4 个不同的值 这些值为 NAME RANGE BITS en
  • 如何将整数转换为 void 指针?

    在 C 中使用线程时 我面临警告 警告 从不同大小的整数转换为指针 代码如下 include
  • 批量更新 SQL Server C#

    我有一个 270k 行的数据库 带有主键mid和一个名为value 我有一个包含中值和值的文本文件 现在我想更新表格 以便将每个值分配给正确的中间值 我当前的方法是从 C 读取文本文件 并为我读取的每一行更新表中的一行 必须有更快的方法来做
  • 等待线程完成

    private void button1 Click object sender EventArgs e for int i 0 i lt 15 i Thread nova new Thread Method nova Start list
  • .NET中的LinkedList是循环链表吗?

    我需要一个循环链表 所以我想知道是否LinkedList是循环链表吗 每当您想要移动列表中的 下一个 块时 以循环方式使用它的快速解决方案 current current Next current List First 电流在哪里Linke
  • 用于 C# 的 TripleDES IV?

    所以当我说这样的话 TripleDES tripledes TripleDES Create Rfc2898DeriveBytes pdb new Rfc2898DeriveBytes password plain tripledes Ke
  • 如何在 C# 中调整图像大小同时保持高质量?

    我从这里找到了一篇关于图像处理的文章 http www switchonthecode com tutorials csharp tutorial image editing saving cropping and resizing htt
  • 如何在按钮单击时模拟按键 - Unity

    我对 Unity 中的脚本编写非常陌生 我正在尝试创建一个按钮 一旦单击它就需要模拟按下 F 键 要拾取一个项目 这是我当前的代码 在编写此代码之前我浏览了所有统一论坛 但找不到任何有效的东西 Code using System Colle
  • 使用 GROUP 和 SUM 的 LINQ 查询

    请帮助我了解如何使用带有 GROUP 和 SUM 的 LINQ 进行查询 Query the database IEnumerable
  • 使用 GhostScript.NET 打印 PDF DPI 打印问题

    我在用GhostScript NET http ghostscriptnet codeplex com打印 PDF 当我以 96DPI 打印时 PDF 打印效果很好 但有点模糊 如果我尝试以 600DPI 打印文档 打印的页面会被极大地放大
  • 当另一个线程可能设置共享布尔标志(最多一次)时,是否可以读取共享布尔标志而不锁定它?

    我希望我的线程能够更优雅地关闭 因此我尝试实现一个简单的信号机制 我不认为我想要一个完全事件驱动的线程 所以我有一个工作人员有一种方法可以使用关键部分优雅地停止它Monitor 相当于C lock我相信 绘图线程 h class Drawi

随机推荐

  • 经纬高(LLA)坐标系地心地固(ECEF)坐标系与东北天(ENU)坐标系转换

    前段时间在做水下机器人项目 添加了RTK 读取到的数据为经纬高坐标系中度分形式的经纬度信息 无法直接用于定位 还需要进行坐标系的转换 后来在学习Cartographer时处理GPS数据也有提到这方面的知识 于是决定汇总到一起进行学习 下文将
  • HTML文本格式化

    目录 HTML 文本格式化实例 如何查看 HTML 源码 文本格式化标签 计算机输出 标签 引用 引用和术语定义 延伸阅读 一个完整的实例 HTML 可定义很多供格式化输出的元素 比如粗体和斜体字 下面有很多例子 可以亲自试试 HTML 文
  • SpringMVC中Controller层注解扫描

    SpringMVC中Controller层扫描 方式一 方式二
  • mysql基于Java web的电动车销售平台毕业设计源码201524

    电动车销售平台的设计与实现 摘 要 信息化社会内需要与之针对性的信息获取途径 但是途径的扩展基本上为人们所努力的方向 由于站在的角度存在偏差 人们经常能够获得不同类型信息 这也是技术最为难以攻克的课题 针对电动车销售平台等问题 对电动车销售
  • 论文研读:SuperGlue vs. LoFTR

    简介 SupeGlue与LoFTR都是对图片间进行特征点匹配的方法 其目的是 找到图像A 图像B中同时存在的相同物体实例 并输出其位置信息 匹配关系 在提取出特征点后 我们通过图神经网络生成匹配代价矩阵 并求解最优匹配矩阵 以获得全局最优的
  • 1.3 起步 - Git 基础

    1 3 起步 Git 基础 版本说明 版本 作者 日期 备注 0 1 loon 2019 3 18 初稿 目录 文章目录 1 3 起步 Git 基础 版本说明 目录 Git 基础 1 直接记录快照 而非差异比较 Figure 4 存储每个文
  • python报错:ValueError: not enough values to unpack

    报错 ValueError not enough values to unpack 分析具体原因 这个错误的信息是 期望有7个返回值 但其实函数只有4个返回值 解决方法 检查函数和接收函数返回值的参数个数是否一致 改成一致即可
  • MySQL - 一文了解MySQL的基础架构及各个组件的作用

    1 概述 不管是开运 运维 测试 都或多或少的要接触MySQL 了解MySQL的基础架构及各个组件之间的关系 有助于我们更加深入的理解MySQL 下面由一张MySQL基础架构图来一起走进MySQL MySQL可以基本划分为Server层和存
  • helm的安装、使用以及自定义Chart

    Helm概述 Helm 是一个 Kubernetes 的包管理工具 类似 Linux 的包管理器 如RedHat系的yum Debian的apt 可以很方便的将之前打包好的 yaml 文件部署到 Kubernetes 上 Helm主要解决以
  • Open3D 基于法线的双边滤波

    目录 一 算法原理 1 算法概述 2 计算步骤 3 参考文献 二 代码实现 三 结果展示 1 原始点云 2 滤波结果 四 相关链接 一 算法原理 1 算法概述 Fleishman 等人提出一种网格双边滤波器 双边滤波器最早应用于灰度图像 该
  • Linux下挂在SATA硬盘时的诡异现象

    ata1 SATA link down SStatus 1 SControl 300 ata1 EH complete ata1 exception Emask 0x10 SAct 0x0 SErr 0x4000000 action 0xa
  • Windows下配置Mask-RCNN环境(各种踩过的坑)

    Windows下配置Mask RCNN pytorch环境 各种踩过的坑 安装Anaconda 1 1 下载和安装Anaconda 安装maskrcnn benchmark项目 2 1 官方建议的安装需求 2 2 逐步安装过程 1 创建虚拟
  • TCP通讯客户端怎样判断与服务器端断开,该如何处理

    TCP通讯客户端怎样判断与服务器端断开 大虾们 神们 C winform里面 采用多线程监听端口 接收方式为阻塞式 创建单一线程进行监听函数 这样阻塞时只阻塞单一线程 对主线程没有影响 并使用异步通信模式 来一个连接后回调函数进行解析入库
  • 动态修改模板字符串中图片--简单解决

    document addEventListener error function e var elem e target if elem id toLowerCase imgurl infowindow 在这内部可以发请求拿到动态的地址 i
  • IP地址,子网掩码、默认网关,DNS的设置和工作原理(总结)

    概念 1 概述 IP地址 人们在Internet上为了区分数以亿计的主机而给每台主机分配的一个专门的地址 通过IP地址就可以访问到每台主机 子网掩码 不能单独存在 它必须结合IP地址一起使用 子网掩码只有一个作用 就是将某个IP地址划分成网
  • Blender教程之魔方全自动特效教学

    魔方玩家在我看来分为三种 一是不懂原理的佛系玩家 三阶魔方可能都要拧很久才能还原 第二种是明白怎么玩的玩家 其实还原一个被打乱的魔方就是做一道层先法的数学题 而第三种就是像我这样虽然不懂解密 但会用Blender做一个魔方来让它 自动还原
  • Android Bluetooth

    Android Bluetooth 使用Android蓝牙API来进行蓝牙通信的四个任务 设置蓝牙 检索周围匹配的或者可用的设备 连接设备 设备间传输数据 所有蓝牙APIs在android bluetooth 包中 创建蓝牙连接所要用到的类
  • 一、人脸识别starter-需求分析

    一 需求来源 对于一些需要本人刷脸认证的场景 比如注册时需要刷脸认证 要求上传身份证必须是本人的 等此场景 二 需求分析 考虑到这是个单独并且可复用的模块 所以决定写一个springboot starter来实现 starter可以上传到自
  • 定位、浮动

    Position 定位 一 position 1 属性描述 设置或获取元素的定位方式 2 版本变更 有 3 语法模板 position static relative absolute fixed 4 默认值 static 尽量避开影响其他
  • C++编译知识笔记(一)——基本知识

    文章目录 一 编译的基本步骤 1 1 预处理阶段 1 2 编译阶段 1 3 汇编阶段 1 4 链接阶段 二 核心常用基本概念 2 1 o目标文件 2 2 符号 2 3 静态链接库 2 4 动态链接库 三 链接和加载 3 1 o文件和静态库的