2012年3月9日 星期五

Introduction of GCC Compiler

對軟體工程師來說,使用 GCC 編譯程式碼是一項基本的技能,GCC 可說與我們的工作內容密不可分,以下就 GCC 的大致功能做一些簡單的整理:
(主要內容參考自 MAXSOLAR'S LINUX BLOG - Makefile 範例教學)
  • 傳統的編譯方式
  • gcc foo.c -o foo
    可在拆解成如下的動作:
    gcc foo.c -c
    gcc foo.o -o foo
    • 編譯的過程是將原始碼(foo.c)先使用 -c 參數編譯成 Object file(foo.o),然後鏈結一個函式庫成為二進位檔(binary)如下。[-c : Compile]
    • gcc foo1.c $SACLIB/sacio.a -O3 -g -Wall -ansi -o foo1
    • 接下來有更多 Makefile 的參數出現了,一一介紹如下:
      • -c:編譯但不進行鏈結,會產生一個跟原始碼相同名稱但副檔名為.o的目的檔。
      • -O:表示最佳化的程度,-O預設就是-O1,可以指定成-O2或-O3,數字越大表示最佳化程度越好,但是會增加編譯時間。
      • -g:把偵錯資訊也編譯進去,當你有需要使用GDB軟體進行偵錯,必須加入-g使GDB能夠讀取。一般情況下可以不用-g,因為它也會增加binary大小。
      • -Wall:顯示警告訊息,使用這個參數會在編譯時顯示更多的警告訊息。這個參數相當有用,特別是找不到 libs/headers 之類的問題。
      • -ansi:使用相容ANSI標準的編譯方式,ANSI 是 American National Standards Institute,即美國國家標準學會的簡稱。-ansi 可以增加程式的可移植性。
      • $SACLIB:是一個路徑變數名稱,必須被指定正確的值。執行這個命令前必須先確定這個變數是有被指派到正確路徑才行。.a 檔是一個靜態函式(static library),關於靜態跟共享的觀念稍候解釋。
    • 再來更多吧!假設今天要編譯 main 這隻程式,source codes 中有 main.c, foo.c, target.h,並且需要 /usr/local/moreFoo/lib/libpthread.so 這個共享函式,以及 /usr/local/moreFoo/include 裡面的 headers;這麼複雜的情況又該怎麼作呢?
    • gcc main.c foo.c -I /usr/local/moreFoo/include -L /usr/local/moreFoo/lib -lpthread -O3 -ansi -o main
      以上新出現的參數定義如下:
      • -I:需要 include 某些 headers 所在的目錄,通常 include 目錄都放置headers,利用-I使編譯器知道去哪裡找原始碼裡宣告的 header。gcc預設會去尋找headers的目錄大致有:
        • /usr/include
        • /usr/local/include
        • /usr/src/linux-headers-`uname -r`/include
        • /usr/lib/gcc/i486-linux-gnu/UR_GCC_VERSION/include
        • Current Dictionary
        因此,當原始碼內有宣告 #include <fakeFoo.h>,但 fakeFoo.h 並不在上述的資料夾內,就需要利用 -I 引導 gcc 找到它。至於 target.h 因為在當前目錄,因此不必額外宣告。當然,也可以利用多個 -I 來指定多個 headers 的路徑。
      • -l:表示編譯過程需要一個 library。-lpthread 代表需要一個名為 libpthread.so的函式。
      • -L:需要額外鏈結函式庫所在的目錄,有時候程式碼經常會呼叫一些函數(methods, functions 或是 subroutines),而這些函數是使用其他人預先寫好的、已經編譯成函式(例如 libpthread.so )供人使用的話,我們就不必自己從頭寫過。gcc 預設會去找函式的目錄大致有:
        • /lib
        • /usr/lib
        • /lib/modules/`uname -r`/kernel/lib
        • /usr/src/linux-headers-`uname -r`/lib
        • /usr/local/lib
        • Current Dictionary
        因此編譯時,利用-L指定目錄告訴編譯器可以該路徑下尋找 libpthread.so。因此,若使用了-l,則必須確定所使用的 library 有在預設尋找的目錄中,否則就必須利用-L來指定路徑給編譯器。當然,可以利用多個-L來指定多個 libraries路徑。
  • 靜態、共享與動態鏈結函式庫
  • 輪子不必重複發明 -- 人家寫好的方法我們可以直接拿來用。不過很多時候,這些方法可能因為某些因素,希望提供給別人使用卻又不希望公佈原始碼,這時候編譯成 libraries 是最好的選擇。
    • 靜態函式(static libraries)
    • 靜態函式其實就是將一系列 .o 檔打包起來,因此它可以直接視為一個巨大的 .o 檔。打造出一個靜態函式的方法很簡單:
      gcc operator.c -c
      ar crsv liboperator.a operator.o
      或者
      gcc -static operator.c -loperator
      兩種方法皆能產生 liboperator.a。假設這個靜態函式在 /usr/local/foo/lib/ 裡,編譯時要與靜態函式作鏈結也很容易:
      gcc main.c /usr/local/foo/lib/liboperator.a -o main
      把靜態函式當成一般的 .o 檔一起納入 binary,也可以像這樣:
      gcc main.c -L /usr/local/foo/lib -loperator -o main
      靜態函式將所有的功能全部打包在一起,因此 binary 會變得很巨大,但是執行這個程式的所有功能都已滿足,不會再有 libraries 相依性的問題。但是缺點在於當某些libraries的功能有所更新時,這個程式就必須重新編譯,無法享受到即時更新的優點。通常商業軟體以及嵌入式系統等功能異動較少的程式,會傾向使用靜態函式。
    • 共享函式(shared libraries)
    • 共享函式跟靜態函式的觀念剛好相反,程式在執行時必須能夠找到相依的函式,否則執行時會出現錯誤訊息。製作一個共享函式的方法也很簡單:
      gcc -shared operator.c -o liboperator.so
      或是先編譯出目的檔再進行鏈結:
      gcc -c operator.c
      gcc -shared operator.o -o liboperator.so
      產生出 liboperator.so。假設這個共享函式在 /usr/local/foo/lib/ 裡,使用共享函式進行鏈結也很容易:
      gcc main.c /usr/local/foo/lib/liboperator.so -o main
      也可以像這樣:
      gcc main.c -L /usr/local/foo/lib -loperator -o main
      共享函式在程式啟動時期會檢查是否存在。以一個分別鏈結了靜態函式與共享函式的 binary 而言,執行的結果大有差別。以靜態函式鏈結的 main 程式可以順利執行,但是假設系統預設尋找函式庫的路徑裡找不到 liboperator.so,以共享函式鏈結的main程式則會出現錯誤訊息:
      ./main: error while loading shared libraries: liboperator.so: cannot open shared object file: No such file or directory
      這時解決的方法有四種:
      1. 把 liboperator.so 複製或是作一個連結到 /usr/lib 裡。
      2. 修改 /etc/ld.so.conf,把 /usr/local/foo/lib 加進系統 libraries 的搜尋範圍內。
      3. 設定 LD_LIBRARY_PATH 變數,累加該路徑進來:如果你不是系統管理員,前兩個方法根本沒辦法執行。我們只好自己加到 ~/.profile 裡:
      4. export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/foo/lib
      5. 改用靜態函式進行鏈結。
      共享函式經常出現在開放原始碼的 linux 世界裡,由於使用所有函式皆是共享的,因此許多程式都可以重複利用既有的功能;有新功能或是 bug 也能簡單的替換掉該函式,所有程式都可以即時享受到這樣的改變,也是最為常見的函式型態。
    • 動態函式(dynamic libraries)
    • 動態函式跟共享函式非常類似,唯一的差別在於程式執行時期並不會去檢查該函式是否存在,而是程式執行到某功能時才進行檢查。這種動態載入的技術最常用在瀏覽器或是大型程式的外掛程式,當有需要用到這個功能時才載入進來。而製作一個動態函式較為麻煩。
      gcc -c -fPIC operator.c
      gcc -shared operator.o -o liboperator.so
      其中的 -fPIC 是產生 position-independent code,也可以用 -fpic,撰寫呼叫動態函式的程式碼也需要傳入相關參數。

0 意見: