源文件到可执行目标文件的过程
预处理阶段
预处理器根据字符#开头的命令,修改原始的C程序,得到另一个C程序,通常是以.i作为文件扩展名。
- 预处理命令
-
include: 就是copy&paste
-
define: 宏定义
-
ifdef
-
pragma once:头文件保护符
-
预处理第二次读到同一个文件时,会自动跳过,防止把单个文件多次include到一个cpp/翻译单元里
-
传统方式:#ifndef… #define… #endif
-
https://blog.csdn.net/fengbingchun/article/details/78696814/
-
-
编译阶段
编译器将文本文件hello.i翻译成文本文件hello.s,它包括一个汇编语言程序。
标记解释(tokenizing),解析(parsing): 生成抽象语法树
编译器优化
-
代码重排序
-
inline
- 不调用用函数,而是在函数调用点上,直接将函数的内容展开
-
编译器甚至可以不执行部分代码,只要没有影响到
the observable behavior
the observable behavior
:compiler can make whatever changes it wants to the program as long as the effect of these changes does not alter the observable behavior
-
https://cloud.tencent.com/developer/article/1858501
-
http://walkerdu.com/2020/04/22/gcc_optimization/
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -g")
汇编阶段
汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中,hello.o文件是一个二进制文件。
链接阶段
链接器会找到每个符号的位置,把所有.o文件合并成一个可执行目标文件,它可以被加载到内存中,由系统执行。
如果只有一个cpp文件也需要链接,因为需要知道入口点在哪(可设置,可以不是main函数)
为了防止链接时有重复的函数(定义),头文件只写函数声明,函数定义放到.cpp文件中
编译器
组成
传统的编译器通常分为三个部分,前端(frontEnd),优化器(Optimizer)和后端(backEnd):
- 前端主要负责词法和语法分析,将源代码转化为抽象语法树;
- 优化器则是在前端的基础上,对得到的中间代码进行优化,使代码更加高效;
- 后端则是将已经优化的中间代码转化为针对各自平台的机器代码。
GCC
GCC(GNU Compiler Collection,GNU 编译器套装),是一套由 GNU 开发的编程语言编译器。可处理C、 C++、Fortran、Pascal、Objective-C、Java 以及 Ada 等他语言,也包含了这些语言的库(如libc.so等)。
gcc是 GNU C Compiler(C编译器)
g++是 GNU C++ Compiler(C++编译器)
MinGW:GCC的windows版本
https://gcc.gnu.org/
https://gcc.gnu.org/onlinedocs/
FLAGS
- https://caiorss.github.io/C-Cpp-Notes/compiler-flags-options.html
Clang+LLVM
LLVM (Low Level Virtual Machine,底层虚拟机))提供了与编译器相关的支持,能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。简而言之,可以作为多种编译器的后台来使用。
Clang 是 LLVM 的前端,可以用来编译 C,C++,ObjectiveC 等语言。Clang 则是以 LLVM 为后端的一款高效易用,并且与IDE 结合很好的编译前端。
链接(linking)
链接的意义
使得分离编译称为可能。我们不用将一个大型应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。
静态链接
链接器(linker)的主要任务
- 符号解析(symbol resolution)
- 重定位(relocation)
可重定位目标文件
汇编器生成的可重定位目标文件由各种不同的代码和数据节(section)组成。指令在一个节中,初始化的全局变量在另一节中,未初始化的变量又在另外一个节中。
符号和符号表
-
符号指的是全局变量和函数
-
每个可重定向目标模块m都有一个符号表,它包含m所定义和引用的符号的信息。在连接器的上下文中,有三种不同的符号:
- 全局符号
- 由m定义并能被其它模块引用
- 例如:non-static function,non-static global variable
- 外部符号
- 由其它模块定义并被m引用的全局符号
- 局部符号
- 只被模块m定义和引用
- static修饰的函数和全局变量
- 全局符号
符号表不包含对应于本地非静态变量的任何符号,这些符号在运行时在栈中被管理,连接器对此类符号不感兴趣。本地静态(static)变量不在栈中管理,编译器在.data和.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地连接器符号。
符号解析
目标文件定义和引用符号,符号解析的目的是将每个符号的引用和一个唯一的符号定义关联起来。
- 如何解析多重定义的全局符号
- 强符号:函数,已初始化的全局变量
- 弱符号:未初始化的全局变量
- 规则
- 不允许有多个强符号
- 如果有一个强符号和多个弱符号,那么选择强符号
- 如果有多个弱符号,那么从这些弱符号中任意选择一个
- 但是仍然会有很多问题,因此需要尽量避免使用全局变量,如果要使用全局变量,那么:
- 尽量用static修饰
- 初始化全局变量
- use extern if you reference an external global variable
重定位
- 重定位有两步组成:
- 重定位节和符号定义
- 把相同类型的节合并为同一类型的新的聚合节
- 当这一步完成后,程序中的每个指令和全局变量都有唯一的运行时存储器地址了
- 重定位节中的符号引用
- 链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行时地址
- 重定位节和符号定义
可执行目标文件
-
可以直接加载到内存中,典型的可执行目标文件如下左图所示
-
右边这张图表示了进程在内存中的构成,也就是下面这张图
打包常用函数
传统方式:静态库(static library)
-
何为静态库
Concatenate related relocatable object files into a single file with an index(called an archive)
- 在windows下以.lib结尾,linux下以.a结尾
Enhance linker so that it tries to resolve unresolved external references by looking for the symbols in one or more archives
If an archive member file resolves reference, link it into the executable
-
常用静态库
-
如何链接静态库
- 优点
- 装载速度很快,运行速度比动态链接快;
- 只需要开发人员在开发机上有完整的.lib文件,不需要在用户机器上有完整的.lib文件,自完备
- 缺点
- 需要定期维护和更新,如果想要使用静态库的最新版本,必须显式地与最新版本的库重新链接
- 函数的代码会被重复复制,造成了内存资源的极大浪费
现代方式:Shared Library
Object files that contain code and data that are loaded and linked into an application dynamically, at either load-time or run-time
Also called: dynamic link libraries, DDLs, .so files
- 优点
- 可执行文件很小;
- 适合大规模软件开发,开发过程耦合度小、独立,便于不同开发人员和开发组织开发;
- 不同编程语言按照约定可以使用同一套动态链接库;
- 动态链接库文件与exe文件独立,如果输出接口相同,更换动态链接库文件不会对exe文件产生影响,可拓展性和可维护性好
- 缺点
- 速度没有静态链接快;
- 不具有自完备,如果用户机器中没有动态链接库文件,程序将无法运行并且报错
常用命令
gcc -shared -fPIC math.c -o libmath.so # 生成动态库
gcc main.c -lmath -L. -o main # 链接动态库
odjdump:对目标文件(obj)或可执行文件进行反汇编(机器码->汇编语言),它以一种可阅读的格式让你更多的了解二进制文件可能带有的附加信息