我想,咱們學習和從事嵌入式系統設計的,對於Linux和GCC都應該不陌生吧!Linux和GCC工具的強大也是大家所有目共睹的,它們對嵌入式系統設計的發展起着關鍵性的作用,是目前用的最多的嵌入式操作系統和嵌入式軟件開發工具,它們二者都是基於GNU和GPL的,可以免費獲得源代碼和使用。下面,我就自己這一年多來學習Linux和GCC的一些經驗總結和學習感悟與大家分享。
在本文中,我先給大家介紹一下GCC工具的使用,然後是Makefile相關知識。重點在於介紹Makefile相關知識。
在學習Linux和GCC的過程中我遇到過許多困難和誘惑,好在都在網上和相關書籍上找到了答案和解決,這裏在此,首先要對他們的工作和付出表示感謝,本文的最終成文和我現在的學習成果都離不開他們(它們)!也希望大家在遇到困難的時候,能夠多到網絡上查一查,問一問。
好了,閒話少說,下面開始進入本文的正題。
運行於Linux 操作系統下的自由軟件GNU gcc 編譯器,不僅可以編譯Linux 操作系統下運行的應用程序,還可以編Linux內核本身,甚至可以作交叉編譯,編譯運行於其它CPU上的程序。所以,在進行嵌入式系統應用程序開發時,這些工具得到了日益廣泛的應用。
GCC 編譯器
GCC 是GNU 組織的免費C 編譯器,Linux 的很多發佈缺省安裝的就是這種。很多流行的自由軟件源代碼基本都能在GCC 編譯器下編譯運行。 所以掌握GCC 編譯器的使用無論是對於編譯系統內核還是自己的應用程序都是大有好處的。
下面通過一個具體的例子,學習如何使用GCC 編譯器。
在Linux 操作系統中,對一個用標準C 語言寫的源程序進行編譯,要使用GNU 的gcc編譯器。
例如下面一個非常簡單的Hello 源程序(hello.c):
#includes <stdio.h>
int main(void)
{
printf("Hello the world\n");
return 0;
}
首先在在Linux 的bash 提示符下輸入命令:
$vi hello.C
即可啓動Linux下的vi編輯器,然後按i鍵進入插入編輯模式,輸入上面的代碼,最後按ESC鍵退出vi的輸入模式進入命令模式,並輸入:qw或者:x存盤退出。接下來,要編譯這個程序,我們只要在Linux 的bash 提示符下輸入命令:
$ gcc -o hello hello.c
gcc編譯器就會生成一個hello的可執行文件。在hello.c 的當前目錄下執行命令 ./hello 就可以看到程序的輸出結果,在屏幕上打印出“Hello the world”的字符串來。
命令行中gcc 表示是用gcc 來編譯源程序;
-o outputfilename 選項表示要求編譯器生成文件名爲outputfilename 的可執行文件,如
果不指定-o 選項,則缺省文件名是 a.out。在這裏生成指定文件名爲hello 的可執行文件,而hello.c 是我們的源程序文件。
gcc 是一個多目標的工具。gcc 最基本的用法是:
gcc [options] file... ,
其中的option 是以-開始的各種選項,file 是相關的文件名。在使用gcc 的時候,必須要給出必要的選項和文件名。gcc 的整個編譯過程,實質上是分四步進行的,每一步完成一個特定的工作,這四步分別是:預處理,編譯,彙編和鏈接。它具體完成哪一步,是由gcc 後面的開關選項和文件類型決定的。
清楚的區別編譯和連接是很重要的。編譯器使用源文件編譯產生某種形式的目標文件(object files)。在這個過程中,外部的符號引用並沒有被解釋或替換,然後我們使用鏈接器來鏈接這些目標文件和一些標準的頭文件,最後生成一個可執行文件。在這個過程中,一個目標文件中對別的文件中的符號的引用被解釋,並報告不能被解釋的引用,一般是以錯誤信息的形式報告出來。
gcc 編譯器有許多選項,但對於普通用戶來說只要知道其中常用的幾個就夠了。在這裏爲大家列出幾個最常用的選項:
-o 選項表示要求編譯器生成指定文件名的可執行文件;
-c 選項表示只要求編譯器進行編譯,而不要進行鏈接,生成以源文件的文件名命名但把其後綴由.c 或.cc 變成.o 的目標文件;
-g 選項要求編譯器在編譯的時候提供以後對程序進行調試的信息;
-E 選項表示編譯器對源文件只進行預處理就停止,而不做編譯,彙編和鏈接;
-S 選項表示編譯器只進行編譯,而不做彙編和鏈接;
-O 選項是編譯器對程序提供的編譯優化選項,在編譯的時候使用該選項,可以使生成的執行文件的執行效率提高;
-Wall 選項指定產生全部的警告信息。
如果你的源代碼中包含有某些函數,則在編譯的時候要鏈接確定的庫,比如代碼中包含了某些數學函數,在Linux 下,爲了使用數學函數,必須和數學庫鏈接,爲此要加入-lm 選項。也許有大家會問,前面那個例子使用printf 函數的時候爲何沒有鏈接庫呢?在gcc 中對於一些常用函數的實現,gcc 編譯器會自動去鏈接一些常用庫,這樣用戶就沒有必要自己去指定了。有時候在編譯程序的時候還要指定庫的路徑,這個時候要用到編譯器的-L 選項指定路徑。比如說我們有一個庫在 /home/hoyt/mylib 下,這樣我們編譯的時候還要加上 -L/home/hoyt/mylib。對於一些標準庫來說,沒有必要指出路徑。只要它們在起缺省庫的路徑下就可以了,gcc 在鏈接的時候會自動找到那些庫的。
GNU 編譯器生成的目標文件缺省格式爲elf(executive linked file)格式,這是Linux 系統所採用的可執行鏈接文件的通用文件格式,elf格式由若干段(section)組成,如果沒有特別指明,由標準c源代碼生成的目標文件中包含以下段:.text(正文段)包含程序的指令代碼,.data(數據段)包含固定的數據,如常量,字符串等,.bss(未初始化數據段)包含未初始化的變量和數組等。大家若想知道更多的選項及其用法,可以查看gcc的幫助文檔,那裏有許多對其它選項的詳細說明。(方法:在bash命令行輸入man gcc 或者 info gcc即可。)
當改變了源文件hello.c後,需要重新編譯它:
$gcc -c hello.c
然後重新鏈接生成:
$gcc –o hello.o
對於本例,因爲只含有一個源文件,所以當改動了源碼後,進行重新的編譯鏈接的過程顯得並不是太繁瑣,但是,如果在一個工程中包含了若干的源碼文件,而這些源碼文件中的某個或某幾個又被其他源碼文件包含,那麼,如果一個文件被改動,則包含它的那些源文件都要進行重新編譯鏈接,工作量是可想而知的。 幸運的是,GNU 提供了使這個步驟變得簡單的工具,就是下面要介紹給大家的GNU Make工具。
GNU Make
make是負責從項目的源代碼中生成最終可執行文件和其他非源代碼文件的工具。make命令本身可帶有四種參數:標誌、宏定義、描述文件名和目標文件名。其標準形式爲:
make [flags] [macro definitions] [targets]
Unix系統下標誌位flags 選項及其含義爲:
-f file 指定file 文件爲描述文件,如果file 參數爲 "-" 符,那麼描述文件指向標準輸入。如果沒有 "-f" 參數,則系統將默認當前目錄下名爲makefile 或者名爲Makefile 的文件爲描述文件。在Linux 中, GNU make 工具在當前工作目錄中按照GNUmakefile、makefile、Makefile 的順序搜索 makefile 文件。
-i 忽略命令執行返回的出錯信息。
-s 沉默模式,在執行之前不輸出相應的命令行信息。
-r 禁止使用隱含規則。
-n 非執行模式,輸出所有執行命令,但並不執行。
-t 更新目標文件。
-q make 操作將根據目標文件是否已經更新返回"0"或非"0"的狀態信息。
-p 輸出所有宏定義和目標文件描述。
-d Debug 模式,輸出有關文件和檢測時間的詳細信息。
Linux 下make 標誌位的常用選項與Unix 系統中稍有不同,下面只列出了不同部分:
-c dir 在讀取 makefile 之前改變到指定的目錄dir。
-I dir 當包含其他 makefile 文件時,利用該選項指定搜索目錄。
-h help 文擋,顯示所有的make 選項。
-w 在處理 makefile 之前和之後,都顯示工作目錄。
通過命令行參數中的target,可指定make 要編譯的目標,並且允許同時定義編譯多個目標,操作時按照從左向右的順序依次編譯target 選項中指定的目標文件。如果命令行中沒有指定目標,則系統默認target 指向描述文件中第一個目標文件。
make 如何實現對源代碼的操作是通過一個被稱之爲makefile 的文件來完成的,接下來,詳細的向大家介紹一下makefile 的相關知識。
makefile 基本結構
GNU Make的主要工作是讀一個文本文件 makefile。makefile 是用bash 語言寫的,bash語言是很像BASIC 語言的一種命令解釋語言。這個文件裏主要描述了有關哪些目標文件是從哪些依賴文件中產生的,是用何種命令來進行這個產生過程的。有了這些信息,make會檢查磁盤的文件,如果目標文件的日期(即該文件生成或最後修改的日期)至少比它的一個依賴文件日期早的話,make就會執行相應的命令,以更新目標文件。makefile 一般被稱爲“makefile”或者“Makefile”。還可以在make的命令行中指定別的文件名。如果沒有特別指定的話,make就會尋找“makefile”或“Makefile”,所以爲了簡單起見,建議大家使用這兩名字。如果要使用其他文件作爲 makefile,則可利用類似下面的make 命令選項指定makefile文件:
$ make -f makefilename
一個 makefile 主要含有一系列的規則,如下:
目標文件名 : 依賴文件名
(tab 鍵) 命令
第一行稱之爲規則,第二行是執行規則的命令,必須要以tab 鍵開始(切忌)。下面舉一個簡單的makefile 的例子。
executable : main.o io.o
gcc main.o io.o -o executable
main.o : main.c
gcc -Wall -O -g -c main.c -o main.o
io.o : io.c
gcc -Wall -O -g -c io.c -o io.o
這是一個最簡單的 makefile,make從第一條規則開始,executable是makefile 最終要生成的目標文件。給出的規則說明executable 依賴於兩個目標文件main.o 和io.o,只要executable 比它依賴的文件中的任何一箇舊的話,下一行的命令就會被執行。但是,在檢查文件main.o和io.o的日期之前,它會往下查找那些把main.o或 io.o做爲目標文件的規則。make先找到了關於main.o的規則,該目標文件的依賴文件是main.c。makefile 後面的文件中再也找不到生成這個依賴文件的規則了。此時,make 開始檢查磁盤上這個依賴文件的日期,如果這個文件的日期比main.o日期新的話,那麼這個規則下面的命令 gcc -c main.c –o main.o 就會執行,以更新文件main.o。同樣make對文件io.o做類似的檢查,它的依賴文件是io.c,對io.o 的處理和main.o 類似。
現在,再回到第一個規則處,如果剛纔兩個規則中的任何一個被執行,最終的目標文件executable 都需要重建(因爲executable 所依賴的其中一個 .o 文件就會比它新),因此鏈接命令就會被執行。有了makefile,對任何一個源文件進行修改後,所有依賴於該文件的目標文件都會被重新編譯(因爲.o 文件依賴於.c 文件),進而最終可執行文件會被重新鏈接(因爲它所依賴的.o文件被改變了),再也不用手工去一個個修改了。
編寫make
接下來的內容,爲大家詳細介紹如何編寫makefile。內容包括,Makefile中的宏定義、Makefile的隱含規則、僞目標和函數等。
1、Makefile 宏定義
makefile裏的宏是大小寫敏感的,一般都使用大寫字母。它們幾乎可以從任何地方被引用,可以代表很多類型,例如可以存儲文件名列表,存儲可執行文件名和編譯器標誌等。
要定義一個宏,在makefile 中,任意一行的開始寫下該宏名,後面跟一個等號,等號後面是要設定的這個宏的值。如果以後要引用到該宏時,使用 $ (宏名),或者是${宏名},注意宏名一定要寫在圓或花括號之內。把上一小節所舉的例子,用引入宏名的方法,可以寫成下面的形式:
OBJS = main.o io.o
CC = gcc
CFLAGS = -Wall -O -g
executable: $(OBJS)
$(CC) $(OBJS) -o executable
main.o : main.c
$(CC) $(CFLAGS) -c main.c -o main.o
io.o : io.c
$(CC) $(CFLAGS) -c io.c -o io.o
在這個makefile 中引入了三個宏定義,所以如果當這些宏中的某些值發生變化時,開發者只需在要修改的宏處,將其宏值修改爲要求的值即可,makefile 中用到這些宏的地方會自動變化。在 make 中還有一些已經定義好的內部變量,有幾個較常用的變量是$@, $< ,$?,$*, $^ (注意:這些變量不需要括號括住)。
$@ 擴展爲當前規則的目標文件名;
$< 擴展爲當前規則依賴文件列表中的第一個依賴文件;
$? 擴展爲所有的修改日期比當前規則的目標文件的創建日期更晚的依賴文件,該值只有在使用顯式規則時纔會被使用;
$* 擴展成當前規則中目標文件和依賴文件共享的文件名,不含擴展名;
$^ 擴展爲整個依賴文件的列表(除掉了所有重複的文件名)。
利用這些變量,可以把上面的makefile寫成:
OBJS = main.o io.o
CC = gcc
CFLAGS = -Wall -O -g
executable: $(OBJS)
$(CC) $^ -o $@
main.o : main.c
$(CC) $(CFLAGS) –c $< -o $@
io.o : io.c
$(CC) $(CFLAGS) -c $< -o $@
可以將宏變量應用到其他許多地方,尤其是當把它們和函數混合使用的時候,正確使用宏,會給開發者帶來極大的便利。
2、隱含規則
請注意,在上面的例子裏,幾個產生.o 文件的命令都是以.c 文件作爲依賴文件產生.o 目標(obj)文件,這是一個標準的生成目標文件的步驟。如果把生成main.o 和io.o 的規則從makefile 中刪除,make 會查找它的隱含規則,然後會找到一個適當的命令去執行。實際上make 已經知道該如何生成這些目標文件,它使用變量CC做爲編譯器,並且傳遞宏 CFLAGS給C編譯器(CXXFLAGS 用於C++編譯器),CPPFLAGS(C預處理選項), TARGET_ARCH (就目前例子而言,還不用考慮這個宏),然後它加入開關選項-c,後面跟預定義宏 $<(第一個依賴文件名),最後是開關項-o,後跟預定義宏$@ (目標文件名)。一個C編譯的具體命令將會是:
$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@
在make 工具中所包含的這些內置的或隱含的規則,定義瞭如何從不同的依賴文件建立特定類型的目標。Unix 系統通常支持一種基於文件擴展名即文件名後綴的隱含規則。這種後綴規則定義瞭如何將一個具有特定文件名後綴的文件(例如.c 文件),轉換成爲具有另一種文件名後綴的文件(例如.o 文件): 系統中默認的常用文件擴展名及其含義爲:
.o 目標文件
.c C 源文件
.f FORTRAN 源文件
.s 彙編源文件
.y Yacc-C 源語法
.l Lex 源語法
而GNUmake除了支持後綴規則外還支持另一種類型的隱含規則即模式規則。這種規則更加通用,因爲可以利用模式規則定義更加複雜的依賴性規則。同時可用來定義目標和依賴文件之間的關係,例如下面的模式規則定義瞭如何將任意一個.c 文件轉換爲文件名相同的.o 文件:
%.o : %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
3、僞目標
如果需要最終產生兩個和更多的可執行文件,但這些文件是相互獨立的,也就是說任何一個目標文件的重建,不會影響其他目標文件。此時,可以通過使用所謂的僞目標來達到這一目的。一個僞目標和一個真正的目標文件的唯一區別在於,這個目標文件本身並不存在。因此, make 總是會假設它需要被生成,當make 把該僞目標文件的所有依賴文件都更新後,就會執行它的規則裏的命令行。
舉一個簡單的例子,如果makefile 開始處輸入
all : executable1 executable2
這裏executable1 和executable2 是最終希望生成的兩個可執行文件。 make 把這個"all"做爲它的主要目標,每次執行時都會嘗試把"all"更新。但是,由於這行規則裏並沒有命令來作用在一個叫"all"的實際文件上(事實上, all 也不會實際生成),所以這個規則並不真的改變"all"的狀態。可既然這個文件並不存在,所以make會嘗試更新all規則,因此就檢查它的依賴文件 executable1, exectable2 是否需要更新,如果需要,就把它們更新,從而達到生成兩個目標文件的目的,僞目標在makefile 中廣泛使用。
4、函數
makefile 裏的函數跟它的宏很相似,在使用的時候,用一個 $ 符號開始後跟圓括號,在圓括號內包含函數名,空格後跟一系列由逗號分隔的參數。例如,在 GNU Make 裏有一個名爲 "wildcard" 的函 數,它只有一個參數,功能是展開成一列所有符合由其參數描述的文件名,文件間以空格間隔。可以像下面所示使用這個命令:
SOURCES = $(wildcard *.c)
這樣會產生一個所有以".c"結尾的文件的列表,然後存入變量 SOURCES 裏。當然你不需要一定要把結果存入一個變量。
另一個有用的函數是 patsubst (patten substitude, 匹配替換的縮寫) 函數。它需要3個參數:第一個是一個需要匹配的模式,第二個表示用什麼來替換它,第三個是一個需要被處理的由空格分隔的字列。例如,處理那個經過上面定義後的變量,
OBJS = $(patsubst %.c,%.o,$(SOURCES))
這個語句將處理所有在 SOURCES 宏中的文件名後綴是 ".c"的文件 ,用 ".o" 把 ".c" 取代。注意這裏的 % 符號是通配符,匹配一個或多個字符,它每次所匹配的字符串叫做一個‘柄’(stem) 。在第二個參數裏,% 被解釋成用第一參數所匹配的那個柄。
感興趣的大家如果需要更進一步的瞭解,請參考GNU Make 手冊。
總結:makefile的一個具體例子
最後,在這裏給大家舉一個簡單的makefile 的例子,希望通過對這個makefile 的屆時,來鞏固前面介紹的相關知識。
INCLUDES =-I/home/nie/mysrc/include \
-I/home/nie/mysrc/extern/include \
-I/home/nie/mysrc/src \
-I/home/nie/mysrc/libsrc \
-I. \
-I..
EXT_CC_OPTS = -DEXT_MODE
CPP_REQ_DEFINES = -DMODEL=tune1 -DRT -DNUMST=2 \
-DTID01EQ=1 -DNCSTATES=0 \
-DMT=0 -DHAVESTDIO
RTM_CC_OPTS = -DUSE_RTMODEL
CFLAGS = -O -g
CFLAGS += $(CPP_REQ_DEFINES)
CFLAGS += $(EXT_CC_OPTS)
CFLAGS +=$(RTM_CC_OPTS)
SRCS = tune1.c rt_sim.c rt_nonfinite.c grt_main.c rt_logging.c \
ext_svr.c updown.c ext_svr_transport.c ext_work.c
OBJS = $(SRCS:.c=.o)
RM = rm –f
CC = gcc
LD = gcc
all: tune1
%.o : %.c
$(CC) -c -o $@ $(CFLAGS) $(INCLUDES) $<
tune1 : $(OBJS)
$(LD) -o $@ $(OBJS) -lm
clean :
$(RM) $(OBJS)
在這個makefile 中首先定義了十個宏:
"INCLUDES =-I …"(省略號代表-I 後面的內容),"-I dirname" 表示將dirname 所指的目錄加入到程序頭文件目錄列表中去,是在進行預處理過程中使用的參數;
"EXT_CC_OPTS = -DEXT_MODE " 表示在程序中定義了宏EXT_MODE,等價於在源代碼寫入語句"#define EXT_MODE " ;
接下來的兩個宏定義CPP_REQ_DEFINES和RTM_CC_OPTS 起到和EXT_CC_OPTS類似的作用;
"CFLAGS =-O -g "是編譯器的編譯選項,表示在編譯的過程中對代碼進行基本優化,併產生能被GNU 調試器(如gdb)使用的調試信息;
"CFLAGS += " 表示對這個宏定義在原來的基礎上增加新的內容;
"SRCS = …"代表了所有要編譯的源代碼文件列表;
"OBJS = $(SRCS:.c=.o)"表示把宏SRC 所代表的所有以.c 結尾的文件名用.o 結尾的文件名替換,即表示各個源文件所對應的目標文件名;
"RM = rm –f " 表示刪除命令,-f 是強制刪除選項,使用該符號,在對文件進行刪除時,沒有提示;
"CC = gcc"表示編譯器是用gcc;
"LD = gcc" 表示鏈接命令是用gcc;
all 和clean 是兩個僞目標,在使用make 命令的時候,如果不指明目標文件名,則是以在makefile 中出現的第一個目標作爲最終目標,所以如果鍵入命令make,則僞目標all 被作爲最終的目標而執行,由於這個文件並不存在,所以 make 會嘗試更新 all 規則,因此就檢查它的依賴文件 tune1 是否需要更新,如果需要,就把它更新,這樣僞目標下面的兩條規則就會被執行,從而生成可執行文件tune1。如果要執行刪除命令,只需要鍵入命令make clean,就會把所有以.o 結尾的中間文件刪除。
另外,請大家注意在本makefile 的例子中多次用到" \",該符號用於在makefile 中,如果一條語句過長時,可以用"\"放在這條語句的右邊界,通過回車換行,使下面新一行的語句成爲該語句的續行。
在makefile 文件中,用符號"#"作爲註釋行語句的開始,以增強makefile 文件的可讀性。
本例假設makefile 文件名爲makefile,當然也可按照個人的喜好取其他文件名,如果文件名不是makefile,在用make 命令是,請使用make –f makefilename指定你自己的Makefile文件。
至此,我已向大家分享完了我在學習Linux和LinuxC語言編程時所遇到和掌握的Gcc編譯工具及其Makefile的編寫相關內容,希望大家能夠對大家的學習有所幫助。