详解C/C++代码的预处理、编译、汇编、链接全过程
1.C/C++运行的四个步骤编写完成一个C/C++程序后,想要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。每个步骤都会生成对应的文件,如下图所示(注意后缀名):第3节将通过一个简易C++工程演示图中的全过程,并解释细节。
2.名词解释为了后面过程的介绍更方便,这里对C++编译过程中涉及的一些常用名词进行解释。
2.1GCC、GNU、gcc与g++GNU:一个操作系统,具体内容不重要,感兴趣可以参考:GCC、GNU到底啥意思?GCC:GNUCompilerCollection(GNU编译器集合)的缩写,可以理解为一组GNU操作系统中的编译器集合,可以用于编译C、C++、Java、Go、Fortan、Pascal、Objective-C等语言。gcc:GCC(编译器集合)中的GNUCCompiler(C编译器)g++:GCC(编译器集合)中的GNUC++Compiler(C++编译器)简单来说,gcc调用了GCC中的CCompiler,而g++调用了GCC中的C++Compiler。
对于*.c和*.cpp文件,gcc分别当作c和cpp文件编译,而g++则统一当作cpp文件编译。2.2代码编译命令gcc/g++常用命令:
指令选项功能-E(注意大写)预处理(Preprocess)指定的源文件,但不进行编译(Compile),这一步生成*.i文本文件-S(注意大写)编译指定的源文件,但不进行汇编(Assemble),这一步生成*.s汇编文件-c编译、汇编指定的源文件,但不进行链接(Link),这一步生成*.o目标文件-o指定生成文件的文件名-Iliblib表示库文件或头文件目录,该指令选项用于手动链接程序可以调用的库文件、头文件-std=手动指定编程语言标准,如-std=c++98、-std=c++11等2.3GDB(gdb)GDB(gdb)全称“GNUsymbolicdebugger”,是Linux下常用的程序调试器。为了能够使用gdb调试,需要在代码编译的时候加上-g,如
g++-g-otesttest.cpp本文中只演示从源代码生成可执行二进制文件的过程,暂不涉及调试过程。调试的配置会在另一篇文章中专门介绍。
3.C++编译过程详解主要参考:
C++预编译,编译,汇编,链接C/C++语言编译链接过程本节内容用下面的简单C++工程做演示。示例的文件结构如下:
|——include|——func.h|——src|——func.cpp|——main.cpp其中,main.cpp是主要代码,include/func.h是自定义函数的头文件,src/func.cpp是函数的具体实现
各个文件的内容如下:
//main.cpp#include#include"func.h"usingnamespacestd;intmain(){inta=1;intb=2;coutcoutC++编译链接全过程
今天博文主要讨论的问题是:我们编写的程序代码是怎样运行起来的?到底运行的是什么内容?平时我们所说的编译主要包括预编译、编译、汇编三部分,这三部分分别都干什么工作,主要职能有哪些,接下来我们一步步探讨总结。
(一)预编译
(1)由源文件“.cpp/.c”生成“.i”文件,这是在预编译阶段完成的;gcc-E.cpp/.c--->.i
(2)主要功能
展开所有的宏定义,消除“#define”; 处理所有的预编译指令,比如#if、#ifdef等; 处理#include预编译指令,将包含文件插入到该预编译的位置; 删除所有的注释“/**/”、"//"等; 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及错误提醒; 保留所有的#program编译指令,原因是编译器要使用它们;(3)缺点:不进行任何安全性及合法性检查
(二)编译---核心
编译过程就是把经过预编译生成的文件进行一系列语法分析、词法分析、语义分析优化后生成相应的汇编代码文件。
(1)由“.i”文件生成“.s”文件,这是在编译阶段完成的;gcc-S.i--->.s
(2)主要功能
词法分析:将源代码文件的字符序列划分为一系列的记号,一般词法分析产生的记号有:标识符、关键字、数字、字符串、特殊符号(加号、等号);在识别记号的同时也将标识符放好符号表、将数字、字符放入到文字表等;有一个lex程序可以实现词法扫描,会按照之前定义好的词法规则将输入的字符串分割成记号,所以编译器不需要独立的词法扫描器; 语法分析:语法分析器将对产生的记号进行语法分析,产生语法树----就是以表达式尾节点的树,一步步判断如何执行表达式操作。下图为一个语法树:
如果存在括号不匹配或者表达式错误,编译器就会报告语法分析阶段的错误;相同的存在一个yacc程序可以根据用户输入的语法规则生成语法树;
语义分析:由语法阶段完成分析的并没有赋予表达式或者其他实际的意义,比如乘法、加法、减法,必须经过语义阶段才能赋予其真正的意义;语义分析主要分为静态语义和动态语义两种;静态语义通常包括声明和类型的匹配、类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含了一个浮点型到整型转换的过程。只要存在类型不匹配编译器会报错。经过语义分析后的语法树的所有表达式都有了类型。动态语义分析只有在运行阶段才能确定;
优化后生成相应的汇编代码文件汇总所有符号(三)汇编:生成可重定位的二进制文件;(.obj文件)
(1)由“.s”文件生成的“.obj”文件;gcc-c.s-->.o;
(2)此文件中生成符号表,能够产生符号的有:所有数据都要产生符号、指令只产生一个符号(函数名);
(四)链接
链接阶段主要分为两部分:
(1)合并所有“.obj”文件的段并调整段偏移和段长度(按照段的属性合并,属性可以是“可读可写”、“只读”、“可读可执行”,合并后将相同属性的组织在一个页面内,比较节省空间),合并符号表,进行符号解析完成后给符号分配地址;其中符号解析的意思是:所有.obj符号表中对符号引用的地方都要找到该符号定义的地方。在编译阶段,有数据的地方都是0地址,有函数的额地方都是下一行指令的偏移量-4(由于指针是4字节);可执行文件以页面对齐。
符号重定位举例:main.c externintgdata; test.c intgdata=10;
main.o *UND*gdata -------->test.o gdata //符号重定位
在进行符号解析时要注意只对global符号进行处理,对于local符号不做处理;
(2)符号的重定位(链接核心):将符号分配的虚拟地址写回原先未分配正确地址的地方
对于数据符号会存准确地址,对于函数符号,相对于存下一行指令的偏移量(从PC寄存器取地址,并且PC中下一行指令的地址)
(五)程序的运行
(1)创建虚拟地址空间到物理空间的映射(创建内核地址映射结构体),创建页目录和页表;
(2)加载代码段和数据段
(3)把可执行文件的入口地址写到CPU的PC寄存器里
(六)目标文件类型
Linux下的ELF文件主要有以下四种:
(1)可重定位文件.obj,这种文件包括数据和指令,可以被链接成为可执行文件(.exe)或者共享目标文件(.so),静态链接库可以归为这一类;
(2)可执行文件.exe,这种文件包含了可以直接运行的程序,它的代表就是ELF可执行文件,他们一般都没有扩展名;
(3)共享目标文件.so,这种文件包含了数据和指令,可以在以下两种情况下使用:一是链接器使用这种文件与其他可重定位文件和共享目标文件链接,二是动态链接器将几个共享目标文件与可执行文件结合,作为进程映像的一部分使用。
(4)核心转储文件,当进程意外终止时,系统可以将该进程的地址空间的内容及种植的一些信息转储到核心文件中,比如coredump文件。
(七)可重定位文件与可执行文件的结构比较
在编译链接的全过程中,汇编完成后生成“可重定位的二进制文件.obj”,链接阶段完成后生成可执行文件.exe,这两者有何区别呢?可重定位文件为什么不可以运行?接下来将比较这种文件的结构布局,以回答上面的疑惑。
当一个程序运行时,操作系统会给进程分配的虚拟地址空间以达到每个进程都有自己独立的运行空间,但是各个进程空间共享内核空间,在32位下,这个空间大小为4G,在64位下,这个虚拟地址空间为8G;下图为32为下虚拟地址空间的布局:
其中内核空间中的ZONE_DMA直接内存访问,占16M,用于磁盘与内存的文件数据交换;
ZONE_NORMAL:平时使用的正常的内核空间;
ZONE_HIGHMEM:高端内存处理。处理高端内存大于1G的数据;(64位没有)由于此空间非常大以至于映射后的虚拟地址空间不足。
(1)可重定位文件(.obj)的组织布局和可执行文件(.exe)组织格式的比较
(1)readelf -h*.o查看.o文件的文件头ELFHEADER信息,包括class(一般为32位)、data、programheader、一些地址记录、size记录等;改变(函数入口地址0x0+符号0x0,汇编阶段完成后)
readelf-S*.o 查看sectionheaders中的内容 包括段的内容、偏移量、属性等;
objdump-d*.o objdump-S*.o 得到汇编后的机器码文件
objdump-t*.o查看符号表 objdump-h*.o查看.o文件的各个段(常用的段.data/.text/.bss/.comment)
(2)在虚拟地址空间上存在的.bss段主要存储未初始化的或者初始化为0的全局变量或者静态变量,但是在.obj和.exe中并不存在此段,那么上述中的数据存储在文件的哪里呢?答案是存储在了“*.comment*”块中,这是因为存在强弱数据类型所导致的,请看下图中所示的情况:
原则上根据.bss的存储内容可以得知gdata3其空间存储,但是却放在了.comment块中,gdata3是一个弱类型,其原因是由于我们不确定其他文件是否会存在同名强类型或者大于其字节数的弱类型出现,造成外文件的变量引用,因此,先将其存放在*COM*中;强弱类型的区分为:强类型(已经初始化的变量)、弱类型(未初始化的变量)。使用规则是:
在.c文件中,假如我们同一目录下的main.c和test.c文件中两者优先规则:
(1)两个文件中都同时定义了int类型的x变量,那么在编译时会提醒有重定义类型;
(2)两个文件中强类型和弱类型都存在时,选择强类型;
(3)两个文件中弱类型同时出现时,选择字节数大的弱类型;
从上图中看到.obj文件中的段信息中.bss 和.comment占据同一地址,说明.bss段并未占据文件空间,只占据虚拟地址空间;那么我们如何知道虚拟地址空间中.bss段是否存有数据。请看下图所示布局信息:
从上面两幅图中可以看到用readelf-h*.o可以查看文件头ELFHEADER信息,其中包括sectionheader,而通过readelf -S*.o可以查看到段的所有信息,从而看到.bss段是否存有数据。
(3)从obj和exe的组织形式比较中发现,exe文件比obj多了一个programheader域,可使用readelf-l可执行文件名查看programheader域的具体信息,如下图所示:
由于我们运行程序只加载数据和指令,并且我们所有的obj文件和exe文件都是以“页”为对齐方式,同时每个页存储的内容按照属性进行分页存储,所以programheader有两个加载页面,只有将数据和指令存储于页中才能真正的给符号分配地址从而运行程序。数据有只读和只写、指令有只读和执行,故而可以根据这个属性确定那些段应该放在哪一个页中,这个属性以及只能加载数据和指令决定了只存在两个load页在磁盘上。到这里为止,我们已经准备好了这个程序可以运行的所有条件。此时可执行文件里的内容按照load的布局被存储在磁盘中,那么如何运行呢?由下面这幅图来说明:
从上图就可以看出一个程序从编译链接到运行的全过程。
欢迎大家留言指出不足。
C++程序编译过程
C++程序编译过程1.编译流程图2.预处理3.编译4.汇编5.链接1.编译流程图2.预处理编译器将C程序的头文件编译进来,还有宏的替换,可以用gcc的参数-E来参看。
主要处理源代码文件中的以“#”开头的预编译指令。处理规则见下
1、删除所有的#define,展开所有的宏定义。
2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4、删除所有的注释,“//”和“/**/”。
5、保留所有的#pragma编译器指令,编译器需要用到他们,如:#pragmaonce是为了防止有文件被重复引用。
6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
命令:unix>gcc–ohellohello.c作用:将hello.c预处理输出hello.i
3.编译这个阶段编译器主要做词法分析、语法分析、语义分析等,在检查无错误后后,把代码翻译成汇编语言[2]。可用gcc的参数-S来参看。编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。一条低级机器语言指令。
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
1、词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。
2、语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。
3、语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
4、优化:源代码级别的一个优化过程。
5、目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。
6、目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
命令:gcc-Shello.i-ohello.s作用:将预处理输出文件hello.i汇编成hello.s文件
4.汇编汇编器as将hello.s翻译成机器语言保存在hello.o中(二进制文本形式)。
将汇编代码转变成机器可以执行的指令(机器码文件)。只是根据汇编指令和机器指令的对照表一一翻译过来,汇编过程有汇编器AS完成。经汇编之后,产生目标文件(与可执行文件格式几乎一样)xxx.o(Windows下)、xxx.obj(Linux下)。
5.链接printf函数存在于一个名为printf.o的单独预编译目标文件中。必须得将其并入到hello.o的程序中,链接器就是负责处理这两个的并入,结果得到hello文件,它就是一个可执行的目标文件。
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接:
1、静态链接:
函数和数据被编译进一个二进制文件。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。
空间浪费:因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,会出现同一个目标文件都在内存存在多个副本;
更新困难:每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。
运行速度快:但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
2、动态链接:
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
共享库:就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;
更新方便:更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。
性能损耗:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
10 分钟看懂 C++ 编译过程
发表日期:2020-03-20
10分钟看懂C++编译过程——看不懂包退
作者:StephenWelch
C++是一门高性能编程语言,它被广泛应用于世界最前沿的技术应用当中——从数据挖掘、大数据,到自动驾驶汽车、机器人,再到电子游戏和视频处理,C++的身影可谓无处不在。作为一名C++程序员,你已经相当精通这门语言,对多线程和并行编程等主题也有了一定的了解。但你是否曾经揭开过编译器那神秘的面纱,是否好奇过在编译期间到底发生了什么事?
这是一个非常值得讨论的问题,今天这篇文章里,我们将详细聊聊你最需要了解的几个重点细节。对编译器内部工作原理的了解能帮你更深刻地理解代码,避开许多常见的“坑”,从而进一步提高你的编程水平。
编译流程现在,让我们一起打开编译器的“小黑盒”,用最简明的办法来解释C++编译器到底都对你的代码施了什么“魔法”吧。
作为一门高级编程语言,C++让程序员的编程工作变得更加容易——低级机器语言一板一眼的本性难以用于编写足够复杂的现代应用程序。编译器通过将C++源码转换成计算机可以执行的二进制文件,填补了高级C++语言和机器语言之间的空白。
总的来说,编译过程还比较复杂,一般可以分为三个阶段:
预处理Preprocessing在实际编译工作开始之前,预处理器指令指示编译器对源码进行临时扩充,以为之后的步骤做好准备。
在C++中,预处理器指令以#号开头,比如#include、#define和#if等。在这一阶段,编译器逐个处理C++源码文件。对于#define指令,编译器将源码中的宏替换成宏定义中的内容;对于#if、#ifdef和#ifndef指令,编译器将有选择地跳过或选中部分源代码;而对于#include指令,编译器将把对应的库的源码插入到当前源代码中——这通常是一些通用的声明。被#include指令引入的头文件(.h)往往会包含大量的代码,你引入的越多,最后生成的预编译文件就越大。总的来说,预编译过的文件会比原来的C++源码更大一些。
通过上面这些替换和插入操作,预处理器产生的是被合为一体的输出文件。预处理器还会在代码中插入记号,使编译器能分辨出每一行来自哪个文件,以便在调试过程中能生成对应的错误信息。在开发调试你的C++程序时,这些错误信息能给你很多帮助。
编译和汇编Compilation&assembly在这一阶段,编译器通过两个连续的步骤,将预处理器产生的代码编译成目标文件(objectfile)。
首先,编译器将去除了预编处理器指令的纯C++代码编译成底层汇编代码。在这一步中,编译器会对代码进行检查优化,指出语法错误、重载决议错误及其他各种编译错误。在C++中,如果一个对象只声明,不进行定义,编译器仍然可以从源代码产生目标文件——因为这个对象也可以指向某些当前代码中还未定义的标识符。
其次,汇编器将上一步生成的汇编代码逐行转换成字节码(也就是机器码)。实际上,如果你希望把代码的不同部分分开编译的话,编译过程在这一步之后就可以停止了。这一步生成的目标文件可以被放在被称为静态库的包中,以备后续使用——也就是说,如果你只修改了一个文件,你并不需要重新编译整个项目的源代码。
链接Linking链接器利用编译器产生的目标文件,生成最终结果。
在这一阶段,编译器将把上一阶段中编译器产生的各种目标文件链接起来,将未定义标识符的引用全部替换成它们对应的正确地址。没有把目标文件链接起来,就无法生成能够正常工作的程序——就像一页没有页码的目录一样,没什么用处。完成链接工作之后,链接器根据编译目的不同,把链接的结果生成为一个动态链接库,或是一个可执行文件。
链接的过程也会抛出各种异常,通常是重复定义或者缺失定义等错误。不只是没进行定义的情况,如果你忘记将对某个库或是目标文件的引用导入进来,让链接器能找到定义的话,也会发生这类错误。重复定义则刚好相反,当有两个库或目标文件中含有对同一个标识符的定义时,就可能出现重复定义错误。
理解编译过程有什么用?当你对编译过程的各个阶段有了新的理解,你就能更好地理解编译错误或连接错误产生的原因,并避免这些与编译相关的潜在问题。比如,如果你理解了预处理过程,你就能利用头文件保护符(用于保护头文件内容不被错误地多次包含的预编译器指令)防止一些编译错误的出现。
对C++编译细节的充分了解,能使你从一个完全不同的角度看待整个编程过程,也让你对原先以为是理所当然的编译过程有了新的认识。
如何使用C++编译器构建并运行一个C++程序所需的基本步骤有:
使用一个编辑器或是编程环境(IDE),构建一个语法正确的C++源文件。运行编译器对源文件进行编译,生成可执行文件。运行生成的文件。编译器的特性差异很大,即使在同一个编译器的不同版本之间也是这样;同样,它们的选项也非常丰富,比如在代码生成、调试、浮点数行为、库处理等方面,都有着相当多的选项。
C++编译器纵览现在你已经对C++的编译有了一定的了解,那么你该用哪种编译器呢?
总体上说,你可以按编译器的许可类型(免费或是收费),使用方式(本地安装或是在线编译)以及所支持的操作系统(Windows、OSX或Linux)来分类。
下面是几点建议:
如果你在Linux上进行编程,GNU编译器套装(GCC)是个非常流行的选项。它是免费的,而且你所用的Linux发行版的软件包仓库里通常就有。对于macOS来说,Clang是个默认选项,它随Xcode的命令行工具一起安装。使用Clang也是免费的。Cygwin项目为Windows系统提供了一系列Linux工具集,包括GCC在内。你可以使用Cygwin来运行GCC或Clang,但请注意,用这种方式生成的代码需要Cygwin才能运行。Windows系统的另外一种选择是MinGW,它不依赖于Cygwin,而且能生成可原生在Windows上运行的可执行程序。有些IDE本身在代码编辑器之外就已经包含了编译器。比如macOS上的Xcode,以及Windows上的VisualStudio等。此外,还有许多专业化的编译器,比如英特尔C++编译器等,为特定的需求专门定制了一些特性。比如,英特尔的编译器在自家的处理器上能更有效地利用多核心架构,产生的代码在英特尔的硬件上运行速度更快。这类专业化的编译器常常需要用户购买价格不菲的授权才能使用。
如果你发现自己正在考虑使用某种不是很流行的编译器,请认真了解它的标准依从性。避免使用那些不符合ISO标准,或不提供可靠实现的标准库的编译器。这里提到的“标准库”是C++自带的大量库文件;而“库文件”,则是已经“打包”好,可以在其他程序中重复使用的预编译代码的集合。
有些编译器和库文件一起被嵌入在软件开发工具(IDE)提供的框架中。这些框架很有用,但如果你打算更换你的工具链,你可能很难脱离它们。
在线C++编译器在线编译器是种很有用的工具,它能让你快速编译代码,而不需要在电脑上安装完整的编译工具链。这让程序员能更轻易的摆弄代码,熟悉最新的语言特性,或是在线分享代码片段,实时合作编辑,以及测试各种不同的编译器等。除了狭义的“编译”功能之外,大部分在线编译器还会执行编译完的程序,并将输出结果显示出来。
和离线编译器一样,在线编译器支持的C++标准版本和提供的特性也千差万别,从使用flag标识来定义编译参数,到处理标准输入并传入命令行和运行时参数等待,不一而足。
常用的在线编译器有下面这几个:
CompileExplorerRepl.itIDEoneCodepad你还可以在这里看到关于更多在线编译器的列表,已按特性进行分类。
总结本文中,我们介绍了C++编译过程的各个阶段,更加详细地了解了整个过程。通过学习俗如何使用C++编译器,并对各种C++编译器进行概述,你得以一窥编译过程的幕后细节,并对它有了一些深入的了解,希望能给你带来帮助。
希望了解更多关于C++编译过程的详细信息?想要学习更多C++知识?欢迎报名参加我们的C++纳米学位课程!
(本文已投稿给「优达学城」。原作:StephenWelch翻译:欧剃转载请保留此信息)
编译来源:https://blog.udacity.com/2020/02/c-compilers-explained.html
标签:Udacity、Translate、C++、Nanodegree