编译过程

GCC编译C语言的过程分成四个步骤:

  • 预处理(Preprocessing),由.c文件到.i文件

  • 编译(Compilation),由.i文件到.s文件

  • 汇编(Assembly),由.s文件到.o文件

  • 链接(Linking),由.o文件到可执行文件

一、预处理

C语言提供的3种预处理功能:

(1)宏定义(2)文件包含(3)条件编译

预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。

预处理的过程主要处理包括以下过程:

  • 将所有的#define删除,并且展开所有的宏定义#define a b ;对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的 a则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。

  • 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等:这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

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

  • 删除所有注释 “//”和”/* */”.

  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。

    • __LINE__ :表示正在编译的文件的行号(十进制数)

    • __FILE__ :表示正在编译的文件的名字(C源程序的名称)

    • __DATE__表示编译时刻的日期字符串,例如: "25 Dec 2007"

    • __TIME__ :表示编译时刻的时间字符串,例如: "12:30:55"

    • __STDC__ :判断该文件是不是定义成标准 C 程序

  • 保留所有的#pragma编译器指令,因为编译器需要使用它们

通常使用以下命令来进行预处理:

gcc -E hello.c -o hello.i

参数-E表示只进行预处理 或者也可以使用以下指令完成预处理过程

cpp hello.c > hello.i      /*  cpp – The C Preprocessor  */ 

1、预处理指令:

预处理指令是以#号开头的代码行,#号必须是该行除了任何空白字符外的第一个字符。#号后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。

1、#define

在c++中,一般用const/枚举/内联去替代宏。但是,define宏在某些方面真的是非常好用。

  1. 替代路径 #define ENG_PATH_1 C:\Program Files (x86)

  2. 针对编译器版本不兼容报错 #define _CRT_SECURE_NO_WARNINGS 1

  3. 条件编译 #ifdef 标识符 程序段 1 #else 程序段 2 #endif

  4. 使用库中的宏

2、#error 预处理,#line 预处理,#pragma 预处理

#error 预处理指令的作用是,编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。

#line 的作用是改变当前行数和文件名称,使用#line命令可改变预定义宏__LINE__与__FILE__的内容,该命令的基本形如下:

#line number[“filename”]

其中的数字为一个正整数,可选的文件名为有效文件标识符。行号为源代码中当前行号,文件名为源文件的名字。命令为#line主要用于调试以及其他特殊应用。

#pragma 是比较重要且困难的预处理指令。

#pragma once

这个的做用就是防止头文件多次包含当然,还有另外一种风格,防止被包含,我同时给出来,是巧妙地利用了define宏

#ifndef _SOME_H
#define _SOME_H  ...//(some.h头文件内容)  
#endif

变量的防止重复定义则利用extern,在头文件中不初始化只声明。引用该头文件即可,在链接过程中。就可以使用到这个变量。

#pragma warning
#pragma warning( disable : 4507 34; once : 4385; error : 164 )
//等价于:
#pragma warning(disable:4507 34) // 不显示 4507 和 34 号警告信息
#pragma warning(once:4385) // 4385 号警告信息仅报告一次
#pragma warning(error:164) // 把 164 号警告信息作为一个错误。

#pragma pack
//使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
//使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
//在#pragma pack (n)和#pragma pack ()之间的代码按 n 个字节对齐。
#pragma pack(push) //保存当前对其方式到 packing stack
#pragma pack(push,n) 等效于
#pragma pack(push)
#pragma pack(n) //n=1,2,4,8,16 保存当前对齐方式,设置按 n 字节对齐
#pragma pack(pop) //packing stack 出栈,并将对其方式设置为出栈的对齐

#pragma message(消息文本)
//当编译器遇到这条指令时,就在编译输出窗口中将消息文本显示出来

#pragma code_seg([“section_name”[,section_class]])
//它能够设置程序中函数代码存放的代码段,在开发驱动程序的时候就会使用到它。

3、#运算符和##预算符

#define SQR(x) printf("The square of "#x" is %d.\n", ((x)*(x)));

这段代码中#就是帮助x作为一个变量,表现出来,而不是一个简单的字母

如果有#,SQR(3)运算出来就是 The square of 3 is 9

如果没有#,SQL(3)运算出来就是 The square of x is 9

##运算符可以使用类函数宏的替换部分。另外,##还可以用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。

例如:

#define X(n) x##n

这样宏调用:X(5)

展开后:x5

与操作符#类似,操作符##也可用在带参宏中替换部分内容。该操作符将宏中的两个部分连接成一个内容。

例如,定义如下宏:

#define VAR(n)  v##n

当使用一下方式引用宏:VAR(1)

预处理时,将得到以下形式:v1

4、可变宏 ... 和 _ _VA_ARGS_ _

实现思想就是宏定义中参数列表的最后一个参数为省略号(也就是三个点)。这样预定义宏__VA_ARGS__就可以被用在替换部分中,以表示省略号代表什么。

比如:

#define DBUG(...) printf(__VA_ARGS__)
DBUG("hello world\n");相当于:printf("hello world\n");
DBUG("int= %d, float= %.2f",5,2.55);相当于:printf("int= %d, float= %.2f",5,2.55);
//省略号只能代替最后面的宏参数。
#define W(x  ... , y) 错误!

5、#@ 字符化操作符

#@x只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。作用是将传的单字符参数名转换成字符,以一对单引用括起来其实就是给x加上单引号,结果返回是一个const char。

6、\ 行继续操作

\ 行继续操作当定义的宏不能用一行表达完整时,可以用”\”(反斜线)表示下一行继续此宏的定义。

注意:最后一行不要加续行符。

VC的预处理器在编译之前会自动将\与换行回车去掉(写成多行时,反斜杠后不能有空格,否则编译器(ARM或VC)会报错!),这样一来既不影响阅读,又不影响逻辑。

1、宏函数和函数的区别:

属性

#define宏

函数

代码长度

每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增长

函数代码只出现于一个地方,每次使用这个函数时,都会调用那个地方的用一份代码

执行速度

更快

存在函数调用/返回的额外开销

操作符优先级

宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果

函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式的求值结果更容易预测

参数求值

参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果

参数在函数被调用前只求值一次。在函数中多次使用参数并不会导致多种求值过程。参数的副作用并不会造成任何特殊的问题

参数类型

宏宇类型无关,只要对参数的操作是合法的,它可以使用于任何参数类型

函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的

二、编译

经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字等。编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化,这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

三、汇编

汇编实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。

通常一个目标文件中至少有两个段:  

代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。  

数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。 

UNIX环境下主要有三种类型的目标文件:

  1. 可重定位文件:其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。

  2. 共享的目标文件:这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。

  3. 可执行文件:它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

四、链接

由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。  

根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

  1. 静态链接在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

  2. 动态链接:在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

最后更新于