Linux下C語言編程

 

1 LinuxC語言編程簡介

本章將簡要介紹一下什麼是Linux,C語言的特點,程序開發的預備知識,Linux下C語言開發的環境,程序設計的特點和原則以及編碼風格等。通過本章的學習,可以對在Linux下使用C語言編程有一個基本的瞭解。

1.1 Linux

Linux是能夠自由傳播並繼承了UNIX內核的操作系統,是對UNIX的簡化和改進,它既保留了UNIX系統的高安全性,同時也使其操作更加簡單方便,從而使單機用戶也可以使用。UNIX內核指的是操作系統底層的核心程序代碼。

因爲 Linux本身脫胎於UNIX系統,所以Linux程序與UNIX程序是十分相似的。事實上,UNIX下編寫的各種程序基本上都可以在 Linux下編譯和運行。此外,許多在UNIX操作系統下創建的一些商業化應用軟件,其二進制形式幾乎可以在不作任何修改的情況下直接運行在 Linux系統上。

Linux是由芬蘭的赫爾辛基大學 (Helsinki)學生Linus Torvalds把Minix 系統向x86移植的結果。當時 Linus 手邊有個 Minix 系統(UNIX 的一個分支),他對這個操作系統相當有興趣,由於當時他正好有一臺個人計算機,他想把這個系統移植到該計算機(x86 架構)上來使用。由於受益於Stallman提倡的開放源代碼(Open Source)思想,他得以接觸到UNIX操作系統的一些源代碼,並仔細研讀了UNIX 的核心,然後去除較爲繁複的核心程序,將它改寫成能夠適用於一般個人計算機的一種操作系統,即Linux系統的雛形。

1992年1月,大概只有100人開始使用Linux,但他們爲Linux的發展壯大作出了巨大貢獻。他們對一些不合理的代碼進行了改進,修補了代碼錯誤並上傳補丁。Linux的騰飛最關鍵的因素是獲得了自由軟件基金(FSF)的支持,他們制定了一個GNU計劃,該計劃的目標就是要編寫一個完全免費的 UNIX版本——包括內核及所有相關的組件,可以讓用戶自由共享並且改寫軟件,而Linux正好符合他們的意願。他們將Linux與其現有的GNU應用軟件很好地結合起來,使Linux擁有了圖形用戶界面。

提示:

Linux 實際上只是提供了操作系統的內核;它實現了多任務和多用戶功能,管理硬件,分配內存,激活應用程序的運行。對初學者來說,最重要的是要明白奇數的內核版本(比如 2.3、2.5、2.7)是實驗用的、正在開發的內核。 穩定的、正式發行的內核版本號則是偶數的(比如 2.2、2.4、2.6)。

1994年3月, Linux 1.0正式版發佈,它的出現無異於網絡的“自由宣言”。從此Linux用戶迅速增加,Linux的核心開發小組也日漸強大。在Linux所包含的數千個文件中,有一個名爲Credits的文件,裏面列出了100多名對Linux有過重要貢獻的黑客,包括他們的名字、地址以及所做的工作。其中的軟件都是經過“優勝劣汰”的達爾文式的選擇方式保存下來的。Linux的發展方法看起來很簡單:所有黑客都可爲其添加額外功能並完善其性能。所謂的β測試也不僅是修補漏洞,而是進行集成並進行更多的改進、創新。Linux發展過程中的這種隨意性,造成了發展過程中出現了各種各樣的Linux版本。

提示:

β測試是由軟件的多個用戶在一個或多個用戶的實際使用環境下進行的測試。這些用戶是與公司簽定了支持產品預發行合同的外部客戶,他們要求使用該產品,並願意返回有關錯誤信息給開發者。開發者通常不在測試現場,因而,β測試是在開發者無法控制的環境下進行的軟件現場應用。在β測試中,由用戶記下遇到的所有問題,包括真實的以及主觀認定的,定期向開發者報告,開發者在綜合用戶的報告之後,作出修改,最終將軟件產品交付給全體用戶使用。由於它處在整個測試的最後階段,因此不能指望這時發現主要問題。同時,產品的所有手冊文本也應該在此階段完全定稿。

Linux操作系統在短短的幾年之內得到了非常迅猛的發展,這與Linux具有的良好特性是分不開的。Linux幾乎包含了UNIX的全部功能和特性,同時又有自己的一些特點。概括地講,Linux具有以下主要特性:

●       開放性

開放性是指系統遵循世界標準規範,特別是遵循開放系統互聯(OSI)國際標準。凡遵循國際標準所開發的硬件和軟件,都能彼此兼容,可方便地實現互聯。

●       多用戶

多用戶是指系統資源可以被不同用戶各自擁有和使用,即每個用戶對自己的資源(例如:文件、設備)有特定的權限,互不影響。Linux繼承了UNIX的多用戶特性。

●       多任務

多任務是現代計算機的最主要的一個特點。它是指計算機同時執行多個程序,而且各個程序的運行互相獨立。Linux系統調度每一個進程,平等地訪問微處理器。由於CPU的處理速度非常快,其結果是,啓動的應用程序看起來好像在並行運行。事實上,從處理器執行一個應用程序中的一組指令到Linux調度微處理器再次運行這個程序之間只有很短的時間延遲,用戶是感覺不出來的。

●       良好的用戶界面

Linux向用戶提供了3種界面:傳統操作界面、系統調用界面和圖形用戶界面。Linux的傳統操作界面是基於文本的命令行界面,即Shell,它既可以聯機使用,又可在文件上脫機使用。Shell有很強的程序設計能力,用戶可方便地用它編制程序,從而爲用戶擴充系統功能提供了更高級的手段。可編程Shell是指將多條命令組合在一起,形成一個Shell程序,這個程序可以單獨運行,也可以與其他程序同時運行。

系統調用界面是爲用戶提供編程時使用的界面。用戶可以在編程時直接使用系統提供的系統調用命令。系統通過這個界面爲用戶程序提供低級、高效率的服務。

Linux還爲用戶提供了圖形用戶界面。它利用鼠標、菜單、窗口、滾動條等設施,給用戶呈現一個直觀、易操作、交互性強的友好的圖形化界面。

●       設備獨立性

Linux是具有設備獨立性的操作系統,它的內核具有高度的適應能力。隨着越來越多的程序員開發Linux系統,將會有更多的硬件設備加入到各種Linux內核和發行版本中。另外,由於用戶可以免費得到Linux的內核源代碼,因此,用戶可以根據需要修改內核源代碼,以便適應新增加的外部設備。

設備獨立性是指操作系統把所有外部設備統一當作文件來看待,只要安裝它們的驅動程序,任何用戶都可以像使用文件一樣,操縱、使用這些設備,而不必知道它們的具體存在形式。

具有設備獨立性的操作系統,通過把每一個外圍設備看作一個獨立文件來簡化增加新設備的工作。當需要增加新設備時,系統管理員就在內核中增加必要的連接。這種連接(也稱作設備驅動程序)能保證每次調用設備提供的服務時,內核能以相同的方式來處理它們。當新的或更好的外設被開發並交付給用戶時,系統允許在這些設備連接到內核後,能不受限制地立即訪問它們。設備獨立性的關鍵在於內核的適應能力。其他操作系統只允許一定數量或一定種類的外部設備連接。而設備獨立性的操作系統卻能夠容納任意種類及任意數量的設備,因爲每一個設備都是通過其與內核的專用連接進行獨立訪問的。

●       提供了豐富的網絡功能

完善的內置網絡是Linux的一大特點。Linux在通信和網絡功能方面優於其他操作系統。其他操作系統不包含如此緊密地和內核結合在一起的連接網絡的能力,也沒有內置這些聯網特性的靈活性。而Linux爲用戶提供了完善的、強大的網絡功能。

支持Internet是其網絡功能之一。Linux免費提供了大量支持Internet的軟件,通過Internet,用戶能用Linux與世界上各個地區的人方便地通信。它內建了http、ftp、dns等功能,支持所有常見的網絡服務,包括ftp、telnet、NFS、TCP、IP等,加上超強的穩定性,因此很多ISP(Internet Service Providers)都是採用Linux來架設郵件服務器、FTP服務器及Web 服務器等各種服務器的。Linux在最新發展的內核中還包含了一些通用的網絡協議,比如IPv4、IPv6、AX.25、X.25、IPX、DDP(Appletalk)、NetBEUI、Netrom 等。用戶能通過一些Linux命令完成內部信息或文件的傳輸。 Linux不僅允許進行文件和程序的傳輸,它還爲系統管理員和技術人員提供了訪問其他系統的接口。

另外,還可以進行遠程訪問。通過這種遠程訪問的功能,一位技術人員能夠有效地爲多個系統服務,即使那些系統位於相距很遠的地方。穩定的核心中目前包含的網絡協議有TCP、IPv4、IPX、DDP、AX等。另外還提供Netware的客戶機和服務器,以及現在最熱門的Samba(讓用戶共享Mircosoft Network資源)。

●       可靠的系統安全

Linux採取了許多安全技術措施,包括對讀/寫進行權限控制、帶保護的子系統、審計跟蹤、核心授權等,這爲網絡多用戶環境中的用戶提供了必要的安全保障。

●       良好的可移植性

可移植性是指將操作系統從一個平臺轉移到另一個平臺上,並使它仍然能按其自身的方式運行的能力。

Linux是一種可移植的操作系統,能夠在從微型計算機到大型計算機的任何環境中運行。可移植性爲運行Linux的不同計算機平臺與其他任何計算機進行準確而有效的通信提供了手段,不需要另外增加特殊的和昂貴的通信接口。

1.2 C語言的簡介和特點

C語言是貝爾實驗室的Dennis Ritchie在B語言的基礎上開發出來的,1972年在一臺DEC PDP-11計算機上實現了最初的C語言。C語言是與硬件無關的,用C語言編寫的程序能移植到大多數計算機上。C語言在各種計算機上的快速推廣導致了許多C語言版本。這些版本雖然是類似的,但通常是不兼容的。爲了明確定義與機器無關的C語言,1989年美國國家標準協會制定了C語言的標準(ANSI C)。在ANSI標準化後,C語言的標準在相當長的一段時間內都基本保持不變,儘管C++進行了改進(實際上,Normative Amendment1在1995年已經開發了一個新的C語言版本,但是這個版本很少爲人所知)。ANSI標準在20世紀90年代又經歷了一次比較大的改進,這就是ISO9899:1999(1999年出版)。這個版本就是通常提及的C99。它被ANSI於2000年2月採用。

C 語言之所以發展迅速,而且成爲最受歡迎的語言之一,主要是因爲它具有強大的功能。許多著名的系統軟件,如UNIX/Linux、Windows、DBASE Ⅲ PLUS、DBASE Ⅳ 都是由C 語言編寫的。用C 語言加上一些彙編語言子程序,就更能顯示C 語言的優勢,像PC- DOS 、WORDSTAR等就是用這種方法編寫的。

歸納起來,C 語言具有下列特點:

●       中級語言。它把高級語言的基本結構和語句與低級語言的實用性結合起來。C 語言可以像彙編語言一樣對位、字節和地址進行操作,而這三者是計算機最基本的工作單元。

●       結構式語言 。結構式語言的顯著特點是代碼及數據的模塊化,即程序的各個部分除了必要的信息交流外彼此獨立。這種結構化方式可使程序層次清晰,便於使用、維護以及調試。C 語言是以函數形式提供給用戶的, 這些函數可方便地調用, 並採用多種循環、條件語句控制程序流向,從而使程序完全結構化。

●       功能齊全。C 語言具有各種各樣的數據類型,並引入了指針概念,可使程序效率更高。另外,C 語言也具有強大的圖形功能,支持多種顯示器和驅動器。而且計算功能、邏輯判斷功能也比較強大,可以實現決策目的。   

●       可與Linux無縫結合。Linux本身是使用C語言開發的,在Linux上用C語言作開發,效率很高。

1.3 Linux程序設計基礎知識

對一個Linux開發人員來說,在使用一種編程語言編寫程序以前,對操作系統中程序的保存位置有一個透徹的瞭解是很重要的。比如,應知道軟件工具和開發資源保存在什麼位置是很重要的。下面首先簡單介紹Linux的幾個重要的子目錄和文件。

這部分內容雖然是針對 Linux的,但同樣也適用於其他類UNIX系統。

1.3.1 程序安裝目錄

Linux下的程序通常都保存在專門的目錄裏。系統軟件可以在/usr/bin子目錄裏找到。系統管理員爲某個特定的主機系統或本地網絡添加的程序可以在/usr/local/bin子目錄裏找到。

系統管理員一般都喜歡使用/usr/local子目錄,因爲它可以把供應商提供的文件和後來添加的程序以及系統本身提供的程序隔離開來。/usr子目錄的這種佈局方法在需要對操作系統進行升級的時候非常有用,因爲只有/usr/local子目錄裏的東西需要保留。我們建議讀者編譯自己的程序時,按照/usr/local子目錄的樹狀結構來安裝和訪問相應的文件。

某些隨後安裝的軟件都有它們自己的子目錄結構,其執行程序也保存在特定的子目錄裏,最明顯的例子就是 X窗口系統,它通常安裝在一個名爲/usr/X11R6的子目錄裏,XFree論壇組織發行的用於英特爾處理器芯片的各種XFree 86窗口系統變體也安裝在這裏。

GNU的C語言編譯器gcc(後面的程序設計示例中使用的就是它)通常安裝在/usr/bin或者/usr/local/bin子目錄裏,但通過它運行的各種編譯器支持程序一般都保存在另一個位置。這個位置是在用戶使用自己的編譯器時指定的,隨主機類型的不同而不同。對 Linux系統來說,這個位置通常是/usr/lib/gcc-lib/目錄下以其版本號確定的某個下級子目錄。GN的C/C++編譯器的各種編譯程序以及GNU專用的頭文件都保存在這裏。

1.3.2 頭文件

在使用C語言和其他語言進行程序設計的時候,我們需要頭文件來提供對常數的定義和對系統及庫函數調用的聲明。對C語言來說,這些頭文件幾乎永遠保存在/usr/include及其下級子目錄裏。那些賴於所運行的 UNIX或Linux操作系統特定版本的頭文件一般可以在/usr/include/sys或/usr/include/linux子目錄裏找到。其他的程序設計軟件也可以有一些預先定義好的聲明文件,它們的保存位置可以被相應的編譯器自動查找到。比如,X窗口系統的/usr/include/X1R6子目錄和GNU C++編譯器的/usr/include/g++ -2子目錄等。

在調用C語言編譯器的時候,可以通過給出“ -I”編譯命令標誌來引用保存在下級子目錄或者非標準位置的頭文件,類似命令如下:

[david@localhost linux]$ gcc -I /usr/openwin/include hello.c

該命令會使編譯器在/usr/openwin/include子目錄和標準安裝目錄兩個位置查找fred.c程序裏包含的頭文件。具體情況可以參考第3章。

用grep命令來查找含有某些特定定義與函數聲明的頭文件是很方便的。假設想知道用來返回程序退出狀態的文件的名字,可以使用如下方法:

先進入/usr/include子目錄,然後在grep命令裏給出該名字的幾個字母,如下所示:

[david@localhost linux]$ grep KEYSPAN *.h

pci_ids.h:#define PCI_SUBVENDOR_ID_KEYSPAN      0x11a9

pci_ids.h:#define PCI_SUBDEVICE_ID_KEYSPAN_SX2 0x5334

grep命令會在該子目錄裏所有名字以.h結尾的文件裏查找字符串“KEYSPAN”。在上面的例子裏,(從其他文件中間)可以查找到文件pci_ids.h。

1.3.3 庫文件

庫文件是一些預先編譯好的函數的集合,那些函數都是按照可再使用的原則編寫的。它們通常由一組互相關聯的用來完成某項常見工作的函數構成。比如用來處理屏幕顯示情況的函數(curses庫)等。我們將在後續章節講述這些函數庫文件。

標準的系統庫文件一般保存在/lib或者/usr/lib子目錄裏。編譯時要告訴 C語言編譯器(更確切地說是鏈接程序)應去查找哪些庫文件。默認情況下,它只會查找 C語言的標準庫文件。這是從計算機速度還很慢、CPU價格還很昂貴的年代遺留下來的問題。在當時,把一個庫文件放到標準化子目錄裏然後寄希望於編譯器自己找到它是不實際的。庫文件必須遵守一定的命名規則,還必須在命令行上明確地給出來。

庫文件的名字永遠以lib這幾個字母打頭,隨後是說明函數庫情況的部分(比如用c表示這是一個 C語言庫;而m表示這是一個數學運算庫等)。文件名的最後部分以一個句點(.)開始,然後給出這個庫文件的類型,如下所示:

●       .a 傳統的靜態型函數庫。

●       .so和. sa 共享型函數庫(見下面的解釋)。

函數庫一般分爲靜態和共享兩種格式,用ls /usr/lib命令查一下就能看到。在通知編譯器查找某個庫文件的時候,既可以給出其完整的路徑名,也可以使用–l標誌。詳細內容可以參考第3章。

1. 靜態庫

函數庫最簡單的形式就是一組處於可以“拿來就用”狀態下的二進制目標代碼文件。當有程序需要用到函數庫中的某個函數時,就會通過 include語句引用對此函數做出聲明的頭文件。編譯器和鏈接程序負責把程序代碼和庫函數結合在一起成爲一個獨立的可執行程序。如果使用的不是標準的C語言運行庫而是某個擴展庫,就必須用–l選項指定它。 

靜態庫也叫做檔案(archive),它們的文件名按慣例都以. a結尾。比如 C語言標準庫爲/usr/lib/libc.a、X11庫爲/usr/X11R6/lib/libX11.a等。

自己建立和維護靜態庫的工作並不困難,用ar(“建立檔案”的意思)程序就可以做到,另外要注意的是,應該用gcc -c命令對函數分別進行編譯。應該儘量把函數分別保存到不同的源代碼文件裏去。如果函數需要存取普通數據,可以把它們放到同一個源代碼文件裏並使用在其中聲明爲static類型的變量。

2. 共享庫

靜態庫的缺點是,如果我們在同一時間運行多個程序而它們又都使用着來自同一個函數庫裏的函數時,內存裏就會有許多份同一函數的備份,在程序文件本身也有許多份同樣的備份。這會消耗大量寶貴的內存和硬盤空間。

許多UNIX系統支持共享庫,它同時克服了在這兩方面的無謂消耗。對共享庫和它們在不同系統上實現方法的詳細討論超出了本書的範圍,所以我們把注意力集中在眼前 Linux環境下的實現方法上。

共享庫的存放位置和靜態庫是一樣的,但有着不同的文件後綴。在一個典型的 Linux系統上,C語言標準庫的共享版本是 /usr/lib/libc.so N,其中的N是主版本號。

1.4 LinuxC語言編程環境概述

Linux下C語言編程常用的編輯器是vim或emacs,編譯器一般用gcc,編譯鏈接程序用make,跟蹤調試一般使用gdb,項目管理用makefile。下面先通過一個小程序來熟悉這些工具的基本應用。各個工具的詳細使用方法將在後面的各個章節逐步講解。

(1) 要編輯C源程序,應首先打開vim或emacs編輯器,然後錄入以下多段源代碼。使用main函數調用mytool1_print、mytool2_print這兩個函數。

 

 

#include "mytool1.h"

#include "mytool2.h"

 

int main(int argc,char **argv)

{

mytool1_print("hello");

mytool2_print("hello");

}

(2) 在mytool1.h中定義mytool1.c的頭文件。

 

 

/* mytool1.h */

#ifndef_MYTOOL_1_H

#define_MYTOOL_1_H

 

void mytool1_print(char *print_str);

 

#endif

(3) 用mytool1.c實現一個簡單的打印顯示功能。

 

 

/* mytool1.c */

#include "mytool1.h"

void mytool1_print(char *print_str)

{

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

}

(4) 在mytool2.h中定義mytool2.c頭文件。

 

 

/* mytool2.h */

#ifndef _MYTOOL_2_H

#define _MYTOOL_2_H

 

void mytool2_print(char *print_str);

 

#endif

(5) mytool2.c實現的功能與mytool1.c相似。

 

 

/* mytool2.c */

#include "mytool2.h"

void mytool2_print(char *print_str)

{

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

}

(6) 使用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

         mytool1.o:mytool1.c mytool1.h

         gcc -c mytool1.c

         mytool2.o:mytool2.c mytool2.h

         gcc -c mytool2.c

(7) 將源程序文件和makefile文件保存在Linux下的同一個文件夾下,然後運行make編譯鏈接程序如下:

[david@localhost 1c]$ make

[david@localhost 1c]$ ./main

This is mytool1 print hello

This is mytool2 print hello

至此,這個小程序算是完成了,如果想跟蹤調試可以參考第4章。

1.5 Linux程序設計的特點

在進行程序設計時首先應養成良好的程序設計風格。Linux操作系統的設計師們鼓勵人們採用一種獨到的程序設計風格。下面是Linux程序和系統所共有的一些特點。

(1) 簡單性。許多最有用的 Linux軟件工具都是非常簡單的,程序小而易於理解。

(2) 重點性。一個所謂功能齊全的程序可能既不容易使用,也不容易維護。如果程序只用於一個目的,那麼當更好的算法或更好的操作界面被開發出來的時候,它就更容易得到改進。在 Linux世界裏,通常會在需求出現的時候把小的工具程序組合到一起來完成一項更大的任務,而不是用一個巨大的程序預測一個用戶的需求。

(3) 可反覆性。使用的程序組件把應用程序的核心部分組建成一個庫。帶有簡單而又靈活的程序設計接口並且文檔齊備的函數庫能夠幫助其他人開發同類的項目,或者能夠把這裏的技巧用在新的應用領域。例如dbm數據庫函數庫就是一套由不同功能的函數組成的集合,而不是一個單一的數據庫管理系統。

(4) 過濾性。許多Linux應用程序可以用作過濾器,即它們可以把自己的輸入轉換爲另外一種形式的輸出。在後面將會講到,Linux提供的工具程序能夠將其他Linux程序組合成相當複雜的應用軟件,其組合方法既新穎又奇特。當然,這類程序組合正是由Linux獨特的開發方法支撐着的。

(5) 開放性。文件格式比較成功和流行的 Linux程序所使用的配置文件和數據文件都是普通的 ASCII文本。如果在程序開發中遵循該原則,將是一種很好的做法。它使用戶能夠利用標準的軟件工具對配置數據進行改動和搜索,從而開發出新的工具,並通過新的函數對數據文件進行處理。源代碼交叉引用檢查軟件 ctags就是一個這樣的好例子,它把程序中的符號位置信息以規則表達式的形式記錄下來供檢索程序使用。

(6) 靈活性。因爲你根本無法預測一個不太聰明的用戶會怎樣使用你的程序,因此在進行程序設計時,要儘可能地增加靈活性,儘量避免給數據域長度或者記錄條數加上限制。同時如果可能,應儘量編寫能夠響應網絡訪問的程序,使它既能夠跨網絡運行又能夠在本地單機上運行。

1.6 LinuxC語言編碼的風格

Linux作爲GN家族的一員,其源代碼數以萬計,而在閱讀這些源代碼時我們會發現,不同的源代碼的美觀程度和編程風格都不盡相同,例如下面的glibc代碼:

static voidrelease_libc_mem (void)

{

/*Only call the free function if we still are running in mtrace mode. */

if (mallstream != NULL)

__libc_freeres ();

}

或者Linux的核心代碼:

static int do_linuxrc(void * shell)

static char *argv[] = { "linuxrc",NULL,};

close(0);close(1);close(2);

setsid();

(void) open("/dev/console",O_RDWR,0);

(void) dup(0);

(void) dup(0);

return execve(shell,argv,envp_init);

}

比較一下,上面的這些代碼是否看起來讓人賞心悅目?而有些程序員編寫的程序由於沒有很好的縮進及順序,讓人看起來直皺眉頭。編寫乾淨美觀的代碼,不僅僅使代碼更容易閱讀,還能使代碼成爲一件藝術品。與微軟的匈牙利命名法一樣,Linux上的編程主要有兩種編程風格:GNU風格和Linux核心風格,下面將分別介紹。

1.6.1 GNU編程風格

下面是基於GNU的編程風格,編寫代碼時應遵循這些基本要求。

●       函數開頭的左花括號放到最左邊,避免把任何其他的左花括號、左括號或者左方括號放到最左邊。

à       盡力避免讓兩個不同優先級的操作符出現在相同的對齊方式中。

à       每個程序都應該有一段簡短地說明其功能的註釋開頭。例如:fmt - filter for simplefilling of text。

●       請爲每個函數書寫註釋,以說明函數做了些什麼,需要哪些種類的參數,參數可能值的含義以及用途。

à       不要在聲明多個變量時跨行。在每一行中都以一個新的聲明開頭。

à       當在一個if語句中嵌套了另一個if-else語句時,應用花括號把if-else括起來。

●       要在同一個聲明中同時說明結構標識和變量,或者結構標識和類型定義(typedef)。

à       盡力避免在if的條件中進行賦值。

à       請在名字中使用下劃線以分隔單詞,儘量使用小寫; 把大寫字母留給宏和枚舉常量,以及根據統一的慣例使用的前綴。

à       命令一個命令行選項時,給出的變量應該在選項含義的說明之後,而不是選項字符之後。

1.6.2 Linux 內核編程風格

下面是 Linux 內核所要求的編程風格:

●       注意縮進格式。

●       將開始的大括號放在一行的最後,而將結束大括號放在一行的第一位。

●       命名系統。變量命名儘量使用簡短的名字。

●       函數最好要短小精悍,一個函數最好只作一件事情。

●       註釋。註釋說明代碼的功能,而不是說明其實現原理。

看了上面兩種風格的介紹,讀者是不是覺得有些太多了,難以記住?不要緊,Linux有很多工具來幫助我們。除了vim和emacs以外,還有一個非常有意思的小工具 indent可以幫我們美化C/C++源代碼。

下面用這條命令將Linux 內核編程風格的程序quan.c轉變爲 GNU編程風格,代碼如下:

[david@localhost ~]$ indent -gnu quan.c

利用indent這個工具,大家就可以方便地寫出漂亮的代碼來。

 

 

 

 

 

 

第2章 vi與emacs編輯器

從本章開始,我們將進入Linux充滿挑戰的C語言編程世界,首先介紹的是文本編輯器。

文本編輯器可以說是計算機最基本的應用,修改設置文件、編寫程序或者建立文件都需要用到它。Linux提供了齊全的文本編輯器,可以讓用戶按照自己的喜好進行選擇。本章主要介紹vim、emacs等編輯器,對Linux其他的編輯器也稍作介紹。通過本章的學習,可以對Linux下的編輯器有一個深入的瞭解,爲今後編程打下良好基礎。

2.1 vim概述及應用

vim(vi improve)可以說是Linux中功能最爲強大的編輯器,它是由UNIX系統下的傳統文本編輯器vi發展而來的。下面首先介紹一下vi。

vi是個可視化的編輯器(vi就意味着可視化——visual)。 那麼,什麼是可視化的編輯器呢?可視化的編輯器就是可以在編輯文本的時候看到它們。非可視化的編輯器的例子可以舉出不少,如ed、sed和edlin(它是DOS自帶的最後一個編輯器) 等。vi成爲BSD UNIX的一部分,後來AT&T也開始用vi,於是標準UNIX也開始 用vi。Linux下的vim是vi的一個增強版本,有彩色和高亮等特性,對編程有很大的幫助。

1. 啓動與退出vim

由於vim的功能很多,首先來看如何啓動和退出vim。

(1) 在Linux提示符下鍵入vim(或使用vim myfile來編輯已經存在的文件)即可啓動它。

(2) 要退出vim,先按下Esc鍵回到命令行模式,然後鍵入“:”,此時光標會停留在最下面一行,再鍵入“q”,最後按下Enter鍵即可,見圖2-1。

技巧:

在X-Window下也可以通過在“開始”菜單裏找到“編程”︱Vi I Mproved來運行X-Window下的vim。此時其界面如圖2-2所示。

圖2-1 退出vim

圖2-2 X-Window下的vim界面

2. 命令行模式的操作

命令行模式提供了相當多的按鍵及組合按鍵來執行命令,幫助用戶編輯文件。由於這些命令相當多,在此僅作簡單介紹。

(1) 移動光標

在命令行模式和插入模式下,都可以使用上、下、左、右4個方向鍵來移動光標的位置。但是有些情況下,如使用telnet遠程登陸時,方向鍵就不能用,必須用命令行模式下的光標移動命令。這些命令及作用見表2-1。

 

表2-1 常用的移動光標的命令

命    令

操 作 說 明

h

將光標向左移動一格

l

將光標向右移動一格

j

將光標向上移動一格

k

將光標向下移動一格

0

將光標移動到該行的最前面

$

將光移動到該行的最後面

G

將光標移動到最後一行的開頭

W或w

將光標移動到下一個字

e

將光標移動到本單詞的最後一個字符。如果光標所在的位置爲本單詞的最後一個字符,則跳動到下一個單字的最後一個字符。標點符號如“.”、“,”或“/”等字符都會被當成一個字

b

將光標移動到單詞的最後一個字符,如果光標所在位置爲本單詞的第一個字符,則跳到上一個單詞的第一個字符

{

將光標移動到前面的“{”處。在C語言編程時,如果按兩次就會找到函數開頭“{”處,如果再次連續按兩次還可以找到上一個函數的開頭處

}

同“{”的使用,將光標移動到後面的“}”

Ctrl+b

如果想要翻看文章的前後,可以使用Page Down和Page Up;但當這兩個鍵不能使用時,可以使用Ctrl+b將光標向前卷一頁,相當於Page Up

Ctrl+f

將光標向後卷一頁,相當於Page Down

Ctrl+u

將光標向前移半頁

Ctrl+d

將光標向後移半頁

Ctrl+e

將光標向下卷一行

Ctrl+y

將光標向後卷一行

N+/

將光標移至第n行(n爲數字)

(2) 複製文本

複製文本可以節省重複輸入的時間,vim也提供了以下的操作命令,見表2-2。

表2-2 常用的複製文本的命令

命    令

操 作 說 明

y+y

將光標目前所在的位置整行復制

y+w

複製光標所在的位置到整個單詞所在的位置

n+y+w

若輸入3yw,則會將光標所在位置到單詞結束以及後面兩個單詞(共3個單詞)一起復制

n+y+y

若按3yy,則將連同光標所在位置的一行與下面兩行一起復制

p

將複製的內容粘貼光標所在的位置。若複製的是整行文本,則會將整行內容粘貼到光標所在的位置

(3) 刪除文本

刪除文本命令一次可刪除一個字符,也可以一次刪除好幾個字符或是整行文本,見表2-3。

表2-3 常用的刪除文本的命令

命    令

操 作 說 明

d+左方向鍵

連續按d和左方向鍵,將光標所在位置前一個字符刪除

d+右方向鍵

將光標所在位置字符刪除

d+上方向鍵

將光標所在位置行與其上一行同時刪除

d+下方向鍵

將光標所在位置行與下一行同時刪除

d+d

連按兩次d,可將光標所在的行刪除,若是連續刪除,可以按住d不放

d+w

刪除光標所在位置的單詞,若是光標在兩個字之間,則刪除光標後面的一個字符

n+d+d

刪除包括光標所在行及向下的n行(n爲數字)

n+d+上方向鍵

刪除包括光標所在行及向上的n行

n+d+下方向鍵

同n+d+d命令

D

將光標所在行後所有的單詞刪除

x

將光標所在位置的字符刪除

X

將光標所在位置前一個字符刪除

n+x

刪除光標所在位置及其後的n個字符

n+X

刪除光標所在位置及其前的n個字符

 

(4) 找出行數及其他按鍵

當我們編寫程序時,常常需要跳到某一行去修改,因此每一行的行號就相當重要。vim爲此提供的命令見表2-4。

表2-4 常用的找出行數的命令

命    令

操 作 說 明

Ctrl+g

在最後一行中顯示光標所在位置的行數及文章的總行數

nG

將光標移至n行(n爲數字)

r  

修改光標所在字符

R

修改光標所在位置的字符,可以一直替換字符,直到按下ESC鍵

u

表示復原功能

U

取消對行所做的所有改變

.

重複執行上一命令

Z+Z

連續兩次輸入z,表示保存文件並退出vi

%

符號匹配功能,在編輯時,如果輸入“%(”,系統將會自動匹配相應的“)”

3. 命令行模式切換到輸入模式

進入vim時,默認的模式是命令行模式,而要進入輸入模式輸入數據時,可以用下列按鍵:

●       按“a”鍵 從目前光標所在位置的下一個字符開始輸入。

●       按“i”鍵 從光標所在位置開始插入新輸入的字符。

●       按“o”鍵 新增加一行,並將光標移到下一行的開頭。

4. 最後行模式的操作

vim的最後行模式是指可以在界面最底部的一行顯示的輸入命令,一般用來執行查找特定的字符串、保存及退出等任務。在命令行模式下輸入冒號“:”,就可以進入最後行模式了,還可以使用“?”和“/”鍵進入最後行模式。比起命令行模式的諸多操作命令,最後行模式的操作命令就少多了,見表2-5。

表2-5 最後行模式主要的操作命令

命    令

操 作 說 明

e

在vi中編輯時,可以使用e創建新的文件

n

加載新文件

w

寫文件,也就是將編輯的內容保存到文件系統中。vim在編輯文件時,先將編輯內容保存在臨時文件中,如果沒有執行寫操作直接退出的話,修改內容並沒有保存到文件中

w!

如果想寫只讀文件,可以使用w!強制寫入文件

q!

表示退出vim,但是文件內容有修改的話,系統會提示要先保存,如果不保存退出,需要使用命令q!強制退出

set nu

set可以設置vim 的某些特性,這裏是設置每行開頭提示行數。想取消設置,使用命令set none

/

查找匹配字符串功能。在編輯時,想查找包含某一個字符串,可以用“/字符串”自動查找,系統會突出顯示所有找到的字符串,並轉到找到的第一個字符串。如果想繼續向下查找,可以按n鍵;向前繼續查找則按N鍵

也可以使用“?字符串”查找特定字符串,它的使用與“/”相似,但它是向前查找字符串

 

5. vim的注意事項

由於Linux系統的vim編輯器是從UNIX下的vi發展而來的,而UNIX下的vi編輯器是從行編輯器ed發展而來的。因此,vim不如目前流行的微軟推出的同類編輯器易用、直觀,但是它的強大功能卻是微軟同類產品無法比擬的。因此一些人學習時可能會感到有一些不便和困惑。針對這類問題,這裏列出了使用vim中應注意的一些事項。當然要熟練使用vim,還需要平時操作中不斷地提高和積累。

●       插入編輯方式和命令方式切換時出現混亂

這種情況產生的原因通常是:還未輸入插入命令便開始進行文本輸入,從而無法在正確位置輸入文本;另外,當插入信息後,還未按Esc鍵結束插入方式,就又輸入其他的命令信息,從而使命令無法執行。

當出現這種情況時,首先要確定自己所處的操作方式,然後再確定下一步做什麼工作。若不易搞清楚當前所處的狀態,還可以使用Esc鍵退回到命令方式重新進行輸入。

●       在進行文檔編輯時,vim編輯器會產生混亂

這種狀態的產生往往是由於屏幕刷新有誤,此時可以使用Ctrl+l鍵對屏幕進行刷新,如果是在終端,可以用Ctrl+r進行屏幕刷新。

●       對屏幕中顯示的信息進行操作時,系統沒有反應。

 出現這種情況可能是由於屏幕的多個進程被掛起(如不慎用了Ctrl+s鍵等),此時可用Ctrl+q進行解脫,然後重新進行輸入。

●       當編輯完成後,不能正確退出vim

出現這種情況的原因可能是系統出現了意外情況。如:文件屬性爲只讀、用戶對編輯的文件沒有寫的權限。如果強行執行退出命令“:w!”仍無法退出,可以用“:w newfile”命令將文件重新存盤後再退出,以減少工作中的損失,這個新文件newfile應是用戶有寫權限的文件。

如果暫時沒有可以使用的文件,可以借用/tmp目錄建一個新的文件。因爲Linux系統中的/tmp是一個臨時目錄,系統啓動時總要刷新該目錄,因此操作系統一般情況下不對此目錄下進行保護。但當處理完成後,切記應將新文件進行轉儲,否則依然會造成信息損失。

●       在使用vim時,萬一發生了系統掉電或者突然當機的情況怎麼辦?

工作時發生了掉電和當機,對正做的工作無疑是一種損失,但是vim程序可使損失降到最小。因爲,對vim的操作實際上是對編輯緩衝區的數據操作,而系統經常會將緩衝區的內容自動進行保存。因此,當機後用戶可以在下次登陸系統後使用-r選項進入vi,將系統中最後保存的內容恢復出來。例如,在編輯cd文件的時候突然斷電或者系統崩潰後的恢復命令爲:

[david@DAVID david]$ vi cd -r

vim的學習應側重於實際的應用,在瞭解vim的使用規則後應該多上機操作,不斷積累經驗,逐步地使自己成爲vi編輯能手。

2.2 emacs簡介及應用

emacs編輯器是一款自由軟件產品,在Linux系統中比較流行。emacs的涵義是宏編輯器(macro editor)。emacs最開始是由richard stallman編寫的,他的初衷是將emacs設計成一個Linux的shell,同時還增加了一些現代操作系統應支持的用戶環境(比如,mail的收發、web的查詢、新聞閱讀、日誌功能等)。另外,在emacs中還包括了list語言的解釋執行功能。

emacs的一個缺點是它佔用的磁盤空間比較大,因此爲了支持用戶的使用,emacs提供多種模式以適用於不同的用戶需求。進行安裝時,可根據選項設置指定的模式,以減少磁盤的使用量。

1. emacs的啓動和退出

emacs中包含的命令很多,對於初學者來說有一些困難,但是一旦適應了它的使用方法,就會感到它的方便和靈活。

在文本模式下要進入emacs,只要鍵入emacs即可:

[david@DAVID david]$ emacs

或者鍵入emacs [filename]來編輯文件:

[david@DAVID david]$ emacs [filename]

啓動emacs後,看到的是emacs的基本情況描述信息。

File Edit Options Buffers Tools Help

Welcome to GNU Emacs, one component of a Linux-based GNU system.

 

Get help                       C-h (Hold down CTRL and press h)

Undo changes         C-x u       Exit Emacs                             C-x C-c

Get a tutorial             C-h t       Use Info to read docs    C-h i

Ordering manuals                   C-h RET

Activate menubar F10 or ESC ' or   M-'

('C-' means use the CTRL key. 'M-' means use the Meta (or Alt) key.

If you have no Meta key, you may instead type ESC followed by the

character.)

 

GNU Emacs 21.2.1 (i386-redhat-Linux-gnu, X toolkit, Xaw3d scroll bars)

 of 2003-02-20 on porky.devel.redhat.com

Copyright (C) 2001 Free Software Foundation, Inc.

 

GNU Emacs comes with ABSOLUTELY NO WARRANTY; type C-h C-w for full details.

Emacs is Free Software--Free as in Freedom--so you can redistribute

copies

of Emacs and modify it; type C-h C-c to see the conditions.

Type C-h C-d for information on getting the latest version.

 

-uuu:---F1 *scratch* (Lisp Interaction)--L1--All--------

[35dFor information about the GNU Project and its goals, type C-h C-p.

 

提示:

要退出 emacs,只要鍵入Ctrl+x或Ctrl+c即可。 即先按住鍵盤上的 Ctrl 鍵不放,再按下英文字母x或c 即可。當然啓動或退出 emacs 的方法還有多種,將在以下各小節中陸續介紹。

技巧:

在X-Window下也可以通過在“開始”菜單裏找到“編程”︱emacs來運行X-Window下的emacs,見圖2-3。

圖2-3 X-Window下的emacs

2. 文本編輯

在emacs中的文本編輯的方式與vim的編輯方式有很大的區別,現在只簡單介紹一些常用操作。

(1) 刪除文本

●       刪除光標左側的字符:按Delete鍵可刪除光標左側的字符。

●       刪除光標所在的字符:按Ctrl+d鍵可刪除光標所在的字符。

●       刪除光標左側單詞:按Alt+Delete鍵可刪除光標左側的單詞。

●       刪除光標右側單詞:按Alt+d鍵可以刪除光標右側的單詞。

●       刪除至行尾:按Ctrl+k鍵可以從光標處開始刪除至尾行。

●       刪除多行:不要移動光標,連續在同一位置按Ctrl+k鍵。

●       刪除一個句子:按Alt+k從光標處開始刪到句子尾。

(2) 行的分割、合併與新增

●       分割一行:在要分割處按下Enter鍵。

●       合併兩行:在行尾處按Ctrl+d或於次行首按Delete。

●       新增空白行:按Ctrl+e將光標移至尾行再按下Enter鍵。

(3) 命令的復原與取消

●       復原上一個命令:按下Ctrl+x u、Ctrl +/或Ctrl+_ (同時按下Ctrl+Shift+_3個鍵),可以恢復到上一個命令。

●       取消目前再執行的命令:按Ctrl+g可以取消目前正在執行的命令,按錯命令時可用此按鍵取消。

(4) 剪切與粘貼

在瞭解剪切(cut)與粘貼文本的按鍵操作前,先了解一下刪除與剪切命令的區別。

●       刪除:凡是一次只刪除一個字符的按鍵命令多屬於刪除命令,如上述的Delete、Ctrl+d、Alt+Delete與Alt+d等按鍵。使用這些按鍵所刪除的字符無法被恢復。

●       剪切:剪切命令可以將選擇的內容複製到粘貼板上,並將原文中的內容刪除。上面提到的Ctrl+k、Alt+k等按鍵就是剪切命令。

●       粘貼:按Ctrl+y會將當前粘貼板上的內容複製到光標所在位置。

(5) 複製文本與區塊

●       複製文本:先剪切,再粘貼。可以在選擇完內容後按Ctrl+k剪切文本,再按Ctrl+y複製文本。

●       複製區塊:在一個地方(A)按下Ctrl+Spase或Ctrl+@ (Ctrl+Shift+2)使它成爲一個表示點,將光標移至另一處(B),再按下Alt+w,可將A與B之間的文本複製到系統的內存中,稍後可用粘貼命令將它們粘貼回來。

3. 查找與替換

(1) 一般查找

在emacs中可用Ctrl+s及Ctrl+r兩組命令進行漸進式查找。其中Ctrl+s會從光標所在的位置向文件尾方向查找,而Ctrl+r則是從光標所在的位置向文件頭的方向查找。

按下Ctrl+s或Ctrl+r後,響應區會出現:

-search:

或者出現

-search backward:

可以在響應區輸入要查找的文本,並按Enter鍵,光標便會移至符合查找條件的字符串位置,此時可以繼續按Ctrl+s鍵,將光標移至下一個符合查找條件的字符串,或按Ctrl+r鍵,將光標移至上一個符合條件的字符串。

 

如果查找失敗,就會出現如下的信息:

Failing I-search: sdfsdfsdfsdfsdfsdfsd

(2) 替換全部字符串

使用此功能,可將光標後所有的匹配字符串一次性替換掉,系統並不會詢問用戶來進行確認,因此使用時要特別小心。操作過程如下:

按Alt+x鍵,並於響應區輸入“replace-string”(實際輸入時要使用替換文本),即可開始字符串的替換。在提示符後面輸入原始的字符串,並按Enter鍵,再在提示符後輸入替換後的新字符串,即可替換光標後所有匹配的字符串。

(3) 選擇性替換

選擇性替換就是在替換時詢問一下用戶的意見,然後根據指示來決定是否替換。操作過程如下:

按下Alt+x鍵,於響應區輸入“query-replace”,即可進行選擇性替換,並在提示符後輸入原始字符串,按Enter鍵,再提示輸入替換後的新字符串。此時如果系統發現可替換的字符串,可按Enter鍵進行替換、按n鍵跳至下一個匹配的字符串,或按q鍵中止替換操作。操作的更詳細說明可按F1鍵獲得。

2.3 Linux下的其他編輯器

前兩節介紹的vim和emacs都是Linux下的最常用的編輯器,儘管功能強大,但是操作也比較複雜,本節介紹兩款操作簡單的編輯器,即ed和pico。

2.3.1 最簡單的文本編輯器ed

ed可以說是Linux下功能最簡單的編輯器。ed一次僅能編輯一行,而非以全屏的方式來操作。

要進入ed編輯環境,只需要在命令行輸入ed即可:

[david@DAVID david]$ ed

ed有兩種模式,分別是命令行模式與輸入模式。當第一次執行ed時,進入的是ed的命令行模式,此模式下只能執行一些命令。由於進入ed後沒有任何的說明文本,如果輸入的命令不正確,則會出現問號“?”。如下代碼所示,表示ed無法確認當前的操作,此時應重新輸入正確命令。

[david@DAVID david]$ ed

david

?

Linux

?

1. 輸入文本

由於命令行模式僅能輸入命令,因此要開始編輯文件內容,必須轉到輸入模式。進入編輯模式有3種方式,見表2-6。

表2-6 輸入模式下3種輸入方式

命    令

操 作 說 明

A

將新輸入的內容接在最後一行後面

i

將新輸入的內容加到最後輸入的一行的前一行

c

將新輸入的內容替換原來的最後一行

 

下面是三個命令的應用實例。

 a命令應用實例:

[david@DAVID david]$ ed

a

i am david

i'm a Linuxer

 i命令應用實例:

[david@DAVID david]$ ed

a

i am david

.

i

i am a Linuxer

c命令應用實例:

[david@DAVID david]$ ed

a

i am david

.

c

i am david

如果想編輯一個已經存在的文件(比如david.txt),則可用下面的方式來執行ed:

[david@DAVID david]$ ed david.txt

11

提示:

ed無法讓用戶一次看到全部的內容,但是可以在命令行模式下看到最後輸入的一行,例如:

[david@DAVID david]$ ed david.txt

11

.

i am david

2. 插入一行

若輸入內容後,想在前面插入一行,則可輸入i:

[david@DAVID david]$ ed

a

i am david

i am a Linuxer

.

i

i love xueer    這一行將插入到“i am a Linuxer”之前

3. 存盤和退出

當建立文件時或完成編輯後,可以隨時在命令行模式輸入“w”保存文件,而要退出則輸入“q”即可:

[david@DAVID david]$ ed

a

i am david

i am a Linuxer

.

i

i love xueer

.

w xueer.txt   將文件保存爲xueer.txt。如果是編輯已有的文件,則輸入“w”即可

47

q             退出ed

[david@DAVID david]$   回到Linux提示符下

以上對Linux下的ed作了簡單的介紹,雖然ed的功能不是太強,但當我們只需建立一個簡單的文件時,也不失爲一個相當方便的工具。

2.3.2 最容易上手的編輯器pico

如果覺得vim和emacs太難學,而ed功能又太簡單,那麼不妨試試pico。pico的使用界面有點像DOS下的PE2,即使是第一次使用的人也能夠很快熟悉這種操作方式。這是Linux下最容易使用的入門級文本編輯器。

1. pico的編輯環境

可以在Linux提示符下執行pico(或者執行pico filename 加載一個文件)來啓動它:

UW PICO(tm) 4.2                New Buffer

 

^G Get Help ^O WriteOut ^R Read File ^Y Prev Pg ^K Cut Text ^C Cur Pos

^X Exit ^J Justify ^W Where is ^V Next Pg ^U UnCut Text^T To Spell

pico不像其他編輯器那樣有命令行模式與輸入模式之分,用戶可以直接在編輯區輸入文本。按Enter鍵可換行,按空格鍵可將光標向右移動。當要刪除字符時,將光標移動到該字符的右邊,然後按Backspace鍵即可刪除(按Delete鍵無效)。

2. pico的操作按鍵

在pico編輯環境的下方,有兩排共12組操作按鍵,這些只是最常用的部分,其他比較少用的操作按鍵沒有列出來。下面分別詳述其功能。

(1) 顯示輔助功能—— Ctrl+G

按Ctrl+G出現pico的幫助文檔,再按Ctrl+V顯示下一頁,裏面會列出所有的操作按鍵(除了這裏介紹的12個之外,還有約12個操作按鍵,試試看)。

UW PICO(tm) 4.2                New Buffer

 

Pico is designed to be a simple, easy-to-use text editor with a

layout very similar to the pine mailer. The status line at the

top of the display shows pico's version, the current file being

edited and whether or not there are outstanding modifications

that have not been saved. The third line from the bottom is used

to report informational messages and for additional command input.

The bottom two lines list the available editing commands.

 

Each character typed is automatically inserted into the buffer

at the current cursor position. Editing commands and cursor

movement (besides arrow keys) are given to pico by typing

special control-key sequences. A caret, '^', is used to denote

the control key, sometimes marked "CTRL", so the CTRL-q key

combination is written as ^Q.

 

The following functions are available in pico (where applicable,

corresponding function key commands are in parentheses).

 

^G (F1)   Display this help text.

 

^F        move Forward a character.

^B        move Backward a character.

^P        move to the Previous line.

^N        move to the Next line.

^A        move to the beginning of the current line.

^E        move to the End of the current line.

^V (F8)   move forward a page of text.

^Y (F7)   move backward a page of text.

                    [ Unknown Command. ]

 

^X Exit Help                           ^V Next Pg

(2) 保存文件——Ctrl+O

按Ctrl+O後,操作會變成下面這個樣子:

File Name to write : LINUX.TXT 輸入文件名後按Enter鍵即可

^G Get Help ^T To Files

^C Cancel    TAB Complete

注意:

此處出現的幾個操作按鍵,其中Ctrl+C顯示當前內容對應的幫助文檔,與Ctrl+G不同,Ctrl+C表示不保存內容而返回到原來的編輯環境。Ctrl+T會顯示目錄,由用戶選擇要保存爲哪一個文件。TAB則會幫用戶添上完整的文件名稱。

(3) 插入文件——Ctrl+R

按Ctrl+R可以在文件中插入一個文本文件的內容。

File to insert from home directory: /home/david/david.txt 此處輸入要插

入的文件的名稱

^G Get Help ^T To Files

^C Cancel

(4) 滾動頁面Ctrl+Y、Ctrl+C

按Ctrl+Y可切換到前一頁,如同按下PageUp; 按Ctrl+Y 可以切換到下一頁,如同按下PageDown。

(5) 剪切和粘貼整行文本——Ctrl+K、Ctrl+U

當要剪切整行文本時,可將光標移動到要剪切的那一行,然後按Ctrl+K。當剪切之後在其他位置粘貼的時候,則將光標移動到粘貼的位置的下一行,再按Ctrl+U。用戶也可以連續按3次 Ctrl+K 剪切3行(中間不可以有其他按鍵),再將光標移動到要粘貼的位置,然後按Ctrl+U。如單獨使用Ctrl+K,就如同刪除整行的操作按鍵。

(6) 自動調整文本的對齊——Ctrl+J、Ctrl+U

在輸入文本的時候,可能沒有注意到文本排列的美觀,而造成每一行有長有短參差不齊的情況:

UW PICO(tm) 4.2   New Buffer                       Modified

 

hello david

i love Linux

i am a liuxer how are you i am fine and you

此時若將光標放到調整的段落中,然後按下Ctrl+J,則整段文本會重新對齊,如下所示:

UW PICO(tm) 4.2     New Buffer                         Modified

 

hello david i love Linux i am a liuxer how are you i am fine and you

每一次Ctrl+J只對一個段落起作用,如果覺得pico對齊的樣子還比不上原來的好看,此時不要移動光標(只要一移動就不能恢復了),接着按Ctrl+Uj即可恢復。

(7) 查找字符串——Ctrl+W

若要在文章中查找某一個字符串,按Ctrl+W。

Search : Linux

^G Get Help ^Y First Line ^T LineNumber^O End of Par

^C Cancel    ^V Last Line ^W Start of P

(8) 顯示目前光標的位置——Ctrl+C

在pico中不能顯示行號,因此我們可能會不知道目前光標所在的位置,但是隻要按Ctrl+C,就會顯示光標在全部行數中的第幾行了。

(9) pico還可以按Ctrl+T檢查拼寫錯誤

UW PICO(tm) 4.2     New Buffer                       Modified

 

hello david

i love Linux

i am a liuxer how are you i am fine and you

 

 

Edit a replacement: david

^G Get Help

^C Cancel

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

第3章 gcc 編譯器

Linux的各發行版中包含了很多軟件開發工具,它們中的很多是用於C和C++應用程序開發的。本章將介紹如何使用Linux下的C 編譯器和其他C編程工具。

3.1 gcc 簡 介

在爲Linux開發應用程序時,絕大多數情況下使用的都是C語言,因此幾乎每一位Linux程序員面臨的首要問題都是如何靈活運用C編譯器。目前Linux下最常用的C語言編譯器是gcc(GNU Compiler Collection),它是GNU項目中符合ANSI C標準的編譯系統,能夠編譯用C、C++和Object C等語言編寫的程序。gcc不僅功能十分強大,結構也異常靈活。最值得稱道的一點就是它可以通過不同的前端模塊來支持各種語言,如Java、Fortran、Pascal、Modula-3和Ada等。gcc是可以在多種硬體平臺上編譯出可執行程序的超級編譯器,其執行效率與一般的編譯器相比,平均效率要高20%~30%。gcc支持編譯的一些源文件的後綴及其解釋見表3-1。

表3-1 gcc所支持的語言

後 綴 名

所支持的語言

.c

C原始程序

.C

C++原始程序

.cc

C++原始程序

.cxx

C++原始程序

.m

Objective-C原始程序

.i

已經過預處理的C原始程序

.ii

已經過預處理的C++原始程序

.s

組合語言原始程序

.S

組合語言原始程序

.h

預處理文件(標頭文件)

.o

目標文件

.a

存檔文件

 

開放、自由和靈活是Linux的魅力所在,而這一點在gcc上的體現就是程序員通過它能夠更好地控制整個編譯過程。

在使用gcc編譯程序時,編譯過程可以細分爲4個階段:

●       預處理(Pre-Processing)

●       編譯(Compiling)

●       彙編(Assembling)

●       鏈接(Linking)

Linux程序員可以根據自己的需要讓gcc在編譯的任何階段結束,檢查或使用編譯器在該階段的輸出信息,或者對最後生成的二進制文件進行控制,以便通過加入不同數量和種類的調試代碼來爲今後的調試做好準備。與其他常用的編譯器一樣,gcc也提供了靈活而強大的代碼優化功能,利用它可以生成執行效率更高的代碼。

gcc提供了30多條警告信息和3個警告級別,使用它們有助於增強程序的穩定性和可移植性。此外,gcc還對標準的C和C++語言進行了大量的擴展,提高了程序的執行效率,有助於編譯器進行代碼優化,能夠減輕編程的工作量。

3.2 使 用 gcc

gcc的版本可以使用如下gcc –v命令查看:

[david@DAVID david]$ gcc -v

Reading specs from /usr/lib/gcc-lib/i386-redhat-linux/3.2.2/specs

Configured with: ../configure --prefix=/usr --mandir=/usr/share/man

--infodir=/

sr/share/info --enable-shared --enable-threads=posix

--disable-checking --with-

ystem-zlib --enable-__cxa_atexit --host=i386-redhat-linux

Thread model: posix

gcc version 3.2.2 20030222 (Red Hat Linux 3.2.2-5)

以上顯示的就是Redhat linux 9.0裏自帶的gcc的版本3.2.2。

下面將以一個實例來說明如何使用gcc編譯器。例3-1能夠幫助大家迅速理解gcc的工作原理,並將其立即運用到實際的項目開發中去。

實例3-1 hello.c­­­­­­­­­­­­­­­­­­­­­­­­­­­­

 

 

#include <stdio.h>

int main (int argc,char **argv) {

printf("Hello Linux/n");

}

要編譯這個程序,只要在命令行下執行如下命令:

[david@DAVID david]$ gcc hello.c -o hello

[david@DAVID david]$ ./hello

Hello Linux

這樣,gcc 編譯器會生成一個名爲hello的可執行文件,然後執行./hello就可以看到程序的輸出結果了。

命令行中 gcc表示用gcc來編譯源程序,-o 選項表示要求編譯器輸出的可執行文件名爲hello ,而hello.c是源程序文件。從程序員的角度看,只需簡單地執行一條gcc命令就可以了;但從編譯器的角度來看,卻需要完成一系列非常繁雜的工作。首先,gcc需要調用預處理程序cpp,由它負責展開在源文件中定義的宏,並向其中插入#include語句所包含的內容;接着,gcc會調用ccl和as將處理後的源代碼編譯成目標代碼;最後,gcc會調用鏈接程序ld,把生成的目標代碼鏈接成一個可執行程序。

爲了更好地理解gcc的工作過程,可以把上述編譯過程分成幾個步驟單獨進行,並觀察每步的運行結果。

第一步要進行預編譯,使用-E參數可以讓gcc在預處理結束後停止編譯過程:

[david@DAVID david]$ gcc -E hello.c -o hello.i

此時若查看hello.i文件中的內容,會發現stdio.h的內容確實都插到文件裏去了,而且被預處理的宏定義也都作了相應的處理。

# 1 "hello.c"

# 1 "<built-in>"

# 1 "<command line>"

# 1 "hello.c"

# 1 "/usr/include/stdio.h" 1 3

# 28 "/usr/include/stdio.h" 3

# 1 "/usr/include/features.h" 1 3

# 291 "/usr/include/features.h" 3

# 1 "/usr/include/sys/cdefs.h" 1 3

# 292 "/usr/include/features.h" 2 3

# 314 "/usr/include/features.h" 3

# 1 "/usr/include/gnu/stubs.h" 1 3

# 315 "/usr/include/features.h" 2 3

# 29 "/usr/include/stdio.h" 2 3

# 1 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 1 3

# 213 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 3

typedef unsigned int size_t;

# 35 "/usr/include/stdio.h" 2 3

# 1 "/usr/include/bits/types.h" 1 3

# 28 "/usr/include/bits/types.h" 3

# 1 "/usr/include/bits/wordsize.h" 1 3

# 29 "/usr/include/bits/types.h" 2 3

# 1 "/usr/lib/gcc-lib/i386-redhat-linux/3.2.2/include/stddef.h" 1 3

# 32 "/usr/include/bits/types.h" 2 3

 

"hello.i" 838L, 16453C                         1,1           Top

下一步是將hello.i編譯爲目標代碼,這可以通過使用-c參數來完成:

[david@DAVID david]$ gcc -c hello.i -o hello.o

gcc默認將.i文件看成是預處理後的C語言源代碼,因此上述命令將自動跳過預處理步驟而開始執行編譯過程,也可以使用-x參數讓gcc從指定的步驟開始編譯。最後一步是將生成的目標文件鏈接成可執行文件:

[david@DAVID david]$ gcc hello.o -o hello

在採用模塊化的設計思想進行軟件開發時,通常整個程序是由多個源文件組成的,相應地就形成了多個編譯單元,使用gcc能夠很好地管理這些編譯單元。假設有一個由david.c和xueer.c兩個源文件組成的程序,爲了對它們進行編譯,並最終生成可執行程序davidxueer,可以使用下面這條命令:

[david@DAVID david]$ gcc david.c xueer.c -o davidxueer

如果同時處理的文件不止一個,gcc仍然會按照預處理、編譯和鏈接的過程依次進行。如果深究起來,上面這條命令大致相當於依次執行如下3條命令:

[david@DAVID david]$ gcc david.c -o david.o

[david@DAVID david]$ gcc xueer.c -o xueer.o

[david@DAVID david]$ gcc david.o xueer.o -o davidxueer

在編譯一個包含許多源文件的工程時,若只用一條gcc命令來完成編譯是非常浪費時間的。假設項目中有100個源文件需要編譯,並且每個源文件中都包含10 000行代碼,如果像上面那樣僅用一條gcc命令來完成編譯工作,那麼gcc需要將每個源文件都重新編譯一遍,然後再全部鏈接起來。很顯然,這樣浪費的時間相當多,尤其是當用戶只是修改了其中某一個文件的時候,完全沒有必要將每個文件都重新編譯一遍,因爲很多已經生成的目標文件是不會改變的。要解決這個問題,關鍵是要靈活運用gcc,同時還要藉助像make這樣的工具。關於make,將在第5章作詳細的介紹。

3.3 gcc警告提示功能

gcc包含完整的出錯檢查和警告提示功能,它們可以幫助Linux程序員儘快找到錯誤代碼,從而寫出更加專業和優美的代碼。先來讀讀例3-2所示的程序,這段代碼寫得很糟糕,仔細檢查一下不難挑出如下毛病:

●       main函數的返回值被聲明爲void,但實際上應該是int;

●       使用了GNU語法擴展,即使用long long來聲明64位整數,仍不符合ANSI/ISO C語言標準;

●       main函數在終止前沒有調用return語句。

實例3-2 bad.c­­­­­­­­­­­­­­­­­­­­­­­­­­­­

 

 

#include <stdio.h>

void main(void)

{

 long long int var = 1;

 printf("It is not standard C code!/n");

}

下面看看gcc是如何幫助程序員來發現這些錯誤的。當gcc在編譯不符合ANSI/ISO C語言標準的源代碼時,如果加上了-pedantic選項,那麼使用了擴展語法的地方將產生相應的警告信息:

[david@DAVID david]$ gcc -pedantic bad.c -o bad

bad.c: In function 'main':

bad.c:4: warning: ISO C89 does not support 'long long'

bad.c:3: warning: return type of 'main' is not 'int'

需要注意的是,-pedantic編譯選項並不能保證被編譯程序與ANSI/ISO C標準的完全兼容,它僅僅用來幫助Linux程序員離這個目標越來越近。換句話說,-pedantic選項能夠幫助程序員發現一些不符合ANSI/ISO C標準的代碼,但不是全部。事實上只有ANSI/ISO C語言標準中要求進行編譯器診斷的那些問題纔有可能被gcc發現並提出警告。

除了-pedantic之外,gcc還有一些其他編譯選項也能夠產生有用的警告信息。這些選項大多以-W開頭,其中最有價值的當數-Wall了,使用它能夠使gcc產生儘可能多的警告信息。例如:

[david@DAVID david]$ gcc -Wall bad.c -o bad

bad.c:3: warning: return type of 'main' is not 'int'

bad.c: In function 'main':

bad.c:4: warning: unused variable 'var'

bad.c:6:2: warning: no newline at end of file

gcc給出的警告信息雖然從嚴格意義上說不能算作是錯誤,但很可能成爲錯誤的棲身之所。一個優秀的Linux程序員應該儘量避免產生警告信息,使自己的代碼始終保持簡潔、優美和健壯的特性。
    在處理警告方面,另一個常用的編譯選項是-Werror,它要求gcc將所有的警告當成錯誤進行處理,這在使用自動編譯工具(如make等)時非常有用。如果編譯時帶上-Werror選項,那麼gcc會在所有產生警告的地方停止編譯,迫使程序員對自己的代碼進行修改。只有當相應的警告信息消除時,纔可能將編譯過程繼續朝前推進。執行情況如下:

[david@DAVID david]$ gcc -Werror bad.c -o bad

cc1: warnings being treated as errors

bad.c: In function 'main':

bad.c:3: warning: return type of 'main' is not 'int'

bad.c:6:2: no newline at end of file

對Linux程序員來講,gcc給出的警告信息是很有價值的,它們不僅可以幫助程序員寫出更加健壯的程序,而且還是跟蹤和調試程序的有力工具。建議在用gcc編譯源代碼時始終帶上-Wall選項,並把它逐漸培養成爲一種習慣,這對找出常見的隱式編程錯誤很有幫助。

3.4 庫 依 賴

在Linux下使用C語言開發應用程序時,完全不使用第三方函數庫的情況是比較少見的,通常來講都需要藉助一個或多個函數庫的支持才能夠完成相應的功能。從程序員的角度看,函數庫實際上就是一些頭文件(.h)和庫文件(.so或者.a)的集合。雖然Linux下大多數函數都默認將頭文件放到/usr/include/目錄下,而庫文件則放到/usr/lib/目錄下,但並不是所有的情況都是這樣。正因如此,gcc在編譯時必須讓編譯器知道如何來查找所需要的頭文件和庫文件。

gcc採用搜索目錄的辦法來查找所需要的文件,-I選項可以向gcc的頭文件搜索路徑中添加新的目錄。例如,如果在/home/david/include/目錄下有編譯時所需要的頭文件,爲了讓gcc能夠順利地找到它們,就可以使用-I選項:

[david@DAVID david]$ gcc david.c -I /home/david/include -o david

同樣,如果使用了不在標準位置的庫文件,那麼可以通過-L選項向gcc的庫文件搜索路徑中添加新的目錄。例如,如果在/home/david/lib/目錄下有鏈接時所需要的庫文件libdavid.so,爲了讓gcc能夠順利地找到它,可以使用下面的命令:

[david@DAVID david]$ gcc david.c -L /home/david/lib –ldavid -o david

值得詳細解釋一下的是-l選項,它指示gcc去連接庫文件david.so。Linux下的庫文件在命名時有一個約定,那就是應該以lib三個字母開頭。由於所有的庫文件都遵循了同樣的規範,因此在用-l選項指定鏈接的庫文件名時可以省去lib三個字母。也就是說gcc在對-l david進行處理時,會自動去鏈接名爲libdavid.so的文件。

Linux下的庫文件分爲兩大類,分別是動態鏈接庫(通常以.so結尾)和靜態鏈接庫(通常以.a結尾),兩者的差別僅在於程序執行時所需的代碼是在運行時動態加載的,還是在編譯時靜態加載的。默認情況下,gcc在鏈接時優先使用動態鏈接庫,只有當動態鏈接庫不存在時才考慮使用靜態鏈接庫。如果需要的話可以在編譯時加上-static選項,強制使用靜態鏈接庫。例如,如果在/home/david/lib/目錄下有鏈接時所需要的庫文件libfoo.so和libfoo.a,爲了讓gcc在鏈接時只用到靜態鏈接庫,可以使用下面的命令:

[david@DAVID david]$ gcc foo.c -L /home/david/lib -static –ldavid -o

david

3.5 gcc代碼優化

代碼優化指的是編譯器通過分析源代碼,找出其中尚未達到最優的部分,然後對其重新進行組合,目的是改善程序的執行性能。gcc提供的代碼優化功能非常強大,它通過編譯選項-On來控制優化代碼的生成,其中n是一個代表優化級別的整數。對於不同版本的gcc來講,n的取值範圍及其對應的優化效果可能並不完全相同,比較典型的範圍是從0變化到2或3。

編譯時使用選項-O可以告訴gcc同時減小代碼的長度和執行時間,其效果等價於-O1。在這一級別上能夠進行的優化類型雖然取決於目標處理器,但一般都會包括線程跳轉(Thread Jump)和延遲退棧(Deferred Stack Pops)兩種優化。

選項-O2告訴gcc除了完成所有-O1級別的優化之外,同時還要進行一些額外的調整工作,如處理器指令調度等。

選項-O3則除了完成所有-O2級別的優化之外,還包括循環展開和其他一些與處理器特性相關的優化工作。

通常來說,數字越大優化的等級越高,同時也就意味着程序的運行速度越快。許多Linux程序員都喜歡使用-O2選項,因爲它在優化長度、編譯時間和代碼大小之間取得了一個比較理想的平衡點。

下面通過具體實例來感受一下gcc的代碼優化功能,所用程序如例3-3所示。

實例3-3 count.c­­­­­­­­­­­­­­­­­­­­­­­­­­­­

 

 

#include <stdio.h>

 int main(void)

{ double counter;

   double result;

   double temp;

   for (counter = 0; counter < 4000.0 * 4000.0 * 4000.0 / 20.0 + 2030;   

counter += (5 - 3 +2 + 1 ) / 4)

     { temp = counter / 1239;

        result = counter;   

       } 

                printf("Result is %lf/n", result); 

                return 0;

}

首先不加任何優化選項進行編譯:

[david@DAVID david]$ gcc -Wall count.c -o count

藉助Linux提供的time命令,可以大致統計出該程序在運行時所需要的時間:

[david@DAVID david]$ time ./count

Result is 3200002029.000000

real    1m59.357s

user    1m59.140s

sys     0m0.050s

接下來使用優化選項來對代碼進行優化處理:

[david@DAVID david]$ gcc -Wall count.c -o count2

在同樣的條件下再次測試一下運行時間:

[david@DAVID david]$ time ./count2

Result is 3200002029.000000

real    0m26.573s

user    0m26.540s

sys     0m0.010s

對比兩次執行的輸出結果不難看出,程序的性能的確得到了很大幅度的改善,由原來的1分59秒縮短到了26秒。這個例子是專門針對gcc的優化功能而設計的,因此優化前後程序的執行速度發生了很大的改變。儘管gcc的代碼優化功能非常強大,但作爲一名優秀的Linux程序員,首先還是要力求能夠手工編寫出高質量的代碼。如果編寫的代碼簡短,並且邏輯性強,編譯器就不會做更多的工作,甚至根本用不着優化。

優化雖然能夠給程序帶來更好的執行性能,但在如下一些場合中應該避免優化代碼。

●       程序開發的時候:優化等級越高,消耗在編譯上的時間就越長,因此在開發的時候最好不要使用優化選項,只有到軟件發行或開發結束的時候,才考慮對最終生成的代碼進行優化。

●       資源受限的時候:一些優化選項會增加可執行代碼的體積,如果程序在運行時能夠申請到的內存資源非常緊張(如一些實時嵌入式設備),那就不要對代碼進行優化,因爲由這帶來的負面影響可能會產生非常嚴重的後果。

●       跟蹤調試的時候:在對代碼進行優化的時候,某些代碼可能會被刪除或改寫,或者爲了取得更佳的性能而進行重組,從而使跟蹤和調試變得異常困難。

3.6 加    速

在將源代碼變成可執行文件的過程中,需要經過許多中間步驟,包含預處理、編譯、彙編和連接。這些過程實際上是由不同的程序負責完成的。大多數情況下gcc可以爲Linux程序員完成所有的後臺工作,自動調用相應程序進行處理。

這樣做有一個很明顯的缺點,就是gcc在處理每一個源文件時,最終都需要生成好幾個臨時文件才能完成相應的工作,從而無形中導致處理速度變慢。例如,gcc在處理一個源文件時,可能需要一個臨時文件來保存預處理的輸出,一個臨時文件來保存編譯器的輸出,一個臨時文件來保存彙編器的輸出,而讀寫這些臨時文件顯然需要耗費一定的時間。當軟件項目變得非常龐大的時候,花費在這上面的代價可能會變得很大。

解決的辦法是,使用Linux提供的一種更加高效的通信方式——管道。它可以用來同時連接兩個程序,其中一個程序的輸出將直接作爲另一個程序的輸入,這樣就可以避免使用臨時文件,但編譯時卻需要消耗更多的內存。

注意:

在編譯過程中使用管道是由gcc的-pipe選項決定的。下面的這條命令就是藉助gcc的管道功能來提高編譯速度的:

[david@DAVID david]$ gcc -pipe david.c -o david

在編譯小型工程時使用管道,編譯時間上的差異可能還不是很明顯,但在源代碼非常多的大型工程中,差異將變得非常明顯。

3.7 gcc常用選項

gcc作爲Linux下C/C++重要的編譯環境,功能強大,編譯選項繁多。爲了方便大家日後編譯方便,在此將常用的選項及說明羅列出來,見表3-2。

表3-2 gcc的常用選項

選 項 名

作    用

-c

通知gcc取消連接步驟,即編譯源碼並在最後生成目標文件

-Dmacro

定義指定的宏,使它能夠通過源碼中的#ifdef進行檢驗

-E

不經過編譯預處理程序的輸出而輸送至標準輸出

-g3

獲得有關調試程序的詳細信息,它不能與-o選項聯合使用

-Idirectory

在包含文件搜索路徑的起點處添加指定目錄

-llibrary

提示連接程序在創建最終可執行文件時包含指定的庫

-O、-O2、-O3

將優化狀態打開,該選項不能與-g選項聯合使用

-S

要求編譯程序生成來自源代碼的彙編程序輸出

-v

啓動所有警報

.h

預處理文件(標頭文件)

-Wall

在發生警報時取消編譯操作,即將警報看作是錯誤

-w

禁止所有的報警

 

3.8 gcc的錯誤類型及對策

如果gcc編譯器發現源程序中有錯誤,就無法繼續進行,也無法生成最終的可執行文件。爲了便於修改,gcc給出錯誤信息,必須對這些錯誤信息逐個進行分析、處理,並修改相應的源代碼,才能保證源代碼的正確編譯連接。.gcc給出的錯誤信息一般可以分爲四大類,下面我們分別討論其產生的原因和對策。

●       第一類:C語法錯誤

錯誤信息:文件source.c中第n行有語法錯誤(syntex errror)。這種類型的錯誤,一般都是C語言的語法錯誤,應該仔細檢查源代碼文件中第n行及該行之前的程序,有時也需要對該文件所包含的頭文件進行檢查。有些情況下,一個很簡單的語法錯誤,gcc會給出一大堆錯誤,我們最主要的是要保持清醒的頭腦,不要被其嚇倒,必要的時候再參考一下C語言的基本教材。在這裏推薦一本由Andrew Koenig寫的《C 陷阱與缺陷》(此書已由人民郵電出版社翻譯出版),說得誇張一點就是此書可以幫助你減少C代碼和初級C++代碼中的90%的bug。

●       第二類:頭文件錯誤

錯誤信息:找不到頭文件head.h(Can not find include file head.h)。這類錯誤是源代碼文件中包含的頭文件有問題,可能的原因有頭文件名錯誤、指定的頭文件所在目錄名錯誤等,也可能是錯誤地使用了雙引號和尖括號。

●       第三類:檔案庫錯誤

錯誤信息:連接程序找不到所需的函數庫,例如:

ld: -lm: No such file or directory

這類錯誤是與目標文件相連接的函數庫有錯誤,可能的原因是函數庫名錯誤、指定的函數庫所在目錄名稱錯誤等。檢查的方法是使用find命令在可能的目錄中尋找相應的函數庫名,確定檔案庫及目錄的名稱並修改程序中及編譯選項中的名稱。

●       第四類:未定義符號

錯誤信息:有未定義的符號(Undefined symbol)。這類錯誤是在連接過程中出現的,可能有兩種原因:一是用戶自己定義的函數或者全局變量所在源代碼文件,沒有被編譯、連接,或者乾脆還沒有定義,這需要用戶根據實際情況修改源程序,給出全局變量或者函數的定義體;二是未定義的符號是一個標準的庫函數,在源程序中使用了該庫函數,而連接過程中還沒有給定相應的函數庫的名稱,或者是該檔案庫的目錄名稱有問題,這時需要使用檔案庫維護命令ar檢查我們需要的庫函數到底位於哪一個函數庫中,確定之後,修改gcc連接選項中的-l和-L項。

排除編譯、連接過程中的錯誤,應該說只是程序設計中最簡單、最基本的一個步驟,可以說只是開了個頭。這個過程中的錯誤,只是我們在使用C語言描述一個算法中所產生的錯誤,是比較容易排除的。我們寫一個程序,到編譯、連接通過爲止,應該說剛剛開始,程序在運行過程中所出現的問題,是算法設計有問題,說得嚴重點兒是對問題的認識和理解不夠,還需要更加深入地測試、調試和修改。一個程序,稍爲複雜的程序,往往要經過多次的編譯、連接、測試和修改。 gcc是在Linux下開發程序時必須掌握的工具之一。

以上對gcc作了一個簡要的介紹,主要講述瞭如何使用gcc編譯程序、產生警告信息、和加快gcc的編譯速度。對所有希望早日跨入Linux開發者行列的人來說,gcc就是成爲一名優秀的Linux程序員的起跑線。關於調試 C 程序的更多信息請看第4章關於gdb的內容。

 

第4章 gdb 調試器

4.1 gdb 概 述

無論多麼優秀的程序員,必須經常面對的一個問題就是調試。當程序編譯完成後,他可能無法正常運行;或許程序會徹底崩潰;或許只是不能正常地運行某些功能;或許它的輸出會被掛起;或許不會提示要求正常的輸入。無論在何種情況下,跟蹤這些問題,特別是在大的工程中,將是開發中最困難的部分,本章將介紹使用gdb(GNU debugger)調試程序的方法,該程序是一個調試器,是用來幫助程序員尋找程序中的錯誤的軟件。

gdb是GNU開發組織發佈的一個強大的UNIX/Linux下的程序調試工具。或許,有人比較習慣圖形界面方式的,像VC、BCB等IDE環境,但是在UNIX/Linux平臺下做軟件,gdb這個調試工具有比VC、BCB的圖形化調試器更強大的功能。所謂“寸有所長,尺有所短”就是這個道理。

一般來說,gdb主要幫忙用戶完成下面4個方面的功能:

●       啓動程序,可以按照用戶自定義的要求隨心所欲的運行程序。

●       可讓被調試的程序在用戶所指定的調試的斷點處停住 (斷點可以是條件表達式)。

●       當程序停住時,可以檢查此時程序中所發生的事。

●       動態地改變程序的執行環境。

從上面來看,gdb和一般的調試工具區別不大,基本上也是完成這些功能,不過在細節上,會發現gdb這個調試工具的強大。大家可能習慣了圖形化的調試工具,但有時候,命令行的調試工具卻有着圖形化工具所不能完成的功能。下面通過實例4-4進行說明。

實例4-1 test.c­­­­­­­­­­­­­­­­­­­­­­­­­­­­

 

 

#include <stdio.h>

int func(int n)

{

     int sum=0,i;

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

     {

        sum+=i;

       }

     return sum;

}

 main()

{

     int i;

     long result = 0;

     for(i=1; i<=100; i++)

      {

        result += i;

      }

     printf("result[1-100] = %d /n", result );

     printf("result[1-250] = %d /n", func(250) );

 }

編譯生成執行文件(Linux下):

[david@DAVID david]$ gcc –g test.c -o test

使用gdb調試:

[david@DAVID david]$ gdb test <---------- 啓動gdb

GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)

Copyright 2003 Free Software Foundation, Inc.

gdb is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for gdb. Type "show warranty" for details.

This gdb was configured as "i386-redhat-Linux-gnu"...

(gdb)

鍵入 l命令相當於list命令,從第一行開始列出源碼:

[david@DAVID david]$ gdb test

GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)

Copyright 2003 Free Software Foundation, Inc.

gdb is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for gdb. Type "show warranty" for details.

This gdb was configured as "i386-redhat-Linux-gnu"...

(gdb) l

7                    {

8                           sum+=i;

9                    }

10                  return sum;

11           }

12

13              main()

14           {

15                   int i;

16                   long result = 0;

(gdb)

17                   for(i=1; i<=100; i++)

18                   {

19                           result += i;

20                   }

21                     printf("result[1-100] = %d /n", result );

22                 printf("result[1-250] = %d /n", func(250) );

23           }

(gdb) break 16 <-------------------- 設置斷點,在源程序第16行處。

Breakpoint 1 at 0x804836a: file test.c, line 16.

(gdb) break func <-------------------- 設置斷點,在函數func()入口處。

Breakpoint 2 at 0x804832e: file test.c, line 5.

(gdb) info break <-------------------- 查看斷點信息。

Num Type           Disp Enb Address    What

1   breakpoint     keep y   0x0804836a in main at test.c:16

2   breakpoint     keep y   0x0804832e in func at test.c:5

(gdb) r <--------------------- 運行程序,run命令簡寫

Starting program: /home/david/test

 

Breakpoint 1, main () at test.c:16 <---------- 在斷點處停住。

16                   long result = 0;

(gdb) n <--------------------- 單條語句執行,next命令簡寫。

17                   for(i=1; i<=100; i++)

(gdb) n

19                           result += i;

(gdb) n

17                   for(i=1; i<=100; i++)

(gdb) n

19                           result += i;

(gdb) n

17                   for(i=1; i<=100; i++)

(gdb) c     <--------------------- 繼續運行程序,continue命令簡寫。

Continuing.

result[1-100] = 5050  <----------程序輸出。

 

Breakpoint 2, func (n=250) at test.c:5

5                   int sum=0,i;

(gdb) n

6                    for(i=0; i<n; i++)

(gdb) p I    <--------------------- 打印變量i的值,print命令簡寫。

$1 = 1107620064

(gdb) n

8                           sum+=i;

(gdb) n

6                    for(i=0; i<n; i++)

(gdb) p sum

$2 = 0

(gdb) bt     <--------------------- 查看函數堆棧。

#0 func (n=250) at test.c:6

#1 0x080483b2 in main () at test.c:22

#2 0x42015574 in __libc_start_main () from /lib/tls/libc.so.6

(gdb) finish <--------------------- 退出函數。

Run till exit from #0 func (n=250) at test.c:6

0x080483b2 in main () at test.c:22

22   printf("result[1-250] = %d /n", func(250) );

Value returned is $3 = 31125

(gdb) c <--------------------- 繼續運行。

Continuing.

result[1-250] = 31125

 

Program exited with code 027. <--------程序退出,調試結束。

(gdb) q     <--------------------- 退出gdb。

[david@DAVID david]$

有了以上的感性認識,下面來系統地學習一下gdb。

4.2 使 用 gdb

gdb主要調試的是C/C++的程序。要調試C/C++的程序,首先在編譯時,必須要把調試信息加到可執行文件中。使用編譯器(cc/gcc/g++)的 -g 參數即可。如:

[david@DAVID david]$ gcc -g hello.c -o hello

[david@DAVID david]$ g++ -g hello.cpp -o hello

如果沒有-g,將看不見程序的函數名和變量名,代替它們的全是運行時的內存地址。當用-g把調試信息加入,併成功編譯目標代碼以後,看看如何用gdb來調試。

啓動gdb的方法有以下幾種:

1. gdb <program>

program也就是執行文件,一般在當前目錄下。

2. gdb <program> core

用gdb同時調試一個運行程序和core文件,core是程序非法執行後,core dump後產生的文件。

3. gdb <program> <PID>

如果程序是一個服務程序,那麼可以指定這個服務程序運行時的進程ID。gdb會自動attach上去,並調試它。program應該在PATH環境變量中搜索得到。 

gdb啓動時,可以加上一些gdb的啓動開關,詳細的開關可以用gdb -help查看。下面只列舉一些比較常用的參數:

-symbols <file>

-s <file>

從指定文件中讀取符號表。

-se file

從指定文件中讀取符號表信息,並把它用在可執行文件中。

-core <file>

-c <file>

調試時core dump的core文件。

-directory <directory>

-d <directory>

加入一個源文件的搜索路徑。默認搜索路徑是環境變量中PATH所定義的路徑。

4.2.1 gdb的命令概貌

啓動gdb後,進入gdb的調試環境中,就可以使用gdb的命令開始調試程序了。gdb的命令可以使用help命令來查看,如下所示:   

[david@DAVID david]$ gdb

GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)

Copyright 2003 Free Software Foundation, Inc.

gdb is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for gdb. Type "show warranty" for details.

This gdb was configured as "i386-redhat-Linux-gnu".

(gdb) help

List of classes of commands:

aliases -- Aliases of other commands

breakpoints -- Making program stop at certain points

data -- Examining data

files -- Specifying and examining files

internals -- Maintenance commands

obscure -- Obscure features

running -- Running the program

stack -- Examining the stack

status -- Status inquiries

support -- Support facilities

tracepoints -- Tracing of program execution without stopping the program

user-defined -- User-defined commands

 

Type "help" followed by a class name for a list of commands in that class.

Type "help" followed by command name for full documentation.

Command name abbreviations are allowed if unambiguous.

(gdb)

gdb的命令很多,gdb將之分成許多種類。help命令只是列出gdb的命令種類,如果要看其中的命令,可以使用help <class> 命令。如:

(gdb) help data

Examining data.

List of commands:

append -- Append target code/data to a local file

call -- Call a function in the program

delete display -- Cancel some expressions to be displayed when program stops

delete mem -- Delete memory region

disable display -- Disable some expressions to be displayed when program stops

disable mem -- Disable memory region

disassemble -- Disassemble a specified section of memory

display -- Print value of expression EXP each time the program stops

dump -- Dump target code/data to a local file

enable display -- Enable some expressions to be displayed when program stops

enable mem -- Enable memory region

inspect -- Same as "print" command

mem -- Define attributes for memory region

output -- Like "print" but don't put in value history and don't print newline

print -- Print value of expression EXP

printf -- Printf "printf format string"

ptype -- Print definition of type TYPE

restore -- Restore the contents of FILE to target memory

set -- Evaluate expression EXP and assign result to variable VAR

set variable -- Evaluate expression EXP and assign result to variable VAR

undisplay -- Cancel some expressions to be displayed when program stops

whatis -- Print data type of expression EXP

x -- Examine memory: x/FMT ADDRESS

 

Type "help" followed by command name for full documentation.

Command name abbreviations are allowed if unambiguous.

(gdb)

也可以直接用help [command]來查看命令的幫助。

gdb中,輸入命令時,可以不用輸入全部命令,只用輸入命令的前幾個字符就可以了。當然,命令的前幾個字符應該標誌着一個惟一的命令,在Linux下,可以按兩次TAB鍵來補齊命令的全稱,如果有重複的,那麼gdb會把它全部列出來。

示例一:在進入函數func時,設置一個斷點。可以輸入break func,或是直接輸入b func。

(gdb) b func

Breakpoint 1 at 0x804832e: file test.c, line 5.

(gdb)

示例二:輸入b按兩次TAB鍵,你會看到所有b開頭的命令。

(gdb) b

backtrace break bt

要退出gdb時,只用輸入quit或其簡稱q就行了。 

4.2.2 gdb中運行Linux的shell程序

在gdb環境中,可以執行Linux的shell命令:

shell <command string>

調用Linux的shell來執行<command string>,環境變量SHELL中定義的Linux的shell將會用來執行<command string>。如果SHELL沒有定義,那就使用Linux的標準shell:/bin/sh(在Windows中使用Command.com或cmd.exe)。

還有一個gdb命令是make:

make <make-args>

可以在gdb中執行make命令來重新build自己的程序。這個命令等價於shell make <make-args>。 

4.2.3 在gdb中運行程序

當以gdb <program>方式啓動gdb後,gdb會在PATH路徑和當前目錄中搜索<program>的源文件。如要確認gdb是否讀到源文件,可使用l或list命令,看看gdb是否能列出源代碼。

在gdb中,運行程序使用r或是run命令。程序的運行,有可能需要設置下面四方面的事。

1. 程序運行參數

set args 可指定運行時參數。如:

set args 10 20 30 40 50

show args 命令可以查看設置好的運行參數。

2. 運行環境

path <dir> 可設定程序的運行路徑。

show paths 查看程序的運行路徑。

set environment varname [=value] 設置環境變量。如:

set env USER=hchen

show environment [varname] 查看環境變量。

3. 工作目錄

cd <dir> 相當於shell的cd命令。

pwd 顯示當前的所在目錄。

4. 程序的輸入輸出

info terminal 顯示程序用到的終端的模式。

使用重定向控制程序輸出。如:

run > outfile

tty命令可以指寫輸入輸出的終端設備。如:

tty /dev/ttyb

4.2.4 調試已運行的程序

調試已經運行的程序有兩種方法:

●       在Linux下用ps(第一章已經對ps作了介紹)查看正在運行的程序的PID(進程ID),然後用gdb <program> PID格式掛接正在運行的程序。

●       先用gdb <program>關聯上源代碼,並進行gdb,在gdb中用attach命令來掛接進程的PID,並用detach來取消掛接的進程。

4.2.5 暫停/恢復程序運行

調試程序中,暫停程序運行是必需的,gdb可以方便地暫停程序的運行。可以設置程序在哪行停住,在什麼條件下停住,在收到什麼信號時停往等,以便於用戶查看運行時的變量,以及運行時的流程。

當進程被gdb停住時,可以使用info program 來查看程序是否在運行、進程號、被暫停的原因。

在gdb中,有以下幾種暫停方式:斷點(BreakPoint)、觀察點(WatchPoint)、捕捉點(CatchPoint)、信號(Signals)及線程停止(Thread Stops)。

如果要恢復程序運行,可以使用c或是continue命令。

1. 設置斷點(BreakPoint)  

用break命令來設置斷點。有下面幾種設置斷點的方法:

break <function>

在進入指定函數時停住。C++中可以使用class::function或function(type,type)格式來指定函數名。

break <linenum>

在指定行號停住。

break +offset

break -offset

在當前行號的前面或後面的offset行停住。offiset爲自然數。

break filename:linenum

在源文件filename的linenum行處停住。

break filename:function

在源文件filename的function函數的入口處停住。

break *address

在程序運行的內存地址處停住。

break

該命令沒有參數時,表示在下一條指令處停住。

break ... if <condition>

condition表示條件,在條件成立時停住。比如在循環體中,可以設置break if i=100,表示當i爲100時停住程序。

查看斷點時,可使用info命令,如下所示(注:n表示斷點號):

info breakpoints [n]

info break [n] 

2. 設置觀察點(WatchPoint)  

觀察點一般用來觀察某個表達式(變量也是一種表達式)的值是否變化了。如果有變化,馬上停住程序。有下面的幾種方法來設置觀察點: 

watch <expr>

爲表達式(變量)expr設置一個觀察點。一旦表達式值有變化時,馬上停住程序。    

rwatch <expr>

當表達式(變量)expr被讀時,停住程序。   

awatch <expr>

當表達式(變量)的值被讀或被寫時,停住程序。

info watchpoints

列出當前設置的所有觀察點。

3. 設置捕捉點(CatchPoint)

可設置捕捉點來補捉程序運行時的一些事件。如載入共享庫(動態鏈接庫)或是C++的異常。設置捕捉點的格式爲:

catch <event>

當event發生時,停住程序。event可以是下面的內容:

●       throw 一個C++拋出的異常 (throw爲關鍵字)。

●       catch 一個C++捕捉到的異常 (catch爲關鍵字)。

●       exec 調用系統調用exec時(exec爲關鍵字,目前此功能只在HP-UX下有用)。

●       fork 調用系統調用fork時(fork爲關鍵字,目前此功能只在HP-UX下有用)。

●       vfork 調用系統調用vfork時(vfork爲關鍵字,目前此功能只在HP-UX下有)。

●       load 或 load <libname> 載入共享庫(動態鏈接庫)時 (load爲關鍵字,目前此功能只在HP-UX下有用)。

●       unload 或 unload <libname> 卸載共享庫(動態鏈接庫)時 (unload爲關鍵字,目前此功能只在HP-UX下有用)。

tcatch <event>

只設置一次捕捉點,當程序停住以後,應點被自動刪除。

4. 維護停止點

上面說了如何設置程序的停止點,gdb中的停止點也就是上述的三類。在gdb中,如果覺得已定義好的停止點沒有用了,可以使用delete、clear、disable、enable這幾個命令來進行維護。

Clear

清除所有的已定義的停止點。

clear <function>

clear <filename:function>

清除所有設置在函數上的停止點。

clear <linenum>

clear <filename:linenum>

清除所有設置在指定行上的停止點。

delete [breakpoints] [range...]

刪除指定的斷點,breakpoints爲斷點號。如果不指定斷點號,則表示刪除所有的斷點。range 表示斷點號的範圍(如:3-7)。其簡寫命令爲d。

比刪除更好的一種方法是disable停止點。disable了的停止點,gdb不會刪除,當還需要時,enable即可,就好像回收站一樣。

disable [breakpoints] [range...]

disable所指定的停止點,breakpoints爲停止點號。如果什麼都不指定,表示disable所有的停止點。簡寫命令是dis.

enable [breakpoints] [range...]

enable所指定的停止點,breakpoints爲停止點號。

enable [breakpoints] once range...

enable所指定的停止點一次,當程序停止後,該停止點馬上被gdb自動disable。

enable [breakpoints] delete range...

enable所指定的停止點一次,當程序停止後,該停止點馬上被gdb自動刪除。

5. 停止條件維護

前面在介紹設置斷點時,提到過可以設置一個條件,當條件成立時,程序自動停止。這是一個非常強大的功能,這裏,專門介紹這個條件的相關維護命令。

一般來說,爲斷點設置一個條件,可使用if關鍵詞,後面跟其斷點條件。並且,條件設置好後,可以用condition命令來修改斷點的條件(只有break和watch命令支持if,catch目前暫不支持if)。

condition <bnum> <expression>

修改斷點號爲bnum的停止條件爲expression。

condition <bnum>

清除斷點號爲bnum的停止條件。

還有一個比較特殊的維護命令ignore,可以指定程序運行時,忽略停止條件幾次。

ignore <bnum> <count>

表示忽略斷點號爲bnum的停止條件count次。

6. 爲停止點設定運行命令

可以使用gdb提供的command命令來設置停止點的運行命令。也就是說,當運行的程序在被停住時,我們可以讓其自動運行一些別的命令,這很有利行自動化調試。

commands [bnum]

... command-list ...

end

爲斷點號bnum指定一個命令列表。當程序被該斷點停住時,gdb會依次運行命令列表中的命令。

例如:

break foo if x>0

commands

printf "x is %d/n",x

continue

end

斷點設置在函數foo中,斷點條件是x>0,如果程序被斷住後,也就是一旦x的值在foo函數中大於0,gdb會自動打印出x的值,並繼續運行程序。

如果要清除斷點上的命令序列,那麼只要簡單地執行一下commands命令,並直接在輸入end就行了。

7. 斷點菜單

在C++中,可能會重複出現同一個名字的函數若干次(函數重載)。在這種情況下,break <function>不能告訴gdb要停在哪個函數的入口。當然,可以使用break <function(type)>,也就是把函數的參數類型告訴gdb,以指定一個函數。否則的話,gdb會列出一個斷點菜單供用戶選擇所需要的斷點。只要輸入菜單列表中的編號就可以了。如:

(gdb) b String::after

[0] cancel

[1] all

[2] file:String.cc; line number:867

[3] file:String.cc; line number:860

[4] file:String.cc; line number:875

[5] file:String.cc; line number:853

[6] file:String.cc; line number:846

[7] file:String.cc; line number:735

> 2 4 6

Breakpoint 1 at 0xb26c: file String.cc, line 867.

Breakpoint 2 at 0xb344: file String.cc, line 875.

Breakpoint 3 at 0xafcc: file String.cc, line 846.

Multiple breakpoints were set.

Use the "delete" command to delete unwanted

breakpoints.

(gdb)   

可見,gdb列出了所有after的重載函數,選一下列表編號就行了。0表示放棄設置斷點,1表示所有函數都設置斷點。

8. 恢復程序運行和單步調試

當程序被停住後,可以用continue命令恢復程序的運行直到程序結束,或下一個斷點到來。也可以使用step或next命令單步跟蹤程序。

continue [ignore-count]

c [ignore-count]

fg [ignore-count]

恢復程序運行,直到程序結束,或是下一個斷點到來。ignore-count表示忽略其後的斷點次數。continue,c,fg三個命令都是一樣的意思。

step <count>

單步跟蹤,如果有函數調用,它會進入該函數。進入函數的前提是,此函數被編譯有debug信息。很像VC等工具中的step in。後面可以加count也可以不加,不加表示一條條地執行,加表示執行後面的count條指令,然後再停住。

next <count>

同樣單步跟蹤,如果有函數調用,它不會進入該函數(很像VC等工具中的step over)。後面可以加count也可以不加,不加表示一條條地執行,加表示執行後面的count條指令,然後再停住。

set step-mode

set step-mode on

打開step-mode模式。在進行單步跟蹤時,程序不會因爲沒有debug信息而不停住。這個參數有很利於查看機器碼。

set step-mod off

關閉step-mode模式。

finish

運行程序,直到當前函數完成返回。並打印函數返回時的堆棧地址和返回值及參數值等信息。

until 或 u

當厭倦了在一個循環體內單步跟蹤時,這個命令可以運行程序直到退出循環體。

stepi 或 si

nexti 或 ni

單步跟蹤一條機器指令。一條程序代碼有可能由數條機器指令完成,stepi和nexti可以單步執行機器指令。與之一樣有相同功能的命令是display/i $pc,當運行完這個命令後,單步跟蹤會在顯示出程序代碼的同時顯示出機器指令(也就是彙編代碼)。

9. 信號(Signals)

信號是一種軟中斷,是一種處理異步事件的方法。

一般來說,操作系統都支持許多信號,尤其是Linux,比較重要的應用程序一般都會處理信號。Linux定義了許多信號,比如SIGINT表示中斷字符信號,也就是Ctrl+C的信號,SIGBUS表示硬件故障的信號;SIGCHLD表示子進程狀態改變信號;SIGKILL表示終止程序運行的信號等。信號量編程是UNIX下非常重要的一種技術。

gdb有能力在調試程序的時候處理任何一種信號。可以告訴gdb需要處理哪一種信號;可以要求gdb收到所指定的信號時,馬上停住正在運行的程序,以供用戶進行調試。可用gdb的handle命令來完成這一功能。

handle <signal> <keywords...>

在gdb中定義一個信號處理。信號<signal>可以以SIG開頭或不以SIG開頭,可以定義一個要處理信號的範圍(如:SIGIO-SIGKILL,表示處理從SIGIO信號到SIGKILL的信號,其中包括SIGIO,SIGIOT,SIGKILL三個信號),也可以使用關鍵字all來標明要處理所有的信號。一旦被調試的程序接收到信號,運行程序馬上會被gdb停住,以供調試。其<keywords>可以是以下幾種關鍵字中的一個或多個。

nostop

當被調試的程序收到信號時,gdb不會停住程序的運行,但會顯示出消息告訴用戶收到這種信號。

stop

當被調試的程序收到信號時,gdb會停住程序。

print

當被調試的程序收到信號時,gdb會顯示出一條信息。

noprint

當被調試的程序收到信號時,gdb不會告訴用戶收到信號的信息。

Pass

noignore

當被調試的程序收到信號時,gdb不處理信號。這表示gdb會把這個信號交給被調試程序處理。

Nopass

ignore

當被調試的程序收到信號時,gdb不會讓被調試程序來處理這個信號。

info signals

info handle

查看有哪些信號在被gdb檢測。

 

 

10. 線程(Thread Stops)

如果程序是多線程的話,可以定義斷點是否在所有的線程上,或是在某個特定的線程上。gdb很容易完成這一工作。

break <linespec> thread <threadno>

break <linespec> thread <threadno> if ...

linespec指定了斷點設置的源程序的行號。threadno指定了線程的ID,注意,這個ID是gdb分配的,可以通過info threads命令來查看正在運行程序中的線程信息。如果不指定thread <threadno>則表示斷點設在所有線程上面。還可以爲某線程指定斷點條件。如:

(gdb) break frik.c:13 thread 28 if bartab > lim

當程序被gdb停住時,所有的運行線程都會被停住。這方便用戶查看運行程序的總體情況。而在恢復程序運行時,所有的線程也會被恢復運行。那怕是主進程在被單步調試時。

4.2.6 查看棧信息

當程序被停住時,需要做的第一件事就是查看程序是在哪裏停住的。當程序調用了一個函數時,函數的地址、函數參數、函數內的局部變量都會被壓入“棧”(Stack)中。可以用gdb命令來查看當前的棧中的信息。

下面是一些查看函數調用棧信息的gdb命令:

backtrace

bt

打印當前的函數調用棧的所有信息。如:

(gdb) bt

#0 func (n=250) at tst.c:6

#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30

#2 0x400409ed in __libc_start_main () from /lib/libc.so.6

從上可以看出函數的調用棧信息:__libc_start_main --> main() --> func()   

backtrace <n>

bt <n>

n是一個正整數,表示只打印棧頂上n層的棧信息。

backtrace <-n>

bt <-n>

-n表示一個負整數,表示只打印棧底下n層的棧信息。     

如果要查看某一層的信息,需要切換當前的棧。一般來說,程序停止時,最頂層的棧就是當前棧,如果要查看棧下面層的詳細信息,首先要做的是切換當前棧。

frame <n>

f <n>

n是一個從0開始的整數,是棧中的層編號。比如:frame 0表示棧頂,frame 1表示棧的第二層。 

up <n>

表示向棧的上面移動n層,可以不輸入n,表示向上移動一層。

down <n>

表示向棧的下面移動n層,可以不輸入n,表示向下移動一層。    

上面的命令,都會輸出移動到的棧層的信息。如果不想讓其輸出信息。可以使用這三個命令:

●       select-frame <n> 對應於 frame 命令。

●       up-silently <n> 對應於 up 命令。

●       down-silently <n> 對應於 down 命令。

查看當前棧層的信息,可以用以下gdb命令:

frame 或 f

顯示出這些信息:棧的層編號,當前的函數名,函數參數值,函數所在文件及行號,函數執行到的語句。

info frame

info f

命令會顯示出更爲詳細的當前棧層的信息,只不過,大多數都是運行時的內內地址。比如:函數地址,調用函數的地址,被調用函數的地址,目前函數的程序語言、函數參數地址及值、局部變量的地址等。如:

(gdb) bt

#0 main () at test.c:23

#1 0x42015574 in __libc_start_main () from /lib/tls/libc.so.6

(gdb) info f

Stack level 0, frame at 0xbfffef48:

 eip = 0x80483c3 in main (test.c:23); saved eip 0x42015574

 called by frame at 0xbfffef68

 source language c.

 Arglist at 0xbfffef48, args:

 Locals at 0xbfffef48, Previous frame's sp in esp

 Saved registers:

 ebp at 0xbfffef48, eip at 0xbfffef4c

(gdb)

info args

顯示出當前函數的參數名及值。

info locals

顯示出當前函數中所有局部變量及值。  

info catch

顯示出當前函數中的異常處理信息。  

4.2.7 查看源程序

1. 顯示源代碼

gdb 可以打印出所調試程序的源代碼,當然,在程序編譯時一定要加上-g參數,把源程序信息編譯到執行文件中,不然就看不到源程序了。當程序停下來以後,gdb會報告程序停在了程序的第幾行上。可以用list命令來顯示程序的源代碼。下面介紹查看源代碼的gdb命令。

list <linenum>

顯示程序第linenum行的周圍的源程序。

list <function>

顯示函數名爲function的函數的源程序。    

list

顯示當前行後面的源程序。 

list -

顯示當前行前面的源程序。

一般是顯示當前行的上5行和下5行,或者顯示當前行的上2行和下8行,默認共顯示10行。當然,也可以定製顯示的範圍。使用下面的命令可以設置一次顯示源程序的行數:

set listsize <count>

設置一次顯示源代碼的行數。   

show listsize

查看當前listsize的設置。    

list命令還有下面的用法:

list <first>, <last>

顯示從first行到last行之間的源代碼。

list , <last>

顯示從當前行到last行之間的源代碼。

list +

向後顯示源代碼。     

一般來說,在list後面可以跟以下這些的參數:

●       <linenum>   行號

●       <+offset>   當前行號的正偏移量

●       <-offset>   當前行號的負偏移量

●       <filename:linenum> 哪個文件的哪一行

●       <function> 函數名

●       <filename:function> 哪個文件中的哪個函數

●       <*address> 程序運行時的語句在內存中的地址

2. 搜索源代碼

不僅如此,gdb還提供了源代碼搜索的命令:

forward-search <regexp>

search <regexp>

向前面搜索。

reverse-search <regexp>

全部搜索。 

其中,<regexp>就是正則表達式,也是一個字符串的匹配模式,關於正則表達式,就不在這裏講了,請查看相關資料。

3. 指定源文件的路徑

某些時候,用-g編譯過後的執行程序中只是包括了源文件的名字,沒有路徑名。gdb提供了可以讓用戶指定源文件的路徑的命令,以便gdb進行搜索。

directory <dirname ... >

dir <dirname ... >

加一個源文件路徑到當前路徑的前面。如果要指定多個路徑,在UNIX下可以使用“:”,在Windows下可以使用“;”。

directory

清除所有的自定義的源文件搜索路徑信息。

show directories

顯示定義了的源文件搜索路徑。   

4. 源代碼的內存

可以使用info line命令來查看源代碼在內存中的地址。info line後面可以跟“行號” 、“函數名” 、“文件名:行號” 、“文件名:函數名”,這個命令會顯示出所指定的源碼在運行時的內存地址,如:

((gdb) info line test.c : func

Line 4 of "test.c" starts at address 0x8048328 <func>

and ends at 0x804832e <func+6>.

還有一個命令(disassemble)可以查看源程序的當前執行時的機器碼,這個命令會把目前內存中的指令dump出來。如下面的示例表示查看函數func的彙編代碼:

(gdb) disassemble func

Dump of assembler code for function func:

0x08048328 <func+0>:    push   %ebp

0x08048329 <func+1>:    mov    %esp,%ebp

0x0804832b <func+3>:    sub    $0x8,%esp

0x0804832e <func+6>:    movl   $0x0,0xfffffffc(%ebp)

0x08048335 <func+13>:   movl   $0x0,0xfffffff8(%ebp)

0x0804833c <func+20>:   mov    0xfffffff8(%ebp),%eax

0x0804833f <func+23>:   cmp    0x8(%ebp),%eax

0x08048342 <func+26>:   jl     0x8048346 <func+30>

0x08048344 <func+28>:   jmp    0x8048355 <func+45>

0x08048346 <func+30>:   mov    0xfffffff8(%ebp),%eax

0x08048349 <func+33>:   lea    0xfffffffc(%ebp),%edx

0x0804834c <func+36>:   add    %eax,(%edx)

0x0804834e <func+38>:   lea    0xfffffff8(%ebp),%eax

0x08048351 <func+41>:   incl   (%eax)

0x08048353 <func+43>:   jmp    0x804833c <func+20>

0x08048355 <func+45>:   mov    0xfffffffc(%ebp),%eax

0x08048358 <func+48>:   leave

0x08048359 <func+49>:   ret

End of assembler dump.

(gdb)

4.2.8 查看運行時數據 

在調試程序時,當程序被停住時,可以使用print命令(簡寫命令爲p),或是同義命令inspect來查看當前程序的運行數據。print命令的格式是:

print <expr>

print /<f> <expr>

<expr>是表達式,是所調試的程序的語言的表達式(gdb可以調試多種編程語言);<f>是輸出的格式。比如,如果要把表達式按16進制的格式輸出,那麼就是/x。

 1. 表達式

print和許多gdb的命令一樣,可以接受一個表達式,gdb會根據當前的程序運行的數據來計算這個表達式。既然是表達式,那麼就可以是當前程序運行中的常量、變量、函數等內容。可惜的是gdb不能使用在程序中所定義的宏。

表達式的語法應該是當前所調試的語言的語法,由於C/C++是一種大衆型的語言,所以,本文中的例子都是關於C/C++的。而關於用gdb調試其他語言的內容,將在後面介紹。

在表達式中,有幾種gdb所支持的操作符,它們可以用在任何一種語言中。

@

是一個和數組有關的操作符,在後面會有更詳細的說明。    

::

指定一個在文件或是一個函數中的變量。  

{<type>} <addr>

表示一個指向內存地址<addr>的類型爲type的一個對象。       

2. 程序變量

在gdb中,可以隨時查看以下3種變量的值:

●       全局變量(所有文件可見的)

●       靜態全局變量(當前文件可見的)

●       局部變量(當前Scope可見的)    

如果局部變量和全局變量發生衝突(也就是重名),一般情況下是局部變量會隱藏全局變量。也就是說,如果一個全局變量和一個函數中的局部變量同名時,如果當前停止點在函數中,用print顯示出的變量的值會是函數中的局部變量的值。如果此時想查看全局變量的值,可以使用“::”操作符:

file::variable

function::variable

可以通過這種形式指定所要查看的變量,是哪個文件中的或是哪個函數中的。例如,查看文件f2.c中的全局變量x的值:

(gdb) p 'f2.c'::x

當然,“::”操作符會和C++中的發生衝突,gdb能自動識別“::”是否C++的操作符,所以不必擔心在調試C++程序時會出現異常。

另外,需要注意的是,如果程序編譯時開啓了優化選項,那麼在用gdb調試被優化過的程序時,可能會發生某些變量不能訪問,或是取值錯誤的情況。這個是很正常的,因爲優化程序會刪改程序,整理程序的語句順序,剔除一些無意義的變量等。所以在gdb調試這種程序時,運行時的指令和所編寫的指令就不一樣,也就會出現想象不到的結果。對付這種情況,需要在編譯程序時關閉編譯優化。一般來說,幾乎所有的編譯器都支持編譯優化的開關,例如,GNU的C/C++編譯器GCC,可以使用-gstabs選項來解決這個問題。關於編譯器的參數,請查看編譯器的使用說明文檔。

3. 數組

有時候需要查看一段連續的內存空間的值。比如數組的一段,或是動態分配的數據的大小。可以使用gdb的@操作符。@的左邊是第一個內存地址的值,@的右邊則是想查看的內存的長度。例如,程序中有這樣的語句:

int *array = (int *) malloc (len *sizeof (int));

於是,在gdb調試過程中,可以用如下命令顯示出這個動態數組的取值:

p *array@len

@的左邊是數組的首地址的值,也就是變量array所指向的內容,右邊則是數據的長度,其保存在變量len中。其輸出結果,大約是下面這個樣子的:   

(gdb) p *array@len
        $1={2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40}

如果是靜態數組的話,直接用print數組名就可以顯示數組中所有數據的內容了。

4. 輸出格式

一般來說,gdb會根據變量的類型輸出變量的值。但也可以自定義gdb的輸出格式。例如,想輸出一個整數的十六進制或是二進制來查看這個整型變量中的位的情況。要做到這樣,可以使用gdb的數據顯示格式:

●       x  按十六進制格式顯示變量。

●       d  按十進制格式顯示變量。

●       u  按十六進制格式顯示無符號整型。

●       o  按八進制格式顯示變量。

●       t  按二進制格式顯示變量。

●       a  按十六進制格式顯示變量。

●       c  按字符格式顯示變量。

●       f  按浮點數格式顯示變量。

(gdb) p i

$21 = 101   

(gdb) p/a i

$22 = 0x65

(gdb) p/c i

$23 = 101 'e'

(gdb) p/f i

$24 = 1.41531145e-43

(gdb) p/x i

$25 = 0x65

(gdb) p/t i

$26 = 1100101

5. 查看內存

可以使用examine命令(簡寫是x)來查看內存地址中的值。x命令的語法如下所示:

x/<n/f/u> <addr> 

●       n、f、u是可選的參數。 

●       n 是一個正整數,表示顯示內存的長度,也就是說從當前地址向後顯示幾個地址的內容。

●       f 表示顯示的格式,參見上面。如果地址所指的是字符串,那麼格式可以是s,如果是指令地址,那麼格式可以是i。

●       u 表示從當前地址往後請求的字節數,如果不指定的話,gdb默認是4個bytes。u參數可以用下面的字符來代替:b表示單字節,h表示雙字節,w表示四字節,g表示八字節。當指定了字節長度後,gdb會從指定的內存地址開始,讀寫指定字節,並把其當作一個值取出來。

●       <addr>表示一個內存地址。

n/f/u三個參數可以一起使用。例如:

x/3uh 0x54320

表示從內存地址0x54320讀取內容,h表示以雙字節爲1個單位,3表示3個單位,u表示按十六進制顯示。

6. 自動顯示

可以設置一些自動顯示的變量,當程序停住時,或是在單步跟蹤時,這些變量會自動顯示。相關的gdb命令是display。

display <expr>

display/<fmt> <expr>

display/<fmt> <addr>

expr是一個表達式,fmt表示顯示的格式,addr表示內存地址。當用display設定好了一個或多個表達式後,只要程序停下來,gdb會自動顯示所設置的這些表達式的值。  

格式i和s同樣被display支持,一個非常有用的命令是: 

display/i $pc

$pc是gdb的環境變量,表示指令的地址,/i則表示輸出格式爲機器指令碼,也就是彙編。於是當程序停下後,就會出現源代碼和機器指令碼相對應的情形,這是一個很有意思的功能。

下面是一些和display相關的gdb命令:

undisplay <dnums...>

delete display <dnums...>

刪除自動顯示,dnums意爲設置好了的自動顯示的編號。如果要同時刪除幾個編號,可以用空格分隔;如果要刪除一個範圍內的編號,可以用減號表示(如:2-5)。

disable display <dnums...>

enable display <dnums...>

disable和enalbe不刪除自動顯示的設置,而只是讓其失效和恢復。

info display

查看display設置的自動顯示的信息。gdb會顯示出一張表格,報告調試中設置了多少個自動顯示設置,其中包括設置的編號、表達式及是否enable。

7. 設置顯示選項

gdb中關於顯示的選項比較多,這裏只列舉大多數常用的選項。

set print address

set print address on

打開地址輸出,當程序顯示函數信息時,gdb會顯出函數的參數地址。系統默認爲打開的,如:       

(gdb) f

#0 set_quotes (lq=0x34c78 "<<", rq=0x34c88 ">>")

at input.c:530

530         if (lquote != def_lquote)

set print address off

關閉函數的參數地址顯示,如:

(gdb) set print addr off

(gdb) f

#0 set_quotes (lq="<<", rq=">>") at input.c:530

530         if (lquote != def_lquote)

show print address

查看當前地址顯示選項是否打開。  

set print array

set print array on

打開數組顯示。打開後當數組顯示時,每個元素佔一行;如果不打開的話,每個元素以逗號分隔。這個選項默認是關閉的。

set print array off

show print array

set print elements <number-of-elements>

這個選項主要是設置數組的,如果數組太大了,就可以指定一個<number-of-elements>來指定數據顯示的最大長度,當達到這個長度時,gdb就不再往下顯示了。如果設置爲0,則表示不限制。  

show print elements

查看print elements的選項信息。       

set print null-stop <on/off>

如果打開了這個選項,那麼當顯示字符串時,遇到結束符則停止顯示。這個選項默認爲off。  

set print pretty on

如果打開printf pretty這個選項,那麼當gdb顯示結構體時會比較漂亮。如:

                             $1 = {

                                next = 0x0,

                                          flags = {

                                                  sweet = 1,

                                                 our = 1

                                          },

                                          meat = 0x54 "Pork"

                           }

set print pretty off

關閉printf pretty這個選項,gdb顯示結構體時會如下顯示:

$1 = {next = 0x0, flags = {sweet = 1, sour = 1}, meat = 0x54 "Pork"}  

show print pretty

查看gdb是如何顯示結構體的。

set print sevenbit-strings <on/off>

設置字符顯示,是否按“/nnn”的格式顯示。如果打開,則字符串或字符數據按/nnn顯示,如“/065”。

show print sevenbit-strings

查看字符顯示開關是否打開。 

set print union <on/off>

設置顯示結構體時,是否顯示其內的聯合體數據。例如有以下數據結構:

               typedef enum {Tree, Bug} Species;

                      typedef enum {Big_tree, Acorn, Seedling} Tree_forms;

                    typedef enum {Caterpillar, Cocoon, Butterfly}

                             Bug_forms;

       

                    struct thing {

                  Species it;

                  union {

                Tree_forms tree;

                Bug_forms bug;

                    } form;

                    };

struct thing foo = {Tree, {Acorn}};

當打開這個開關時,執行 p foo 命令後,會如下顯示:

$1 = {it = Tree, form = {tree = Acorn, bug = Cocoon}}

當關閉這個開關時,執行 p foo 命令後,會如下顯示:

$1 = {it = Tree, form = {...}}

show print union

查看聯合體數據的顯示方式。

set print object <on/off>

在C++中,當一個對象指針指向其派生類時,如果打開這個選項,gdb會自動按照虛方法調用的規則顯示輸出,如果關閉這個選項,gdb就不管虛函數表了。這個選項默認是off。

show print object

查看對象選項的設置。  

set print static-members <on/off>

這個選項表示,當顯示一個C++對象中的內容時,是否顯示其中的靜態數據成員。默認是on。

show print static-members

查看靜態數據成員選項設置。  

set print vtbl <on/off>

當此選項打開時,gdb將用比較規整的格式來顯示虛函數表時。其默認是關閉的。 

show print vtbl

查看虛函數顯示格式的選項。     

8. 歷史記錄

當用gdb的print查看程序運行時的數據時,每一個print都會被gdb記錄下來。gdb會以$1, $2, $3 .....這樣的方式爲每一個print命令編上號。於是,可以使用這個編號訪問以前的表達式,如$1。這個功能所帶來的好處是,如果先前輸入了一個比較長的表達式,並想查看這個表達式的值,可以使用歷史記錄來訪問,省去了重複輸入。  

9. gdb環境變量

可以在gdb的調試環境中定義自己的變量,用來保存一些調試程序中的運行數據。

要定義一個gdb的變量很簡單,只需使用gdb的set命令。gdb的環境變量和UNIX一樣,也是以$起頭。如:

set $foo = *object_ptr 

使用環境變量時,gdb會在第一次使用時創建這個變量,而在以後的使用中,則直接對其賦值。環境變量沒有類型,可以給環境變量定義任意的類型,包括結構體和數組。

show convenience

該命令查看當前所設置的所有的環境變量。    

這是一個比較強大的功能,環境變量和程序變量的交互使用將使得程序調試更爲靈活便捷。例如:

set $i = 0

print bar[$i++]->contents

輸入這樣的命令後,只用按回車鍵,重複執行上一條語句,環境變量會自動累加,從而完成逐個輸出的功能。   

10. 查看寄存器

要查看寄存器的值,很簡單,可以使用如下命令:

info registers

查看寄存器的情況(除了浮點寄存器)。

info all-registers

查看所有寄存器的情況(包括浮點寄存器)。

info registers <regname ...>

查看所指定的寄存器的情況。

寄存器中放置了程序運行時的數據,比如程序當前運行的指令地址(ip),程序的當前堆棧地址(sp)等。同樣可以使用print命令來訪問寄存器的情況,只需要在寄存器名字前加一個$符號就可以了,如p。

4.2.9 改變程序的執行

一旦使用gdb掛上被調試程序,當程序運行起來後,可以根據自己的調試思路來動態地在gdb中更改當前被調試程序的運行線路或是其變量的值。這個強大的功能能夠讓用戶更好地調試程序。比如,可以在程序的一次運行中走遍程序的所有分支。

1. 修改變量值

修改被調試程序運行時的變量值在gdb中很容易實現,使用gdb的print命令即可完成。如:

(gdb) print x=4

x=4這個表達式是C/C++的語法,意爲把變量x的值修改爲4,如果當前調試的語言是Pascal,那麼可以使用Pascal的語法x:=4。

在某些時候,變量很有可能和gdb中的參數衝突,如:      

(gdb) whatis width

type = double

(gdb) p width

$4 = 13

(gdb) set width=47

Invalid syntax in expression.

因爲,set width是gdb的命令,所以,出現了Invalid syntax in expression的設置錯誤。此時,可以使用set var命令來告訴gdb,width不是gdb的參數,而是程序的變量名,如:

(gdb) set var width=47    

另外,還可能有些情況,gdb並不報告這種錯誤。所以保險起見,在改變程序變量取值時,最好都使用set var格式的gdb命令。   

2. 跳轉執行

一般來說,被調試程序會按照程序代碼的運行順序依次執行。gdb提供了亂序執行的功能,也就是說,gdb可以修改程序的執行順序,可以讓程序執行隨意跳躍。這個功能可以由gdb的jump命令來實現:

jump <linespec>

指定下一條語句的運行點。<linespce>可以是文件的行號,可以是file:line格式,可以是+num這種偏移量格式。表示下一條運行語句從哪裏開始。

 

jump <address>

這裏的<address>是代碼行的內存地址。

注意:

jump命令不會改變當前的程序棧中的內容,所以,從一個函數跳到另一個函數時,當函數運行完返回進行彈棧操作時必然會發生錯誤,可能結果還是非常奇怪的,甚至於產生程序Core Dump。所以最好是在同一個函數中進行跳轉。

熟悉彙編的人都知道,程序運行時,有一個寄存器用於保存當前代碼所在的內存地址。所以,jump命令也就是改變了這個寄存器中的值。可以使用set $pc來更改跳轉執行的地址。如: 

set $pc = 0x485

3. 產生信號量

使用singal命令可以產生一個信號量給被調試的程序。如中斷信號Ctrl+C。這非常方便於程序的調試,可以在程序運行的任意位置設置斷點,並在該斷點用gdb產生一個信號量。精確地在某處產生信號非常有利程序的調試。

其語法是:

signal <singal>

Linux的系統信號量通常從1到15。所以<singal>的取值也在這個範圍。

signal命令和shell的kill命令不同,系統的kill命令發信號給被調試程序時,是由gdb截獲的,而signal命令所發出的信號則是直接發給被調試程序的。

4. 強制函數返回

    如果調試斷點在某個函數中,還有語句沒有執行完,可以使用return命令強制函數忽略還沒有執行的語句並返回。

return

return <expression>

使用return命令取消當前函數的執行,並立即返回。如果指定了<expression>,那麼該表達式的值會被當作函數的返回值。

5. 強制調用函數

    call <expr>

表達式中也可以是函數,以達到強制調用函數的目的,顯示函數的返回值,如果函數返回值是void,那麼就不顯示。

另一個相似的命令也可以完成這一功能——print。print後面可以跟表達式,所以也可以用它來調用函數。print和call的不同之處是,如果函數返回void,call則不顯示,print則顯示函數返回值,並把該值存入歷史數據中。

6. 在不同語言中使用gdb

gdb支持下列語言:C、C++、Fortran、PASCAL、Java、Chill、assembly 和 Modula-2。一般來說,gdb會根據所調試的程序來確定所用的調試語言。比如:發現文件名後綴爲.c的,gdb會認爲是C程序;文件名後綴爲.C、.cc、.cp、.cpp、.cxx、.c++的,gdb會認爲是C++程序;後綴是.f, .F的,gdb會認爲是Fortran程序;後綴爲.s、.S的會認爲是彙編語言。

也就是說,gdb會根據所調試的程序的語言,來設置自己的語言環境,並讓gdb的命令跟着語言環境的改變而改變。比如一些gdb命令需要用到表達式或變量時,這些表達式或變量的語法,完全是根據當前的語言環境而改變的。例如,C/C++中對指針的語法是*p,而在Modula-2中則是p^。並且,如果當前的程序是由幾種不同語言一同編譯成的,到調試過程中,gdb也能根據不同的語言自動地切換語言環境。這種跟着語言環境而改變的功能,確實是一種體貼開發人員的設計。

下面是幾個關於gdb語言環境的命令:

show language

查看當前的語言環境。如果gdb不能識別所調試的編程語言,那麼,C語言被認爲是默認的環境。

info frame

查看當前函數的程序語言。

info source

查看當前文件的程序語言。

如果gdb沒有檢測出當前的程序語言,那麼用戶也可以手動設置當前的程序語言。使用set language命令即可做到。

如果set language命令後什麼也不跟,可以查看gdb所支持的語言種類:   

(gdb) set language

The currently understood settings are:

 

local or auto    Automatic setting based on source file

c              Use the C language

c++             Use the C++ language

asm             Use the Asm language

fortran        Use the Fortran language

java            Use the Java language

modula-2       Use the Modula-2 language

pascal                  Use the Pascal language

scheme                 Use the Scheme language

可以在set language後加上被列出來的程序語言名,來設置當前的語言環境。

gdb是一個強大的命令行調試工具。大家知道命令行的強大在於,其可以形成執行序列,形成腳本。Linux下的軟件以命令行的較多,這給程序開發提供了極大的便利。命令行軟件的優勢在於,它們可以非常容易地集成在一起,使用幾個簡單的已有工具的命令,就可以實現一個非常強大的功能。

 因此Linux下的軟件比Windows下的軟件更能有機地結合,各自發揮長處,組合起來具有更爲強大的功能。而Windows下的圖形軟件基本上是各自爲營,互相不能調用,很不利於各種軟件的相互集成。這裏並不是要和Windows作個什麼比較,所謂“寸有所長,尺有所短”,圖形化工具還是有不如命令行的地方。

 

 

第5章 使用 make

make是一個解釋makefile文件中的指令的命令工具。一般來說,大多數的IDE都有這個命令,比如DelphimakeVisual C++nmakeLinuxGNUmake

什麼是makefile文件?make命令執行時,需要一個 makefile文件,以告訴make命令需要怎樣去編譯和連接程序。或許很多Windows程序員都不知道這個工具,因爲那些WindowsIDE都沒有提供該功能。作一個專業的程序員尤其是作爲Linux下的程序員,要進行Linux下的軟件編程,理解makefile文件是必需的,因爲會不會寫makefile文件,直接關係到是否具備完成大型工程的能力,makefile文件關係到了整個工程的編譯規則。這就好像儘管現在有很多HTML編輯器,但如果想成爲一個專業網頁設計師,還需要了解HTML標識的含義一樣。

一個工程中的源文件數量很多,其按類型、功能、模塊分別放在若干個目錄中,makefile文件定義了一系列的規則來指定哪些文件需要先編譯,哪些文件需要後編譯,哪些文件需要重新編譯,甚至進行更復雜的操作。makefile文件就像一個Shell腳本,其中也可以執行操作系統的命令。makefile文件帶來的好處是——自動化編譯,一旦寫好,只需要一個make命令,就可自動編譯整個工程,極大地提高了軟件開發的效率。

5.1 makefile實例文件分析

本部分將用一個示例來說明如何建立一個makefile文件,以便給大家一個感性認識。這個示例來源於GNUmake使用手冊,工程中有8C文件和3個頭文件,要寫一個makefile文件來告訴make命令如何編譯和連接這幾個文件。makefile文件的操作規則是:

●       如果這個工程沒有編譯過,所有C文件都要編譯並被連接。

●       如果這個工程的某幾個C文件被修改,只需編譯被修改的C文件,並連接目標程序。

●       如果這個工程的頭文件被改變了,需要編譯引用了這幾個頭文件的C文件,並連接目標程序。

只要makefile文件寫得足夠好,所有的這一切,只用一個make命令就可以完成,make命令會自動智能地根據當前文件的修改情況來確定哪些文件需要重新編譯,從而自動編譯所需要的文件並連接目標程序。

在講述這個makefile文件之前,還是先來粗略地看一看下面的代碼:

target ... : prerequisites ...

command

...

...

上面的代碼中target是一個目標文件可以是Object 文件也可以是執行文件還可以是一個標籤(Label)。對於標籤的特性,在5.3.5節中講解。

prerequisites是要生成的target所需要的文件或是目標。

commandmake需要執行的命令(任意的Shell命令)

這是一個文件的依賴關係,target這一個或多個的目標文件依賴於prerequisites中的文件,其生成規則定義在command中。prerequisites中如果有一個以上的文件比target文件更新的話,command所定義的命令就會執行。這就是makefile文件的規則,也就是makefile文件中最核心的內容。下面結合實例作詳細說明。

5.1.1 makemakefile文件的關係

下面通過一個實例來講述makemakefile文件的關係。

實例5-1是一個完整的makefile文件,在一個工程中有3個頭文件和8C文件,其中應用到了前面講述的3個規則。

實例5-1

 

 

edit : main.o kbd.o command.o display.o /

           insert.o search.o files.o utils.o

            gcc -o edit main.o kbd.o command.o display.o /

                       insert.o search.o files.o utils.o

    main.o : main.c defs.h

            gcc -c main.c

    kbd.o : kbd.c defs.h command.h

            gcc -c kbd.c

    command.o : command.c defs.h command.h

            gcc -c command.c

    display.o : display.c defs.h buffer.h

            gcc -c display.c

    insert.o : insert.c defs.h buffer.h

            gcc -c insert.c

    search.o : search.c defs.h buffer.h

            gcc -c search.c

    files.o : files.c defs.h buffer.h command.h

            gcc -c files.c

    utils.o : utils.c defs.h

            gcc -c utils.c

    clean :

            rm edit main.o kbd.o command.o display.o /

               insert.o search.o files.o utils.o

 

提示:

反斜槓/是換行符的意思。這樣使makefile文件更易讀。可以把這個內容保存在makefile文件makefile文件夾的文件中,然後在該目錄下直接輸入命令make,就可以生成執行文件edit。如果要刪除執行文件和所有的中間目標文件,只要簡單地執行一下make clean就可以了。

在這個makefile文件中,目標文件(target)包含如下內容:執行文件edit和中間目標文件(*.o);依賴文件(prerequisites),即冒號後面的那些 .c 文件和 .h文件。每一個 .o 文件都有一組依賴文件,而這些 .o 文件又是執行文件edit的依賴文件。依賴關係的實質是說明目標文件由哪些文件生成,換言之,目標文件是哪些文件更新的結果。在定義好依賴關係後,後續的代碼定義瞭如何生成目標文件的操作系統命令,其一定要以一個Tab鍵作爲開頭。

提示:

make並不管命令是怎麼工作的,它只管執行所定義的命令。make會比較targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期比targets文件的日期要新,或者target不存在,make就會執行後續定義的命令。另外,clean不是一個文件,它只不過是一個動作名字,有點像C語言中的lable一樣,冒號後什麼也沒有,這樣make就不會自動去找文件的依賴性,也就不會自動執行其後所定義的命令。要執行其後的命令,就要在make命令後明顯地指出這個lable的名字。這樣的方法非常有用,可以在一個makefile文件中定義不用的編譯或是和編譯無關的命令,比如程序的打包或備份等。

在默認方式下,只輸入make命令。其會做如下工作:

make會在當前目錄下找名字爲“makefile文件”或“makefile文件夾”的文件。如果找到,它會找文件中的第一個目標文件(target)。在上面的例子中,它會找到edit這個文件,並把這個文件作爲最終的目標文件;如果edit文件不存在,或是edit所依賴的後面的 .o 文件的修改時間要比edit這個文件新,它就會執行後面所定義的命令來生成edit文件。

如果edit所依賴的.o文件也存在,make會在當前文件中找目標爲.o文件的依賴性,如果找到,則會根據規則生成.o文件(這有點像一個堆棧的過程)。

當然,C文件和H文件如果存在,make會生成 .o 文件,然後再用 .o 文件生成make的最終結果,也就是執行文件edit。

這就是整個make的依賴性,make會一層又一層地去找文件的依賴關係,直到最終編譯出第一個目標文件。在找尋的過程中,如果出現錯誤,比如最後被依賴的文件找不到,make就會直接退出,並報錯。而對於所定義的命令的錯誤,或是編譯不成功,make就不會處理。如果在make找到了依賴關係之後,冒號後面的文件不存在,make仍不工作。

通過上述分析,可以看出像clean這樣沒有被第一個目標文件直接或間接關聯時,它後面所定義的命令將不會被自動執行,不過,可以顯式使make執行。即使用命令make clean,以此來清除所有的目標文件,並重新編譯。

在編程中,如果這個工程已被編譯過了,當修改了其中一個源文件時,比如file.c,根據依賴性,目標file.o會被重新編譯(也就是在這個依賴性關係後面所定義的命令),則file.o文件也是最新的,即file.o文件的修改時間要比edit要新,所以edit也會被重新連接了。而如果改變了command.hkdb.ocommand.ofiles.o都會被重新編譯,並且edit會被重新連接。

5.1.2 makefile文件中使用變量

在上面的例子中,先通過實例5-2來看看edit的規則。

實例5-2

 

 

edit : main.o kbd.o command.o display.o /

                  insert.o search.o files.o utils.o

            gcc -o edit main.o kbd.o command.o display.o /

                       insert.o search.o files.o utils.o

可以看到,[.o]文件的字符串被重複了兩次。如果這個工程需要加入一個新的[.o]文件,需要在兩個位置插入(實際是3個位置,還有一個位置在clean)。當然,這個makefile文件並不複雜,所以在兩個位置加就可以了。但如果makefile文件變得複雜,就要在第3個位置插入,該位置容易被忘掉,從而會導致編譯失敗。所以,爲了makefile文件的易維護,在makefile文件中可以使用變量。makefile文件的變量也就是一個字符串,可以理解成C語言中的宏。比如,聲明一個變量objects,在makefile文件一開始可以這樣定義,見實例5-3

實例5-3

 

 

objects = main.o kbd.o command.o display.o /

              insert.o search.o files.o utils.o

於是就可以很方便地在makefile文件中以$(objects)的方式來使用這個變量了。改良版的makefile文件就變成實例5-4的樣子

實例5-4

 

 

objects = main.o kbd.o command.o display.o /

              insert.o search.o files.o utils.o

    edit : $(objects)

            gcc -o edit $(objects)

    main.o : main.c defs.h

            gcc -c main.c

    kbd.o : kbd.c defs.h command.h

            gcc -c kbd.c

    command.o : command.c defs.h command.h

            gcc -c command.c

    display.o : display.c defs.h buffer.h

            gcc -c display.c

    insert.o : insert.c defs.h buffer.h

            gcc -c insert.c

    search.o : search.c defs.h buffer.h

            gcc -c search.c

    files.o : files.c defs.h buffer.h command.h

            gcc -c files.c

    utils.o : utils.c defs.h

            gcc -c utils.c

    clean :

            rm edit $(objects)

如果有新的.o文件加入,只需簡單地修改一下objects變量就可以了。

5.1.3 讓make自動推導依賴關係

GNUmake功能很強大,它可以自動推導文件以及文件依賴關係後面的命令,此時就沒有必要在每一個[.o]文件後都寫上類似的命令,因爲make會自動識別,並自己推導命令。

只要make看到一個.o文件,它就會自動把[.c]文件加在依賴關係中;如果make找到whatever.o,則whatever.c就會成爲whatever.o的依賴文件。並且 gcc -c whatever.c 也會被推導出來,於是,makefile文件再也不用寫得太複雜,可以簡化爲實例5-5

實例5-5

 

 

objects = main.o kbd.o command.o display.o /

              insert.o search.o files.o utils.o

    edit : $(objects)

            gcc -o edit $(objects)

    main.o : defs.h

    kbd.o : defs.h command.h

    command.o : defs.h command.h

    display.o : defs.h buffer.h

    insert.o : defs.h buffer.h

    search.o : defs.h buffer.h

    files.o : defs.h buffer.h command.h

    utils.o : defs.h

    .PHONY : clean

    clean :

            rm edit $(objects) 

這種方法也就是make隱晦規則。上面的文件內容中,.PHONY表示clean是個僞目標文件。

5.1.4 另類風格的makefile文件

即然make可以自動推導命令,則可以將過多的[.o][.h]進行簡化,刪除重複的[.h],結果如實例5-6

實例5-6

 

 

objects = main.o kbd.o command.o display.o /

              insert.o search.o files.o utils.o

    edit : $(objects)

           gcc -o edit $(objects)

    $(objects) : defs.h

    kbd.o command.o files.o : command.h

    display.o insert.o search.o files.o : buffer.h

    .PHONY : clean

    clean :

            rm edit $(objects)

 

注意

這種風格讓makefile文件變得很簡單,但文件依賴關係就顯得有點凌亂了。魚和熊掌不可得兼,所以並不推薦這種風格,一是文件的依賴關係看不清楚,二是文件一多,要加入幾個新的.o文件,那就更不清楚了。

5.1.5 清空目標文件的規則

每個makefile文件中都應該寫一個清空目標文件(.o和執行文件)的規則,這不僅便於重新編譯,也很利於保持文件的清潔。一般的風格如下:

實例5-7

 

 

clean:

            rm edit $(objects)

更爲穩健的做法是:

實例5-8

 

 

.PHONY : clean

        clean :

                -rm edit $(objects)

前面說過,.PHONY表示clean是一個僞目標,而在rm命令前面加了一個小減號的目的是,如果某些文件出現問題將被忽略,繼續進行後面的操作。當然,clean的規則不要放在文件的開頭,否則會變成make的默認目標。不成文的規矩是clean從來都放在文件的最後

上面講述的實例是一個makefile文件的概貌,也是編寫一般makefile文件的基礎。

5.2 makefile文件概述

makefile文件主要包含了5部分內容:顯式規則、隱式規則、變量定義、文件指示和註釋。

●       顯式規則。顯式規則說明了如何生成一個或多個目標文件。這要由makefile文件的創作者指出,包括要生成的文件、文件的依賴文件、生成的命令。

●       隱式規則。由於make有自動推導的功能,所以隱式的規則可以比較粗糙地簡略書寫makefile文件,這是由make所支持的。

●       變量定義。在makefile文件中要定義一系列的變量,變量一般都是字符串,這有點兒像C語言中的宏。當makefile文件執行時,其中的變量都會擴展到相應的引用位置上。

●       文件指示。其包括3個部分,一個是在一個makefile文件中引用另一個makefile文件,就像C語言中的include一樣;另一個是指根據某些情況指定makefile文件中的有效部分,就像C語言中的預編譯#if一樣;還有就是定義一個多行的命令。

●       註釋。makefile文件中只有行註釋,和UNIX的Shell腳本一樣,其註釋用“#”字符,這個就像C/C++中的“/* */”和“//”一樣。如果要在makefile文件中使用“#”字符,可以用反斜框進行轉義,如:“/#”。

技巧:

makefile文件中的命令必須要以[Tab]鍵開始。

默認情況下,make命令會在當前目錄下按順序找尋文件名爲GNUmakefile文件Makefile文件makefile文件的文件,找到後解釋這個文件。在這3個文件名中,最好使用makefile文件這個文件名。最好不要用GNUmakefile文件,這個文件是GNUmake識別的。注意,一些makeMakefile文件文件名不敏感,但是大多數的make都支持Makefile文件makefile文件這兩種默認文件名。當然,可以使用別的文件名來書寫makefile文件,比如:Make.LinuxMake.SolarisMake.AIX等。如果要指定特定的makefile文件,可以使用make-f--file參數,如:make -f Make.Linux。命令如下:

[david@DAVID david]$ make -f makelinux

makefile文件中使用include關鍵字可以把別的makefile文件包含進來這類似於C語言的#include被包含的文件會保持原來狀態並放在當前文件的包含位置。include的語法是:

    include <filename>

filename可以是當前操作系統Shell的文件模式(可以保含路徑和通配符)。在include前面可以有一些空字符但是絕不能以[Tab]鍵開始。include<filename>可以用一個或多個空格隔開。舉個例子,有這樣幾個makefile文件:a.mkb.mkc.mk,還有一個文件叫foo.make,以及一個變量$(bar),其包含了e.mkf.mk,下面的語句:

include foo.make *.mk $(bar)

等價於:

include foo.make a.mk b.mk c.mk e.mk f.mk

make命令開始時會找尋include所指出的其他makefile文件並把其內容安置在當前的位置就好像C/C++#include指令一樣。如果文件都沒有指定絕對路徑或相對路徑,make會在當前目錄下首先尋找,如果當前目錄下沒有找到,make還會在下面的幾個目錄下尋找:

●       如果make執行時,有-I或--include-dir參數,make就會在這個參數所指定的目錄下去尋找。

●       如果目錄<prefix>/include(一般是:/usr/local/bin或/usr/include)存在,make也會去找。

如果有文件沒有找到,make會生成一條警告信息,但不會馬上出現致命錯誤。它會繼續載入其他文件,一旦完成makefile文件的讀取,make會再重試這些沒有找到或是不能讀取的文件,如果還是不行,make纔會出現一條致命信息。如果想讓make不理那些無法讀取的文件,而繼續執行,可以在include前加一個減號。如:

-include <filename>

這表示,無論include過程中出現什麼錯誤,都不會報錯而是繼續執行。和其他版本make兼容的相關命令是sinclude,其作用和inculde相同。

如果當前環境中定義了環境變量MAKEFILESmake會把這個變量中的值作一個類似於include的動作。這個變量中的值是其他的makefile文件,用空格分隔。只是它和include不同的是,從這個環境變量中引入的makefile文件的目標不會起作用,如果環境變量中定義的文件發現錯誤,make也會忽略。

在這裏建議不要使用這個環境變量,因爲只要這個變量一旦被定義,當使用make時,所有的makefile文件都會受到它的影響,這絕不是希望看到的結果。此處是爲了提醒大家,也許有時候makefile文件出現了問題,此時可以看看當前環境中有沒有定義這個變量。

GNUmake工作時的執行步驟如下:

(1) 讀入所有的makefile文件。

(2) 讀入被include包括的其他makefile文件。

(3) 初始化文件中的變量。

(4) 推導隱式規則,並分析所有規則。

(5) 爲所有的目標文件創建依賴關係鏈。

(6) 根據依賴關係,決定哪些目標要重新生成。

(7) 執行生成命令。

(1)(5)步爲第一個階段,(6)(7)爲第二個階段。第一個階段中,如果定義的變量被使用了,make會把其在使用的位置展開。但make並不會馬上完全展開,make使用的是拖延戰術,如果變量出現在依賴關係的規則中,僅當這條依賴被決定要使用了,變量纔會在其內部展開。

5.3 make書寫規則

make書寫規則包含兩個部分,一個是依賴關係,一個是生成目標的方法。在makefile文件中,規則的順序是很重要的。因爲makefile文件中只應該有一個最終目標,其他的目標都是被這個目標所連帶出來的,所以一定要讓make知道最終目標是什麼。一般來說,定義在makefile文件中的目標可能會有很多,但是第一條規則中的目標將被確立爲最終的目標。make所完成的也就是這個目標。

5.3.1 規則舉例

實例5-9

 

 

foo.o : foo.c defs.h       # foo模塊

      gcc -c -g foo.c

看到這個例子,應該不是很陌生了,前面也已說過,foo.o是目標,foo.cdefs.h是目標所依賴的源文件,此處可使用命令gcc -c -g foo.c (Tab鍵開頭)。這個規則說明兩件事:

●       文件的依賴關係。foo.o依賴於foo.c和defs.h文件,如果foo.c和defs.h文件的日期比foo.o文件的日期新,或者foo.o不存在,則發生依賴關係。

●       如果生成(或更新)foo.o文件,要用到gcc命令。

5.3.2 在規則中使用通配符

如果想定義一系列比較類似的文件,很自然地就想起使用通配符。make支持3種通配符:*?[...]。波浪號(~)字符在文件名中也有比較特殊的用途。如果是~/test,就表示當前用戶的$HOME目錄下的test目錄。而~hchen/test則表示用戶hchen的宿主目錄下的test目錄(這些都是Linux下的常識,make也支持)。而在Windows或是MS-DOS下,用戶沒有宿主目錄,波浪號所指的目錄則根據環境變量HOME而定。

通配符代替了一系列的文件,如*.c表示後綴爲c的文件。一個需要注意的是,如果文件名中有通配符,如*,可以用轉義字符/,如/*來表示真實的*字符,而不是任意長度的字符串。

下面還是先來看幾個例子:

實例5-10

 

 

clean:

   rm -f *.o

實例5-10是操作系統Shell所支持的通配符。這是在命令中的通配符。

實例5-11

 

 

print: *.c

lpr -p $?

touch print

實例5-11說明了通配符也可以在規則中,目標print依賴於所有的[.c]文件。其中的$?是一個自動化變量,將會在後面作詳細介紹。

實例5-12

 

 

objects = *.o

實例5-12表示了,通配符同樣可以用在變量中。makefile文件中的變量其實就是C/C++中的宏。如果要讓通配符在變量中展開,也就是讓objects的值成爲所有[.o]的文件名的集合,可以像實例5-13這樣:

實例5-13

 

 

objects := $(wildcard *.o)

這種用法由關鍵字wildcard指出。

5.3.3 文件搜尋

在一些大的工程中,有大量的源文件,通常的做法是把這許多的源文件分類,並存放在不同的目錄中。所以,當make需要去找尋文件的依賴關係時,可以在文件前加上路徑,但最好的方法是把一個路徑告訴make,讓make自動去找。makefile文件中的特殊變量VPATH就是完成這個功能的。如果沒有指明這個變量,make只會在當前的目錄中去找尋依賴文件和目標文件;如果定義了這個變量,make就會在當前目錄找不到的情況下,到所指定的目錄中去找尋文件。

實例5-14

VPATH = src:../headers

實例5-14的定義指定兩個目錄:src../headersmake會按照這個順序進行搜索。目錄由冒號分隔(當然,當前目錄永遠是最高優先搜索的位置)。另一個設置文件搜索路徑的方法是使用makevpath關鍵字(注意,它是全小寫的),這不是變量,這是一個make的關鍵字,而這和上面提到的那個VPATH變量很類似,但是它更爲靈活。它可以指定不同的文件在不同的搜索目錄中。這是一個很靈活的功能,它的使用方法有三種:

●       vpath <pattern> <directories>

爲符合模式<pattern>的文件指定搜索目錄<directories>

●       vpath <pattern>

清除符合模式<pattern>的文件搜索目錄。

●       vpath

清除所有已被設置好了的文件搜索目錄。

vapth使用方法中的<pattern>需要包含%字符。%的意思是匹配一個以上的字符,例如,%.h表示所有以.h結尾的文件。<pattern>指定了要搜索的文件集,而<directories>則指定了<pattern>的文件集的搜索目錄。例如實例5-15

實例5-15

vpath %.h ../headers    

該語句表示,要求make../headers目錄下搜索所有以.h結尾的文件(如果某文件在當前目錄沒有找到的話)。可以連續地使用vpath語句,以指定不同搜索策略。如果連續的vpath語句中出現了相同的<pattern>,或是被重複了的<pattern>make會按照vpath語句的先後順序來執行搜索。如實例5-16

 

實例5-16

vpath %.c foo

vpath %   blish

vpath %.c bar

其表示.c結尾的文件先在foo目錄然後在blish目錄最後在bar目錄中進行搜索。

實例5-17

vpath %.c foo:bar

vpath %   blish

實例5-17的語句則表示.c結尾的文件先在foo目錄然後在bar目錄最後纔在blish目錄中進行搜索。

5.3.4 僞目標

在前面的例5-1中,提到過一個clean的目標,這是一個僞目標

實例5-18

clean:

rm *.o temp

正像前面例子中的clean一樣,既然生成了許多編譯文件,也應該提供一個清除它們的目標以備完整地重新編譯時用(make clean來使用該目標)。因爲並不生成clean這個文件,僞目標並不是一個文件,只是一個標籤。由於僞目標不是文件,所以make無法生成它的依賴關係和決定它是否要執行,只有通過顯式地指明這個目標才能讓其生效。當然,僞目標的取名不能和文件名重名,不然其就失去了僞目標的意義了。

爲了避免和文件重名這種情況,可以使用一個特殊的標記.PHONY來顯式地指明一個目標是僞目標,向make說明不管是否有這個文件,這個目標就是僞目標

PHONY : clean

只要有這個聲明,不管是否有clean文件,要運行clean這個目標,整個過程可以這樣寫:

.PHONY: clean

clean:

     rm *.o temp

僞目標一般沒有依賴的文件,但是,也可以爲僞目標指定所依賴的文件。僞目標同樣可以作爲默認目標,只要將其放在最前面即可。比如,如果makefile文件需要連續生成若干個可執行文件,而只想簡單地輸入一個make就讓其執行,並且所有的目標文件都寫在一個makefile文件中,可以使用僞目標這個特性,如實例5-19

實例5-19

all : prog1 prog2 prog3

    .PHONY : all

  prog1 : prog1.o utils.o

            gcc -o prog1 prog1.o utils.o

 prog2 : prog2.o

            gcc -o prog2 prog2.o

 prog3 : prog3.o sort.o utils.o

            gcc -o prog3 prog3.o sort.o utils.o

makefile文件中的第1個目標會被作爲其默認目標,聲明瞭一個all的僞目標,其依賴於其他3個目標。由於僞目標的特性是總會被執行,所以其依賴的那3個目標就總不如all這個目標新。所以,其他3個目標的規則總是會被採納,也就達到了一下子生成多個目標的目的。.PHONY : all聲明瞭all這個目標爲僞目標

從實例5-19可以看出,目標也可以成爲依賴關係。所以,僞目標同樣也可成爲依賴關係,如實例5-20

實例5-20

.PHONY: cleanall cleanobj cleandiff

    cleanall : cleanobj cleandiff

            rm program

    cleanobj :

            rm *.o

    cleandiff :

            rm *.diff

make clean將清除所有需要被清除的文件。cleanobjcleandiff這兩個僞目標有點像子程序的意思。可以輸入make cleanallmake cleanobj以及make cleandiff命令來達到清除不同種類文件的目的。

5.3.5 多目標

makefile文件規則中的目標可以不止一個,其支持多目標。有可能多個目標同時依賴於一個文件,並且其生成的命令大體類似,於是就能把其合併起來。當然,多個目標的生成規則的執行命令是同一個,這可能會帶來麻煩,不過可以使用一個自動化變量$@。這個變量表示目前規則中所有目標的集合,這樣說可能很抽象,如實例5-21

 

實例5-21

bigoutput littleoutput : text.g

generate text.g -$(subst output,,$@) > $@

上述規則等價於實例5-22

實例5-22

bigoutput : text.g

generate text.g -big > bigoutput

littleoutput : text.g

generate text.g -little > littleoutput

其中-$(subst output$@)中的$表示執行一個makefile文件的函數函數名爲subst後面的爲參數。$@表示目標的集合,就像一個數組,$@依次取出目標,並執行命令。

5.3.6 靜態模式

靜態模式可以更加容易地定義多目標的規則,可以讓規則變得更加靈活和有彈性。語法如下:

實例5-23

<targets ...>: <target-pattern>: <prereq-patterns ...>

          <commands>

          ...

●       targets定義了一系列的目標文件,可以有通配符,表示目標的一個集合。

●       target-pattern指明瞭targets的模式,也就是目標集模式。

●       prereq-patterns是目標的依賴模式,它對target-pattern形成的模式再進行一次依賴目標的定義。

如果<target-pattern>定義成%.o,表示<target>集合中都是以.o結尾的;而如果<prereq-patterns>定義成%.c,表示對<target-pattern>所形成的目標集進行二次定義。其計算方法是,取<target-pattern>模式中的%(也就是去掉了擴展符[.o]),併爲其加上擴展符[.c],從而形成新的集合。

所以,目標模式或是依賴模式中都應該有%這個字符,如果文件名中有%,可以使用反斜槓/進行轉義,以標明真實的%字符。

看一個例子:

實例5-24

objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c

$(gcc) -c $(CFLAGS) $< -o $@

實例5-24指明瞭目標從$object中獲取%.o代表所有以.o結尾的目標也就是foo.o bar.o即變量$object集合的模式而依賴模式%.c則取模式%.o%也就是foo bar併爲其加上.c的後綴於是依賴目標就是foo.c bar.c。而命令中的$<$@是自動化變量,$<表示所有的依賴目標集(也就是foo.c bar.c)$@表示目標集(也就是foo.o bar.o)。於是,上面的規則展開後等價於實例5-25的規則:

實例5-25

foo.o : foo.c

   $(gcc) -c $(CFLAGS) foo.c -o foo.o

bar.o : bar.c

   $(gcc) -c $(CFLAGS) bar.c -o bar.o

試想,如果“%.o”有幾百個,只要用這種很簡單的“靜態模式規則”就可以寫完一堆規則,簡化多了。“靜態模式規則”的用法很靈活,如果用得好,將是一個很強大的功能。再看一個例子:

實例5-26

 files = foo.elc bar.o lose.o

    $(filter %.o,$(files)): %.o: %.c

            $(gcc) -c $(CFLAGS) $< -o $@

    $(filter %.elc,$(files)): %.elc: %.el

            emacs -f batch-byte-compile $<

$(filter %.o,$(files))表示調用makefile文件的filter函數過濾$filter這個例子展示了makefile文件更大的彈性。

5.3.7  自動生成依賴性

makefile文件中,依賴關係可能會需要包含一系列的頭文件,比如,如果main.c中有一句#include "defs.h",依賴關係應該是:

main.o : main.c defs.h

但是,如果是一個比較大型的工程,必須清楚哪些C文件包含了哪些頭文件,並且,在加入或刪除頭文件時,也需要小心地修改makefile文件,這是一項繁瑣的工作。爲了避免這種繁瑣而又容易出錯的工作,可以使用GCC的一個-MM的選項,即自動尋找源文件中包含的頭文件,並生成一個依賴關係。例如,如果執行下面的命令:

gcc -M main.c

其輸出是:

main.o : main.c defs.h

於是由編譯器自動生成依賴關係,而不必再手動書寫若干文件的依賴關係,並由編譯器自動生成。

編譯器的這個功能如何與makefile文件聯繫在一起呢?因爲makefile文件也要根據這些源文件重新生成,讓makefile文件自已依賴於源文件。這樣並不現實,不過可以用其他手段來迂迴地實現這一功能。GNU組織建議把編譯器爲每一個源文件自動生成的依賴關係放到一個文件中,爲每一個name.c的文件都生成一個name.dmakefile文件,.d文件中就存放了對應.c文件的依賴關係。

於是,可以寫出.c文件和.d文件的依賴關係,讓make自動更新或生成.d文件,並把其包含在主makefile文件中,就可以自動地生成每個文件的依賴關係了。這裏,給出了一個模式規則來產生.d文件:

%.d: %.c

      @set -e; rm -f $@; /

      $(gcc) -M $(CPPFLAGS) $< > $@.$$$$; /

      sed 's,/($*/)/.o[ :]*,/1.o $@ : ,g' < $@.$$$$ > $@; /

      rm -f $@.$$$$

這個規則的意思是所有的.d文件依賴於.c文件。rm -f $@的意思是刪除所有的目標也就是.d文件。第二行的意思是,爲每個依賴文件$<,也就是.c文件生成依賴文件,$@表示模式%.d文件,如果有一個C文件是name.c%就是name$$$$意爲一個隨機編號,第2行生成的文件有可能是name.d.12345,第3行使用sed命令作了一個替換,第4行就是刪除臨時文件。

總之,這個模式要做的事就是在編譯器生成的依賴關係中加入.d文件的依賴,即把依賴關係:

main.o : main.c defs.h

轉成:

main.o main.d : main.c defs.h

於是,.d文件也會自動更新,並會自動生成。當然,在這個.d文件中加入的不只是依賴關係,生成的命令也可一併加入,讓每個[.d]文件都包含一個完賴的規則。一旦完成這個工作,接下來就要把這些自動生成的規則放進主makefile文件中。可以使用makefile文件的include命令來引入別的makefile文件(前面講過),例如:

sources = foo.c bar.c

include $(sources:.c=.d)

上述語句$(sources:.c=.d)中的.c=.d的意思是作一個替換把變量$(sources)中所有[.c]的字串都替換成[.d]關於這個替換的內容在後面會有更爲詳細的講述。當然,使用時應注意次序,因爲include是按次序來載入文件,最先載入的[.d]文件中的目標會成爲默認目標。

5.4 使用命令

每條規則中的命令和操作系統Shell的命令行是一致的。make會按順序一條一條地執行命令,每條命令必須以[Tab]鍵開頭,除非命令緊跟在依賴規則後面的分號後。在命令行之間的空格或是空行會被忽略,但是如果該空格或空行是以Tab鍵開頭的,make會認爲其是一個空命令,除非特別指定一個其他的Shellmakefile文件中,#是註釋符,很像C/C++中的//,其後的本行字符都視爲註釋。

5.4.1 顯示命令

通常,make會把其要執行的命令行在命令執行前輸出到屏幕上。當在命令行前用@字符時,這個命令將不被make顯示出來。最具代表性的例子是,用這個功能向屏幕顯示一些信息。如:

@echo 正在編譯XXX模塊......

當執行make時,會輸出正在編譯XXX模塊……字串,但不會輸出命令。如果沒有@make將輸出:

echo 正在編譯XXX模塊......

正在編譯XXX模塊......

如果make執行時,帶入make參數-n--just-print,其只是顯示命令,但不會執行命令。這個功能有利於調試makefile文件,可預覽書寫的命令的運行順序及結果。make參數-s--slient表示全面禁止命令的顯示。

5.4.2 執行命令

當依賴目標新於目標時,也就是當規則的目標需要更新時,make會一條一條地執行其後的命令。需要注意的是,如果要讓上一條命令的結果應用在下一條命令上,應該使用分號分隔這兩條命令。比如第一條命令是cd命令,希望第二條命令在cd之後的基礎上運行,就不能把這兩條命令寫在兩行上,而應該把這兩條命令寫在一行上,用分號分隔。如

示例一:

exec:

   cd /home/hchen

     pwd

示例二

exec:

    cd /home/hchen; pwd

當執行make exec第一個例子中的cd沒起作用pwd會打印出當前的makefile文件目錄而第二個例子中cd就起作用了pwd會打印出/home/hchen

5.4.3 命令出錯

每當命令運行完後,make會檢測每個命令的返回碼。如果命令返回成功,make會執行下一條命令,當規則中所有的命令成功返回後,這個規則就算是成功完成了。如果一個規則中的某個命令出錯了(命令退出碼非零)make就會終止執行當前規則,這將有可能終止所有規則的執行。

有些時候,命令的出錯並不表示就是錯誤的。例如,mkdir命令用於建立一個目錄,如果目錄不存在,mkdir就成功執行,萬事大吉;如果目錄存在,就會出錯。在使用mkdir時,不希望因mkdir出錯而終止規則的運行。此時就要忽略命令的出錯信息,此時可以在makefile文件中的命令行前加一個減號-(Tab鍵之後),則此時不管命令是否出錯,都認爲是成功的,如實例5-27

實例5-27

clean:

      -rm -f *.o

 還有一個辦法是make加上-i或是--ignore-errors參數這樣makefile文件中的所有命令都會忽略錯誤。而如果一個規則是以.IGNORE作爲目標的,這個規則中的所有命令都將會忽略錯誤。這些是不同級別的防止命令出錯的方法,可以根據自己的需要設置。

還有需要提一下的make參數是-k或是--keep-going,這個參數的意思是,如果某規則中的命令出錯了,就終止該規則的執行,但繼續執行其他規則。

5.4.4 嵌套執行make

在一些大的工程中,會將不同模塊及不同功能的源文件放在不同的目錄中,可以在每個目錄中都書寫一個該目錄的makefile文件,這有利於讓makefile文件變得更加簡潔,而不至於把所有的東西全部寫在一個makefile文件中,這樣維護makefile文件時會變得困難。這個技術對於模塊編譯和分段編譯有非常大的好處。

例如,有一個子目錄叫subdir,這個目錄下有個makefile文件,來指明這個目錄下文件的編譯規則。總控的makefile文件可以這樣書寫:

subsystem:

   cd subdir && $(MAKE)

其等價於

subsystem:

   $(MAKE) -C subdir

定義$(MAKE)宏變量是因爲,也許make需要一些參數,所以定義成一個變量比較利於維護。這兩個例子的意思都是先進入subdir目錄,然後執行make命令。

把這個makefile文件叫做總控makefile文件,總控makefile文件的變量可以傳遞到下級的makefile文件中(如果顯式地聲明),但是不會覆蓋下層的makefile文件中所定義的變量,除非指定了-e參數。

如果要傳遞變量到下級makefile文件中,可以使用這樣的聲明:

export <variable ...>

如果不想讓某些變量傳遞到下級makefile文件中,可以這樣聲明:

unexport <variable ...>

示例一:

export variable = value

其等價於

variable = value

export variable

等價於:

export variable := value

等價於:

variable := value

export variable

示例二

export variable += value

其等價於

variable += value

export variable

如果要傳遞所有的變量,只要一個export就行了。後面什麼也不用跟,表示傳遞所有的變量。

需要注意的是,有兩個變量,一個是SHELL,一個是MAKEFLAGS,這兩個變量不管是否進行輸出,其總是要傳遞到下層makefile文件中。特別是MAKEFILES變量,其中包含了make的參數信息,如果執行總控makefile文件時有make參數或是在上層makefile文件中定義了這個變量,MAKEFILES變量將會是這些參數,並會傳遞到下層makefile文件中,這是一個系統級的環境變量。

但是make命令中有幾個參數並不往下傳遞,它們是-C-f-h-o-W。如果不想往下層傳遞參數,可以這樣寫:

subsystem:

cd subdir && $(MAKE) MAKEFLAGS=

如果定義了環境變量MAKEFLAGS,確信其中的選項是大家都會用到的。如果其中有-t-n-q參數,將會有意想不到的結果。

還有一個在嵌套執行中比較有用的參數,-w或是--print-directory會在make執行的過程中輸出一些信息,並看到目前的工作目錄。比如,如果下級make目錄是/home/hchen/gnu/make,如果使用make -w來執行,當進入該目錄時,會看到:

make: Entering directory '/home/hchen/gnu/make'.

而在完成下層make後離開目錄時,會看到:

make: Leaving directory '/home/hchen/gnu/make'

當使用-C參數來指定make下層makefile文件時,-w會自動打開。如果參數中有-s (--slient)或是--no-print-directory-w總是失效的。

5.4.5 定義命令包

如果makefile文件中出現一些相同命令序列,可以爲這些相同的命令序列定義一個變量。定義這種命令序列的語法以define開始,以endef結束,如:

實例5-28

define run-yacc

 yacc $(firstword $^)

   mv y.tab.c $@

endef

這裏run-yacc是這個命令包的名字其不要和makefile文件中的變量重名。在defineendef中的兩行就是命令序列。這個命令包中的第一個命令是運行Yacc程序,因爲Yacc程序總是生成y.tab.c的文件,所以第二行的命令就是把這個文件改個名字。還是把這個命令包放到一個實例5-29中來看一下效果。

實例5-29

foo.c : foo.y

       $(run-yacc)

可以看見,要使用這個命令包,就好像使用變量一樣。在這個命令包的使用中,命令包run-yacc中的$^就是foo.y$@就是foo.cmake在執行命令包時,命令包中的每個命令會依次獨立執行。

5.5 使用變量

makefile文件中定義的變量,就像是C/C++語言中的宏一樣,它代表了一個文本字串,在makefile文件中執行的時候,其會自動原樣展開在所使用的位置。其與C/C++所不同的是,可以在makefile文件中改變其值。在makefile文件中,變量可以使用在目標、依賴目標、命令或是makefile文件的其他部分中。變量的命名字可以包含字符、數字、下劃線(可以是數字開頭),但不應該含有:#=或是空字符(空格、回車等)

變量是大小寫敏感的,fooFooFOO3個不同的變量名。傳統的makefile文件的變量名是全大寫的命名方式,但推薦使用大小寫搭配的變量名,如MakeFlags。這樣可以避免因與系統的變量衝突而導致意外的事情。

有一些變量是很奇怪的字串,如$<$@等,這些是自動化變量。

5.5.1 變量的基礎

變量在聲明時需要給予初值,而在使用時,需要在變量名前加上$符號,但最好用小括號()或是大括號{}把變量包括起來。如果要使用真實的$字符,需要用$$來表示。變量可以使用在許多位置,如規則中的目標、依賴、命令以及新的變量中。先看實例5-30

實例5-30

objects = program.o foo.o utils.o

    program : $(objects)

           gcc -o program $(objects)

    $(objects) : defs.h

變量會在使用它的位置精確地展開就像C/C++中的宏一樣。例如

實例5-31

foo = c

    prog.o : prog.$(foo)

            $(foo)$(foo) -$(foo) prog.$(foo)

展開後得到:

prog.o : prog.c

gcc -c prog.c

當然,千萬不要在makefile文件中這樣使用,這裏只是舉個例子來表明makefile文件中的變量在使用處展開的真實樣子。可見其就是一個替代作用。另外,給變量加上括號完全是爲了更加安全地使用這個變量,在上面的例子中,如果不想給變量加上括號也可以,但還是強烈建議給變量加上括號,因爲這樣可使代碼更清晰。

5.5.2 賦值變量

在定義變量的值時,可以使用其他變量來構造變量的值,在makefile文件中有兩種方式可以用變量定義變量的值。

先看第一種方式,也就是簡單地使用=號,在=左側是變量,右側是變量的值,右側變量的值可以定義在文件的任何一處,也就是說,右側中的變量不一定非要是已定義好的值,其也可以使用後面定義的值。如實例5-32

實例5-32

foo = $(bar)

    bar = $(ugh)

    ugh = Huh?

    all:

            echo $(foo)

執行make all將會打出變量$(foo)的值是Huh?($(foo)的值是$(bar)$(bar)的值是$(ugh)$(ugh)的值是Huh?)。可見,變量是可以使用後面的變量來定義的。

這個功能有利有弊,好處是可以把變量的真實值推到後面來定義,如:

CFLAGS = $(include_dirs) -O

    include_dirs = -Ifoo -Ibar

CFLAGS在命令中被展開時,會是-Ifoo -Ibar -O。但這種形式也有弊端,那就是遞歸定義,如:

CFLAGS = $(CFLAGS) -O

或:

A = $(B)

B = $(A)

這會讓make陷入無限的變量展開過程中。當然,make有能力檢測這樣的定義,並會報錯。另外,如果在變量中使用函數,這種方式會讓make運行時非常慢,更糟糕的是,它在使用兩個make函數wildcardshell時會出現不可預知的錯誤,因爲不會知道這兩個函數會被調用多少次。

爲了避免上面的情形,可以使用make中的另一種用變量來定義變量的方法。這種方法使用的是:=操作符,如:

x := foo

y := $(x) bar

x := later

其等價於:

y := foo bar

x := later

值得一提的是,前面的變量不能使用後面的變量,只能使用前面已定義好了的變量。如果是這樣:

y := $(x) bar

x := foo

y的值是bar,而不是foo bar

上面都是一些比較簡單的變量應用。下面來看一個複雜的例子,其中包括了make函數、條件表達式和一個系統變量MAKELEVEL的使用:

實例5-33

ifeq (0,${MAKELEVEL})

    cur-dir := $(shell pwd)

    whoami   := $(shell whoami)

    host-type := $(shell arch)

    MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}

    endif

系統變量MAKELEVEL表示:如果make有一個嵌套執行動作,這個變量會記錄當前makefile文件的調用層數。

請先看一個例子,如果要定義一個變量,其值是一個空格,可以這樣處理:

nullstring :=

    space := $(nullstring) # end of the line

nullstring是一個Empty變量,不含任何內容,而space的值是一個空格。因爲在操作符的右邊是很難描述一個空格的,這裏採用的技術很管用,先用一個Empty變量來標明變量定義開始,後面再採用#註釋符來表示變量定義終止,這樣,可以定義出其值是一個空格的變量。請注意這裏關於#的使用,註釋符#的這種特性值得注意。如果定義一個變量:

dir := /foo/bar    # directory to put the frobs in

dir變量的值是/foo/bar後面還跟了4個空格如果使用該變量來指定別的目錄——$(dir)/file就會出現不可預期的效果。

還有一個比較有用的操作符是?=,先看示例:

實例5-34

FOO ?= bar 

其含義是,如果FOO沒有被定義過,變量FOO的值就是bar;如果FOO先前被定義過,這條語句將什麼也不做,其等價於:

實例5-35

If eq ($(origin FOO), undefined)

      FOO = bar

    endif

5.5.3 變量的高級用法

這裏介紹兩種變量的高級使用方法,第一種是變量值的替換。

可以替換變量中的共有部分,其格式是$(var:a=b)或是${var:a=b}。其意思是,把變量var中所有以a字串結尾的a替換成b字串。這裏的結尾意思是空格或是結束符。

再看一個示例:

實例5-36

foo := a.o b.o c.o

    bar := $(foo:.o=.c)

這個示例中先定義了一個$(foo)變量而第二行的意思是把$(foo)中所有.o擴展符全部替換成.c所以$(bar)的值就是a.c b.c c.c

另外一種變量替換的技術是以靜態模式定義的,如實例5-37

實例5-37

foo := a.o b.o c.o

    bar := $(foo:%.o=%.c)

這依賴於被替換字串中是否有相同的模式,模式中必須包含一個%字符,這個例子同樣讓$(bar)變量的值變爲a.c b.c c.c

2種高級用法是把變量的值再當成變量。如實例5-38

實例5-38

x = y

    y = z

    a := $($(x))

在這個例子中$(x)的值是y所以$($(x))就是$(y)於是$(a)的值就是z (注意x=y而不是x=$(y))

還可以使用更多的層次:

實例5-39

x = y

    y = z

    z = u

    a := $($($(x)))

這裏的$(a)的值是u,相關的推導留給讀者自己去做。

再複雜一點,使用上在變量定義中使用變量的第1個方式,如實例5-40

  實例5-40

x = $(y)

    y = z

    z = Hello

    a := $($(x))

這裏的$($(x))被替換成了$($(y))因爲$(y)值是z所以最終結果是a:=$(z)也就是Hello

再複雜一點,再加上一些函數,如實例5-41

實例5-41

x = variable1

    variable2 := Hello

    y = $(subst 1,2,$(x))

    z = y

    a := $($($(z)))

這個例子中,$($($(z)))擴展爲$($(y)),而其再次被擴展爲$($(subst 1,2,$(x)))$(x)的值是variable1subst函數把variable1中的所有1字串替換成2字串,於是,variable1變成variable2,再取其值。所以,最終$(a)的值就是$(variable2)的值——Hello

在這種方式中,可以使用多個變量來組成一個變量的名字,然後再取其值:   

first_second = Hello

    a = first

    b = second

    all = $($a_$b)

這裏的$a_$b組成了first_second於是$(all)的值就是Hello

再來看看結合第1種技術的例子   

a_objects := a.o b.o c.o

    1_objects := 1.o 2.o 3.o

    sources := $($(a1)_objects:.o=.c)

這個例子中如果$(a1)的值是a$(sources)的值就是a.c b.c c.c如果$(a1)的值是1$(sources)的值是1.c 2.c 3.c

再來看一個這種技術和函數與條件語句一同使用的例子:   

ifdef do_sort

    func := sort

    else

    func := strip

    endif

    bar := a d b g q c

    foo := $($(func) $(bar))

這個示例中,如果定義了do_sort, :foo= $(sort a d b g q c),於是$(foo)的值就是a b c d g q。而如果沒有定義do_sort, :foo= $(sort a d b g q c),調用的就是strip函數。

當然,把變量的值再當成變量這種技術,同樣可以用在操作符的左邊:   

dir = foo

    $(dir)_sources := $(wildcard $(dir)/*.c)

    define $(dir)_print

    lpr $($(dir)_sources)

    endef

這個例子中定義了3個變量dirfoo_sourcesfoo_print

5.5.4 追加變量值

可以使用+=操作符給變量追加值,如:

objects = main.o foo.o bar.o utils.o

objects += another.o

於是$(objects)值變成main.o foo.o bar.o utils.o another.o (another.o被追加進去了)

使用+=操作符,可以模擬爲下面的這種例子:

objects = main.o foo.o bar.o utils.o

objects := $(objects) another.o

所不同的是,用+=更爲簡潔。

如果變量之前沒有定義過,+=會自動變成=;如果前面有變量定義,+=會繼承於前一次操作的賦值符;如果前一次的是:=+=會以:=作爲其賦值符,如:

variable := value

variable += more

等價於

variable := value

variable := $(variable) more

但如果是這種情況:

variable = value

variable += more

由於前次的賦值符是=,所以+=也會以=來作爲賦值,這樣就會發生變量的遞歸定義,這是我們不希望看到的。不過make會自動解決這個問題,因此不必擔心。

5.5.5 override 指示符

如果有變量是make的命令行參數設置的,makefile文件中對這個變量的賦值會被忽略。如果想在makefile文件中設置這類參數的值,可以使用override指示符。其語法是:

override <variable> = <value>

override <variable> := <value>

還可以追加:

override <variable> += <more text>

對於多行的變量定義,用define指示符,在define指示符前,也同樣可以使用ovveride指示符,如:

override define foo

    bar

    endef

5.5.6 多行變量 

還有一種設置變量值的方法是使用define關鍵字。使用define關鍵字設置變量的值可以包括換行符,這有利於定義一系列的命令。

define指示符後面跟的是變量的名字,而重起一行定義變量的值,定義以endef關鍵字結束。其工作方式和=操作符一樣。變量的值可以包含函數、命令、文字,或是其他變量。因爲命令需要以[Tab]鍵開頭,所以如果用define定義的命令變量中沒有以[Tab]鍵開頭,make就不會將其作爲命令。

實例5-42展示了define的用法:

  

實例5-42

define two-lines

    echo foo

    echo $(bar)

    endef

5.5.7 環境變量

make運行時的系統環境變量可以在make開始運行時被載入到makefile文件中,但是如果makefile文件中已定義了這個變量,或者這個變量由make命令行帶入,系統的環境變量的值將被覆蓋(如果make指定了-e參數,系統環境變量將覆蓋makefile文件中定義的變量)

因此,如果在環境變量中設置了CFLAGS環境變量,就可以在所有的makefile文件中使用這個變量了。這對於使用統一的編譯參數有比較大的好處。如果makefile文件中定義了CFLAGS,則會使用makefile文件中的這個變量;如果沒有定義,則使用系統環境變量的值,一個共性和個性的統一,很像全局變量局部變量的特性。

make嵌套調用時,上層makefile文件中定義的變量會以系統環境變量的方式傳遞到下層的makefile文件中。當然,默認情況下,只有通過命令行設置的變量會被傳遞。而定義在文件中的變量,如果要向下層makefile文件傳遞,則需要使用exprot關鍵字來聲明。

當然,並不推薦把許多變量都定義在系統環境中,這樣,在執行不用的makefile文件時,擁有的是同一套系統變量,這可能會帶來更多的麻煩。

5.5.8 目標變量

前面所講的在makefile文件中定義的變量都是全局變量,在整個文件中都可以訪問這些變量。當然,自動化變量除外,如$<等這種自動化變量屬於規則型變量,這種變量的值依賴於規則的目標和依賴目標的定義。當然,同樣可以爲某個目標設置局部變量,這種變量稱爲Target-specific Variable,它可以和全局變量同名,因爲它的作用範圍只在這條規則以及連帶規則中,所以其值也只在作用範圍內有效。而不會影響規則鏈以外的全局變量的值。其語法是:

<target ...> : <variable-assignment>

<target ...> : overide <variable-assignment>

<variable-assignment>可以是前面講過的各種賦值表達式,如=:=+=或是=。第二個語法是針對make命令行帶入的變量,或是系統環境變量。

這個特性非常有用,當設置了這樣一個變量,這個變量會作用到由這個目標所引發的所有規則中去。如實例5-43

實例5-43

prog : CFLAGS = -g

    prog : prog.o foo.o bar.o

            $(gcc) $(CFLAGS) prog.o foo.o bar.o

    prog.o : prog.c

            $(gcc) $(CFLAGS) prog.c

    foo.o : foo.c

            $(gcc) $(CFLAGS) foo.c

bar.o : bar.c

            $(gcc) $(CFLAGS) bar.c

這個示例中不管全局的$(CFLAGS)的值是什麼prog目標以及其所引發的所有規則中(prog.o foo.o bar.o的規則)$(CFLAGS)的值都是-g

5.5.9 模式變量

在GNU的make中,還支持模式變量(Pattern-specific Variable),通過上面的目標變量,變量可以定義在某個目標上。模式變量的好處就是,可以給定一種模式,可以把變量定義在符合這種模式的所有目標上。

make的模式一般是至少含有一個“%”的,所以,可以以如下方式給所有以[.o]結尾的目標定義目標變量:

%.o : CFLAGS = -O

同樣,模式變量的語法和目標變量一樣:

<pattern ...> : <variable-assignment>

<pattern ...> : override <variable-assignment>

override同樣是針對系統環境傳入的變量,或是make命令行指定的變量。

5.6 使用條件判斷

使用條件判斷,可以讓make根據運行時的不同情況選擇不同的執行分支。條件表達式可以是比較變量的值,或是比較變量和常量的值。

5.6.1 示例

下面的例子,判斷$(CC)變量是否是gcc,如果是的話,則使用GNU函數編譯目標。 

libs_for_gcc = -lgnu

    normal_libs =

    foo: $(objects)

    ifeq ($(CC),gcc)

            $(CC) -o foo $(objects) $(libs_for_gcc)

    else

            $(CC) -o foo $(objects) $(normal_libs)

    endif

可見,在上面示例的這個規則中,目標foo可以根據變量$(CC)的值選取不同的函數庫來編譯程序。

可以從上面的示例中看到3個關鍵字:ifeq、else和endif。ifeq表示條件語句的開始,並指定一個條件表達式,表達式包含兩個參數,以逗號分隔,表達式以圓括號括起。else表示條件表達式爲假的情況。endif表示一個條件語句的結束,任何一個條件表達式都應該以endif結束。

當變量$(CC)的值是gcc時,目標foo的規則是:   

foo: $(objects)

          $(CC) -o foo $(objects) $(libs_for_gcc)

而當變量$(CC)值不是gcc(比如cc),目標foo的規則是: 

foo: $(objects)

          $(CC) -o foo $(objects) $(normal_libs)

當然,還可以把上面的那個例子寫得更簡潔一些,如實例5-44:

實例5-44

ibs_for_gcc = -lgnu

    normal_libs =

    ifeq ($(CC),gcc)

      libs=$(libs_for_gcc)

    else

      libs=$(normal_libs)

    endif

    foo: $(objects)

            $(CC) -o foo $(objects) $(libs)

5.6.2 語法

條件表達式的語法爲:   

<conditional-directive>

    <text-if-true>

    endif

以及

<conditional-directive>

    <text-if-true>

    else

    <text-if-false>

    endif

其中<conditional-directive>表示條件關鍵字,如ifeq。這個關鍵字有4個。第一個是前面所見過的ifeq:

ifeq (<arg1>, <arg2>)

    ifeq '<arg1>' '<arg2>'

    ifeq "<arg1>" "<arg2>"

    ifeq "<arg1>" '<arg2>'

    ifeq '<arg1>' "<arg2>"

比較參數arg1和arg2的值是否相同。當然,參數中還可以使用make的函數。如:   

ifeq ($(strip $(foo)),)

    <text-if-empty>

    endif

這個示例中使用了strip函數,如果這個函數的返回值是空(Empty),<text-if-empty>就生效。

第2個條件關鍵字是ifneq。語法是:   

ifneq (<arg1>, <arg2>)

    ifneq '<arg1>' '<arg2>'

    ifneq "<arg1>" "<arg2>"

    ifneq "<arg1>" '<arg2>'

    ifneq '<arg1>' "<arg2>"

其比較參數arg1和arg2的值是否相同,如果不同,則爲真。

第3個條件關鍵字是ifdef。語法是:

ifdef <variable-name>

如果變量<variable-name>的值非空,那麼表達式爲真;否則,表達式爲假。當然,<variable-name>同樣可以是一個函數的返回值。注意,ifdef只是測試一個變量是否有值,其並不會把變量擴展到當前位置。還是來看兩個例子:

實例5-45

示例一:

bar =

    foo = $(bar)

    ifdef foo

    frobozz = yes

    else

    frobozz = no

    endif

示例二:

    foo =

    ifdef foo

    frobozz = yes

    else

    frobozz = no

    endif

實例5-46

    foo =

    ifdef foo

    frobozz = yes

    else

    frobozz = no

    endif

實例5-45$(frobozz)值是yes實例5-46則是no

4個條件關鍵字是ifndef。其語法是:

ifndef <variable-name>

這個和ifdef是相反的意思。在<conditional-directive>這一行上,多餘的空格是允許的,但是不能以[Tab]鍵作爲開始(不然就被認爲是命令)。而註釋符“#”同樣也是安全的。else和endif也一樣,只要不是以[Tab]鍵開始就行了。

注意:

make在讀取makefile文件時就計算條件表達式的值,並根據條件表達式的值來選擇語句,所以,最好不要把自動化變量(如$@等)放入條件表達式中,因爲自動化變量是在運行時纔有的。而且,爲了避免混亂,make不允許把整個條件語句分成兩部分放在不同的文件中。

5.7 使用函數

在makefile文件中可以使用函數來處理變量,從而讓命令或是規則更爲靈活和智能化。make所支持的函數不是很多,不過已經足夠使用了。函數調用後,函數的返回值可以當作變量來使用。

5.7.1 函數的調用語法

函數調用很像變量的使用,也是以“$”來標識的,其語法如下:  

$(<function> <arguments>)

或是:   

i ${<function> <arguments>}

這裏,<function>就是函數名,make支持的函數不多。<arguments>是函數的參數,參數間以逗號“,”分隔,而函數名和參數之間以“空格”分隔。函數調用以“$”開頭,以圓括號或花括號把函數名和參數括起。

感覺很像一個變量,是不是?函數中的參數可以使用變量。爲了風格的統一,函數和變量的括號最好一樣,如使用$(subst a,b,$(x))這樣的形式,而不是$(subst a,b,${x})的形式。因爲統一會更清楚,也會減少一些不必要的麻煩。還是來看一個例子:

實例5-47

comma:= ,

    empty:=

    space:= $(empty) $(empty)

    foo:= a b c

    bar:= $(subst $(space),$(comma),$(foo))

在這個示例中,$(comma)的值是一個逗號。$(space)使用了$(empty)定義了一個空格,$(foo)的值是“a b c”。$(bar)的定義,調用了函數subst,這是一個替換函數,這個函數有3個參數:第1個參數是被替換字串,第2個參數是替換字串,第3個參數是替換操作用的字串。這個函數也就是把$(foo)中的空格替換成逗號,所以$(bar)的值是“a,b,c”。

5.7.2 字符串處理函數

●       $(subst <from>,<to>,<text>)

名稱:字符串替換函數——subst。

功能:把字串<text>中的<from>字符串替換成<to>

返回:函數返回被替換過後的字符串。

示例:       

$(subst ee,EE,feet on the street)       

把feet on the street中的ee替換成EE,返回結果是fEEt on the strEEt。

●       $(patsubst <pattern>,<replacement>,<text>)

名稱:模式字符串替換函數——patsubst。

功能:查找<text>中的單詞(單詞以空格、Tab鍵或回車、換行符分隔)是否符合模式<pattern>,如果匹配的話,則以<replacement>替換。這裏,<pattern>可以包括通配符“%”,表示任意長度的字串。如果<replacement>中也包含“%”,<replacement>中的這個“%”將是<pattern>中的那個“%”所代表的字串 (可以用“/”來轉義,以“/%”來表示真實含義的“%”字符)。

返回:函數返回被替換過後的字符串。

示例:

$(patsubst %.c,%.o,x.c.c bar.c)

把字串x.c.c bar.c符合模式[%.c]的單詞替換成[%.o],返回結果是x.c.o bar.o。

這和前面變量章節說過的相關知識有點相似。如:

$(var:<pattern>=<replacement>)

相當於

$(patsubst <pattern>,<replacement>,$(var))

$(var: <suffix>=<replacement>)

則相當於

$(patsubst %<suffix>,%<replacement>,$(var))

例如有:

objects = foo.o bar.o baz.o,

“$(objects:.o=.c)”和“$(patsubst %.o,%.c,$(objects))”是一樣的。

$(strip <string>)

名稱:去空格函數——strip

功能:去掉<string>字串中開頭和結尾的空字符。

返回:返回被去掉空格的字符串值。

示例:       

$(strip a b c )

把字串 a b c 去掉開頭和結尾的空格結果是a b c

$(findstring <find>,<in>)

名稱:查找字符串函數——findstring。

功能:在字串<in>中查找<find>字串。

返回:如果找到,返回<find>,否則返回空字符串。

示例:

$(findstring a,a b c)

$(findstring a,b c)

第一個函數返回a字符串,第二個返回“”字符串(空字符串)

●       $(filter <pattern...>,<text>)

名稱:過濾函數——filter

功能:以<pattern>模式過濾<text>字符串中的單詞,保留符合模式<pattern>的單詞。可以有多個模式。

返回:返回符合模式<pattern>的字串。

示例:

sources := foo.c bar.c baz.s ugh.h

foo: $(sources)

cc $(filter %.c %.s,$(sources)) -o foo

$(filter %.c %.s,$(sources)) 返回的值是“foo.c bar.c baz.s”。

$(filter-out <pattern...>,<text>)

名稱:反過濾函數——filter-out

功能:以<pattern>模式過濾<text>字符串中的單詞,去除符合模式<pattern>的單詞。可以有多個模式。

返回:返回不符合模式<pattern>的字串。

示例:

objects=main1.o foo.o main2.o bar.o

mains=main1.o main2.o       

$(filter-out $(mains),$(objects)) 返回值是“foo.o bar.o”。       

$(sort <list>)

名稱:排序函數——sort。

功能:給字符串<list>中的單詞排序(升序)

返回:返回排序後的字符串。

示例:

$(sort foo bar lose) 返回“bar foo lose” 。

 

備註:

sort函數會去掉<list>中相同的單詞。

$(word <n>,<text>)

名稱:取單詞函數——word

功能:取字符串<text>中的第<n>個單詞(從一開始)

返回:返回字符串<text>中的第<n>個單詞。如果<n><text>中的單詞數要大,返回空字符串。

示例:

$(word 2, foo bar baz) 返回值是“bar”。

$(wordlist <s>,<e>,<text>) 

名稱:取單詞串函數——wordlist

功能:從字符串<text>中取從<s>開始到<e>的單詞串。<s><e>是一個數字。

返回:返回字符串<text>中從<s><e>的單詞字串。如果<s><text>中的單詞數要大,返回空字符串;如果<e>大於<text>的單詞數,返回從<s>開始,到<text>結束的單詞串。

示例:

$(wordlist 2, 3, foo bar baz) 返回值是“bar baz”。

$(words <text>)

名稱:單詞個數統計函數——words。

功能:統計<text>中字符串中的單詞個數。

返回:返回<text>中的單詞數。

示例:

$(words, foo bar baz) 返回值是“3”。

備註:

如果要取<text>中最後的一個單詞,可以這樣:$(word $(words <text>),<text>)。

$(firstword <text>)。

名稱:首單詞函數——firstword。

功能:取字符串<text>中的第一個單詞。

返回:返回字符串<text>的第一個單詞。

示例:

$(firstword foo bar) 返回值是“foo”。

備註:

這個函數可以用word函數來實現:$(word 1,<text>)。

以上是所有的字符串操作函數,如果搭配使用,可以完成比較複雜的功能。這裏,舉一個現實中應用的例子。make使用VPATH變量來指定依賴文件的搜索路徑。於是,可以利用這個搜索路徑來指定編譯器對頭文件的搜索路徑參數CFLAGS,如:

override CFLAGS += $(patsubst %,-I%,$(subst:, ,$(VPATH)))

如果$(VPATH)的值是src:../headers,$(patsubst %,-I%,$(subst :, ,$(VPATH)))將返回-Isrc -I../headers,這正是cc或gcc搜索頭文件路徑的參數。

5.7.3 文件名操作函數

下面要介紹的函數主要是處理文件名的,每個函數的參數字符串都會被當做一個或是一系列的文件名來對待。

$(dir <names...>)

名稱:取目錄函數——dir。

功能:從文件名序列<names>中取出目錄部分。目錄部分是指最後一個反斜槓“/”之前的部分。如果沒有反斜槓,返回“./”。

返回:返回文件名序列<names>的目錄部分。

示例:

$(dir src/foo.c hacks)返回值是“src/ ./”。

$(notdir <names...>)

名稱:取文件函數——notdir。

功能:從文件名序列<names>中取出非目錄部分。非目錄部分是指最後一個反斜槓“/”之後的部分。

返回:返回文件名序列<names>的非目錄部分。

示例:

$(notdir src/foo.c hacks)返回值是“foo.c hacks”。

$(suffix <names...>)    

名稱:取後綴函數——suffix。

功能:從文件名序列<names>中取出各個文件名的後綴。

返回:返回文件名序列<names>的後綴序列,如果文件沒有後綴,則返回空字串。

示例:

$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。

●       $(basename <names...>)

名稱:取前綴函數——basename。

功能:從文件名序列<names>中取出各個文件名的前綴部分。

返回:返回文件名序列<names>的前綴序列,如果文件沒有前綴,則返回空字串。

示例:

$(basename src/foo.c src-1.0/bar.c hacks) 返回值是“src/foo src-1.0/bar

hacks”。

$(addsuffix <suffix>,<names...>)

名稱:加後綴函數——addsuffix。

功能:把後綴<suffix>加到<names>中的每個單詞後面。

返回:返回加過後綴的文件名序列。

示例:

$(addsuffix .c,foo bar) 返回值是“foo.c bar.c”。

$(addprefix <prefix>,<names...>)

名稱:加前綴函數——addprefix。

功能:把前綴<prefix>加到<names>中的每個單詞後面。

返回:返回加過前綴的文件名序列。

示例:

$(addprefix src/,foo bar) 返回值是“src/foo src/bar”。

$(join <list1>,<list2>)

名稱:連接函數——join。

功能:把<list2>中的單詞對應地加到<list1>的單詞後面。如果<list1>的單詞個數比<list2>多,<list1>中多出來的單詞將保持原樣。如果<list2>的單詞個數比<list1>多,<list2>中多出來的單詞將被複制到<list2>中。

返回:返回連接過後的字符串。

示例:

$(join aaa bbb , 111 222 333)  返回值是“aaa111 bbb222 333”。

5.7.4 foreach 函數

foreach函數和別的函數不同。因爲這個函數是用來做循環用的,makefile文件中的foreach函數幾乎是仿照Unix標準Shell(/bin/sh)中的for語句,或是C-Shell(/bin/csh)中的foreach語句而構建的。它的語法是: 

$(foreach <var>,<list>,<text>)

這個函數的意思是,把參數<list>中的單詞逐一取出放到參數<var>所指定的變量中,然後再執行<text>所包含的表達式。每一次<text>會返回一個字符串,循環過程中,<text>所返回的每個字符串會以空格分隔,最後當整個循環結束時,<text>所返回的每個字符串所組成的整個字符串(以空格分隔)將會是foreach函數的返回值。所以,<var>最好是一個變量名,<list>可以是一個表達式,而<text>中一般會使用<var>這個參數來依次枚舉<list>中的單詞。如實例5-48:

 

實例5-48

names := a b c d

    files := $(foreach n,$(names),$(n).o)

上面的例子中,$(name)中的單詞會被挨個取出,並存到變量n中,$(n).o每次根據$(n)計算出一個值,這些值以空格分隔,最後作爲foreach函數的返回,所以,$(files)的值是“a.o b.o c.o d.o”。

 注意:

 foreach中的<var>參數是一個臨時的局部變量,foreach函數執行完後,參數<var>的變量將不再起作用,其作用域只在foreach函數當中。

5.7.5 if 函數 

if函數很像GNU的make所支持的條件語句——ifeq(參見前面章節所述),if函數的語法是:  

$(if <condition>,<then-part>)

或是

$(if <condition>,<then-part>,<else-part>)

可見,if函數可以包含else部分,或是不含。即if函數的參數可以是兩個,也可以是3個。<condition>參數是if的表達式,如果其返回的爲非空字符串,這個表達式就相當於返回真,於是,<then-part>會被執行,否則<else-part>會被執行。

而if函數的返回值是,如果<condition>爲真(非空字符串),那個<then-part>會是整個函數的返回值;如果<condition>爲假(空字符串),<else-part>會是整個函數的返回值,此時如果<else-part>沒有被定義,整個函數返回空字符串。所以,<then-part>和<else-part>只會有一個被執行。

5.7.6 call函數 

call函數是惟一一個可以用來創建新的參數化的函數。可以寫一個非常複雜的表達式,這個表達式中,可以定義許多參數,然後可以用call函數來向這個表達式傳遞參數。其語法是:

$(call <expression>,<parm1>,<parm2>,<parm3>...)

當make執行這個函數時,<expression>參數中的變量,如$(1),$(2),$(3)等,會被參數<parm1>,<parm2>,<parm3>依次取代。而<expression>的返回值就是call函數的返回值。例如:

實例5-49

reverse = $(1) $(2)

    foo = $(call reverse,a,b)

foo的值就是“a b”。當然,參數的次序是可以自定義的,不一定是順序的。如實例5-50:

實例5-50

reverse = $(2) $(1)

    foo = $(call reverse,a,b)

此時,foo的值就是“b a”。

5.7.7 origin函數

origin函數不像其他的函數,它並不操作變量的值,它只是告訴這個變量是哪裏來的。其語法是:

$(origin <variable>) 

 

注意:

<variable>是變量的名字,不應該是引用。所以最好不要在<variable>中使用“$”字符。

Origin函數會以其返回值來告訴這個變量的出生情況。下面是origin函數的返回值:

●       undefined

如果<variable>從來沒有定義過,origin函數返回值爲undefined。

●       default

如果<variable>是一個默認的定義,比如CC這個變量,這種變量將在後面講述。

●       environment

如果<variable>是一個環境變量,並且當makefile文件被執行時,-e參數沒有被打開。

●       file

如果<variable>這個變量被定義在makefile文件中。

●       command line

如果<variable>這個變量是被命令行定義的。

●       override

如果<variable>是被override指示符重新定義的。

●       automatic

如果<variable>是一個命令運行中的自動化變量(關於自動化變量將在後面講述)。

這些信息對於編寫makefile文件是非常有用的。例如,假設有一個makefile文件,其包了一個定義文件Make.def,在Make.def中定義了一個變量bletch,而環境中也有一個環境變量bletch,此時,可判斷一下,如果變量來源於環境,就把它重定義了,如果來源於Make.def或是命令行等非環境的,就沒有重新定義它。於是,在makefile文件中,可以這樣寫:

實例5-51

ifdef bletch

    ifeq "$(origin bletch)" "environment"

    bletch = barf, gag, etc.

    endif

    endif

當然,使用override關鍵字不就可以重新定義環境中的變量了嗎?爲什麼需要使用這樣的步驟?是的,用override是可以達到這樣的效果,可是override同時會把從命令行定義的變量也覆蓋了。這裏只想重新定義環境傳來的,而不想重新定義命令行傳來的。

5.7.8 shell函數

shell函數也不像其他函數。顧名思義,它的參數應該是操作系統Shell的命令。也就是說,shell函數把執行操作系統命令後的輸出作爲函數返回。於是,可以用操作系統命令以及字符串處理命令awk,sed等命令來生成一個變量,如: 

contents := $(shell cat foo)

files := $(shell echo *.c)

 

注意:

這個函數會新生成一個Shell程序來執行命令,所以要注意其運行性能。如果makefile文件中有一些比較複雜的規則,並大量使用了這個函數,對於系統性能是有害的。特別是makefile文件的隱式規則可能會讓shell函數執行的次數比想象的多得多。

5.7.9 控制make的函數      

make提供了一些函數來控制make的運行。通常,需要檢測一些運行makefile文件時的運行時信息,並且根據這些信息來決定是讓make繼續執行,還是停止。

$(error <text ...>)

產生一個致命的錯誤,<text ...>是錯誤信息。注意,error函數不會在一被使用時就產生錯誤信息,所以如果把其定義在某個變量中,並在後續的腳本中使用這個變量,也是可以的。例如:

實例5-52

ifdef ERROR_001

    $(error error is $(ERROR_001))

    endif

實例5-53

ERR = $(error found an error!)

    .PHONY: err

    err: ; $(ERR)

例5-52會在變量ERROR_001定義後執行時產生error調用,而示例5-53則在目錄err被執行時才發生error調用。

 $(warning <text ...>)

這個函數很像error函數,它並不會讓make退出,只是輸出一段警告信息,而make繼續執行。

5.8 make 的運行

一般來說,最簡單的就是直接在命令行下輸入make命令,make命令會查找當前目錄的makefile文件來執行,一切都是自動的。但有時也許只想讓make重編譯某些文件,而不是整個工程。而有時候有幾套編譯規則,以便在不同的時候使用不同的編譯規則。本節就講述如何使用make命令。

5.8.1 make的退出碼

make命令執行後有3個退出碼:

●       0 表示成功執行。

●       1 如果make運行時出現錯誤,其返回值爲1。

●       2 如果使用了make的-q選項,並且make使得一些目標不需要更新,返回2。

5.8.2 指定makefile文件

前面說過,GNU make找尋默認的makefile文件的規則是在當前目錄下依次找3個文件——GNUmakefile文件、Makefile文件和makefile文件。其按順序找這3個文件,一旦找到,就開始讀取這個文件並執行。

當然,也可以給make命令指定一個特殊名字的makefile文件。要實現這個功能,需要使用make的-f或是--file參數(--makefile文件參數也行)。例如,有一個makefile文件的名字是hchen.mk,可以這樣讓make來執行這個文件:

make –f hchen.mk 

如果在make的命令行中不只一次地使用了-f參數,所有指定的makefile文件將會被連在一起傳遞給make執行。

5.8.3 指定目標

一般來說,make的最終目標是makefile文件中的第一個目標,而其他目標一般是由這個目標連帶出來的,這是make的默認行爲。當然,一般來說,makefile文件中的第一個目標由許多目標組成,可以指示make,讓其完成所指定的目標。要實現這一目的很簡單,只要在make命令後直接跟目標的名字就可以完成(如前面提到的make clean形式)。

任何在makefile文件中的目標都可以被指定成終極目標,但是除了以“-”打頭,或是包含了“=”的目標。因爲有這些字符的目標,會被解析成命令行參數或是變量。甚至沒有被明確寫出來的目標也可以成爲make的終極目標。也就是說,只要make可以找到其隱含推導規則,這個隱含目標同樣可以被指定成終極目標。

有一個make的環境變量叫MAKECMDGOALS,這個變量中會存放所指定的終極目標的列表,如果在命令行上沒有指定目標,這個變量是空值。這個變量可以使用在一些比較特殊的情形下。如實例5-54:

實例5-54

sources = foo.c bar.c

    ifneq ( $(MAKECMDGOALS),clean)

    include $(sources:.c=.d)

    endif

基於上面這個例子,只要輸入的命令不是make clean,makefile文件會自動包含foo.d和bar.d這兩個makefile文件。

使用指定終極目標的方法可以很方便地編譯程序,例如實例5-55

實例5-55

PHONY: all

    all: prog1 prog2 prog3 prog4

從這個例子中可以看到,這個makefile文件中有4個需要編譯的程序——prog1、 prog2、prog3和prog4,可以使用make all命令來編譯所有的目標(如果把all置成第一個目標,只需執行make),也可以使用make prog2來單獨編譯目標prog2。

既然make可以指定所有makefile文件中的目標,也包括“僞目標”,於是可以根據這種性質來讓makefile文件根據指定的不同目標來完成不同的事。在Linux中,軟件發佈時,特別是GNU這種開放源代碼的軟件發佈時,其makefile文件都包含了編譯、安裝、打包等功能。可以參照這種規則來書寫makefile文件中的目標。

下面說明一些常用的僞目標的功能。

●        all   這個僞目標是所有目標的目標,其功能一般是編譯所有的目標。

●       clean  這個僞目標功能是刪除所有被make創建的文件。

●       install  這個僞目標功能是安裝已編譯好的程序,其實就是把目標執行文件復 制到指定的目標中去。

●       print   這個僞目標的功能是列出改變過的源文件。

●       tar  這個僞目標的功能是把源程序打包備份。也就是一個tar文件。

●       dist  這個僞目標的功能是創建一個壓縮文件,一般是把tar文件壓成Z文件, 或是gz文件。

●       TAGS  這個僞目標用於更新所有的目標,以備完整地重編譯使用。

●       check和test 這兩個僞目標一般用來測試makefile文件的流程。

當然,一個項目的makefile文件中也不一定要書寫這樣的目標,這些都是GNU的內容。如果makefile文件中有這些功能,一是很實用,二是可以使makefile文件顯得很專業。

5.8.4 檢查規則

有時候,不想讓makefile文件中的規則執行,只想檢查一下命令,或是執行的序列。於是可以使用make命令的下述參數:

-n

--just-print

--dry-run

--recon

不執行makefile中的參數,這些參數只是打印命令,不管目標是否更新,把規則和連帶規則下的命令打印出來,但不執行,這些參數對於調試makefile文件很有用處。

-t

--touch

這個參數是把目標文件的時間更新,但不更改目標文件。也就是說,make假裝編譯目標,但不是真正編譯目標,只是把目標變成已編譯過的狀態。

-q

--question

這個參數的行爲是找目標,如果目標存在,其什麼也不會輸出,當然也不會執行編譯;如果目標不存在,其會打印出一條出錯信息。

-W <file>

--what-if=<file>

--assume-new=<file>

--new-file=<file>

這個參數需要指定一個文件,一般是源文件(或依賴文件),make會根據規則推導來運行依賴於這個文件的命令。一般來說,可以和-n參數一同使用,來查看這個依賴文件所發生的規則命令。

另外一個很有意思的用法是結合-p和-v來輸出makefile文件被執行時的信息(該內容將在後面講述)。

5.8.5 make的參數

下面列舉了所有GNU make 3.80版的參數定義。其他版本和其他廠商的make大同小異,不過其他廠商的make的具體參數還是請參考各自的產品文檔。

-b

-m

這兩個參數的作用是忽略和其他版本make的兼容性。

-B

--always-make

認爲所有的目標都需要更新(重編譯)。

-C <dir>

--directory=<dir>

指定讀取makefile文件的目錄。如果有多個-C參數,make的解釋是後面的路徑以前面的路徑作爲相對路徑,並以最後的目錄作爲被指定目錄。如:

make –C ~hchen/test –C prog

等價於

make –C ~hchen/test/prog。

--debug[=<options>]

輸出make的調試信息。它有幾種不同的級別可供選擇,如果沒有參數,那就是輸出最簡單的調試信息。下面是<options>的取值:

●       a 也就是all,輸出所有的調試信息。

●       b 也就是basic,只輸出簡單的調試信息。即輸出不需要重編譯的目標。

●       v 也就是verbose,在b選項的級別之上。輸出的信息包括哪個makefile文件被解析,不需要被重編譯的依賴文件(或是依賴目標)等。

●       i 也就是implicit,輸出所有的隱含規則。

●       j 也就是jobs,輸出執行規則中命令的詳細信息,如命令的PID、返回碼等。

●       m 也就是makefile文件,輸出make,讀取makefile文件,更新makefile文件,執行makefile文件的信息。

-d

相當於“--debug=a”。

-e

--environment-overrides

指明環境變量的值,覆蓋makefile文件中定義的變量的值。

-f=<file>

--file=<file>

--makefile文件=<file>

指定需要執行的makefile文件。

-h

--help

顯示幫助信息。

-i

--ignore-errors

在執行時忽略所有的錯誤。

-I <dir>

--include-dir=<dir>

指定一個包含makefile文件的搜索目標。可以使用多個-I參數來指定多個目錄。

-j [<jobsnum>]

--jobs[=<jobsnum>]

指定同時運行命令的個數。如果沒有這個參數,make運行命令時能運行多少就運行多少。如果有一個以上的-j參數,僅最後一個-j纔是有效的 (注意這個參數在MS-DOS中是無用的)。

-k

--keep-going

出錯也不停止運行。如果生成一個目標失敗了,依賴於其上的目標就不會被執行。

-l <load>

--load-average[=<load]

--max-load[=<load>]

指定make運行命令的負載。

-n

--just-print

--dry-run

--recon

僅輸出執行過程中的命令序列,但並不執行。

-o <file>

--old-file=<file>

--assume-old=<file>

不重新生成指定的<file>,即使這個目標的依賴文件比它新。

-p

--print-data-base

輸出makefile文件中的所有數據,包括所有的規則和變量。這個參數會讓一個簡單的makefile文件都輸出一堆信息。如果只是想輸出信息而不想執行makefile文件,可以使用make -qp命令。如果想查看執行makefile文件前的預設變量和規則,可以使用make –p –f /dev/null。這個參數輸出的信息會包含makefile文件的文件名和行號,所以,用這個參數來調試的makefile文件很有用,特別是環境變量很複雜的時候。

-q

--question

不運行命令,也不輸出。僅僅是檢查所指定的目標是否需要更新。如果是0則說明要更新,如果是2則說明有錯誤發生。

-r

--no-builtin-rules

禁止make使用任何隱式規則。

-R

--no-builtin-variabes

禁止make使用任何作用於變量上的隱式規則。

-s

--silent

--quiet

在命令運行時不顯示命令的輸出。

-S

--no-keep-going

--stop

取消-k選項的作用。因爲有些時候,make的選項是從環境變量MAKEFLAGS中繼承下來的。所以可以在命令行中使用這個參數來讓環境變量中的-k選項失效。

-t

--touch

相當於Linux的touch命令,只是把目標的修改日期變成最新的,也就是阻止生成目標的命令運行。

-v

--version

輸出make程序的版本、版權等關於make的信息。

-w

--print-directory

輸出運行makefile文件之前和之後的信息。這個參數對於跟蹤嵌套式調用make時很有用。

--no-print-directory

禁止-w選項。

-W <file>

--what-if=<file>

--new-file=<file>

--assume-file=<file>

假定目標<file>需要更新,如果和-n選項使用,這個參數會輸出該目標更新時的運行動作。如果沒有-n,就像運行UNIX的touch命令一樣,使得<file>的修改時間爲當前時間。

--warn-undefined-variables

只要make發現有未定義的變量,就輸出警告信息。

5.9 隱含規則

在使用makefile文件時,有一些會經常使用而且使用頻率非常高的東西,比如,編譯C/C++的源程序爲中間目標文件(Linux下是.o文件,Windows下是.obj文件)。本章講述的就是一些在makefile文件中隱含的,早先約定了的,不需要再寫出來的規則。

隱含規則也就是一種慣例,make會按照這種“慣例”心照不喧地來運行,哪怕makefile文件中沒有書寫這樣的規則。例如,把.c文件編譯成.o文件這一規則,根本就不用寫出來,make會自動推導出這種規則,並生成需要的.o文件。

隱含規則會使用一些系統變量,可以改變這些系統變量的值來定製隱含規則運行時的參數。如系統變量CFLAGS可以控制編譯時的編譯器參數。還可以通過模式規則的方式寫下自己的隱含規則。用後綴規則來定義隱含規則會有許多限制。使用模式規則會更加智能和清楚,但後綴規則可以用來保證makefile文件的兼容性。

瞭解了隱含規則,可以讓其更好地服務,不至於在運行makefile文件時出現一些莫名其妙的東西。當然,任何事物都是矛盾的,“水能載舟,亦可覆舟”,所以,有時候“隱含規則”也會給造成不小的麻煩。只有瞭解了它,才能更好地使用它。

5.9.1 使用隱含規則

如果需要使用隱含規則生成需要的目標,所需要做的就是不要寫出這個目標的規則,make會試圖去自動推導產生這個目標的規則和命令。如果make可以自動推導生成這個目標的規則和命令,這個行爲就是隱含規則的自動推導。當然,隱含規則是make事先約定好的一些東西。例如,有下面的一個makefile文件:

實例5-56

foo : foo.o bar.o

          gcc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)

可以注意到,這個makefile文件中並沒有寫下如何生成foo.o和bar.o這兩目標的規則和命令。因爲make的隱含規則功能會自動去推導這兩個目標的依賴目標和生成命令。

make會在自己的隱含規則庫中尋找可以用的規則,如果找到,就會使用。如果找不到,就會報錯。在上面的例子中,make調用的隱含規則是,把.o的目標依賴文件置成.c,並使用C的編譯命令cc –c $(CFLAGS) [.c]來生成.o的目標。也就是說,完全沒有必要寫下下面的兩條規則:

foo.o : foo.c

            gcc –c foo.c $(CFLAGS)

    bar.o : bar.c

         gcc –c bar.c $(CFLAGS)

因爲,這是“約定”好了的事,make已約定好了用C編譯器gcc生成.o文件的規則,這就是隱含規則。當然,如果爲.o文件書寫了自己的規則,make就不會自動推導並調用隱含規則,它會按照寫好的規則忠實地執行。

還有,在make的隱含規則庫中,每一條隱含規則都在庫中有其順序,越靠前的則是越經常使用的,所以,這會導致有些時候即使顯式地指定了目標依賴,make也不會管。如下面這條規則(沒有命令):

foo.o : foo.p

依賴文件foo.p (Pascal程序的源文件)有可能變得沒有意義。如果目錄下存在foo.c文件,隱含規則一樣會生效,並會通過foo.c調用C的編譯器生成foo.o文件。因爲,在隱含規則中,Pascal的規則出現在C的規則之後,所以,make找到可以生成foo.o的C的規則後就不再尋找下一條規則了。如果確實不希望任何隱含規則推導,就不要只寫出依賴規則而不寫命令。

5.9.2 隱含規則一覽

這裏將講述所有預先設置(也就是make內建)的隱含規則。如果不明確地寫下規則,make就會在這些規則中尋找所需要的規則和命令。當然,也可以使用make的參數-r或--no-builtin-rules選項來取消所有預設置的隱含規則。

當然,即使指定了-r參數,某些隱含規則還是會生效,因爲有許多隱含規則都使用了後綴規則來定義。所以,只要隱含規則中有後綴列表 (也就是系統定義在目標.SUFFIXES的依賴目標),隱含規則就會生效。默認的後綴列表是:.out、.a、.ln、 .o、 .c、 .cc、 .C、 .p、.f、 .F、.r、 .y、 .l、.s、.S、 .mod、 .sym、 .def、.h、.info、 .dvi、 .tex、 .texinfo、 .texi、 .txinfo、.w、.ch、 .web、.sh、.elc、.el。具體的細節會在後面講述。

還是先來看一看常用的隱含規則。

1. 編譯C程序的隱含規則

 <n>.o的目標的依賴目標會自動推導爲<n>.c,並且其生成命令是$(CC) –c $(CPPFLAGS) $(CFLAGS)。

2. 編譯C++程序的隱含規則

<n>.o的目標的依賴目標會自動推導爲<n>.cc或是<n>.C,並且其生成命令是$(CXX) –c $(CPPFLAGS) $(CFLAGS) (建議使用.cc作爲C++源文件的後綴,而不是.C)。

3. 編譯Pascal程序的隱含規則

<n>.o的目標的依賴目標會自動推導爲<n>.p,並且其生成命令是$(PC) –c  $(PFLAGS)。

4. 編譯Fortran/Ratfor程序的隱含規則

<n>.o的目標的依賴目標會自動推導爲<n>.r、<n>.F或<n>.f,並且其生成命令是:

.f  $(FC) –c  $(FFLAGS)

.F  $(FC) –c  $(FFLAGS) $(CPPFLAGS)

.f  $(FC) –c  $(FFLAGS) $(RFLAGS)

5. 預處理Fortran/Ratfor程序的隱含規則

<n>.f的目標的依賴目標會自動推導爲<n>.r或<n>.F。這個規則只是轉換Ratfor或有預處理的Fortran程序到一個標準的Fortran程序。其使用的命令是:

.F  $(FC) –F $(CPPFLAGS) $(FFLAGS)

.r               $(FC) –F $(FFLAGS) $(RFLAGS)

6. 編譯Modula-2程序的隱含規則

<n>.sym的目標的依賴目標會自動推導爲<n>.def,並且其生成命令是$(M2C) $(M2FLAGS) $(DEFFLAGS)。

<n.o> 的目標的依賴目標會自動推導爲<n>.mod,並且其生成命令是$(M2C) $(M2FLAGS) $(MODFLAGS)。

7. 彙編和彙編預處理的隱含規則

<n>.o 的目標的依賴目標會自動推導爲<n>.s,默認使用編譯品as,並且其生成命令是$(AS) $(ASFLAGS)。

<n>.s 的目標的依賴目標會自動推導爲<n>.S,默認使用C預編譯器cpp,並且其生成命令是$(AS) $(ASFLAGS)。

8. 鏈接Object文件的隱含規則

<n>目標依賴於<n>.o,通過運行C的編譯器來運行鏈接程序生成(一般是ld),其生成命令是$(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)。

這個規則對於只有一個源文件的工程有效,同時也對多個Object文件(由不同的源文件生成)有效。例如如下規則:

         x : y.o z.o

並且x.c、y.c和z.c都存在時,隱含規則將執行如下命令:

cc -c x.c -o x.o

cc -c y.c -o y.o

cc -c z.c -o z.o

cc x.o y.o z.o -o x

rm -f x.o

rm -f y.o

rm -f z.o

如果沒有一個源文件(如上例中的x.c)和目標名字(如上例中的x)相關聯,最好寫出自己的生成規則,不然,隱含規則會報錯。

9. Yacc C程序時的隱含規則

<n>.c的依賴文件自動推導爲n.y (Yacc生成的文件),其生成命令是$(YACC) $(YFALGS) (Yacc是一個語法分析器,關於其細節請查看相關資料)。

10. Lex C程序時的隱含規則

<n>.c的依賴文件自動推導爲n.l (Lex生成的文件),其生成命令是$(LEX) $(LFALGS) (關於Lex的細節請查看相關資料)。

11. Lex Ratfor程序時的隱含規則

<n>.r的依賴文件自動推導爲n.l (Lex生成的文件),其生成命令是$(LEX) $(LFALGS)。

12. 從C程序、Yacc文件或Lex文件創建Lint庫的隱含規則

<n>.ln (lint生成的文件)的依賴文件自動推導爲n.c,其生成命令是:$(LINT) $(LINTFALGS) $(CPPFLAGS) -i。

對於<n>.y和<n>.l也是同樣的規則。

5.9.3 隱含規則使用的變量

在隱含規則的命令中,基本上都使用了一些預先設置的變量。可以在makefile文件中改變這些變量的值,或是在make的命令行中傳入這些值,或是在環境變量中設置這些值。無論怎麼樣,只要設置了這些特定的變量,其就會對隱含規則起作用。當然,也可以利用make的-R或--no–builtin-variables參數來取消所定義的變量對隱含規則的作用。

例如,第一條隱含規則——編譯C程序的隱含規則的命令是$(CC)–c $(CFLAGS) $(CPPFLAGS)。Make默認的編譯命令是cc,如果把變量$(CC)重定義成gcc,把變量$(CFLAGS)重定義成-g,隱含規則中的命令全部會以gcc –c -g $(CPPFLAGS)的樣子來執行了。

可以把隱含規則中使用的變量分成兩種:一種是命令相關的,如CC;一種是參數相的關,如CFLAGS。下面是所有隱含規則中會用到的變量。

1. 關於命令的變量

●       AR 函數庫打包程序。默認命令是ar。

●       AS 彙編語言編譯程序。默認命令是as。

●       CC C語言編譯程序。默認命令是cc。

●       CXX C++語言編譯程序。默認命令是g++。

●       CO 從 RCS文件中擴展文件程序。默認命令是co。

●       CPP C程序的預處理器(輸出是標準輸出設備)。默認命令是$(CC) –E。

●       FC Fortran 和 Ratfor 的編譯器和預處理程序。默認命令是f77。

●       GET 從SCCS文件中擴展文件的程序。默認命令是get。

●       LEX Lex方法分析器程序(針對於C或Ratfor)。默認命令是lex。

●       PC Pascal語言編譯程序。默認命令是pc。

●       YACC Yacc文法分析器(針對於C程序)。默認命令是yacc。

●       YACCR Yacc文法分析器(針對於Ratfor程序)。默認命令是yacc –r。

●       MAKEINFO 轉換Texinfo源文件(.texi)到Info文件程序。默認命令是makeinfo。

●       TEX 從TeX源文件創建TeX DVI文件的程序。默認命令是tex。

●       TEXI2DVI 從Texinfo源文件創建TeX DVI 文件的程序。默認命令是texi2dvi。

●       WEAVE 轉換Web到TeX的程序。默認命令是weave。

●       CWEAVE 轉換C Web 到 TeX的程序。默認命令是cweave。

●       TANGLE 轉換Web到Pascal語言的程序。默認命令是tangle。

●       CTANGLE 轉換C Web 到 C。默認命令是ctangle。

●       RM 刪除文件命令。默認命令是rm –f。

2. 關於命令參數的變量

下面的這些變量都是上面的命令相關的參數。如果沒有指明其默認值,其默認值都是空。

●       ARFLAGS 函數庫打包程序AR命令的參數。默認值是rv。

●       ASFLAGS 彙編語言編譯器參數(當明顯地調用.s或.S文件時)。

●       CFLAGS C語言編譯器參數。

●       CXXFLAGS C++語言編譯器參數。

●       COFLAGS RCS命令參數。

●       CPPFLAGS C預處理器參數( C 和 Fortran 編譯器也會用到)。

●       FFLAGS Fortran語言編譯器參數。

●       GFLAGS SCCS get程序參數。

●       LDFLAGS 鏈接器參數(如ld)。

●       LFLAGS Lex文法分析器參數。

●       PFLAGS Pascal語言編譯器參數。

●       RFLAGS Ratfor 程序的Fortran 編譯器參數。

●       YFLAGS Yacc文法分析器參數。

5.9.4 隱含規則鏈

有些時候,一個目標可能被一系列的隱含規則所作用。例如,一個.o的文件生成,可能會是先被Yacc的.y文件先成.c,然後再被C的編譯器生成。這一系列的隱含規則叫做“隱含規則鏈”。

在上面的例子中,如果文件.c存在,就直接調用C的編譯器的隱含規則;如果沒有.c文件,但有一個.y文件,Yacc的隱含規則會被調用,生成.c文件,然後,再調用C編譯的隱含規則,最終由.c生成.o文件,達到目標。

這種.c的文件(或是目標)叫做中間目標。不管怎麼樣,make會努力自動推導生成目標的一切方法。不管中間目標有多少,其都會執着地把所有的隱含規則和書寫的規則全部合起來分析,努力達到目標。所以,有些時候,可能會覺得奇怪,怎麼目標會這樣生成?怎麼makefile文件發瘋了?

在默認情況下,中間目標和一般的目標有兩個位置不同:第一個不同是除非中間的目標不存在,纔會引發中間規則;第二個不同是,只要目標成功產生,產生最終目標的過程中,所產生的中間目標文件會被rm -f刪除。

通常,一個被makefile文件指定成目標或是依賴目標的文件不能當作中介。然而,可以顯式地說明一個文件或是目標是中介目標,可以使用僞目標.INTERMEDIATE來強制聲明(如.INTERMEDIATE:mid )。

也可以阻止make自動刪除中間目標。要做到這一點,可以使用僞目標.SECONDARY來強制聲明(如.SECONDARY : sec)。還可以把目標以模式的方式來指定(如%.o)成僞目標.PRECIOUS的依賴目標,以保存被隱含規則所生成的中間文件。

在隱含規則鏈中,禁止同一個目標出現兩次或兩次以上,這樣一來,就可防止在make自動推導時出現無限遞歸的情況。

Make會優化一些特殊的隱含規則,而不生成中間文件。如,從文件foo.c生成目標程序foo。按道理,make會編譯生成中間文件foo.o,然後鏈接成foo,但在實際情況下,這一動作可以被一條cc命令完成(cc –o foo foo.c),於是優化過的規則就不會生成中間文件。 

5.9.5 定義模式規則

可以使用模式規則來定義一個隱含規則。一個模式規則跟一般的規則類似,只是在規則中,目標的定義需要有“%”字符。“%”的意思是表示一個或多個任意字符。在依賴目標中同樣可以使用“%”,只是依賴目標中的“%”的取值取決於其目標。

有一點需要注意的是,“%”的展開發生在變量和函數的展開之後,變量和函數的展開發生在make載入makefile文件時,而模式規則中的“%”則發生在運行時。

1. 模式規則介紹

模式規則中,至少在規則的目標定義中要包含“%”,否則,就是一般的規則。目標中的“%”定義表示對文件名的匹配,“%”表示長度任意的非空字符串。例如: %.c表示以.c結尾的文件名(文件名的長度至少爲3),而s.%.c則表示以s.開頭,.c結尾的文件名(文件名的長度至少爲5)。

如果“%”定義在目標中,目標中的“%”的值決定了依賴目標中的“%”的值,也就是說,目標中的模式的“%”決定了依賴目標中“%”的樣子。例如有一個模式規則如下:

%.o : %.c ; <command ... >

其含義是,指出了從所有的.c文件生成相應的.o文件的規則。如果要生成的目標是a.o b.o,%c就是“a.c b.c”。

一旦依賴目標中的“%”模式被確定,make會被要求去匹配當前目錄下所有的文件名。在模式規則中,目標可能會是多個,如果有模式匹配出多個目標,make就會產生所有的模式目標,此時,make關心的是依賴的文件名和生成目標的命令這兩件事。

2. 模式規則示例

下面這個例子表示了把所有的.c文件都編譯成.o文件。

    %.o : %.c

              $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

其中,“$@”表示所有的目標的逐個值,“$<”表示了所有依賴目標的逐個值。這些奇怪的變量叫“自動化變量”。

下面的這個例子中有兩個目標是模式的:

         %.tab.c %.tab.h: %.y

bison -d $<

這條規則告訴make把所有的.y文件都以bison -d <n>.y執行,然後生成<n>.tab.c和<n>.tab.h文件(其中,<n>表示一個任意字符串)。如果執行程序foo依賴於文件parse.tab.o和scan.o,並且文件scan.o依賴於文件parse.tab.h,如果parse.y文件被更新了,根據上述的規則,bison -d parse.y就會被執行一次,於是,parse.tab.o和scan.o的依賴文件就齊了 (假設parse.tab.o由parse.tab.c生成,scan.o由scan.c生成,而foo由parse.tab.o和scan.o”鏈接生成,而且foo和其.o文件的依賴關係已寫好,所有的目標都會得到滿足)。

3. 自動化變量

在上述的模式規則中,目標和依賴文件都是一系例的文件,如何書寫一個命令來完成從不同的依賴文件生成相應的目標?因爲在每一次的對模式規則的解析時,都會是不同的目標和依賴文件。

自動化變量就是完成這個功能的。在前面,已經對自動化變量有所提及,相信看到這裏已對它有一個感性認識了。所謂自動化變量,就是這種變量會把模式中所定義的一系列的文件自動地逐個取出,直至所有符合模式的文件都取完。這種自動化變量只應出現在規則的命令中。

下面是所有的自動化變量及其說明:

$@

表示規則中的目標文件集。在模式規則中,如果有多個目標,“$@”就是匹配於目標中模式定義的集合。

$%

僅當目標是函數庫文件時,表示規則中的目標成員名。例如,如果一個目標是foo.a(bar.o),“$%”就是bar.o,“$@”就是foo.a。如果目標不是函數庫文件(如Unix下是.a,Windows下是.lib),其值爲空。

$<

依賴目標中的第一個目標名字。如果依賴目標是以模式(即“%”)定義的,“$<”將是符合模式的一系列的文件集。注意,其是一個一個地取出來的。

$?

所有比目標新的依賴目標的集合,以空格分隔。

$^

所有的依賴目標的集合,以空格分隔。如果在依賴目標中有多個重複的,那麼這個變量會去除重複的依賴目標,只保留一份。

$+

這個變量很像“$^”,也是所有依賴目標的集合。只是它不去除重複的依賴目標。

$*

這個變量表示目標模式中“%”及其之前的部分。如果目標是dir/a.foo.b,並且目標的模式是a.%.b,“$*”的值就是dir/a.foo。這個變量對於構造有關聯的文件名比較有效。如果目標中沒有模式的定義,“$*”也就不能被推導出,但是,如果目標文件的後綴是make所識別的,“$*”就是除了後綴的那一部分。例如:如果目標是foo.c,因爲.c是make所能識別的後綴名,所以,“$*”的值就是foo。這個特性是GNU make的,很有可能不兼容於其他版本的make,所以,應該儘量避免使用“$*”,除非是在隱含規則或是靜態模式中。如果目標中的後綴是make所不能識別的,“$*”就是空值。

當希望只對更新過的依賴文件進行操作時,“$?”在顯式規則中很有用。例如,假設有一個函數庫文件叫lib,其由其他幾個object文件更新,把object文件打包的比較有效率的makefile文件規則是:

f lib : foo.o bar.o lose.o win.o

        ar r lib $?

在上述所列出來的自動變量中,4個變量($@、$<、$%、$*)在擴展時只會有1個文件,而另3個的值是一個文件列表。這7個自動化變量還可以取得文件的目錄名或是在當前目錄下的符合模式的文件名,只需要搭配上“D”或“F”字樣。這是GNU make中老版本的特性,在新版本中,使用函數dir或notdir就可以做到了。D的含義是Directory,就是目錄;F的含義是File,就是文件。

下面是對上面的7個變量分別加上“D”或是“F”的含義:

$(@D)

表示“$@”的目錄部分(不以斜槓作爲結尾),如果“$@”值是dir/foo.o,“$(@D)”就是dir,而如果“$@”中沒有包含斜槓,其值就是“.”(當前目錄)。

$(@F)

表示“$@”的文件部分,如果“$@”值是dir/foo.o,“$(@F)”就是foo.o,“$(@F)”相當於函數$(notdir $@)。

$(*D)

$(*F)

和上面所述的同理,也是取文件的目錄部分和文件部分。對於上面的那個例子,“$(*D)”返回dir,而“$(*F)”返回foo。

$(%D)

$(%F)

分別表示了函數包文件成員的目錄部分和文件部分。這對於形同archive(member)形式的目標中的member中包含了不同的目錄很有用。

$(<D)

$(<F)

分別表示依賴文件的目錄部分和文件部分。

$(^D)

$(^F)

分別表示所有依賴文件的目錄部分和文件部分(無相同的)。

$(+D)

$(+F)

分別表示所有依賴文件的目錄部分和文件部分(可以有相同的)

$(?D)

$(?F)

分別表示被更新的依賴文件的目錄部分和文件部分。

最後提醒一下的是,對於“$<”,爲了避免產生不必要的麻煩,最好給“$”後面的那個特定字符都加上圓括號,比如,“$(<)”就要比“$<”好一些。

還要注意的是,這些變量只使用在規則的命令中,而且一般都是“顯式規則”和“靜態模式規則”,其在隱含規則中並沒有意義。

4. 模式的匹配

一般來說,一個目標的模式有一個帶有前綴或是後綴的“%”,或是沒有前後綴,直接就是一個“%”。因爲“%”代表一個或多個字符,所以在定義好了的模式中,把“%”所匹配的內容叫做“莖”,例如“%.c”所匹配的文件test.c中test就是“莖”。因爲在目標和依賴目標中同時有“%”時,依賴目標的“莖”會傳給目標,當做目標中的“莖”。

當一個模式匹配包含有斜槓(實際也不經常包含)的文件時,在進行模式匹配時,目錄部分會首先被移開,然後進行匹配,成功後,再把目錄加回去。在進行“莖”的傳遞時,需要知道這個步驟。例如有一個模式e%t,文件src/eat匹配於該模式,於是src/a就是其“莖”。如果這個模式定義在依賴目標中,而依賴於這個模式的目標中又有個模式c%r,目標就是src/car (“莖”被傳遞)。

5. 重載內建隱含規則

可以重載內建的隱含規則(或是定義一個全新的),例如可以重新構造和內建隱含規則不同的命令,如:

%.o : %.c

$(gcc) -c $(CPPFLAGS) $(CFLAGS) -D$(date)

可以取消內建的隱含規則,只要不在後面寫命令就行。如:

         %.o : %.s

同樣,也可以重新定義一個全新的隱含規則,其在隱含規則中的位置取決於在哪裏寫下這個規則。朝前的位置就靠前。

5.9.6 隱含規則搜索算法

比如有一個目標叫T,下面是搜索目標T的規則的算法。請注意,在下面沒有提到後綴規則,原因是所有的後綴規則在makefile文件被載入內存時,會轉換成模式規則。如果目標是archive(member)的函數庫文件模式,這個算法會運行兩次,第一次是找目標T,如果沒有找到,進入第二次,第二次會把member當作T來搜索。

(1) 把T的目錄部分分離出來,叫D,而剩餘部分叫N(例如,如果T是src/foo.o,D就是src/,N就是foo.o)。

(2) 創建所有匹配於T或是N的模式規則列表。

(3) 如果在模式規則列表中有匹配所有文件的模式,如“%”,從列表中移除其他的模式。

(4) 移除列表中沒有命令的規則。

(5) 對於第一個在列表中的模式規則:

●       推導其“莖”S,S應該是T或是N匹配於模式中“%”非空的部分。

●       計算依賴文件。把依賴文件中的“%”都替換成“莖”S。如果目標模式中沒有包含斜框字符,就把D加在第一個依賴文件的開頭。

●       測試是否所有的依賴文件都存在或是理當存在(如果有一個文件被定義成另外一個規則的目標文件,或者是一個顯式規則的依賴文件,這個文件就叫“理當存在”)。

●       如果所有的依賴文件存在或是理當存在,或是就沒有依賴文件,這條規則將被採用,退出該算法。

(6) 如果經過第5步,沒有找到模式規則,就作更進一步的搜索。對於存在於列表中的第一個模式規則:

●       如果規則是終止規則,那就忽略它,繼續下一條模式規則。

●       計算依賴文件(同第5步)。

●       測試所有的依賴文件是否存在或是理當存在。

●       對於不存在的依賴文件,遞歸調用這個算法,查找它是否可以被隱含規則找到。

●       如果所有的依賴文件存在或是理當存在,或是就根本沒有依賴文件。這條規則被採用,退出該算法。

(7) 如果沒有隱含規則可以使用,查看.DEFAULT規則,如果有,就採用,把.DEFAULT的命令給T使用。

一旦規則被找到,就會執行其相當的命令,而此時,自動化變量的值纔會生成。

5.10 使用make更新函數庫文件

函數庫文件也就是對Object文件(程序編譯的中間文件)的打包文件。在Unix下,一般是由命令ar來完成打包工作。

5.10.1 函數庫文件的成員

一個函數庫文件由多個文件組成。可以以如下格式指定函數庫文件及其組成:

archive(member)

這不是一個命令,而是一個目標和依賴的定義。一般來說,這種用法基本上就是爲了ar命令來服務的。如:

foolib(hack.o) : hack.o

        ar cr foolib hack.o

如果要指定多個member,那就以空格分開,如:

foolib(hack.o kludge.o)

其等價於:

foolib(hack.o) foolib(kludge.o)

還可以使用Shell的文件通配符來定義,如:

foolib(*.o)

5.10.2 函數庫成員的隱含規則

當make搜索一個目標的隱含規則時,一個特性是,如果這個目標是“a(m)”形式的,其會把目標變成“(m)”。於是,如果成員是“%.o”的模式定義,並且如果使用make foo.a(bar.o)的形式調用makefile文件,隱含規則會去找bar.o的規則;如果沒有定義bar.o的規則,內建隱含規則生效,make會去找bar.c文件來生成bar.o。如果找得到,make執行的命令大致如下:

gcc -c bar.c -o bar.o

    ar r foo.a bar.o

    rm -f bar.o

還有一個變量要注意的是“$%”,這是專屬函數庫文件的自動化變量。

5.10.3 函數庫文件的後綴規則

可以使用後綴規則和隱含規則來生成函數庫打包文件,如:   

c.a:

     $(gcc) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o

     $(AR) r $@ $*.o

     $(RM) $*.o

其等效於:

(%.o) : %.c

      $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o

      $(AR) r $@ $*.o

      $(RM) $*.o

5.10.4 注意事項

在生成函數庫打包文件時,請小心使用make的並行機制(-j參數)。如果多個ar命令在同一時間運行在同一個函數庫打包文件上,就很有可能損壞這個函數庫文件。所以,在make未來的版本中,應該提供一種機制來避免並行操作發生在函數打包文件上。但就目前而言,還是儘量不要使用-j參數。

以上基本上就是GNU make的makefile文件的所有細節了。無論什麼樣的make,都是以文件的依賴性爲基礎的,其基本都是遵循一個標準的。對於前述所有的make的細節,不但可以利用make這個工具來編譯程序,還可以利用make來完成其他的工作。因爲規則中的命令可以是任何Shell之下的命令,所以,在Linux下,不一定只使用程序語言的編譯器,還可以在makefile文件中書寫其他的命令,如tar、awk、mail、sed、cvs、compress、ls、rm、yacc、rpm、ftp等,來完成諸如程序打包、程序備份、製作程序安裝包、提交代碼、使用程序模板、合併文件等諸多功能,如文件操作、文件管理、編程開發設計,或是其他一些異想天開的東西

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