GCC编译器原理(三)------编译原理三:编译过程---预处理

发布者:平安幸福最新更新时间:2024-08-22 来源: cnblogs关键字:GCC编译器  编译过程  预处理 手机看文章 扫描二维码
随时随地手机看文章
  • 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 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__

该宏总是定义为编译程序的主要版本号。例如,如果编译程序版本
号为 3.1.2,该宏定义为 3

__GNUC_MINOR__

该宏总是定义为编译程序的次要版本号。例如,如果编译程序版本
号为 3.1.2,该宏定义为 1

__GNUC_PATCHLEVEL__

该宏总是定义为编译程序的修正版本号。例如,如果编译程序版本
号为 3.1.2,该宏定义为 2

__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编译器  编译过程  预处理 引用地址:GCC编译器原理(三)------编译原理三:编译过程---预处理

上一篇:GCC编译器原理(三)------编译原理三:编译过程(2-1)---编译之词法分析
下一篇:GCC编译器原理(二)------编译原理一:ELF文件(3)

推荐阅读最新更新时间:2026-03-22 11:56

Ubuntu 12.04嵌入式交叉编译环境arm-linux-gcc搭建过程图解
安装前的絮叨 首先简单介绍一下,所谓的搭建交叉编译环境,即安装、配置交叉编译工具链。在该环境下编译出嵌入式Linux系统所需的操作系统、应用程序等,然后再上传到目标机上。 交叉编译工具链是为了编译、链接、处理和调试跨平台体系结构的程序代码。对于交叉开发的工具链来说,在文件名称上加了一个前缀,用来区别本地的工具链。例如,arm-linux-表示是对arm的交叉编译工具链;arm-linux-gcc表示是使用gcc的编译器。除了体系结构相关的编译选项以外,其使用方法与Linux主机上的gcc相同,所以Linux编程技术对于嵌入式同样适用。不过,并不是任何一个版本拿来都能用,各种软件包往往存在版本匹配问题。例如,编译内核时需要使用arm
[单片机]
Ubuntu 12.04嵌入式交叉<font color='red'>编译</font>环境arm-linux-gcc搭建<font color='red'>过程</font>图解
GCC编译器原理(二)------编译原理一:ELF文件(1)
二、ELF 文件介绍 2.1 可执行文件格式综述 相对于其它文件类型,可执行文件可能是一个操作系统中最重要的文件类型,因为它们是完成操作的真正执行者。可执行文件的大小、运行速度、资源占用情况以及可扩展性、可移植性等与文件格式的定义和文件加载过程紧密相关。研究可执行文件的格式对编写高性能程序和一些黑客技术的运用都是非常有意义的。 可执行链接格式 ( Executable and Linking Format)最初是由 UNIX 系统实验室 ( UNIX System Laboratories, USL)开发并发布的, 作为应用程序二进制接口 ( Application Binary Interface, ABI)的一部分。
[单片机]
<font color='red'>GCC编译</font>器原理(二)------<font color='red'>编译</font>原理一:ELF文件(1)
u-boot配置和编译过程详解
备注:分析的是OK210开发板自带的uboot_smdkv210,可能有些部分和其他版本不太一样,但是原理都类似。 编译u-boot的步骤 make forlinx_linux_configmake1212 首先生成配置文件,然后编译源码,依次看看这些命令都干了些什么事情 配置过程 #forlinx addforlinx_linux_config : unconfig @$(MKCONFIG) $(@:_config=) arm s5pc11x smdkc110 samsung s5pc110 linux @echo TEXT_BASE = 0xcc800000 $(obj)board/samsung/smdkc110
[单片机]
一套完整的ARM交叉编译环境的搭建过程
在网上查看了不少资料,参考别人的,在加上一些自己遇到的问题结合的,内容直接转载过来了,给和我一样,刚 入门ARM的新手的。虽然网上很多现成的,但是自己动手做作,还是很好的。 编译环境: 内核名称:Linux 内核发行版: 3.3.0-4.fc16.x86_64 内核版本:#1 SMP Tue Mar 20 18:05:40 UTC 2012 硬件架构名称: x86_64 硬件平台:x86_64 操作系统:GNU/Linux 当前系统gcc版本号:4.6.3 在Linux中建立整个ARM交叉编译环境的整体过程为: 1、 下载源码包放在/mnt/hgfs/Document/ 2、 建立编译目录并设置环境变量 3
[单片机]
ucos在s3c2410上运行过程整体剖析之基础知识--ADS编译、链接器
我在学习嵌入式操作系统UCOS时,有很大的迷茫之处,在于我不知道我用的工具到底帮我干了些神马工作。下面就说一下我当时的疑问: 1:编译器和链接器干了些神马?我现在也还没能力知道它是怎么干的这些工作。以后慢慢来呗。但当时我连它干了神马都不清楚。 2:它输入的是源程序,那他输出的是什么?有格式吗?是什么样子那? 重重疑问。 ADS编译器的目的是怎么把高级语言编程能在ARM平台上直接运行的东东。 而我们的目的是看看他干了些神马,让我们了解一下它以及他链接出来的东东是什么样子。对嵌入式整个工程开发做了哪些贡献,这样我们就能更好的使用它了。是不是。 关于ARM嵌入式开发的要点和步骤请参考 基于ARM的嵌入式系统程序开发要点.pdf ,这
[单片机]
Realview MDK中编译器对中断处理的过程详解
在ARM程序的开发过程中,对中断的处理是很普遍的、也是相当重要的。Realview MDK使用的RVCT编译器提供了__irq关键字,用此关键字修饰的函数被作为中断出来函数编译,即在编译的过程中,编译器会自动添加中断处理过程中现场保护和恢复的代码,减小程序的开发难度,加快软件的开发过程。 在理解__irq关键字的作用之前,先看一下ARM核对异常的处理过程。当产生异常时, ARM核拷贝CPSR寄存器的内容SPSR_ mode 寄存器中,同时设置适当的CPSR 位、改变处理器状态进入ARM 态和处理器模式,从而进入相应的异常模式。在设置中断禁止位禁止相应中断(如果需要)后,ARM核保存返回地址到LR_ mode ,同时设置PC为相应的异
[单片机]
编译通过的U-boot和使用的arm-linux-gcc编译
说实话编译U-boot挺累人的,要做的修改不是很多,但是在编译器上花的功夫却很多,经常遇到各种奇怪的问题。 下面是编译通过的U-boot和对应的gcc编译器 GCC下载地址:http://download.csdn.net/detail/king_mcu/9002001 U-Boot下载地址:http://download.csdn.net/detail/king_mcu/9002011 说明: 1、arm-linux-gcc解压到linux电脑上后,添加环境变量,这点不用多说,下面是我电脑上的安装路径: 2、解压U-boot到工作目录,修改Makefile中的编译器路径,改为你电脑上arm-linux-g
[单片机]
<font color='red'>编译</font>通过的U-boot和使用的arm-linux-<font color='red'>gcc编译</font>器
Ubuntu 安装arm-linux-gcc编译
/********************************************************************************** * Ubuntu 安装arm-linux-gcc编译器 * 说明: * Ubuntu下直接安装arm-linux-gcc编译器,省得去配置那些环境。 * * 2017-3-21 深圳 南山平山村 曾剑锋 *********************************************************************************/ 一、参考文档:
[单片机]
小广播
最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

厂商技术中心

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

 
机器人开发圈

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2026 EEWORLD.com.cn, Inc. All rights reserved