隨着編程語言技術的不斷發展,應用程序的開發過程也越來越簡化。然而功能越是強大的工具,其內部結構也越復雜。高級編程語言的背后是一套復雜的編譯系統。編譯系統的任務是把高級語言編寫的程序翻譯成計算機可以直接運行的二進制文件。
4.1 GCC
在Linux平台上,最流行的編譯系統是GCC(GNU Compile Collection)。GCC也是GNU發布的最著名的軟件之一。GCC的功能非常強大,主要體現在兩方面。
1) GCC可以為x86、ARM、MIPS等不同體系結構的硬件平台編譯程序。
2) GCC可以編譯C、C++、Pascal、Java等數十種高級語言。
GCC的這兩項特性對嵌入式應用開發及其重要。此外,GCC的編譯效率也是非常高的,一般要高出其他編譯系統20%到30%左右。所以在嵌入式Linux開發領域,使用的基本上就是GCC編譯系統。
在紅旗Linux 6.0系統中已經集成了GCC,通過命令“gcc -v” 可以查看GCC中C編譯器gcc的詳細信息。
[root@localhost ~]#gcc -v
\Using built-in specs.
Target: x86-pc-linux
Configured with: ../configure--prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --enable-shared--enable-threads=posix --enable-checking=release --with-system-zlib--enable-__cxa_atexit --disable-libunwind-exceptions --enable-libgcj-multifile--disable-libmudflap --enable-languages=c,c++,java,fortran --disable-libgcj--with-cpu=generic --host=x86-pc-linux
Thread model: posix
gcc version 4.2.1
gcc命令的使用格式為:
gcc [選項] [文件名] [選項] [文件名]
gcc命令擁有數量龐大的編譯選項,按類型可以把選項分為以下幾大類。
l 總體選項:用於控制編譯的整個流程。
常用選項:
-c:對源文件進行編譯或匯編。
-E:對源文件進行預處理。
-S:對源文件進行編譯。
-o file:輸出目標文件file。
-v:顯示編譯階段的命令。
l 語言選項:用於支持各種版本的C語言程序。
常用選項:
-ansi:支持符合ANSI標准的C程序。
l 警告選項:用於控制編譯過程中產生的各種警告信息。
常用選項:
-W:屏蔽所有的警告信息。
-Wall:顯示所有類型的警告信息。
-Werror:出現任何警告信息就停止編譯。
l 調試選項:用於控制調試信息。
常用選項:
-g:產生調試信息。
l 優化選項:用於對目標文件進行優化。
常用選項:
-O1:對目標文件的性能進行優化。
-O2:在-O1的基礎上進一步優化,提高目標文件的運行性能。
-O3:在-O2的基礎上進一步優化,支持函數集成優化。
-O0:不進行優化。
l 連接器選項:用於控制鏈接過程。
常用選項:
-static:使用靜態鏈接。
-llibrary:鏈接library函數庫文件。
-L dir:指定連接器的搜索目錄dir。
-shared:生成共享文件。
l 目錄選項:用於指定編譯器的文件搜索目錄。
常用選項:
-Idir:指定頭文件的搜索目錄dir。
-Ldir:指定搜索目錄dir。
此外,還有配置選項等其他選項,這里不做介紹了。
編譯系統本身是一種相當復雜的程序,編寫甚至讀懂這樣的程序都是非常困難的。但是從事嵌入式Linux應用的開發人員都應掌握編譯系統的基本原理和工作流程。
4.1.1 GCC工作流程
在C程序的編譯過程中,依次要進行預處理、編譯、匯編、鏈接四個階段。這里通過編譯C文件test.c來展示GCC的工作流程。
例如:
l test.c
#include<stdio.h>
intmain()
{
printf(“Helloworld!\n”);
return 0;
}
1. 預處理階段
由於在test.c中使用了頭文件stdio.h,所以GCC在編譯時首先要把頭文件stdio.h中的內容加載到test.c中的首部。
在shell中輸入命令“gcc-E test.c -o test.i”。其中,參數E告訴gcc命令只進行預編譯,不做其他處理;參數o用來指明輸出的文件名為test.i。命令運行完畢后就會產生一個名為test.i的文件。如下所示:
[root@localhost home]#gcc -E test.c -o test.i
[root@localhost home]#ls
test.c test.i
test.i文件的代碼有一百多行,如下所示的是test.i文件的最后部分代碼。
externchar *ctermid (char *__s) __attribute__ ((__nothrow__));
#820 "/usr/include/stdio.h" 3 4
externvoid flockfile (FILE *__stream) __attribute__ ((__nothrow__));
externint ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__)) ;
externvoid funlockfile (FILE *__stream) __attribute__ ((__nothrow__));
#850 "/usr/include/stdio.h" 3 4
# 2"test.c" 2
intmain()
{
printf(“Hello world!\n”);
return 0;
}
2. 編譯階段
編譯階段是整個編譯過程中最復雜的一個階段。這里拿自然語言的翻譯過程作個對比。比如在把“I love China”翻譯成中文前,需要依次完成以下幾個步驟:
1)考察這個句子中每個單詞的拼寫是不是正確。
2)考察整個句子的語法(比如主謂賓、定狀補的結構等)是不是正確。
3)考察整個句子的語義是不是正確。
只有以上三個步驟都正常通過了,才能保證句子被正確翻譯。同樣,高級編程語言的編譯階段也必須實現這三個步驟。
1) 步驟1稱為詞法分析,主要負責檢查關鍵字、標識符等是否正確。
2) 步驟2稱為語法分析,主要負責檢查程序中語句的語法是否正確。
3) 步驟3稱為語義分析,主要負責檢查程序中語句的邏輯意義是否正確。
在shell中輸入命令“gcc-S test.i -o test.s”。其中,參數S告訴gcc命令只進行編譯,不做其他處理。命令運行完畢后就會產生一個名為test.s的匯編文件。如下所示:
[root@localhost home]#gcc -S test.i -o test.s
[root@localhost home]#ls
test.c test.i test.s
在學習使用匯編語言編程的時候,對照C文件和其匯編程序是很好的辦法。如下所示的是test.s的代碼。
.file "test.c"
.section .rodata
.LC0:
.string "Hello world!"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $4, %esp
movl $.LC0, (%esp)
call puts
movl $0, %eax
addl $4, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.2.1"
.section .note.GNU-stack,"",@progbits
注意,以上所示的匯編代碼是針對x86平台的。
3. 匯編階段
匯編階段的任務是把匯編程序翻譯成CPU可以識別的二進制文件,該文件又稱為目標文件。
在shell中輸入命令“gcc -c test.s -o test.o”,其中,參數c告訴gcc命令只進行匯編,不做其他處理。命令運行完畢后就會產生一個名為test.o的目標文件。如下所示:
[root@localhost home]#gcc -c test.s -o test.o
[root@localhost home]#ls
test.c test.i test.o test.s
在Windows系統中,目標文件的后綴是obj。
4. 鏈接階段
目標文件雖然已經可以被CPU直接識別,但是單個目標文件一般是無法運行的。原因在於一個程序往往是由多個源文件組成的,每個源文件只對應一個目標文件。也許有人會問,test程序不就只有一個源文件test.c嗎,為什么也不能直接運行呢?原因是test.c使用了stdio.h對應的庫函數,所以必須要把test.o文件和函數庫文件鏈接在一起才能運行。
鏈接階段的任務就是把程序中所有的目標文件和所需的庫文件都鏈接在一起,最終生成一個可以直接運行的文件,稱為可執行文件。
在shell中輸入命令“gcc test.o -o test”,運行完畢后就會產生一個名為test的可執行文件。輸入命令“./test”執行該文件,就可以得到test文件的運行結果“Hello world!”。 如下所示:
[root@localhost home]#gcc test.o -o test
[root@localhost home]#./test
Hello world!
gcc命令生成的可執行文件的有以下三種格式。
1)a.out(Assemblerand Link editor output);
2)COFF(Commonobject file format);
3)ELF(Executableand linkable format);
其中,a.out和COFF格式都是比較老的格式,現在Linux平台上可執行文件的主流格式是ELF。
4.1.2 Glibc
從邏輯功能上看,程序的主體是由一系列函數組成的。所以說編寫程序的主要工作之一就是實現函數。為了有效降低編程的工作量,編程系統會把一些非常基本、常用的函數集中到函數庫中實現。比如信息的打印函數、文件的打開或關閉函數、內存空間的申請與釋放函數、數學計算函數等。當程序需要使用到函數庫中的某個函數時,就可以直接從庫中調用。就好比建造房屋,建築隊並不需要從頭開始制造磚瓦和水泥,而只需要從原材料市場購買就可以了。
每種高級編程語言都有各自的函數庫。比如C語言的C 庫、Visual C++的MFC、Java的JFC等。函數庫中的函數都是由經驗豐富的資深程序員編寫的,具有出色的運行性能和工作效率。所以說,函數庫的使用不光減少了編程的工作量,還能有效提高程序的性能和健壯性。在面向對象語言中,函數被封裝在類中,所以函數庫就演變成了類庫,但其原理和機制是類似的。
函數庫的使用方式分為靜態鏈接和動態鏈接兩種。靜態鏈接是指編譯系統在鏈接階段把程序的目標文件和所需的函數庫文件鏈接在一起,這樣生成的可執行文件就可以在沒有函數庫的情況下運行。就好比火箭把燃料和氧料裝在一起,就可以在沒有空氣的太空中飛行。動態鏈接是指編譯系統在鏈接階段並不把目標文件和函數庫文件鏈接在一起,而是等到程序在運行過程中需要使用時才鏈接函數庫。
使用靜態鏈接方式產生的可執行文件體積較大,但運行效率較高。而使用動態鏈接方式產生的可執行文件由於沒有庫文件,所以體積較小。但由於需要動態加載函數庫,所以運行效率要低一點。
在具體應用時,如果有多個源文件都需要調用函數庫時,那么應該選擇動態鏈接的方式。而只有少數源文件需要調用函數庫時,應該選擇靜態鏈接的方式。可以被靜態鏈接的函數庫稱為靜態庫,可以被動態鏈接的函數庫稱為動態庫,或者共享庫。
Glibc(GNU Library C)是GNU推出的C語言函數庫。Glibc符合ISO C (International Standard for the C programming language)和POSIX(Portable Operating System Interfacefor Computer Environments)標准。其中,ISO C定義了C函式庫的標准格式,POSIX定義了不同計算平台應該遵守的C函數庫標准,是ISO C標准的擴充。因此Glibc可以在各種不同體系結構的計算平台上使用。
Glibc中包含了大量的函數庫,其中libc是最基本的函數庫,每個C程序都需要使用libc庫。此外,常用的還有數學庫libm、加密庫libcrypt、POSIX線程庫libpthread、網絡服務庫libnsl、IEEE浮點運算庫libieee等。Glibc庫為C程序提供了大量功能強大的函數,包括輸入輸出函數、字符串處理函數、數學函數、中斷處理函數、錯誤處理函數、日期時間函數等等。
C程序在調用Glibc中的函數庫時,需要引用與函數庫對應的頭文件。比如stdio.h、string.h、time.h等。這些頭文件都存放在/usr/include目錄下。同時,在編譯命令中需要加入某些函數庫的鏈接參數(在函數庫的使用文檔中會列出具體的鏈接庫名稱參數),並使用符號“-l”進行連接。比如libm庫的鏈接參數為m,libpthread庫的鏈接參數為pthread等。
例如:
l test.c:
#include<stdio.h>
#include<math.h>
intmain()
{
printf(“%d\n”, sin(0));
return 0;
}
編譯命令如下所示:
[root@localhost home]# gcc test.c -o test -lm
[root@localhost home]# ./test
0
在紅旗Linux 6.0系統中,Glibc分布在/lib和/usr/lib目錄下,其中/lib目錄中的函數庫文件主要是給/bin目錄下的系統程序使用的,/usr/lib目錄中的函數庫文件主要是給/usr目錄下的用戶程序使用的。如下所示的是/usr/lib目錄下的部分png函數庫文件。
libpng.a
libpng.la
libpng.so
libpng.so.3
libpng.so.3.16.0
其中,后綴為a的是靜態庫文件,后綴為la的是用來記錄庫文件信息的動態庫文件,后綴為so的是動態庫文件。libpng.so.3.16.0是真正的png動態庫文件,而libpng.so.3是指向libpng.so.3.16.0動態庫文件的符號鏈接文件。
[root@localhost lib]# file libpng.so.3
libpng.so.3: symbolic link to `libpng.so.3.16.0'
[root@localhost lib]# file libpng.so.3.16.0
libpng.so.3.16.0: ELF32-bit LSB shared object, Intel 80386, version 1 (SYSV),
stripped
使用動態鏈接方式編譯程序時,動態庫的符號鏈接文件會寫入二進制文件中。這樣,程序在運行時就可以通過符號鏈接文件找到指定的動態庫文件了。
這里以編譯test.c為例,使用“gcc test.c -o test”命令編譯生成可執行文件test。通過file命令可以查看test文件的相關信息,如下所示。
[root@localhost home]# file test
test: ELF 32-bit LSB executable, Intel 80386, version 1(SYSV), for GNU/Linux
2.6.9, dynamically linked (uses shared libs), for GNU/Linux2.6.9, not stripped
其中,“dynamically linked (uses sharedlibs)”表明test文件使用了動態鏈接庫。test文件的大小是5KB左右。
通過選項static可以使用靜態鏈接方式對程序進行編譯。輸入命令“gcc -static test.c -o test”生成可執行文件test,如下所示。
[root@localhost home]# file test
test: ELF 32-bit LSB executable, Intel 80386, version 1(SYSV), for GNU/Linux
2.6.9, statically linked, for GNU/Linux 2.6.9, not stripped
其中,“statically linked”表明test文件使用了靜態鏈接庫。可以看到,test文件的大小達到了540KB左右。
4. 2 工程管理器
在實際的開發過程中,僅僅通過使用gcc命令對程序進行編譯是非常低效的。原因主要有以下兩點。
1)程序往往是由多個源文件組成的,源文件的個數越多,那么gcc的命令行就會越長。此外,各種編譯規則也會加大gcc命令行的復雜度。所以在開發調試程序的過程中,通過輸入gcc命令行來編譯程序是很麻煩的。
2)在程序的整個開發過程中,調試的工作量占到了整體工作量的70%以上。在調試程序的過程中,每次調試一般只會修改部分源文件。而在使用gcc命令行編譯程序時,gcc會把那些沒有被修改的源文件一起編譯,這樣就會影響編譯的總體效率。
為了提高編譯程序的效率,很多基於Windows平台上的開發工具都提供了工程管理器。用戶只需要點擊一個“make”按鈕就可以啟動工程管理器對整個程序進行自動編譯。在整個編譯的過程中是不需要人工干預的。這種工程管理器形象的稱為全自動工程管理器。
GCC提供了半自動化的工程管理器Make。所謂半自動化是指在使用工程管理器前需要人工編寫程序的編譯規則。所有的編譯規則都保存在Makefile文件中。全自動化的工程管理器在編譯程序前會自動生成Makefile文件。
Make工程管理器的優越性具體體現在以下兩個方面。
(1)使用方便
通過命令“make”就可以啟動Make工程管理器對程序進行編譯,所以不再需要每次都輸入gcc命令行。Make啟動后會根據Makefile文件中的編譯規則命令自動對源文件進行編譯和鏈接,最終生成可執行文件。
(2)調試效率高
為了提高編譯程序的效率,Make會檢查每個源文件的修改時間(時間戳)。只有在上次編譯之后被修改的源文件才會在接下來的編譯過程中被編譯和鏈接,這樣就能避免多余的編譯工作量。為了保證源文件具有正確的時間戳,必須保證操作系統時間的正確性(注意VMWare虛擬機的CMOS時間是否正確)。
4.2.1 Makefile
Make工程管理器是完全根據Makefile文件中的編譯規則命令進行工作的。Makefile 文件由以下三項基本內容組成。
1)需要生成的目標文件(target file)。
2)生成目標文件所需要的依賴文件(dependency file)。
3)生成目標文件的編譯規則命令行(command)。
這三項內容按照如下格式進行組織:
target file :dependency file
command
其中,Makefile規定在書寫command命令行前必須加一個<Tab>鍵。
Make工程管理器在編譯程序時會檢查每個依賴文件的時間戳,一旦發現某個依賴文件的時間戳比目標文件要新,就會執行目標文件的規則命令來重新生成目標文件。這個過程稱為目標文件的依賴規則檢查。依賴規則檢查是Make工程管理器的最核心的工作任務之一。下面以編譯程序test(由a.c、b.c和b.h組成)為例來描述Make的工作過程。例如:
l a.c:
#include “b.h”
int main()
{
hello();
return 0;
}
l b.h:
voidhello();
l b.c:
#include “stdio.h”
void hello()
{
printf(“hello”);
}
l Makefile:
test : a.o b.o
cc -o test a.o b.o
a.o : a.c b.h
cc -c a.c
b.o : b.c
cc -c b.c
Make工程管理器編譯test程序的過程如下:
(1)Make工程管理器首先會在當前目錄下讀取Makefile文件。
(2)查找Makefile文件中的第一個目標文件(在本例中為test),該文件也是Make工程管理器本次編譯任務的最終目標。
(3)把目標文件test的依賴文件當作目標文件進行依賴規則檢查。這是一個遞歸的檢查過程。在本例中就是依次把a.o和b.o作為目標文件來檢查各自的依賴規則。Make會根據以下三種情況進行處理:
1) 如果當前目錄下沒有或缺少依賴文件,則執行其規則命令生成依賴文件(比如缺少a.
文件,則執行命令“cc -c a.c”生成a.o)。
2) 如果存在依賴文件,則把其作為目標文件來檢查依賴規則(假設a.c比a.o新,則執
行命令“cc -c a.c”更新a.o)。
3) 如果目標文件比所有依賴文件新,則不做處理。
(4)遞歸執行第三步后,就會得到目標文件test所有最新的依賴文件了。接着Make會根據以下三種情況進行處理:
1) 如果目標文件test不存在(比如第一次編譯),則執行規則命令生成test。
2) 如果目標文件test存在,但存在比test要新的依賴文件,則執行規則命令更新test。
3) 目標文件test存在,且比所有依賴文件新,則不做處理。
下面通過make的運行結果來印證上述流程。
(1)第一次編譯時,由於沒有test、a.o和b.o,Make會先執行命令“cc -c a.c”生成a.o;然后接着執行命令“cc -c b.c”生成b.o;最后執行命令“cc -o test a.o b.o”生成test文件。如下所示:
[root@localhost home]#make
cc –c a.c
cc –c b.c
cc –o test a.o b.o
(2)如果修改了a.c文件,Make會先執行命令“cc-c a.c”生成a.o;由於b.o沒有修改,所以Make就接着執行命令“cc -o test a.o b.o”生成test文件。如下所示:
[root@localhost home]#make
cc –c a.c
cc –o test a.o b.o
(3)如果刪除了b.o文件,由於a.o沒有修改,所以Make就先執行命令“cc -c b.c”生成b.o;然后接着執行命令“cc -o test a.o b.o”生成test文件。如下所示:
[root@localhost home]#make
cc –c b.c
cc –o test a.o b.o
(4)如果再運行一次make時,因為所有的源文件都沒有改動,所以Make不會有任何動作。如下所示:
[root@localhost home]#make
make: “test”是最新的。
4.2.2 Makefile 特性介紹
源文件數量越是多的程序,其編譯規則就會越復雜,導致Makefile文件也越復雜。為了簡化Makefile的編寫,豐富編譯程序的方法和手段。Makefile提供了很多類似高級編程語言的語法機制。
1. 變量
在Makefile文件中,存在着大量的文件名,而且這些文件名都是重復出現的。所以在源文件比較多的情況下,很容易發生遺漏或寫錯文件名。而且一旦源文件的名稱發生了變化,還容易造成與其他文件名不一致的錯誤。於是,Makefile提供了變量來代替文件名。變量的使用方式為:
$(變量名)
例如:
obj = a.o b.o
test : $(obj)
cc -o test $(obj)
a.o : a.c b.h
cc -c a.c
b.o : b.c
cc -c b.c
該Makefile使用了變量obj來代替“a.o b.o”。當源文件名發生改動或增刪源文件時,只要對變量obj的值進行相應的修改就可以了,這樣可以避免文件名不一致或遺漏的錯誤。Makefile中變量的命名可以使用字符、數字和下划線,但要注意變量名對大小寫是敏感的。
此外,Make工程管理器提供了靈活的變量定義方式,具體有以下幾種實現方式。
(1)通過“=”來實現
例如:
a1= $(a2)
a2= $(a3)
a3= a.o
這種方式下變量a1的值是a.o,也就是說前面的變量可以通過后面的變量來定義。但使用這種方式定義變量時,要防止出現死循環的情況。
(2)通過“:=”來實現
例如:
a1:= a.o
a2:= $(a1) b.o
這種方式下變量a1的值是a.o,變量a2的值是a.o b.o。
例如:
a1:= $(a2) b.o
a2:= a.o
這種方式下變量a1的值是b.o,而不是“a.o b.o”。也就是說前面的變量不能通過后面的變量來定義。
(3)通過“+=”來實現
例如:
a1= a.o
a1+= b.o
這種方式下變量a1的值是“a.ob.o”。也就是說“+=”可以實現給變量追加值。等同於如下示例:
a1= a.o
a1:= $(a1) b.o
可以看到,Makefile的“+=”和C語言中的“+=”是非常相似的。
(4)通過“?=”來實現
例如:
a1:= a.o
a1?=b.o
這種方式下變量a1的值是a.o,而不是b.o。也就是說,如果變量a1已經在前面定義過了,那么后面的定義就無效了。
以上所介紹的變量都是全局變量,也就是在整個Makefile文件中都可以訪問的。
2. 自動推導
為了進一步簡化Makefile的書寫,Make工程管理器提供了自動推導的功能。自動推導功能默認每個目標文件都有一個與之對應的依賴文件。比如a.o文件有依賴文件a.c與之對應)。這樣在Makefile中就不需要指定與目標文件對應的依賴文件名了。此外,自動推導功能還能推導出與目標文件對應的基本編譯規則命令。比如a.o文件的規則命令為“gcc –c –o a.c”。
例如:
obj = a.o b.o
test : $(obj)
cc -o test $(obj)
a.o : b.h
結果為:
[root@localhost home]#make
cc –c –o a.o a.c
cc –c –o b.o b.c
cc –o test a.o b.o
可以看到,Makefile分別推導出了目標文件a.o和b.o的規則命令“cc -c-o a.o a.c”與“cc -c -o b.o b.c”。
3. 偽目標
偽目標不是真正的目標文件,所以通過偽目標可以讓Make工程管理器只執行規則命令,
而不用創建實際的目標文件。偽目標的使用方式為:
make 偽目標名
由於偽目標不是真正的目標文件,只是一個符號。為了不和真實的目標文件混淆,最好使用“.PHONY”對偽目標進行標識。
例如:
obj = a.o b.o
.PHONY : all
all : test $(obj)
test : $(obj)
cc –o test $(obj)
.PHONY : clean
clean :
rm –rf test $(obj)
test_dir =/home/t_d
.PHONY : install
install :
mkdir $(test_dir)
cp test $(test_dir)
.PHONY : uninstall
uninstall :
rm -rf $(test_dir)
(1)all
運行命令“make all”后,Make會把all看成是最終的目標。由於偽目標和真實目標一樣都有依賴文件,所以Make會更新all的依賴文件test、a.o和b.o。如下所示:
[root@localhost home]#make all
cc –c –o a.o a.c
cc –c –o b.o b.c
cc –o test a.o b.o
(2)clean
運行命令“make clean”后,Make會執行命令“rm -rf test $(obj)”。這樣test、a.o和b.o文件就全被刪除了。如下所示:
[root@localhost home]#make clean
rm –rf test a.o b.o
(3)install
運行命令“make clean”后,Make會順序執行命令“mkdir $(test_dir)”和“cp test $(test_dir)”,把test文件復制到test_dir變量指定的目錄中去(這里只是模擬安裝過程,並不是真正的實現安裝方法)。如下所示:
[root@localhost home]#make install
mkdir /home/t_d
cp test /home/t_d
(4)uninstall
運行命令“make clean”后,Make會執行命令“rm -rf $(test_dir)”。這樣就可以把變量test_dir指定的目錄以及目錄中的文件全部刪除。如下所示:
[root@localhosthome]#make uninstall
rm -rf /home/t_d
在Makefile文件中,偽目標是非常有用的。比如在遞歸編譯、並行編譯等場合中, 使用偽目標可以方便地控制編譯過程。
4. 文件查找
為了便於管理和組織,程序的源文件都根據功能的不同放置在不同的子目錄中。但是源文件被分散存儲之后,Makefile又如何才能找到這些源文件呢?Makefile提供了以下兩種方法。
(1)VPATH
VPATH是一個特殊變量。Make在當前路徑找不到源文件的情況下就會自動到VPATH中指定的路徑中去尋找。VPATH的使用方法為:
VPATH = 目錄 : 目錄 …
例如:
VPATH=/a : /b
Make會在當前路徑找不到文件時按照順序依次查找/a和/b目錄。
(2)vpath
和VPATH不同的是,vpath並不是變量而是關鍵字。其作用和VPATH類似,但使用方式更加靈活。vpath的使用方法為:
vpath 模式 目錄: 目錄 …
例如:
vpath %.c /a : /b
Make會在當前路徑找不到文件時按照順序依次查找/a和/b目錄中所有的C文件。vpath也可以對不同的路徑采用不同的搜素模式。
例如:
vpath %.c /a
vpath %.h /b
Make會在當前路徑找不到源文件時先查找/a目錄下的C文件,然后再查找/b目錄下的頭文件。
例如:
首先在/home目錄下新建一個目錄b,然后把b.c文件放入目錄b中。
VPATH = /home/b
obj = a.o b.o
.PHONY : all
all : test $(obj)
test : $(obj)
cc –o test $(obj)
結果為:
[root@localhost home]#make
cc –c –o a.o a.c
cc –c –o b.o /home/b/b.c
如果把“VPATH= /home/b”修改成“vpath %.c /home/b”,運行結果是一樣的。
5. 嵌套執行
如果把所有源文件的編譯規則命令都寫在一個Makefile中,會造成Makefile過於臃腫,為編寫和修改帶來了很大的不便。解決這個問題的辦法是把Makefile分解成多個子Makefile,並放置到程序的每個子目錄中,每個子Makefile文件負責所在目錄下源文件的編譯工作。
Make工程管理器會首先讀取程序根目錄下的Makefile文件(總控Makefile),然后再去讀取各個目錄中的子Makefile文件。這個過程就稱為Make的嵌套執行。
嵌套執行的使用方法為:
cd 子目錄 && $(MAKE)
或:
$(MAKE) –c子目錄
例如:
首先在/home/b目錄下新建一個子Makefile,如下所示:
b.o : b.c
cc -c –o b.o b.c
然后修改/home目錄中的總控Makefile,如下所示:
VPATH = /home/b
obj = a.o b.o
.PHONY : all
all : test $(obj)
test : $(obj)
cc –o test a.o b/b.o
b.o :b.c
cd b && make
結果為:
[root@localhost home]#make
cc –c –o a.o a.c
cd b && make
make[1] : Entering directory ‘/home/b’
cc –c –o b.o b.c
make[1] : Leaving directory ‘/home/b’
cc –o test a.o b/b.o
可以看到,Make產生a.o之后會進入/home/b目錄讀取子Makefile,編譯產生b.o之后再退出該目錄。
在使用嵌套編譯時,上層Makefile把編譯任務下發給各個下層Makefile進行處理。就好比公司的總經理管理部門經理,再由部門經理去管理每個員工。
總控Makefile中的變量可以通過“export 變量”的方式傳遞到各級子Makefile中,但不會覆蓋子Makefile中的變量。也可以通過“unexport 變量”的方式不讓變量傳遞到各級子Makefile中。
6. 條件判斷
和C語言的條件編譯類似,Make工程管理器也可以在運行時對條件進行判斷,然后進入
條件分支繼續編譯。條件判斷的書寫格式如下所示:
條件表達式
如果真執行的文本段
endif
或者:
條件表達式
如果真執行的文本段
else
如果假執行的文本段
endif
條件表達式有以下四種格式:
ifeq(參數1,參數2)
作用:比較參數1和參數2的值是否相同,相同為真,相異為假
ifneq(參數1,參數2)
作用:比較參數1和參數2的值是否相同,相異為真,相同為假
ifdef(參數)
作用:參數非空為真,空為假
ifndef(參數)
作用:參數空為真,非空為假
例如:
a1= a.o
a2= b.o
ifeq ($(a1), $(a2))
a1=x.o
else
a2=y.o
變量a1的值是a.o, 變量a2的值是y.o。
7. 函數
對於編程語言來說,函數的作用是非常重要的。為此,Make工程管理器也引入了函數機制,以豐富Make控制編譯過程的方法。和變量一樣,函數也用符號$進行標識,其使用格式為:
$(函數名 參數,參數…)
其中函數名和參數之間用空格隔開,參數與參數之間用逗號隔開。下面簡單介紹一些常用的基本函數。
l subst
格式:$(subset 參數1,參數2,參數3)
功能:把參數3中的參數1替換成參數2
返回值:被替換后的參數3
例如:
result := $(substchina, the world, I love China)
result的值為“I love the world”。
l patsubst
格式:$(patsubset模式參數,參數1,參數2)
功能:把參數2中符合模式參數的單詞(單詞是指參數中被空格隔開的字符串)替換成參數1
返回值:被替換后的參數2
例如:
result :=$(patsubst %.c, %.o, x.c y.c)
result的值為“x.o y.o”。
l wildcard
格式:$(wildcard模式參數)
功能:列出當前目錄下所有符合模式參數的文件名
返回值:當前目錄下所有符合模式參數的文件名
例如:
result := $(wildcard*.c)
result的值為當前目錄下所有的C文件名。
l strip 參數
格式:$(strip 參數)
功能:去掉參數中開頭和結尾的空格
返回值:被去掉空格的參數
例如:
result :=$(strip China )
result的值為“China”。
l findstring
格式:$(findstring 參數1,參數2)
功能:在參數2中查找參數1
返回值:如果找到返回參數1,如果沒找到返回空
例如:
result :=$(findstring me, you and me)
result的值為“me”。
result :=$(findstring she, you and me)
result的值為“”。
l filter
格式:$(filter模式參數,參數1)
功能:從參數1中篩選出符合模式參數的字符串
返回值:符合參數模式的字符串
例如:
a := x.c y.c z.h
result := $(filter%.c, $(a))
result的值為“x.c y.c”。
l addsuffix
格式:$(addsuffix 參數1,參數2)
功能:在參數2中的每個單詞加上后綴參數1
返回值:加上后綴的所有單詞
例如:
result := $(addsuffix.c, x y)
result的值為“x.c y.c”。
l addprefix
格式:$(addprefix 參數1,參數2)
功能:在參數2中的每個單詞加上前綴參數1
返回值:加上前綴的所有單詞
例如:
result :=$(addprefix src/, x.c y.c)
result的值為“src/x.c src/y.c”。
l foreach
格式:$(foreach 變量參數,參數1,表達式)
功能:循環取出參數1中的單詞賦給變量參數,然后運行表達式
返回值:表達式的運行結果
例如:
a:= x y z
result := $(foreachb, $(a), $(b).c)
result的值為“x.c y.c z.c”。
注意,b在這里是一個臨時的變量。
l call
格式:$(call 變量參數,參數…)
功能:循環把參數依次賦給變量參數中的$(1)、$(2)…
返回值:賦值后的變量值
例如:
a:= $(2) $(1)
result := $(call$(a), x y)
result的值為“yx”。
l if
格式:$(if 條件參數,執行參數)
功能:如果條件參數非空,運行執行參數部分
返回值:條件參數非空,返回執行參數部分
例如:
result := $(if China,world)
result的值為“world”。
格式:$(if 條件參數,執行參數1,執行參數2)
功能:如果條件參數非空,運行執行參數1部分;反之運行執行參數2部分
返回值:條件參數非空,返回執行參數1;反之返回執行參數2
例如:
a:=
result := $(if$(a), China,world)
result的值為“world”。
l dir
格式:$(dir 參數)
功能:從參數中取出目錄部分
返回值:目錄部分
例如:
result:=$(dir/home/test/a.c)
result的值為“/home/test/”。
l error
格式:$(error 參數)
功能:停止“Make”運行並顯示參數
返回值:參數
例如:
result:=$(errorerror occure!)
result的值為“error occure!”。
l warning
格式:$(warning 參數)
功能:“Make”運行時顯示參數
返回值:參數
例如:
result:=$( warningwarning occure!)
result的值為“warning occure!”。
4.3 Makefile的自動生成
使用Make工程管理器雖然能大幅提高編譯和調試程序的效率,但Makefile文件的編寫仍然是件非常麻煩的事。那么可不可以自動生成Makefile文件呢?
在Linux平台上廣泛使用autotools工具來實現Makefile文件的自動生成。Autotools是由一系列工具組成的,每個工具完成一個階段的任務,最后生成一個完整的Makefile文件。下面以4.1.1節中的test.c文件為例來介紹Autotools工具自動生成Makefile的過程(為避免干擾,把test.c文件放在/home/auto目錄下)。
(1)autoscan
autoscan會在當前的目錄中搜索源文件並生成autoscan.log和configure.scan兩個文件。其中,autoscan.log是日志文件。configure. scan是一種腳本配置文件,里面定義了一些宏定義(“#”開頭的是注釋)。宏定義的意義如下所示:
l AC_PREREQ :聲明要求的autoconf版本,本文件的版本是“2.59”。
l AC_INIT :聲明要生成程序的基本信息(包的全稱、版本號以及發送出錯信息的郵箱)。
l AC_CONFIG_SRCDIR :檢查指定的源文件和目錄是否存在與有效。
l AC_CONFIG_HEADER :用於生成config.h文件。
l AC_PROG_CC :用於確定C編譯器。
l AC_PROG_MAKE_SET :用於確定Make的設置。
此外,還需要人工添加以下兩個宏定義:
l AM_INIT_AUTOMAKE :聲明要生成程序的基本信息(程序名稱和版本號,本例默認為“1.0”)。
l AC_CONFIG_FILES :用於生成相應的Makefile文件。
如下所示的是修改后的configure.scan源代碼。
# -*- Autoconf -*-
#Process this file with autoconf to produce a configure script.
AC_PREREQ(2.59)
AC_INIT(FULL-PACKAGE-NAME,VERSION, BUG-REPORT-ADDRESS)
AM_INIT_AUTOMAKE(test,1.0)
AC_CONFIG_SRCDIR([test.c])
AC_CONFIG_HEADER([config.h])
#Checks for programs.
AC_PROG_CC
#Checks for libraries.
#Checks for header files.
#Checks for typedefs, structures, and compiler characteristics.
#Checks for library functions.
AC_CONFIG_FILES(Makefile)
AC_OUTPUT
最后把修改完的configure.scan文件重命名為configure.in。
(2)aclocal
運行aclocal會生成aclocal.m4文件和autom4te.cache文件夾。主要用來處理configure.in中的宏定義。
(3)autoconf
運行autoconf會生成configure文件。
(4)autoheader
運行autoheader會生成config.h.in文件。
(5)automake
運行automake前,需要先創建Makefile.am文件。在Makefile.am文件中需要定義以下三個宏定義:
l AUTOMAKE_OPTIONS :用於設置automake的選項。automake共提供了三種軟件等級,分
別是foreign、gnu和gnits,默認等級為gnu。本例使用foreign等級,該級別只檢測必須的文件。
l bin_PROGRAMS :聲明要產生的二進制文件名。如果要產生多個二進制文件,每個文件
名用空格隔開。本例中的二進制文件名為test。
l test_SOURCES :聲明產生test文件所需要的源文件。如果有多個源文件,則必須把所
有的源文件都列出來,並用空格隔開。
本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。