makefile 基礎知識


gcc Makefile 入門

使用make命令編譯項目文件入門
目錄:
一、make命令的運行過程
二、基本gcc編譯命令
三、簡單Makefile文件的編寫
四、實例

一、make命令的運行過程
    在shell的提示符號下,若輸入"make",則它會到目前的目錄下找尋Makefile這個文件.然後依照Makefile中所記錄的步驟一步一步的來執行.在我們寫程序的時候,如果事先就把compiler程式所需要的步驟先寫在Makefile中的話,想要compiler程序的時候就只要打入make的指令.只要程序無誤的話,就可以獲得所需要的結果了!
    在項目文件中,如果有成百上千個源程序,每次修改其中的一個都需要全部重新編譯是不可想象的事情.但通過編輯Makefile文件,利用make命令就可以只針對其中修改的源文件進行編譯,而不需要全體編譯.這就是make命令在編譯項目文件時體現出來的優勢.能做到這點,主要是基於Makefile文件的編寫,和make命令對Makefile文件的調用.Makefile文件作爲make命令的默認參數,使一個基於依賴關係編寫的結構文件.
    大家經常看到使用make all, make install, make clean等命令,而他們處理的目標都是一個Makefile文件,那麼all、install、clean參數是如何調用Makefile文件的運行呢?在這裏,如果向上面的命令如果能夠正確運行的話,那麼在Makefile文件裏一定有這樣的幾行,他們的以all、install、clean開始
all: ×××××××
       ×××××××××××
install: ××××××
       ×××××××××××
clean: ×××××××××
        ×××××××××××
all,install,clean我們可以用其他的變量來代替,他們是編譯時的一個參數,在Makefile文件中作爲一個標誌存在,也就是我們所說的目標.make all命令,就告訴make我們將執行all所指定的目標.爲了便於理解Make程序的流程,我們給大家看一個與gcc毫無關係的Makefile文件:
#Makefile begin
all:
        @echo you have typed command "make all"
clean:
        @echo you have typed command "make clean"
install:
        @ehco you have typed command "make $@"
#Makefile end
注意在這裏,all:、clean:、install:行要頂格些,而所有的@echo前要加tab鍵來跳格縮進.下面是運行結果
[root@xxx test]#make all
you have typed command "make all"
[root@xxx test]#make clean
you have typed command "make clean"
[root@xxx test]#make install
you have typed command "make install"

二、基本gcc編譯命令

1、源程序的編譯 
    在Linux下面,使用GNU的gcc編譯器編譯一個C語言的源程序.下面我們簡單介紹幾個常用的Gcc編譯命令和參數,這裏不是講解Gcc的使用,只是介紹簡單的基礎知識是我們能看懂一般的makefile文件. 
    我們先看一個使用gcc編譯器的實例.假設我們有下面一個非常簡單的源程序(hello.c): 
    int main(int argc,char **argv) 
    { 
       printf("Hello Linux\n"); 
     } 
要編譯這個程序,我們只要在命令行下執行:     gcc -o hello hello.c 
    gcc 編譯器就會爲我們生成一個hello的可執行文件.執行./hello就可以看到程序的輸出結果了.命令行中 gcc表示我們是用gcc來編譯我們的源程序,-o 選項表示我們要求編譯器給我們輸出的可執行文件名爲hello 而hello.c是我們的源程序文件. 
    gcc的基本格式就是:
       gcc [-option] objectname sourcename
    其中-option是參數,用來控制gcc的編譯方式,常見的參數有如下幾個:
    -o 表示我們要求輸出的可執行文件名:-o binaryname
    -c 表示我們只要求編譯器進行編譯,輸出目標代碼,而不進行連接: -c objectivename.o
    -g 表示我們要求編譯器在編譯的時候提供我們以後對程序進行調試的信息: -g 
    -O2 表示我們希望編譯器在編譯的時候對我們的程序進行一定程度的優化.2表示我們優化的級別是2.範
        圍是1-3.不過習慣上我們都使用2的優化級別.
    -Wall是警告選項,表示我們希望gcc在編譯的時候,讓gcc輸出她認爲的一些程序中可能會出問題的一些警
        告信息,比如指針沒有初始化就進行賦值等等一些警告信息. 
    -l 與之緊緊相連的是表示連接時所要的鏈接,比如多線程,如果你使用了pthread_create函數,那麼         你就應該在編譯語句的最後加上"-lpthread","-l"表示連接,"pthread"表示要連接的,注意他們         在這裏要連在一起寫.如:gcc -o test test1.o test2.o -lpthread
    -I 表示將系統缺省的頭文件路徑擴展到當前路徑,默認的路徑保存在/etc/ld.conf文件中。
    gcc的例子:
        gcc -c test.c,表示只編譯test.c文件,成功時輸出目標文件test.o
        gcc -o test test.o,將test.o連接成可執行的二進制文件test
        gcc -o test test.c,將test.c編譯並連接成可執行的二進制文件test
        gcc -c test.c -o test.o ,與上一條命令完全相同
        gcc test.c -o test,與上一條命令相同
        gcc -c test1.c,只編譯test1.c,成功時輸出目標文件test1.o
        gcc -c test2.c,只編譯test2.c,成功時輸出目標文件test2.o
        gcc -o test test1.o test2.o,將test1.o和test2.o連接爲可執行的二進制文件test
        gcc -c test test1.c test2.c,將test1.o和test2.o編譯並連接爲可執行的二進制文件test

2、程序的鏈接 
試着編譯下面這個程序 
/* temp.c */ 
#include 
int main(int argc,char **argv) 

double value =15; 
printf("Value:%f\n",log(value)); 

這個程序相當簡單,但是當我們用 gcc -o temp temp.c 編譯時會出現下面所示的錯誤. 
/tmp/cc33Kydu.o: In function `main': 
/tmp/cc33Kydu.o(.text+0xe): undefined reference to `log' 
collect2: ld returned 1 exit status 
    出現這個錯誤是因爲編譯器找不到log的具體實現.雖然我們包括了正確的頭文件,但是我們在編譯的時候還是要連接確定的.在Linux下,爲了使用數學函數,我們必須和數學連接,爲此我們要加入 -lm 選項. gcc -o temp temp.c -lm這樣才能夠正確的編譯.也許有人要問,前面我們用printf函數的時候怎麼沒有連接呢?是這樣的,對於一些常用的函數的實現,gcc編譯器會自動去連接一些常用,這樣我們就沒有必要自己去指定了. 有時候我們在編譯程序的時候還要指定的路徑,這個時候我們要用到編譯器的 -L選項指定路徑.比如說我們有一個在 /home/hoyt/mylib下,這樣我們編譯的時候還要加上 -L/home/hoyt/mylib.對於一些標準來說,我們沒有必要指出路徑.只要它們在起缺省的路徑下就可以了.系統的缺省的路徑/lib、/usr/lib、/usr/local/lib(你可以查看你的/etc/ld.conf文件來看看你的系統指定了那幾個缺省的路徑) 在這三個路徑下面的,我們可以不指定路徑. 
    還有一個問題,有時候我們使用了某個函數,但是我們不知道的名字,這個時候怎麼辦呢?很抱歉,對於這個問題我也不知道答案,我只有一個傻辦法.首先,我到標準路徑下面去找看看有沒有和我用的函數相關的,我就這樣找到了線程(thread)函數的文件(libpthread.a). 當然,如果找不到,只有一個笨方法.比如我要找sin這個函數所在的. 就只好用 nm -o /lib/*.so|grep sin>~/sin 命令,然後看~/sin文件,到那裏面去找了. 在sin文件當中,我會找到這樣的一行libm-2.1.2.so:00009fa0 W sin 這樣我就知道了sin在libm-2.1.2.so裏面,我用 -lm選項就可以了(去掉前面的lib和後面的版本標誌,就剩下m了所以是 -lm). 如果你知道怎麼找,請趕快告訴我,我回非常感激的.謝謝!

3、程序的調試 
    我們編寫的程序不太可能一次性就會成功的,在我們的程序當中,會出現許許多多我們想不到的錯誤,這個時候我們就要對我們的程序進行調試了.最常用的調試軟件是gdb.如果你想在圖形界面下調試程序,那麼你現在可以選擇xxgdb.記得要在編譯的時候加入-g選項.關於gdb的使用可以看gdb的幫助文件.由於我很少使用這個軟件,所以我也不能夠詳細的說出如何使用. 不過我不喜歡用gdb.跟蹤一個程序是很煩的事情,我一般用在程序當中輸出中間變量的值來調試程序的.當然你可以選擇自己的辦法,沒有必要去學別人的.現在有了許多IDE環境,裏面已經自己帶了調試器了.你可以選擇幾個試一試找出自己喜歡的一個用.

4、頭文件和系統求助 
    有時候我們只知道一個函數的大概形式,不記得確切的表達式,或者是不記得着函數在那個頭文件進行了說明.這個時候我們可以求助系統.比如說我們想知道fread這個函數的確切形式,我們只要執行 man fread 系統就會輸出着函數的詳細解釋的.和這個函數所在的頭文件說明了. 如果我們要write這個函數的說明,當我們執行man write時,輸出的結果卻不是我們所需要的. 因爲我們要的是write這個函數的說明,可是出來的卻是write這個命令的說明.爲了得到write的函數說明我們要用 man 2 write. 2表示我們用的write這個函數是系統調用函數,還有一個我們常用的是3表示函數是C函數.


三、簡單Makefile文件的編寫

1、Makefile文件的一般組成
(1)註釋:
    在Makefile中,任何以"#"起始的文字都是註釋,make在解釋Makefile的時候會忽略它們.
(2)轉接下行標誌:
    在Makefile中,若一行不足以容納該命令的時候.可在此行之後加一個反斜線(\)表示下一行爲本行的延續
   ,兩行應視爲一行處理
(3)宏(macro)
    宏的格式爲: = 
    例如:
                CFLAGS = -O -systype bsd43
    其實make本身已有許多的default的macro,如果要查看這些macro的話,可以用make -p的命令.
    宏主要是作爲運行make時的一些環境變量的設置,比如制定編譯器等。
    CC 表示我們的編譯器名稱,缺省值爲cc. 
    CFLAGS 表示我們想給編譯器的編譯選項 
    LDLIBS 表示我們的在編譯的時候編譯器的連接選項.(我們的這個程序中還用不到這個選項)
(4)規則(Rules)
    格式如下:
        : 
                
                
                ....
        : 
                
                
                ....
    注意:需要頂格寫,而需要在下一行tab之後寫,由於其是一個批處理形式的文件,所以不
         可以隨便的換行寫,被迫換行的時候要用上面的轉接下行標誌進行連接.            
(5)符號標誌及缺省規則
   $@ 代指目標文件
   $< 第一個依賴文件
   $^ 所有的依賴文件
   $? 爲該規則的依賴
   -   若在command的前面加一個"-",表示若此command發生錯誤不予理會,繼續執行下去.
   $(macro) 應用這個變量,可以自動的將定義的宏加以展開,並替換使用。
   .c.o: 
      gcc -c $<   這個規則表示所有的 .o文件都是依賴與其相應的.c文件的.例如mytool.o依賴於mytool.c
   再一次簡化後的Makefile 
    main:main.o mytool1.o mytool2.o 
         gcc -o $@ $^ 
    .c.o: 
         gcc -c $< -I.; 
    使用宏後進一步的簡化Makefile可以是 
    CC=gcc 
    CFLAGS=-g -Wall -O2 -I. 
    main:main.o mytool1.o mytool2.o 
    .c.o:

2、依賴
    我們現在提出這樣一個問題:我如何用一個make命令將替代所有的make all, make install,make clean命令呢?當然我們可以象剛纔那樣寫一個Makefile文件:
all:
        @echo you have typed command "make all"
clean:
        @echo you have typed command "make clean"
install:
        @ehco you have typed command "make $@"
doall:
        @echo you have typed command "make $@l"
        @echo you have typed command "make all"
        @echo you have typed command "make clean"
        @ehco you have typed command "make install"
[root@xxx test]#make doall
you have typed command "make doall"
you have typed command "make all"
you have typed command "make clean"
you have typed command "make install"
    在這裏,doall:目標有4調語句,他們都是連在一起並都是由tab鍵開始的.當然,這樣能夠完成任務,但是太笨了,我們這樣來寫:
[root@xxx test]#cat Makefile
# #表示Makefile文件中的註釋,下面是Makefile文件的具體內容
all:
        @echo you have typed command "make all"
clean:
        @echo you have typed command "make clean"
install:
        @ehco you have typed command "make $@"
doall: all clean install
        @echo you have typed command "make $@l"
    相信大家已經看清了doall:的運行方式,它先運行all目標,然後運行clean目標,然後是install,最後是自己本身的目標,並且每個$@還是保持着各自的目標名稱.效果大致是一樣的。在這裏,我們稱all, clean, install爲目標doall所依賴的目標,簡稱爲doall的依賴.也就是你要執行doall,請先執行他們(all, clean, install),最後在執行我的代碼.
    注意依賴一定是Makefile裏面的目標,否則你非要運行;一般寫在最前邊,而不是像這樣寫在最後邊。

3、Makefile的編寫 
   在Makefile中,一般採用引導的註釋行開始;下邊一般緊跟macro定義;接下來是標籤(如上面的all、clean等);最後就是Makefile中最重要的是描述文件的依賴關係的說明.一般的格式是: 
   #describe
   macro
   label: label1,label2
   label1:
   : 
                
                
   label2:
   : 
                
                
   ...... 
    
我們來看一個例子:
   /* main.c */ 
#include 
#include 
int main(int argc,char **argv) 

mytool1_print("hello"); 
mytool2_print("hello"); 
}

/* mytool1.h */ 
#ifndef _MYTOOL_1_H 
#define _MYTOOL_1_H 
void mytool1_print(char *print_str); 
#endif

/* mytool1.c */ 
#include 
void mytool1_print(char *print_str) 

printf("This is mytool1 print %s\n",print_str); 
}

/* mytool2.h */ 
#ifndef _MYTOOL_2_H 
#define _MYTOOL_2_H 
void mytool2_print(char *print_str); 
#endif

/* mytool2.c */ 
#include 
void mytool2_print(char *print_str) 

printf("This is mytool2 print %s\n",print_str); 

    因爲我們在程序中使用了我們自己的2個頭文件,而在包含這2個頭文件的時候,我們使用的是<> 這樣編譯器在編譯的時候會去系統默認的頭文件路徑找我們的2個頭文件,由於我們的2個頭文件不在系統能夠的缺省路徑下面,所以我們自己擴展系統的缺省路徑,爲此我們使用了-I.選項,表示將系統缺省的頭文件路徑擴展到當前路徑.這樣的話我們也可以產生main程序,而且也不是很麻煩.但是考慮一下如果有一天我們修改了其中的一個文件(比如說mytool1.c)那麼我們難道還要重新逐一編譯?也許你會說,這個很容易解決啊,我寫一個SHELL腳本,讓她幫我去完成不就可以了.但是當我們把事情想的更復雜一點,如果我們的程序有幾百個源程序的時候,難道也要編譯器重新一個一個的去編譯? 爲此,聰明的程序員們想出了一個很好的工具來做這件事情,這就是make.我們只要執行一下make,就可以把上面的問題解決掉.在我們執行make之前,我們要先編寫一個非常重要的文件.--Makefile.對於上面的那個程序來說,可能的一個Makefile的文件是: 
# 這是上面那個程序的Makefile文件 
main:main.o mytool1.o mytool2.o 
gcc -o main main.o mytool1.o mytool2.o 
main.o:main.c mytool1.h mytool2.h 
gcc -c main.c -I. 
mytool1.o:mytool1.c mytool1.h 
gcc -c mytool1.c -I. 
mytool2.o:mytool2.c mytool2.h 
gcc -c mytool2.c -I. 
    有了這個Makefile文件,不管我們什麼時候修改了源程序當中的什麼文件,我們只要執行make命令,我們的編譯器都只會去編譯和我們修改的文件有關的文件,其它的文件她連理都不想去理的.


四、實例
1、實例一
一個非常簡單的Makefile
   假設我們有一個程式,共分爲下面的部份:
   menu.c       主要的程式碼部份
   menu.h       menu.c的include file
   utils.c      提供menu.c呼叫的一些function calls
   utils.h      utils.c的include file
   同時本程式亦叫用了ncurses的function calls.
   而menu.c和utils.c皆放在/usr/src/menu下.
   但menu.h和utils.h卻放在/usr/src/menu/include下.
   而程式做完之後,執行檔名爲menu且要放在/usr/bin下面.
# This is the Makefile of menu
CC = gcc
CFLAGS = -DDEBUG -c
LIBS = -lncurses
INCLUDE = -I/usr/src/menu/include

all: clean install
install: menu
        chmod 750 menu
        cp menu /usr/bin
menu: menu.o
        $(CC) -o $@ $? $(LIBS)
menu.o:
        $(CC) $(CFLAGS) -o $@ menu.c $(INCLUDE)
utils.o:
        $(CC) $(CFLAGS) -o $@ utils.c $(INCLUDE)
clean:
        -rm *.o
        -rm *~

在上述的Makefile中,要使用某個macro可用$(macro_name)如此的形式.make會自動的加以展開.
$@爲該rule的Target,而$?則爲該rule的depend.
若在command的前面加一個"-",表示若此command發生錯誤則不予理會,繼續執行下去.
上述的Makefile的關係可以表示如下:
                        all
                        / \
                   clean   install
                               \
                               menu
                              /    \
                          menu.o    utils.o

若只想清除source以外的檔案,可以打make clean;若只想做出menu.o可以打make menu.o;若想一次全部做完,可以打make all或是make;要特別注意的是command之前一定要有一個TAB(即TAB鍵).

2、實例二
有了上面的說明,我們就可以開始寫一些簡單的Makefile文件了.比如我們有如下結構的文件:
tmp/
   +---- include/
   |      +---- f1.h
   |      +----f2.h
   +----f1.c    #include "include/f1.h"
   +----f2.c    #include"include/f2.h"
   +---main.c   #include"include/f1.h", #include"include/f2.h"
要將他們聯合起來編譯爲目標爲testmf的文件,我們就可以按下面的方式寫Makefile:
#Makefile,Create testmf from f1.c f2.c main.c
main: main.o f1.o f2.o
        gcc -o testmf main.o f1.o f2.o
f1.o: f1.c
        gcc -c -o file1.o file1.c
f2.o: f2.c
        gcc -c -o file2.o file2.c
main.o
        gcc -c -o main.o main.c
clean:
        rm -rf f1.o f2.o main.o testmf
執行這個Makefile文件
[root@xxx test]make
gcc -c -o main.o main.c
gcc -c -o file1.o file1.c
gcc -c -o file2.o file2.c
gcc -o testmf main.o f1.o f2.o
[root@xxx test]ls
f1.c f1.o f2.c f2.o main.c main.o include/ testmf
如果你的程序沒有問題的話,就應該可以執行了./testmf
    這是一個很簡單的例子,但複雜的例子都是構建在簡單功能基礎上的.後面將繼續介紹詳細的makefile的編寫方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章