天天看點

gcc編譯背後(第一部分:預處理和編譯)

平時在Linux下寫代碼,直接用"gcc -o out in.c"就把代碼編譯好了,但是這後面到底做了什麼事情呢?如果學習過編譯原理則不難了解,一般進階語言程式編譯的過程莫過于:預處理、編譯、彙編、鍊 接。gcc在背景實際上也經曆了這幾個過程,我們可以通過-v參數檢視它的編譯細節,如果想看某個具體的編譯過程,則可以分别使用-E,-S,-c和- O,對應的背景工具則分别為cpp,cc1,as,ld。下面我們将逐漸分析這幾個過程以及相關的内容,諸如文法檢查、代碼調試、彙編語言等。

1、預處理

    開篇簡述:預處理是C語言程式從源代碼變成可執行程式的第一步,主要是C語言編譯器對各種預處理指令進行處理,包括頭檔案的包含、宏定義的擴充、條件編譯的選擇等。

    以前沒怎麼“深入”預處理,腦子對這些東西總是很模糊,隻記得在編譯的基本過程(詞法分析、文法分析)之前還需要對源代碼中的宏定義、檔案包含、條件編譯 等指令進行處理。這三類的指令很常見,主要有#define, #include和#ifdef ... #endif,要特别地注意它們的用法。(更多預處理的指令請查閱相關資料)

    #define除了可以獨立使用以便靈活設定一些參數外,還常常和#ifdef ... #endif結合使用,以便靈活地控制代碼塊的編譯與否,也可以用來避免同一個頭檔案的多次包含。關于#include貌似比較簡單,通過man找到某個 函數的頭檔案,copy進去,加上<>就okay。這裡雖然隻關心一些技巧,不過預處理還是蘊含着很多潛在的陷阱(可參考<C Traps & Pitfalls>),我們也需要注意的。下面僅介紹和預處理相關的幾個簡單内容。

  • 列印出預處理之後的結果:gcc -E hello.c

        這樣我們就可以看到源代碼中的各種預處理指令是如何被解釋的,進而友善了解和查錯。

        實際上gcc在這裡是調用了cpp的(雖然我們通過gcc的-v僅看到cc1),cpp即The C Preprocessor,主要用來預處理宏定義、檔案包含、條件編譯等。下面介紹它的一個比較重要的選項-D。

  • 在指令行定義宏:gcc -Dmacro hello.c

        這個等同于在檔案的開頭定義宏,即#define maco,但是在指令行定義更靈活。例如,在源代碼中有這些語句。

    #ifdef DEBUG

    printf("this code is for debugging\n");

    #endif

        如果編譯時加上-DDEBUG選項,那麼編譯器就會把printf所在的行編譯進目标代碼,進而友善地跟蹤該位置的某些程式狀态。這樣-DDEBUG就可以當作一個調試開關,編譯時加上它就可以用來列印調試資訊,釋出時則可以通過去掉該編譯選項把調試資訊去掉。

    本節參考資料:

    [1] C語言教程第九章:預處理

    http://www.bc-cn.net/Article/kfyy/cyy/jc/200409/9.html

    [2] 更多

    http://www.hemee.com/kfyy/c/6626.html

    http://www.91linux.com/html/article/program/cpp/20071203/8745.html

    http://www.janker.org/bbs/programmer/2006-10-13/327.html

    2、編譯(翻譯)

        開篇簡要:編譯之前,C語言編譯器會進行詞法分析、文法分析(-fsyntax-only),接着會把源代碼翻譯成中間語言,即彙編語言。如果想看到這個 中間結果,可以用-S選項。需要提到的是,諸如shell等解釋語言也會經曆一個詞法分析和文法分析的階段,不過之後并不會進行“翻譯”,而是“解釋”, 邊解釋邊執行。

        把源代碼翻譯成彙編語言,實際上是編譯的整個過程中的第一個階段,之後的階段和彙編語言的開發過程沒有什麼差別。這個階段涉及到對源代碼的詞法分析、文法檢查(通過-std指定遵循哪個标準),并根據優化(-O)要求進行翻譯成彙編語言的動作。

        如果僅僅希望進行文法檢查,可以用-fsyntax-only選項;而為了使代碼有比較好的移植性,避免使用gcc的一些特性,可以結合-std和- pedantic(或者-pedantic-erros)選項讓源代碼遵循某個C語言标準的文法。這裡示範一個簡單的例子。

    Quote:

    $ cat hello.c

    #include <stdio.h>

    int main()

    {

            printf("hello, world\n")

            return 0;

    }

    $ gcc -fsyntax-only hello.c

    hello.c: In function ‘main’:

    hello.c:5: error: expected ‘;’ before ‘return’

    $ vim hello.c

    $ cat hello.c 

    #include <stdio.h>

    int main()

    {

            printf("hello, world\n");

            int i;

            return 0;

    $ gcc -std=c89 -pedantic-errors hello.c    #預設情況下,gcc是允許在程式中間聲明變量的,但是turboc就不支援

    hello.c: In function ‘main’:

    hello.c:5: error: ISO C90 forbids mixed declarations and code

        文法錯誤是程式開發過程中難以避免的錯誤(人的大腦在很多條件下都容易開小差),不過編譯器往往能夠通過文法檢查快速發現這些錯誤,并準确地告訴你文法錯 誤的大概位置。是以,作為開發人員,要做的事情不是“恐慌”(不知所措),而是認真閱讀編譯器的提示,根據平時積累的經驗(最好在大腦中存一份常見文法錯 誤索引,很多資料都提供了常見文法錯誤清單,如<C Traps&Pitfalls>和最後面的參考資料[12]也列出了很多常見問題)和編輯器提供的文法檢查功能(文法加亮、括号比對提示 等)快速定位文法出錯的位置并進行修改。

        文法檢查之後就是翻譯動作,gcc提供了一個優化選項-O,以便根據不同的運作平台和使用者要求産生經過優化的彙編代碼。例如,

    Quote:

    $ gcc -o hello hello.c            #采用預設選項,不優化

    $ gcc -O2 -o hello2 hello.c        #優化等次是2

    $ gcc -Os -o hellos hello.c        #優化目标代碼的大小

    $ ls -S hello hello2 hellos        #可以看到,hellos比較小,hello2比較大

    hello2  hello  hellos

    $ time ./hello

    hello, world

    real    0m0.001s

    user    0m0.000s

    sys     0m0.000s

    $ time ./hello2                #可能是代碼比較少的緣故,執行效率看上去不是很明顯

    hello, world

    real    0m0.001s

    user    0m0.000s

    sys     0m0.000s

    $ time ./hellos                #雖然目标代碼小了,但是執行效率慢了些

    hello, world

    real    0m0.002s

    user    0m0.000s

    sys     0m0.000s

        根據上面的簡單示範,可以看出gcc有很多不同的優化選項,主要看使用者的需求了,目标代碼的大小和效率之間貌似存在一個“糾纏”,需要開發人員自己權衡。

        下面我們通過-S選項來看看編譯出來的中間結果,彙編語言,還是以之前那個hello.c為例。

    Quote:

    $ gcc -S hello.c        #預設輸出是hello.s,可自己指定,輸出到螢幕-o -,輸出到其他檔案-o file

    $ cat hello.s

    cat hello.s

            .file   "hello.c"

            .section        .rodata

    .LC0:

            .string "hello, world"

            .text

    .globl main

            .type   main, @function

    main:

            leal    4(%esp), %ecx

            andl    $-16, %esp

            pushl   -4(%ecx)

            pushl   %ebp

            movl    %esp, %ebp

            pushl   %ecx

            subl    $4, %esp

            movl    $.LC0, (%esp)

            call    puts

            movl    $0, %eax

            addl    $4, %esp

            popl    %ecx

            popl    %ebp

            leal    -4(%ecx), %esp

            ret

            .size   main, .-main

            .ident  "GCC: (GNU) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)"

            .section        .note.GNU-stack,"",@progbits

        不知道看出來沒?和我們在課堂裡學的intel的彙編文法不太一樣,這裡用的是AT&T文法格式。如果之前沒接觸過AT&T的,可以看看 參考資料[2]。如果想學習Linux下的彙編語言開發,從下一節開始哦,下一節開始的所有章節基本上覆寫了Linux下彙編語言開發的一般過程,不過這 裡不介紹彙編語言文法。

        這裡需要補充的是,在寫C語言代碼時,如果能夠對編譯器比較熟悉(工作原理和一些細節)的話,可能會很有幫助。包括這裡的優化選項(有些優化選項可能在彙 編時采用)和可能的優化措施,例如位元組對齊(可以看看這本書"Linux_Assembly_Language_Programming"的第六小節)、 條件分支語句裁減(删除一些明顯分支)等。

    本節參考資料

    [1] Guide to Assembly Language Programming in Linux(pdf教程,社群有下載下傳)

    http://oss.lzu.edu.cn/modules/wfdownloads/singlefile.php?cid=5&lid=94

    [2] Linux彙編語言開發指南(線上):

    http://www.ibm.com/developerworks/cn/linux/l-assembly/index.html

    [3] PowerPC 彙編

    http://www.ibm.com/developerworks/cn/linux/hardware/ppc/assembly/index.html

    [4] 用于 Power 體系結構的彙編語言

    http://www.ibm.com/developerworks/cn/linux/l-powasm1.html

    [5] Linux Assembly HOWTO

    http://mirror.lzu.edu.cn/tldp/HOWTO/Assembly-HOWTO/

    [6] Linux 中 x86 的内聯彙編

    http://www.ibm.com/developerworks/cn/linux/sdk/assemble/inline/index.html

    [7] Linux Assembly Language Programming

    http://mirror.lzu.edu.cn/doc/incoming/ebooks/linux-unix/Linux_EN_Original_Books

  • 文章轉載自:http://www.cppblog.com/cuijixin/archive/2008/03/14/44459.html

繼續閱讀