简介
对于平常的应用程序开发,我们很少需要关注编译和链接过程,因为通常的开发环境都是流行的集成开发环境(IDE),比如Visual Studio等。这样的IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并到一起的过程称为构建(Build)。即使使用命令行来编译一个源代码文件,简单的一句“gcc hello.c”命令就包含了非常复杂的过程。
IDE和编译器提供的默认配置、编译和链接参数对于大部分的应用程序开发而言已经足够使用了。但是在这样的开发过程中,我们往往会被这些复杂的集成工具所提供的强大功能所迷惑,很多系统软件的运行机制与机理被掩盖,其程序的很多莫名其妙的错误让我们无所适从,面对程序运行时种种性能瓶颈我们束手无策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运行背后的机理及支撑软件运行的各种平台和工具,如果能够深入了解这些机制,那么解决这些问题就能够游刃有余,收放自如了。
一、被隐藏了的过程
C语言的经典,“Hello World”程序。
使用c语言做例子吧,我使用cpp的预处理过,但是预处理的文件太长了。
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
在Linux下,当我们使用GCC来编译Hello World程序时,只须使用最简单的命令(源代码文件名为hello.c):
$gcc hello.c
$./a.out
Hello World事实上,上述过程可以分解为4个步骤,分别是预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking),如图所示。
二、预处理器(Prepressing)——cpp
- 预处理有的文章中,也被称为预编译。
1、预处理指令
- 使用预处理器cpp
会把.c文件预处理成一个.i文件。
$cpp hello.c > hello.i
- 实战
2、预处理过程
预处理过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如 “#include”、“#define ”等,主要处理规则如下:
- 将所有的 “#define ”删除,并且展开所有的宏定义。
- 处理所有条件预编译指令,比如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif ”。
- 处理 “#include ”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释“//”和“/* */”。
- 添加行号和文件名标识,比如#2“hello.c”2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的 #pragma 编译器指令,因为编译器须要使用它们。
经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。
3、预处理生成的hello.i文件
三、编译器(Compilation)——cc
1、编译指令
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。我们将在下一节简单介绍编译的具体几个步骤,这涉及编译原理等一些内容,由于它不是本书介绍的核心内容,所以也仅仅是介绍而已。
目前公司产品有用到这部分的内容,因为公司产品中有自己的语言。但是产品底层的词法分析和语法分析等部分不是我负责的。后面有机会的话继续深入的了解下。
上面的编译过程相当于如下命令:
$cc hello.i -S -o hello.s
可以得到汇编输出文件hello.s
2、编译的过程
- 词法分析
- 语法分析
- 语义分析
- 中间语言生成
- 目标代码生成与优化
3、编译生成的文件(如下图)
后面分一篇文章,详细的介绍下以下的指令都是什么意思。
四、汇编器(Assembly)——as
1、汇编指令
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。
- 上面的汇编过程我们可以调用汇编器as来完成:
$as hello.s -o hello.o
- 实战
汇编生成的目标文件hello.o如下图所示
2、查看汇编生成的目标文件
$objdump -h hello.o
五、链接器(Linking)——ld
1、链接器
- 链接器是一个将编译器产生的目标文件打包成可执行文件或者库文件或者目标文件的程序。
从这句话可以看出,链接器实际上是一个程序,它的输入是编译器产生的目标文件,输出是库、可执行文件或者其它目标文件。
目标文件:.so 文件生成.out文件
链接的指令
ld /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib64 -L/usr/lib --start-group -lgcc -lgcc_eh -lc --end-group hello.o -o hello.out
或者
ld /usr/lib64/crt1.o /usr/lib64/crti.o /usr/lib64/crtn.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib64 -L/usr/lib -lstdc++ -lm -lgcc_s -lc -lgcc hello.o -o hello.out
2、查看链接生成的程序
$objdump -h hello.out
- 从下图可知,从汇编的6个,链接后变成了23个。
六、执行hello.out
$./hello.out
- 执行报错bash: ./test.out: /lib/ld64.so.1: bad ELF interpreter: 没有那个文件或目录?
bash: ./test.out: /lib/ld64.so.1: bad ELF interpreter: 没有那个文件或目录
解决方案见:/lib/ld64.so.1: bad ELF interpreter: 没有那个文件或目录,因为这个问题不太好处理,所以单独分了一篇文章,记录下解决问题的过程。
七、gcc能做的事:预处理、编译、汇编、链接
上面预处理、编译、汇编、链接,我们分别使用了cpp、cc、as、ld。
其实,预处理、编译、汇编、链接这四个过程,gcc自己就能干完了。
- 1、使用gcc预处理(-E表示只进行预编译)。
$gcc -E hello.c -o hello.i
- 2、编译
$gcc -S hello.i -o hello.s
或者使用如下命令:
$gcc -S hello.c -o hello.s
- 3、汇编
$gcc -c hello.s -o hello.o
或者使用gcc命令从C源代码文件开始,经过预编译、编译和汇编直接输出目标文件(Object File):
$gcc -c hello.c -o hello.o
备注:
gcc -c hello.c -o hello.o小写的-c才是仅仅汇编,
gcc -C hello.c -o hello.o如果是大写的-C,会进行链接的。
- 4、链接
$gcc hello.o -o hello.out
或者
$gcc hello.o -o hello
- 5、执行
- 5、查看gcc指令
[dev1@localhost test01]$ gcc --help
用法:gcc [选项] 文件...
选项:
-pass-exit-codes 在某一阶段退出时返回最高的错误码
--help 显示此帮助说明
--target-help 显示目标机器特定的命令行选项
--help={common|optimizers|params|target|warnings|[^]{joined|separate|undocumented}}[,...]
显示特定类型的命令行选项
(使用‘-v --help’显示子进程的命令行参数)
--version 显示编译器版本信息
-dumpspecs 显示所有内建 spec 字符串
-dumpversion 显示编译器的版本号
-dumpmachine 显示编译器的目标处理器
-print-search-dirs 显示编译器的搜索路径
-print-libgcc-file-name 显示编译器伴随库的名称
-print-file-name=<库> 显示 <库> 的完整路径
-print-prog-name=<程序> 显示编译器组件 <程序> 的完整路径
-print-multiarch Display the target's normalized GNU triplet, used as
a component in the library path
-print-multi-directory 显示不同版本 libgcc 的根目录
-print-multi-lib 显示命令行选项和多个版本库搜索路径间的映射
-print-multi-os-directory 显示操作系统库的相对路径
-print-sysroot 显示目标库目录
-print-sysroot-headers-suffix 显示用于寻找头文件的 sysroot 后缀
-Wa,<选项> 将逗号分隔的 <选项> 传递给汇编器
-Wp,<选项> 将逗号分隔的 <选项> 传递给预处理器
-Wl,<选项> 将逗号分隔的 <选项> 传递给链接器
-Xassembler <参数> 将 <参数> 传递给汇编器
-Xpreprocessor <参数> 将 <参数> 传递给预处理器
-Xlinker <参数> 将 <参数> 传递给链接器
-save-temps 不删除中间文件
-save-temps=<arg> 不删除中间文件
-no-canonical-prefixes 生成其他 gcc 组件的相对路径时不生成规范化的
前缀
-pipe 使用管道代替临时文件
-time 为每个子进程计时
-specs=<文件> 用 <文件> 的内容覆盖内建的 specs 文件
-std=<标准> 指定输入源文件遵循的标准
--sysroot=<目录> 将 <目录> 作为头文件和库文件的根目录
-B <目录> 将 <目录> 添加到编译器的搜索路径中
-v 显示编译器调用的程序
-### 与 -v 类似,但选项被引号括住,并且不执行命令
-E 仅作预处理,不进行编译、汇编和链接
-S 编译到汇编语言,不进行汇编和链接
-c 编译、汇编到目标代码,不进行链接
-o <文件> 输出到 <文件>
-pie Create a position independent executable
-shared Create a shared library
-x <语言> 指定其后输入文件的语言
允许的语言包括:c c++ assembler none
‘none’意味着恢复默认行为,即根据文件的扩展名猜测
源文件的语言
以 -g、-f、-m、-O、-W 或 --param 开头的选项将由 gcc 自动传递给其调用的
不同子进程。若要向这些进程传递其他选项,必须使用 -W<字母> 选项。
报告程序缺陷的步骤请参见:
<http://bugzilla.redhat.com/bugzilla>.
八、其他知识点
1、cc、cc1、ccl、gcc
1.1、cc
- 一般的CC就是一个软件链接到GCC的。只不过CC是UNIX中常用的编译工具,而在linux中用的是GCC,有一些在UNIX中写好的程序要放在linux中要指定命令CC编译器,所以将CC指定为GCC。其实就是一个东西。
- 从下图,我们可以看到,cc和gcc链接的库都一致,所以说他们是一个东西的说法是正确的,但是并不是cc链接了gcc.
1.2、cc1
cc1是windows系统下提供的编译器,安装MinGW后,再安装gcc可以就可以看到,作用同gcc.
linux下也有,在/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/cc1
1.3、ccl
- 很多书籍和文章都说链接器使用ccl,但是我们并找不到ccl的指令。(除非自己吧gcc改名为ccl例外)。
- 能找到的和编译相关的cll,链接如下
a:
Compiling Directly via the CCL Compiler,从网站我们可以看到,其生成的目标文件是.ccx,而非.s文件。而从下文可以看到其名字叫做c8_compiler 。
This section describes the CCL compiler, and how to invoke it on different platforms.
The CCL compiler is a separate program, which can be invoked by the user.The name of the program is c8_compiler (or c8_compiler.exe on Microsoft Windows) //软件的名字叫c8_compiler
b:
CCL
https://github.com/Clozure/ccl
Because CCL is written in itself, you need an already-working version of CCL to compile it.
b这个ccl,虽然和语言相关,但是并非是c语言的编译器。
c:
https://www.researchgate.net/figure/CCL-compiler-and-interpreter_fig1_228539904
综上所述:很多书籍中提到的ccl编译命令是不存在的,正确的应该是cc1\cc\gcc。
1.4、gcc
GCC:全称GNU Compiler Collection,GNU编译器套装,是广泛应用的Linux系统的默认编译器(特别是用于编译Linux内核)。GCC能够支持多种架构的处理器,跨平台特性相对出色。不过,GNU组织要求全部的代码由自己完成(防止版权问题),所以GCC虽然有广泛的硬件支持,但是在各硬件平台上却并不是性能最优的编译器。
1.5、icc
ICC:全称Intel C++ Compiler,是Intel开发的C/C++/Fortran编译器套装,适用于Linux、Microsoft和Mac OS X操作系统,没有非IA指令集版本(就是说仅供x86架构CPU使用)。ICC广泛应用于高性能计算、分布式计算等商业计算领域,其向量化和并行化性能是业界的标杆,能够充分发挥现代处理器的特性。
2、cpp
参考:
Oracle_手册页部分_用户命令_用户命令cpp
- 名称
cpp - C 语言预处理程序 - 用法概要
/usr/lib/cpp [-BCHMpPRT] [
-undef] [-Dname] [
-Dname = def]
[-Idirectory] [-U
name] [-Ydirectory]
[input-file [output-file]]
- 描述
cpp 是 C 语言预处理程序。cpp 还用作其他 Sun 编译器的第一遍操作的预处理程序。
cpp 可以接受两个文件名作为参数。 input-file 和 output-file 分别是用于预处理程序的输入和输出文件。缺省情况下,它们是标准输入和标准输出。
3、gcc、g++
gcc和g++是什么,有什么区别?
发展至今(2020 年 6 月份),GCC 编译器已经更新至 10.1.0 版本,其功能也由最初仅能编译 C 语言,扩增至可以编译多种编程语言,其中就包括 C++ 。
除此之外,当下的 GCC 编译器还支持编译 Go、Objective-C,Objective-C ++,Fortran,Ada,D 和 BRIG(HSAIL)等程序,甚至于 GCC 6 以及之前的版本还支持编译 Java 程序。但本教程主要讲解如何使用 GCC 编译器编译运行 C 和 C++ 程序,因此有关其它编程语言如何使用 GCC 编译器编译,将不再做具体讲解。
那么,在已编辑好 C 语言或者 C++ 代码的前提下,如何才能调用 GCC 编译器为我们编译程序呢?很简单,GCC 编译器已经为我们提供了调用它的接口,对于 C 语言或者 C++ 程序,可以通过执行 gcc 或者 g++ 指令来调用 GCC 编译器。
值得一提的是,实际使用中我们更习惯使用 gcc 指令编译 C 语言程序,用 g++ 指令编译 C++ 代码。需要强调的一点是,这并不是 gcc 和 g++ 的区别,gcc 指令也可以用来编译 C++ 程序,同样 g++ 指令也可以用于编译 C 语言程序。
那么,gcc 和 g++ 的区别是什么呢?接下来就给读者做详细的讲解。
实际上,只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别,比如:
- xxx.c:默认以编译 C 语言程序的方式编译此文件;
- xxx.cpp:默认以编译 C++ 程序的方式编译此文件。
- xxx.m:默认以编译 Objective-C 程序的方式编译此文件;
- xxx.go:默认以编译 Go 语言程序的方式编译此文件;
当然,gcc 指令也为用户提供了“手动指定代表编译方式”的接口,即使用 -x 选项。例如,gcc -xc xxx 表示以编译 C 语言代码的方式编译 xxx 文件;而 gcc -xc++ xxx 则表示以编译 C++ 代码的方式编译 xxx 文件。有关 -x 选项的用法,后续会给出具体样例。
但如果使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件。也就是说,对于 .c 文件来说,gcc 指令以 C 语言代码对待,而 g++ 指令会以 C++ 代码对待。但对于 .cpp 文件来说,gcc 和 g++ 都会以 C++ 代码的方式编译。
4、问chatGPT:c语言预处理、编译、汇编、链接这四个过程,在linux分别由哪些实现方式
chatGPT答复如下:
在Linux系统上,C语言预处理、编译、汇编和链接这四个过程可以使用以下工具实现:
- 预处理 - 可以使用GNU C Preprocessor (cpp)工具进行预处理。例如,在终端中输入以下命令来生成预处理后的代码:
cpp hello.c > hello.i
- 编译 - 可以使用GCC(GNU Compiler Collection)或Clang等C语言编译器进行编译。例如,在终端中输入以下命令将预处理后的代码编译成目标文件:
gcc -c hello.i -o hello.o
- 汇编 - 可以使用GNU Assembler (gas)或其他汇编器将编译后的目标文件转换为汇编代码。例如,在终端中输入以下命令将目标文件汇编成汇编代码:
objdump -d hello.o > hello.s
- 链接 - 可以使用GNU Linker (ld)或其他链接器将多个目标文件连接成一个可执行文件。例如,在终端中输入以下命令将多个目标文件链接成可执行文件:
gcc -o hello hello.o
请注意,不同的Linux系统可能会使用不同的工具和命令来进行C语言编译和开发。以上示例仅供参考,实际操作时应根据自己的需求和情况进行调整。
5、问chatGPT:c语言预处理生成的hello.i文件,除了使用gcc可以编译成hello.s文件,还有哪些方式可以编译成hello.s文件
除了使用GCC编译器,您可以使用其他C语言编译器或预处理工具来将预处理后的"hello.i"文件编译成汇编代码(".s"文件)。例如:
-
Clang - Clang是另一个流行的开源C语言编译器,与GCC类似,可以将预处理后的C源文件编译成汇编代码。您可以使用以下命令生成汇编代码文件:
clang -S hello.i
-
LLVM IR - LLVM IR是一种中间表示形式,可以通过LLVM编译器将其转换为汇编代码。您可以使用以下命令将预处理后的C源文件编译为LLVM IR:
clang -emit-llvm -S hello.i
然后,您可以使用以下命令将LLVM IR编译为汇编代码:
llc -march=x86-64 hello.ll -o hello.s
-
CPP - 除了使用C语言编译器,您还可以使用预处理工具CPP将预处理后的C源文件转换为汇编代码。您可以使用以下命令生成汇编代码文件:
cpp hello.i > hello.cpp
g++ -S hello.cpp
请注意,不同的编译器和工具可能会产生不同的汇编代码,因此生成的".s"文件可能会有所不同。文章来源:https://www.toymoban.com/news/detail-466847.html
参考
1、《程序员的自我修养链接装载与库》
2、深入理解计算机系统第七章 链接知识整理
3、C编译器翻译c程序的四个阶段
4、程序员C语言快速上手——工程篇(十二)
5、GCC命令 (预处理cpp --> 编译ccl --> 汇编as --> 链接ld)(转载)
6、GCC\LD文章来源地址https://www.toymoban.com/news/detail-466847.html
到了这里,关于【编译、链接、装载一】预处理、编译、汇编、链接的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!