1. 程式人生 > >GCC編譯器原理(三)------編譯原理三:編譯過程---預處理

GCC編譯器原理(三)------編譯原理三:編譯過程---預處理

ddl str dep 數據 路徑 back char 構造 data

  • 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編譯器原理(三)------編譯原理三:編譯過程---預處理