• Gcc的编译流程分为了四个步骤:
    • 预处理,生成预编译文件(.文件):gcc –E hello.c –o hello.i
    • 编译,生成汇编代码(.s文件):gcc –S hello.i –o hello.s
    • 汇编,生成目标文件(.o文件):gcc –c hello.s –o hello.o
    • 链接,生成可执行文件:gcc hello.o –o hello

一、预处理

预编译程序读出源代码,对其中内嵌的指示字进行响应,产生源代码的修改版本,修改后的版本会被编译程序读入。

在 GNU 术语中,预处理程序叫做 CPP。而 GNU 的可执行程序叫做 cpp。

简单来说,预处理就是将要包含(include)的文件插入原文件中、将宏定义展开、根据条件编译命令选择要使用的代码,最后将这些代码输出到一个 ".i" 文件中等待进一步处理。

预编译过程主要处理那些源代码文件中以 "#"开始的预编译指令。比如"#include"、"#define"等,主要处理规则如下:

  • 将所有的 "#define" 删除,并且展开所有的宏定义
  • 处理所有条件预编译指令,比如"#if"、"#ifdef"、"#elif"、"#else"、"#endif"
  • 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件
  • 删除所有的注释"//"和"/* */"
  • 添加行号和文件名标识,比如 #2 "hello.c" 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号
  • 保留所有的 #pragma 编译器指令,因为编译器需要使用它们

经过预编译后的 .i 文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i 文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确的时候,可以查看预编译后的文件来确定问题。

对 hello.c 进行预编译:gcc -E hello.c -o hello .i

# 28 指的是文件 /usr/include/stdio.h 中的第 28 行,后面的是文件标识

1.1 预处理指令

源代码中的预处理指令叫做指示字(directive) ,从源代码中可以轻易发现,它们以井号(#)开始,在每行都是第一个非空字符。而井号通常都在第一列,后面紧跟着指示字的关键字。

指示字

描述

#define

定义宏名字,预处理程序会把这个宏扩展到使用该名字的位置

#elif

由#if 指示字提供一个用于计算的可选表达式

#else

如果#if、#ifdef 或#ifndef 为假,提供一个用于编译的可选代码集合

#error

产生出错消息,挂起预处理程序

#if

如果计算算术表达式的结果为非零值,就编译指示字和它匹配的#endif 之间的代码

#ifdef

如果已经定义了指定的宏,就编译指示字和它匹配的#endif 之间的代码

#ifndef

如果没有定义指定的宏,就编译指示字和它匹配的#endif 之间的代码

#include

查找指示字列表,直到找到指定的文件,然后将文件内容插入,就好像在文本编辑器中插入一样

#include_next

和#include 一样,但该指示字从找到当前文件的目录之后的目录开始查找

#line

指出行号以及可能的文件名,报告给编译程序,用于创建目标文件中的调试信息

#pragma

提供额外信息的标准方法,可用来指出一个编译程序或一个平台

#undef

删除前面用#define 指示字创建的定义

#warning

由预处理程序创建一个警告消息

##

连接操作符,可用于宏内将两个字符串连接成一个

1.1.1 #define

  • 通过处理传递给宏的参数名字,加上井号(#)就可将其"字符串化"

  • 可变的宏是具有可变数目参数的宏。这些参数由省略号代表,被保存在一个由逗号分隔的字符串中作为变量__VA_ARGS__,它会在宏的内部进行扩展。例如,下面的宏接受任何数目的参数:

  • 可变的宏可以包含命名的参数(只要随后有参数的变量长度列表) 。例如,下面的宏有两个固定参数,以及一个变量列表:

前面所有形式的可变宏至少有一个参数需要满足参数变量列表的需求,因为__VA_ARGS__前面是一个逗号,它用于宏内部的 fprintf()函数调用。作为连接操作符的一个特例,可以要求在__VA_ARGS__为空时,将它插入变量列表可以去掉逗号,如下:

1.1.2 #error 和 #warning

#error 指示字会引起预处理程序报告致命错误或中断。它可用来捕获尝试按照某种不可能工作的形式进行编译的条件。例如,下面的例子只有在定义了__unix__的情况下才能成功编译:

#warning 指示字和#error 指示字的工作原理一样

1.1.3 #include_next

#include_next 指示字只用于某些特殊情况。它用在头文件内部来包含其他头文件,会令新头文件的查找由找到当前头文件的目录之后的目录开始

1.1.4 #line

调试器需要将文件名和行号与数据项和可执行代码关联起来,因此预处理程序会将这类信息插入编译程序的输出结果。有必要按这种方式跟踪原始名字和行号,因为预处理程序会组合一些文件。编译程序在编译插入目标代码中的表时,会使用这些数字。

通常,允许预处理程序通过计算来确定行号,这正是需要的,但也有可能用其他一些处理来去掉这些行号。例如,实现 SQL 语句的通常方法就是将它们写成宏,然后用特殊的处理器将这些宏扩展成具体的 SQL 函数调用。这些扩展可在很多行中运行,这样计算行号就很困难。SQL 处理会通过在输出中插入#line 指示字进行更正,这样预处理程序就会跟踪原始源代码的行号。

  • 可用于#line 指示字的特征和规则的列表:
    • :#line 137
    • 为#line 指示字指定行号和文件名,会令预处理程序改变行号以及当前文件的名字。指示字会设置当前位置为文件 muggles.h 的第一行:#line 1 "muggles.h"
    • #line 指示字修改预定义宏__LINE__ 和 __FILE__的内容。
    • #line 指示字对由#include 指示字查找到的文件名或目录没有影响。

1.1.5 #pragma 和_Pragma

指示字#pragma 提供一种标准方法用来指定特定于编译程序的信息。根据标准,编译程序可以附带#pragma 指示字希望的任何意义。

所有 GCC pragma 都定义了两个词——第一个为 GCC,第二个为指定 pragma 的名字。

  • #pragma GCC dependency
    • dependency pragma 测试当前文件的时间戳,对比其他文件的时间戳。如果其他文件更新,就会发出警告消息。测试文件 lexgen.tbl 的时间戳:
    • #pragma GCC dependency "lexgen.tbl"
    • 如果 lexgen.tbl 比当前文件新,预处理程序就会产生如下消息:
    • warning: current file is older than "lexgen.tbl"
    • 可在 pragma 指示字中加入其他文本,它会作为警告消息的一部分,如下例所示:
    • #pragma GCC dependency "lexgen.tbl" Header lex.h needs to be updated
    • 它会创建下面的警告消息:
    • show.c:26: warning: current file is older than "lexgen.tbl"
    • show.c:26: warning: Header lex.h needs to be updated
  • #pragma GCC poison
    • poison pragma 在每次使用指定名字的时候都会发出消息。例如,可用它确保从未调用指定函数。
    • 下面的 pragma 在调用 memcpy 复本函数时就会发出警告消息:
    • #pragma GCC poison memcpy memmove
    • memcpy(target,source,size);
    • 预处理程序会为该代码产生如下警告消息:
    • show.c:38:9: attempt to use poisoned "memcpy
  • #pragma GCC system_header
    • 由 system_header pragma 打头并随后继续到文件尾的代码被看作是系统头文件的一部分。编译系统头文件代码有一些不同,因为运行时库不能被写,因此它们是严格的纯 C 标准格式。限制所有警告消息(除了#warnings 指示字) 。特殊情况下,一定的宏定义和扩展不会发出警告消息。

_Pragma

通常的#pragma 指示字不能作为宏扩展中的一部分包含进来,因此设计_Pragma 操作符是为了生成宏内部的#pragma 指示字。为创建宏内部的 poison pragma,代码如下:_Pragma("GCC poison printf")

反斜线字符用作转义字符,因此可用这种方式插入引用的字符串来创建 dependency
pragma:

_Pragma("GCC dependency \"lexgen.tbl\"")

1.1.6 ##

可用于宏内部将两个源代码权标连接成一个的连接指示字。可用来构造不会被解析器错误解释的名字。

1.2 预定义宏

GCC中包含了很多的预定义宏,常用的预定义宏如下:

描述

__BASE_FILE__

引用的字符串,包含的是命令行中指定源文件的完整路径名(不一定是使用宏的所有文件)。参见__FILE__

__CHAR_UNSIGNED__

定义该宏用来指出目标机器的字符数据类型是无符号的。limits.h中用它来确定CHAR_MIN和CHAR_MAX的值

__cplusplus

只在C++程序中由定义。如果编译程序不完全符合标准,该宏定义为1,否则它会定义为标准的年和月,格式符合C中的__STDC_VERSION__

__DATA__

11个字符的引用字符串,包括编译程序的日期。它的格式为"May 3 2017"

__FILE__

引用字符串,包含使用宏的源文件名。参见__BASE_FILE__

__func__

同__FUNCTION__

__FUNCTION__

引用字符串,包含当前函数的名字

__GNUC__

__GNUC_MINOR__

__GNUC_PATCHLEVEL__

__GNUG__

由 C++编译程序定义。无论何时定义了__cplusplus 和__GNUC__,
就会定义该宏

__INCLUDE_LEVEL__

指出 include 文件当前深度的整数值。该值在基本文件(命令行中指定的文件)时为 0,而每次#include 指示字输入文件就会加 1

__LINE__

使用宏的文件的行号

__NO_INLINE__

在没有扩展内嵌函数的时候,该宏定义为 1,这可能因为没有优化或者不允许进行内嵌函数

__OBJC__

如果程序被编译成 Objective-C,该宏定义为 1

__OPTIMIZE__

无论何时只要指定任何级别的优化处理,该宏就会定义为 1

__OPTIMIZE_SIZE__

如果设置进行尺寸上的优化而不是速度上的优化,该宏就会定义为1

__REGISTER_PREFIX__

该宏为一个权标(而不是字符串) ,它是注册器名的前缀。可用来编写能够移植到多种环境中的汇编语言

__STDC__

定义为 1 指出该编译程序符合标准 C。 在编译 C++和 Objective-C 时不定义该宏,而且在指定-traditional 选项的时候也不会定义该宏

__STDC_HOSTED__

定义为 1 指出"宿主"的环境(其中含有完整的标准 C 库)

__STDC_VERSION__

长整数,指出标准版本号,形式为它的年和月。例如,标准的 1999年修正版为 199901L。在编译 C++和 Objective-C 时不会定义该宏,而且在指定-traditional 选项的时候也不会定义该宏

__STRICT_ANSI__

只有在命令行中指定-ansi 或-std 的时候,会定义该宏。在 GNU 头文件中使用它来限制标准中的那些定义

__TIME__

引用 7 个字符的字符串,包含编译程序的时间。格式为"18:10:34"

__USER_LABEL_PREFIX__

该宏是一个权标(而不是字符串) ,用作汇编语言中的符号前缀。该权标依平台有所变化,但它通常是个下划线字符

__USING_SJLJ_EXCEPTIONS__

如果异常处理机制为 setjmp 和 longjmp,该宏定义为 1

__VERSION__

完整版本号。该信息没有特殊格式,但它至少含有主要和次要版本号

GCC编译器原理(三)------编译原理三:编译过程---预处理的更多相关文章

  1. C代码编译成可执行程序的过程

    C代码通过编译器编译成可执行代码,经历了四个阶段,依次为:预处理.编译.汇编.链接. 接下来详细讲解各个阶段 一.预处理 1.任务:进行宏定义展开.头文件展开.条件编译,不检查语法. 2.命令:gcc ...

  2. 跟vczh看实例学编译原理——三:Tinymoe与无歧义语法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe.   看了前面的三篇文章,大家应该基本对Tinymoe的代码有一个初步的感觉了.在正确分析"print ...

  3. gcc/g++等编译器 编译原理: 预处理,编译,汇编,链接各步骤详解

    摘自http://blog.csdn.net/elfprincexu/article/details/45043971 gcc/g++等编译器 编译原理: 预处理,编译,汇编,链接各步骤详解 C和C+ ...

  4. 正则表达式引擎的构建——基于编译原理DFA(龙书第三章)——3 计算4个函数

    整个引擎代码在github上,地址为:https://github.com/sun2043430/RegularExpression_Engine.git nullable, firstpos, la ...

  5. GCC编译器原理(二)------编译原理一:ELF文件(2)

    四. ELF 文件格式分析 ELF文件(目标文件)格式主要四种: 可重定向文件: 文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件.(目标文件或者静态库文 ...

  6. 学了编译原理能否用 Java 写一个编译器或解释器?

    16 个回答 默认排序​ RednaxelaFX JavaScript.编译原理.编程 等 7 个话题的优秀回答者 282 人赞同了该回答 能.我一开始学编译原理的时候就是用Java写了好多小编译器和 ...

  7. c++随笔之编译器编译原理

    /* C++编译器原理:1)首先明白声明与定义是两个不同的概念 extern int i;是声明,int i;是定义 函数就更简单了2)编译分为: 预编译:将宏替换,include等代码拷贝过来 编译 ...

  8. 浅谈C++编译原理 ------ C++编译器与链接器工作原理

    原文:https://blog.csdn.net/zyh821351004/article/details/46425823 第一篇:      首先是预编译,这一步可以粗略的认为只做了一件事情,那就 ...

  9. C语言编译器 cc 编译原理

    生成一个可执行的文件通常需要经过以下几个步骤: 预处理你的源代码,去掉注释,以及其他技巧性的工作就像在 C 中展开宏. 检查代码的语法看你是否遵守了这个语言的规则.如果没有,编译器会给出 警告. 把源 ...

随机推荐

  1. linux中,使用cat、head、tail命令显示文件指定行

    小文件可以用cat(也可以用head.tail) 显示文件最后20行:cat err.log | tail -n 20 显示文件前面20行:cat err.log | head -n 20 从20行开 ...

  2. 从Java的角度简单修复Cookie越权漏洞

    技术实在是有限,讲解cookie越权的时候可能有点简单和粗糙.这里就简单记录学习下. 首先自己写一段存在漏洞的代码code: sendCookie.java package cookie; impor ...

  3. jquery 表单序列化

    1.序列化为URL 编码文本字符串 var serialize = $("form[name=testForm]").serialize(); console.log(serial ...

  4. JQuery未来元素事件监听写法

    $(document).on('click','.div1',function(){ alert("abc"); }); 格式一致,第一个参数写事件,第二个参数给谁写事件(选择器) ...

  5. c# WebApi之解决跨域问题:Cors

    什么是跨域问题 出于安全考虑,浏览器会限制脚本中发起的跨站请求,浏览器要求JavaScript或Cookie只能访问同域下的内容.由于这个原因,我们不同站点之间的数据访问会被拒绝. Cors解决跨域问 ...

  6. Flask 自定义过滤器多个参数传入

    非完整HTML文件: <div class="container" style="margin-top:50px;"> <div class= ...

  7. How MVC pattern Flows

    以上MVC流程中Model和View不存在依赖关系 以上MVC流程View和Model存在耦合关系(依赖关系越少越好)

  8. 第七篇-列表式App:ListActivity及ListView

    一.新建一个empty activity的项目. 二.修改MainActivity.java: extends AppCompactActivity改为extends ListActivity.注释掉 ...

  9. hystrix实战总结;

    HystrixCircuitBreaker 有三种状态 : 断路器默认是20个请求失败才打开短路器,可以进行配置: CLOSED :关闭 OPEN :打开 HALF_OPEN :半开 1.接口正确,接 ...

  10. iptables防火墙设置

    ubuntu系统: apt-get install iptables #如果默认没有安装,请运行此命令安装防火墙 # whereis iptables #查看系统是否安装防火墙可以看到:iptable ...