GCC - 一切從這裡開始(轉)

post0發表於2007-08-11
GCC - 一切從這裡開始(轉)[@more@]

GCC rules

你能想象使用封閉原始碼的私有編譯器編譯自由軟體嗎?你怎麼知道編譯器在你的可執行檔案中加入了什麼?可能會加入各種後門和木馬。Ken Thompson是一個著名的駭客,他編寫了一個編譯器,當編譯器編譯自己時,就在'login'程式中留下後門和永久的木馬。請到 這裡 閱讀他對這個傑作的描述。幸運的是,我們有了gcc。當你進行 configure; make; make install 時, gcc在幕後做了很多繁重的工作。如何才能讓gcc為我們工作呢?我們將開始編寫一個紙牌遊戲,不過我們只是為了演示編譯器的功能,所以儘可能地精簡了程式碼。我們將從頭開始一步一步地做,以便理解編譯過程,瞭解為了製作可執行檔案需要做些什麼,按什麼順序做。我們將看看如何編譯C程式,以及如何使用編譯選項讓gcc按照我們的要求工作。步驟(以及所用工具)如下:預編譯 (gcc -E), 編譯 (gcc), 彙編 (as),和 連線 (ld)。

開始...

首先,我們應該知道如何呼叫編譯器。實際上,這很簡單。我們將從那個著名的第一個C程式開始。(各位老前輩,請原諒我)。

#include

int main()

{

printf("Hello World!

");

}

把這個檔案儲存為 game.c。 你可以在命令列下編譯它:

gcc game.c

在預設情況下,C編譯器將生成一個名為 a.out 的可執行檔案。你可以鍵入如下命令執行它:

a.out

Hello World

每一次編譯程式時,新的 a.out 將覆蓋原來的程式。你無法知道是哪個程式建立了 a.out。我們可以透過使用 -o 編譯選項,告訴 gcc我們想把可執行檔案叫什麼名字。我們將把這個程式叫做 game,我們可以使用任何名字,因為C沒有Java那樣的命名限制。

gcc -o game game.c

game

Hello World

到現在為止,我們離一個有用的程式還差得很遠。如果你覺得沮喪,你可以想一想我們已經編譯並執行了一個程式。因為我們將一點一點為這個程式新增功能,所以我們必須保證讓它能夠執行。似乎每個剛開始學程式設計的程式設計師都想一下子編一個1000行的程式,然後一次修改所有的錯誤。沒有人,我是說沒有人,能做到這個。你應該先編一個可以執行的小程式,修改它,然後再次讓它執行。這可以限制你一次修改的錯誤數量。另外,你知道剛才做了哪些修改使程式無法執行,因此你知道應該把注意力放在哪裡。這可以防止這樣的情況出現:你認為你編寫的東西應該能夠工作,它也能透過編譯,但它就是不能執行。請切記,能夠透過編譯的程式並不意味著它是正確的。

下一步為我們的遊戲編寫一個標頭檔案。標頭檔案把資料型別和函式宣告集中到了一處。這可以保證資料結構定義的一致性,以便程式的每一部分都能以同樣的方式看待一切事情。

#ifndef DECK_H

#define DECK_H

#define DECKSIZE 52

typedef struct deck_t

{

int card[DECKSIZE];

/* number of cards used */

int dealt;

}deck_t;

#endif /* DECK_H */

把這個檔案儲存為 deck.h。只能編譯 .c 檔案,所以我們必須修改 game.c。在game.c的第2行,寫上 #include "deck.h"。在第5行寫上 deck_t deck;。為了保證我們沒有搞錯,把它重新編譯一次。

gcc -o game game.c

如果沒有錯誤,就沒有問題。如果編譯不能透過,那麼就修改它直到能透過為止。

預編譯

編譯器是怎麼知道 deck_t 型別是什麼的呢?因為在預編譯期間,它實際上把"deck.h"檔案複製到了"game.c"檔案中。原始碼中的預編譯指示以"#"為字首。你可以透過在gcc後加上 -E 選項來呼叫預編譯器。

gcc -E -o game_precompile.txt game.c

wc -l game_precompile.txt

3199 game_precompile.txt

幾乎有3200行的輸出!其中大多數來自 stdio.h 包含檔案,但是如果你檢視這個檔案的話,我們的宣告也在那裡。如果你不用 -o 選項指定輸出檔名的話,它就輸出到控制檯。預編譯過程透過完成三個主要任務給了程式碼很大的靈活性。

1. 把"include"的檔案複製到要編譯的原始檔中。

2. 用實際值替代"define"的文字。

3. 在呼叫宏的地方進行宏替換。

這就使你能夠在整個原始檔中使用符號常量(即用DECKSIZE表示一付牌中的紙牌數量),而符號常量是在一個地方定義的,如果它的值發生了變化,所有使用符號常量的地方都能自動更新。在實踐中,你幾乎不需要單獨使用 -E 選項,而是讓它把輸出傳送給編譯器。

編譯

作為一箇中間步驟,gcc把你的程式碼翻譯成組合語言。它一定要這樣做,它必須透過分析你的程式碼搞清楚你究竟想要做什麼。如果你犯了語法錯誤,它就會告訴你,這樣編譯就失敗了。人們有時會把這一步誤解為整個過程。但是,實際上還有許多工作要gcc去做呢。

彙編

as 把組合語言程式碼轉換為目的碼。事實上目的碼並不能在CPU上執行,但它離完成已經很近了。編譯器選項 -c 把 .c 檔案轉換為以 .o 為副檔名的目標檔案。 如果我們執行

gcc -c game.c

我們就自動建立了一個名為game.o的檔案。這裡我們碰到了一個重要的問題。我們可以用任意一個 .c 檔案建立一個目標檔案。正如我們在下面所看到的,在連線步驟中我們可以把這些目標檔案組合成可執行檔案。讓我們繼續介紹我們的例子。因為我們正在編寫一個紙牌遊戲,我們已經把一付牌定義為deck_t,我們將編寫一個洗牌函式。這個函式接受一個指向deck型別的指標,並把一付隨機的牌裝入deck型別。它使用'drawn' 陣列跟蹤記錄那些牌已經用過了。這個具有DECKSIZE個元素的陣列可以防止我們重複使用一張牌。

#include

#include

#include

#include "deck.h"

static time_t seed = 0;

void shuffle(deck_t *pdeck)

{

/* Keeps track of what numbers have been used */

int drawn[DECKSIZE] = {0};

int i;

/* One time initialization of rand */

if(0 == seed)

{

seed = time(NULL);

srand(seed);

}

for(i = 0; i < DECKSIZE; i++)

{

int value = -1;

do

{

value = rand() % DECKSIZE;

}

while(drawn[value] != 0);

/* mark value as used */

drawn[value] = 1;

/* debug statement */

printf("%i

", value);

pdeck->card[i] = value;

}

pdeck->dealt = 0;

return;

}

把這個檔案儲存為 shuffle.c。我們在這個程式碼中加入了一條除錯語句,以便執行時,能輸出所產生的牌號。這並沒有為我們的程式新增功能,但是現在到了關鍵時刻,我們看看究竟發生了什麼。因為我們的遊戲還在初級階段,我們沒有別的辦法確定我們的函式是否實現了我們要求的功能。使用那條printf語句,我們就能準確地知道現在究竟發生了什麼,以便在開始下一階段之前我們知道牌已經洗好了。在我們對它的工作感到滿意之後,我們可以把那一行語句從程式碼中刪掉。這種除錯程式的技術看起來很粗糙,但它使用最少的語句完成了除錯任務。以後我們再介紹更復雜的偵錯程式。

請注意兩個問題。

1. 我們用傳址方式傳遞引數,你可以從'&'(取地址)運算子看出來。這把變數的機器地址傳遞給了函式,因此函式自己就能改變變數的值。也可以使用全域性變數編寫程式,但是應該儘量少使用全域性變數。指標是C的一個重要組成部分,你應該充分地理解它。

2. 我們在一個新的 .c 檔案中使用函式呼叫。作業系統總是尋找名為'main'的函式,並從那裡開始執行。 shuffle.c 中沒有'main'函式,因此不能編譯為獨立的可執行檔案。我們必須把它與另一個具有'main'函式並呼叫'shuffle'的程式組合起來。

執行命令

gcc -c shuffle.c

並確定它建立了一個名為 shuffle.o 的新檔案。編輯game.c檔案,在第7行,在 deck_t型別的變數 deck 宣告之後,加上下面這一行:

shuffle(&deck);

現在,如果我們還象以前一樣建立可執行檔案,我們就會得到一個錯誤

gcc -o game game.c

/tmp/ccmiHnJX.o: In function `main':

/tmp/ccmiHnJX.o(.text+0xf): undefined reference to `shuffle'

collect2: ld returned 1 exit status

編譯成功了,因為我們的語法是正確的。但是連線步驟卻失敗了,因為我們沒有告訴編譯器'shuffle'函式在哪裡。那麼,到底什麼是連線?我們怎樣告訴編譯器到哪裡尋找這個函式呢?

連線

聯結器ld,使用下面的命令,接受前面由 as 建立的目標檔案並把它轉換為可執行檔案

gcc -o game game.o shuffle.o

這將把兩個目標檔案組合起來並建立可執行檔案 game。

聯結器從shuffle.o目標檔案中找到 shuffle 函式,並把它包括進可執行檔案。目標檔案的真正好處在於,如果我們想再次使用那個函式,我們所要做的就是包含"deck.h" 檔案並把 shuffle.o 目標檔案連線到新的可執行檔案中。

象這樣的程式碼重用是經常發生的。雖然我們並沒有編寫前面作為除錯語句呼叫的 printf 函式,聯結器卻能從我們用 #include 語句包含的檔案中找到它的宣告,並把儲存在C庫(/lib/libc.so.6)中的目的碼連線進來。這種方式使我們可以使用已能正確工作的其他人的函式,只關心我們所要解決的問題。這就是為什麼標頭檔案中一般只含有資料和函式宣告,而沒有函式體。一般,你可以為聯結器建立目標檔案或函式庫,以便連線進可執行檔案。我們的程式碼可能產生問題,因為在標頭檔案中我們沒有放入任何函式宣告。為了確保一切順利,我們還能做什麼呢?

另外兩個重要選項

-Wall 選項可以開啟所有型別的語法警告,以便幫助我們確定程式碼是正確的,並且儘可能實現可移植性。當我們使用這個選項編譯我們的程式碼時,我們將看到下述警告:

game.c:9: warning: implicit declaration of function `shuffle'

這讓我們知道還有一些工作要做。我們需要在標頭檔案中加入一行程式碼,以便告訴編譯器有關 shuffle 函式的一切,讓它可以做必要的檢查。聽起來象是一種狡辯,但這樣做 可以把函式的定義與實現分離開來,使我們能在任何地方使用我們的函式,只要包含新的標頭檔案 並把它連線到我們的目標檔案中就可以了。下面我們就把這一行加入deck.h中。

void shuffle(deck_t *pdeck);

這就可以消除那個警告資訊了。

另一個常用編譯器選項是最佳化選項 -O# (即 -O2)。 這是告訴編譯器你需要什麼級別的最佳化。編譯器具有一整套技巧可以使你的程式碼執行得更快一點。對於象我們這種小程式,你可能注意不到差別,但對於大型程式來說,它可以大幅度提高執行速度。你會經常碰到它,所以你應該知道它的意思。

除錯

我們都知道,程式碼透過了編譯並不意味著它按我們得要求工作了。你可以使用下面的命令驗證是否所有的號碼都被使用了

game | sort - n | less

並且檢查有沒有遺漏。如果有問題我們該怎麼辦?我們如何才能深入底層查詢錯誤呢?

你可以使用偵錯程式檢查你的程式碼。大多數發行版都提供著名的偵錯程式:gdb。如果那些眾多的命令列選項讓你感到無所適從,那麼你可以使用KDE提供的一個很好的前端工具 KDbg。還有一些其它的前端工具,它們都很相似。要開始除錯,你可以選擇 File->Executable 然後找到你的 game 程式。當你按下F5鍵或選擇 Execution->從選單執行時,你可以在另一個視窗中看到輸出。怎麼回事?在那個視窗中我們什麼也看不到。不要擔心,KDbg沒有出問題。問題在於我們在可執行檔案中沒有加入任何除錯資訊,所以KDbg不能告訴我們內部發生了什麼。編譯器選項 -g 可以把必要的除錯資訊加入目標檔案。你必須用這個選項編譯目標檔案(副檔名為.o),所以命令列成了:

gcc -g -c shuffle.c game.c

gcc -g -o game game.o shuffle.o

這就把鉤子放入了可執行檔案,使gdb和KDbg能指出執行情況。除錯是一種很重要的技術,很值得你花時間學習如何使用。偵錯程式幫助程式設計師的方法是它能在原始碼中設定“斷點”。現在你可以用右鍵單擊呼叫 shuffle 函式的那行程式碼,試著設定斷點。那一行邊上會出現一個紅色的小圓圈。現在當你按下F5鍵時,程式就會在那一行停止執行。按F8可以跳入shuffle函式。呵,我們現在可以看到 shuffle.c 中的程式碼了!我們可以控制程式一步一步地執行,並看到究竟發生了什麼事。如果你把游標暫停在區域性變數上,你將能看到變數的內容。太好了。這比那條 printf 語句好多了,是不是?

小結

本文大體介紹了編譯和除錯C程式的方法。我們討論了編譯器走過的步驟,以及為了讓編譯器做這些工作應該給gcc傳遞哪些選項。我們簡述了有關連線共享函式庫的問題,最後介紹了偵錯程式。真正瞭解你所從事的工作還需要付出許多努力,但我希望本文能讓你正確地起步。你可以在 gcc、 as 和 ld的 man 和 info page中找到更多的資訊。

自己編寫程式碼可以讓你學到更多的東西。作為練習你可以以本文的紙牌遊戲為基礎,編寫一個21點遊戲。那時你可以學學如何使用偵錯程式。使用GUI的KDbg開始可以更容易一些。如果你每次只加入一點點功能,那麼很快就能完成。切記,一定要保持程式一直能執行!

要想編寫一個完整的遊戲,你需要下面這些內容:

* 一個紙牌玩家的定義(即,你可以把deck_t定義為player_t)。

* 一個給指定玩家發一定數量牌的函式。記住在紙牌中要增加“已發牌”的數量,以便能知道還有那些牌可發。還要記住玩家手中還有多少牌。

* 一些與使用者的互動,問問玩家是否還要另一張牌。

* 一個能列印玩家手中的牌的函式。 card 等於value % 13 (得數為0到12),suit 等於 value / 13 (得數為0到3)。

* 一個能確定玩家手中的value的函式。Ace的value為零並且可以等於1或11。King的value為12並且可以等於10。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-944549/,如需轉載,請註明出處,否則將追究法律責任。

相關文章