目錄:html
摘要:
要想讀懂本文,你須要對C語言有基本的瞭解,本文將介紹如何使用gcc編譯器。 首先,咱們介紹如何在命令行方式下使用編譯器編譯簡單的C源代碼。 而後,咱們簡要介紹一下編譯器究竟做了那些工做,以及如何控制編譯過程。 咱們也簡要介紹了調試器的使用方法。
你能想象使用封閉源代碼的私有編譯器編譯自由軟件嗎?你怎麼知道編譯器在你的 可執行文件中加入了什麼?可能會加入各類後門和木馬。Ken Thompson是一個著名 的黑客,他編寫了一個編譯器,當編譯器編譯本身時,就在'login'程序中留下後門 和永久的木馬。請到 這裏 閱讀他對 這個傑做的描述。幸運的是,咱們有了gcc。當你進行 configure; make; make install
時, gcc在幕後作了不少繁重的工做。如何才能讓gcc爲咱們工做呢?咱們將開始編寫一個紙牌遊戲, 不過咱們只是爲了演示編譯器的功能,因此儘量地精簡了代碼。 咱們將從頭開始一步一步地作,以便理解編譯過程,瞭解爲了製做可執行文件須要 作些什麼,按什麼順序作。咱們將看看如何編譯C程序,以及如何使用編譯選項 讓gcc按照咱們的要求工做。步驟(以及所用工具)以下: 預編譯 (gcc -E), 編譯 (gcc), 彙編 (as),和 鏈接 (ld)。
首先,咱們應該知道如何調用編譯器。實際上,這很簡單。咱們將從那個著名的第一個C程序開始。 (各位老前輩,請原諒我)。
#include <stdio.h> int main() { printf("Hello World!\n"); }
把這個文件保存爲 game.c
。 你能夠在命令行下編譯它:
gcc game.c
在默認狀況下,C編譯器將生成一個名爲 a.out
的可執行文件。 你能夠鍵入以下命令運行它:
a.outHello World
每一次編譯程序時,新的 a.out
將覆蓋原來的程序。你沒法知道是哪一個 程序建立了 a.out
。咱們能夠經過使用 -o
編譯選項,告訴 gcc咱們想把可執行文件叫什麼名字。咱們將把這個程序叫作 game
,咱們 可使用任何名字,由於C沒有Java那樣的命名限制。
gcc -o game game.c
gameHello 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
選項指定 輸出文件名的話,它就輸出到控制檯。預編譯過程經過完成三個主要任務給了代碼很大的 靈活性。
把"include"的文件拷貝到要編譯的源文件中。
用實際值替代"define"的文本。
在調用宏的地方進行宏替換。
這就使你可以在整個源文件中使用符號常量(即用DECKSIZE表示一付牌中的紙牌數量), 而符號常量是在一個地方定義的,若是它的值發生了變化,全部使用符號常量的地方 都能自動更新。在實踐中,你幾乎不須要單獨使用 -E
選項,而是讓它 把輸出傳送給編譯器。
做爲一箇中間步驟,gcc把你的代碼翻譯成彙編語言。它必定要這樣作,它必須經過分析 你的代碼搞清楚你究竟想要作什麼。若是你犯了語法錯誤,它就會告訴你,這樣編譯就失敗了。 人們有時會把這一步誤解爲整個過程。可是,實際上還有許多工做要gcc去作呢。
as
把彙編語言代碼轉換爲目標代碼。事實上目標代碼並不能在CPU上運行, 但它離完成已經很近了。編譯器選項 -c
把 .c 文件轉換爲以 .o 爲擴展名 的目標文件。 若是咱們運行
gcc -c game.c
咱們就自動建立了一個名爲game.o的文件。這裏咱們碰到了一個重要的問題。咱們能夠用 任意一個 .c 文件建立一個目標文件。正如咱們在下面所看到的,在鏈接步驟中咱們能夠 把這些目標文件組合成可執行文件。讓咱們繼續介紹咱們的例子。由於咱們正在編寫一個 紙牌遊戲,咱們已經把一付牌定義爲 deck_t
,咱們將編寫一個洗牌函數。 這個函數接受一個指向deck類型的指針,並把一付隨機的牌裝入deck類型。它使用'drawn' 數組跟蹤記錄那些牌已經用過了。這個具備DECKSIZE個元素的數組能夠防止咱們重複使用 一張牌。
#include <stdlib.h> #include <stdio.h> #include <time.h> #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\n", value); pdeck->card[i] = value; } pdeck->dealt = 0; return; }
把這個文件保存爲 shuffle.c
。咱們在這個代碼中加入了一條調試語句, 以便運行時,能輸出所產生的牌號。這並無爲咱們的程序添加功能,可是如今到了 關鍵時刻,咱們看看究竟發生了什麼。由於咱們的遊戲還在初級階段,咱們沒有別的 辦法肯定咱們的函數是否實現了咱們要求的功能。使用那條printf語句,咱們就能準確 地知道如今究竟發生了什麼,以便在開始下一階段以前咱們知道牌已經洗好了。在咱們 對它的工做感到滿意以後,咱們能夠把那一行語句從代碼中刪掉。這種調試程序的技術 看起來很粗糙,但它使用最少的語句完成了調試任務。之後咱們再介紹更復雜的調試器。
請注意兩個問題。
咱們用傳址方式傳遞參數,你能夠從'&'(取地址)操做符看出來。這把變量的機器地址 傳遞給了函數,所以函數本身就能改變變量的值。也可使用全局變量編寫程序,可是應該 儘可能少使用全局變量。指針是C的一個重要組成部分,你應該充分地理解它。
咱們在一個新的 .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 <stdlib.h>
語句包含的文件中 找到它的聲明,並把存儲在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。