- 從零開始寫Linux內(nèi)核:一書學(xué)透核心原理與實(shí)現(xiàn)
- 海納
- 5940字
- 2025-04-09 18:37:36
1.4 匯編語言
在進(jìn)行內(nèi)核代碼開發(fā)的過程中,有很多地方需要直接操作CPU寄存器和I/O端口,用C/C++無法完成相應(yīng)的功能。使用匯編的場(chǎng)景主要有兩種:一是操作系統(tǒng)引導(dǎo)和初始化階段需要大量地直接操作I/O端口,使用匯編語言會(huì)很高效;二是在內(nèi)核代碼中需要操作硬件,例如修改某些狀態(tài)寄存器,改變CPU的工作模式等,這種情況就是以內(nèi)嵌匯編為主。
每一種C編譯器的內(nèi)嵌匯編都不盡相同,下面詳細(xì)介紹GCC內(nèi)嵌匯編的語法。
1.4.1 內(nèi)嵌匯編
GCC提供的基本匯編語法形式如下:
__asm__(AssemblerTemplate);
其中,__asm__是內(nèi)嵌匯編命令的關(guān)鍵字,用來聲明內(nèi)嵌匯編表達(dá)式。AssemblerTemplate則是一組插入到C/C++代碼中的匯編指令。
例如,下面的代碼用于在C語言中插入一條mov寄存器的指令:
__asm__("mov %edx, %eax");
內(nèi)嵌匯編指令的書寫方式與直接在匯編文件中寫匯編指令沒有區(qū)別。基本內(nèi)嵌匯編支持匯編器的所有指令形式,包括匯編中的偽指令。
在基本內(nèi)嵌匯編中,我們可以插入一段匯編指令,但是無法讓匯編指令與我們?cè)镜腃/C++程序代碼產(chǎn)生關(guān)聯(lián)。例如,修改或讀取C/C++中的變量等。因此,除了支持基本內(nèi)嵌匯編指令,GCC還支持通過擴(kuò)展內(nèi)嵌匯編的方式讓匯編指令與C/C++代碼進(jìn)行互操作。
GCC的內(nèi)嵌匯編語法形式如代碼清單1-2所示。
代碼清單1-2 內(nèi)嵌匯編語法
1 __asm__asm-qualifiers(
2 AssemblerTemplate
3 :OutputOperands /* 可選 */
4 :InputOperands /* 可選 */
5 :Clobbers) /* 可選 */
從以上語法形式可以看出,GCC的內(nèi)嵌匯編主要分為6個(gè)部分,下面依次進(jìn)行解釋。
1)__asm__:同基本內(nèi)嵌匯編一樣,擴(kuò)展內(nèi)嵌匯編同樣使用__asm__作為關(guān)鍵字,GCC可以識(shí)別__asm__或者asm關(guān)鍵字。該標(biāo)識(shí)符標(biāo)識(shí)了內(nèi)嵌匯編表達(dá)式的開始。
2)asm-qualifiers:該位置可選,一般常用的修飾符是volatile。GCC在優(yōu)化過程中可能會(huì)對(duì)內(nèi)嵌匯編進(jìn)行修改或者消除。例如,當(dāng)優(yōu)化器發(fā)現(xiàn)內(nèi)嵌匯編中某些指令對(duì)最后的輸出沒有影響時(shí),優(yōu)化器會(huì)消除掉這些指令,又或者優(yōu)化器會(huì)對(duì)循環(huán)中的一些不變量進(jìn)行外提操作。在某些情況下,編譯器的這些優(yōu)化并不是程序員所期望的行為,因此可以通過volatile關(guān)鍵字來禁止編譯器對(duì)內(nèi)嵌匯編的類似優(yōu)化。
3)AssemblerTemplate:這個(gè)位置是內(nèi)嵌匯編的主體部分,由一組包含匯編指令的字符串組成。GCC編譯器識(shí)別其中的占位符,替換為對(duì)應(yīng)的輸出操作數(shù)、輸入操作數(shù)等內(nèi)容,最后將替換好的匯編指令作為匯編器的輸入。每條指令最好以\n\t結(jié)尾,這樣GCC產(chǎn)生的匯編文件的格式比較好看。例如下面的例子:
__asm__ __volatile__("mov %%edx,% %eax":);
該例子同基本內(nèi)嵌匯編中的例子的內(nèi)容是一樣的,但這里采用的是擴(kuò)展內(nèi)嵌匯編的方式,因此有兩個(gè)不同的地方:一是因?yàn)樵摾硬簧婕叭魏闻cC/C++交互的地方,所以例子中輸出操作數(shù)、輸入操作數(shù)以及破壞描述部分都為空,需要在最后以一個(gè)冒號(hào)結(jié)尾;二是在擴(kuò)展內(nèi)嵌匯編中,引用寄存器時(shí),需要在寄存器名稱前添加“%%”,這是為了與操作數(shù)占位符的“%”進(jìn)行區(qū)分。
4)OutputOperands:輸出操作數(shù),由逗號(hào)分隔,可以為空。每個(gè)內(nèi)嵌匯編表達(dá)式都可以有0個(gè)或多個(gè)輸出操作數(shù),用來標(biāo)識(shí)在匯編中被修改的C/C++程序變量。
輸出操作數(shù)的形式如下:
[[asmSymbolicName]]constraint(cvariablename)
要理解asmSymbolicName的含義,需要先理解擴(kuò)展內(nèi)嵌匯編中操作數(shù)占位符的作用。在擴(kuò)展內(nèi)嵌匯編指令中,匯編指令的操作數(shù)可以由占位符進(jìn)行引用,占位符代表了輸出操作數(shù)以及輸入操作數(shù)的位置。例如總共有5個(gè)操作數(shù)(2個(gè)輸出操作數(shù),3個(gè)輸入操作數(shù)),則占位符%0~%4分別代表了這5個(gè)操作數(shù),具體的實(shí)現(xiàn)如代碼清單1-3所示。
代碼清單1-3 輸入/輸出參數(shù)
1 int out1,out2;
2 int in1=1,in2=2,in3=3;
3 __asm__ __volatile__(
4 "add %3,%4\n\t"
5 "add %2,%3\n\t"
6 "mov %4,%1\n\t"
7 "mov %3,%0\n\t"
8 :"=r"(out1),"=r"(out2)
9 :"r"(in1),"r"(in2),"r"(in3)
10 :
11);
例子中占位符%0~%4分別指向C代碼中out1、out2、in1、in2、in3這5個(gè)變量。
雖然數(shù)字類型的占位符比較方便,但是如果輸出/輸入操作數(shù)太多,則容易使得數(shù)字類型占位符過于混亂。因此,asmSymbolicName提供了一種別名的方式,允許在擴(kuò)展內(nèi)嵌匯編中使用別名來操作占位符。上面例子也可以修改為別名的形式,具體實(shí)現(xiàn)如代碼清單1-4所示。
代碼清單1-4 別名形式的參數(shù)
1 int out1,out2;
2 int in1=1,in2=2,in3=3;
3 __asm__ __volatile__(
4 "add %[in2],%[in3]\n\t"
5 "add %[in1],%[in2]\n\t"
6 "mov %[in3],%[out2]\n\t"
7 "mov %[in2],%[out1]\n\t"
8 :[out1]"=r"(out1),[out2]"=r"(out2)
9 :[in1]"r"(in1),[in2]"r"(in2),[in3]"r"(in3)
10:
11);
constraint表明操作數(shù)的約束,即上面例子中out1和out2的“=r”。對(duì)輸出操作數(shù)而言,約束必須以“=”(意思是對(duì)當(dāng)前變量進(jìn)行寫操作)或“+”(意思是對(duì)當(dāng)前變量進(jìn)行讀和寫操作)開頭。在前綴之后,必須有一個(gè)或多個(gè)附加約束來描述值所在的位置。常見的約束包括代表寄存器的“r”和代表內(nèi)存的“m”。上述例子中“=r(out1)”的約束含義是:內(nèi)嵌匯編指令將會(huì)對(duì)out1變量進(jìn)行寫操作,并且會(huì)將out1與一個(gè)寄存器進(jìn)行關(guān)聯(lián)。GCC內(nèi)嵌匯編中的約束符還有很多,詳細(xì)列表可以查看GCC官方手冊(cè),此處不再贅述。
(cvariablename)表示該輸出操作符所綁定的C/C++程序的變量,這個(gè)比較好理解。
最后再看一下來自Linux 0.11中的具體例子,如代碼清單1-5所示。
代碼清單1-5 Linux 0.11中的真實(shí)示例
1 inline unsigned long get_fs(){
2 unsigned short_v;
3 __asm__("mov %%fs,%%ax":"=a"(_v):);
4 return_v;
5 }
這個(gè)函數(shù)的功能是獲取當(dāng)前fs寄存器的值并返回。在函數(shù)get_fs()中,輸出操作數(shù)為變量_v,其形式為“=a(_v)”。這里約束“=a”表明輸出操作符與寄存器%ax綁定,因此內(nèi)嵌匯編的作用就是將寄存器%fs的值存儲(chǔ)到變量_v中。
5)InputOperands:輸入操作數(shù),由逗號(hào)分隔,可以為空。輸入操作數(shù)集合標(biāo)識(shí)了哪些C/C++變量是需要在匯編代碼中讀取使用的。
輸入操作數(shù)的形式如下:
[[asmSymbolicName]]constraint(cvariablename)
同輸出操作數(shù)語法形式一致。這里需要單獨(dú)對(duì)輸入操作數(shù)的constraint進(jìn)行說明,與輸出操作數(shù)不同,輸入約束字符串不能以“=”或“+”開頭,另外,輸入約束也可以是數(shù)字。這表明指定的輸入變量必須與輸出約束列表中(從零開始的)索引處的輸出變量指向同一個(gè)變量。
例如以下例子:
1 __asm__ __volatile__(
2 "add %2,%0"
3 :"=r"(a)
4 :"0"(a),"r"(b)
5 :
6);
在這個(gè)例子中,變量a對(duì)應(yīng)的寄存器既要作為輸入變量,也要作為輸出變量。這里通過“0”約束將輸入操作數(shù)與輸出操作數(shù)綁定。
我們最后再看一下Linux 0.11中的具體例子:
1 inline void set_fs(unsigned long val){
2 __asm__("mov %0,%%fs"::"a"((unsigned short)val));
3 }
該函數(shù)的作用是將變量val的值存到%fs寄存器中。在對(duì)應(yīng)的內(nèi)嵌匯編中,輸出操作數(shù)為空,而輸入操作數(shù)則為val變量,在匯編指令里通過%0占位符來表示。
6)Clobbers:破壞描述部分。該位置需要列出除了輸出操作數(shù)列表中會(huì)被修改的值之外,其他會(huì)被內(nèi)嵌匯編修改的寄存器值。破壞描述部分的列表內(nèi)容是寄存器的名稱,要通過引號(hào)引起來,如果需要多個(gè)寄存器的話,則需要使用逗號(hào)進(jìn)行分隔。這里的作用是通知編譯器,說明在內(nèi)嵌匯編中有哪些寄存器的值會(huì)被修改,使得編譯器在內(nèi)嵌匯編語句之前保存對(duì)應(yīng)的寄存器值。
例如以下例子:
1 __asm__ __volatile__(
2 "mov %0, %eax"
3 ::"a"(a):" %eax"
4);
例子中將變量a的值寫到 %eax寄存器中,這里 %eax寄存器既非輸出操作數(shù),又非輸入操作數(shù),因此需要在破壞描述部分進(jìn)行聲明。
除了通用寄存器,clobbers list還有兩個(gè)特殊的參數(shù)有著不同的含義:一個(gè)是“cc”,它用來表示內(nèi)嵌匯編修改了標(biāo)志寄存器(flags register);另一個(gè)是“memory”,它用于通知編譯器匯編代碼對(duì)列表中的項(xiàng)目執(zhí)行內(nèi)存讀取或?qū)懭耄ɡ纾L問由輸入?yún)?shù)指向的內(nèi)存)。為了確保內(nèi)存包含正確的值,GCC可能需要在執(zhí)行內(nèi)嵌匯編之前將特定的寄存器值保存到內(nèi)存中。此外,編譯器不會(huì)假設(shè)在內(nèi)嵌匯編之前從內(nèi)存讀取的值保持不變,它會(huì)根據(jù)需要重新加載這些值。“memory clobber”的作用等同于為編譯器添加了一個(gè)讀寫內(nèi)存屏障。
1.4.2 鏈接器的工作原理
從GCC編譯源碼到得到可執(zhí)行二進(jìn)制文件的過程主要分為4步。
1)預(yù)編譯:將源碼中的預(yù)處理指令進(jìn)行展開,如#include以及#define等指令。
2)編譯:編譯是將源碼經(jīng)過一系列的分析和優(yōu)化,生成對(duì)應(yīng)架構(gòu)的匯編代碼。其中包括編譯器前端的詞法分析、語法分析、語義分析,編譯器中端的IR(Intermediate Representation,中間表示)、IR之間的分析/變換/優(yōu)化,以及編譯器后端的指令調(diào)度、指令選擇、寄存器分配以及代碼生成等。
3)匯編:第2步生成的.s匯編文件此時(shí)還是人類可讀的ASCII格式的文件,但是CPU執(zhí)行的機(jī)器碼需要的是二進(jìn)制指令。因此第3步需要將.s中人類可讀的指令與數(shù)據(jù)一一翻譯成CPU可讀的二進(jìn)制文件。這個(gè)過程比較簡(jiǎn)單,只需要查表翻譯即可。
4)鏈接:前邊3步的預(yù)處理、編譯、匯編的過程都是對(duì)單一的編譯單元來進(jìn)行的,也就是只有一個(gè)源文件。因此,編譯器在執(zhí)行完前面3個(gè)步驟后,會(huì)得到多個(gè)編譯單元后綴為.o的目標(biāo)文件,此時(shí)就需要鏈接器來將這些目標(biāo)文件鏈接到一起生成最終的可執(zhí)行文件。
由此可以看到,鏈接器做的事情主要是對(duì)編譯器生成的多個(gè).o文件進(jìn)行合并,一般采取的策略是把各個(gè)目標(biāo)文件中相同的段進(jìn)行合并,例如多個(gè).text段合并成可執(zhí)行文件中的一個(gè).text段。在這個(gè)階段中,鏈接器對(duì)輸入的各個(gè)目標(biāo)文件進(jìn)行掃描,獲取各個(gè)段的大小,同時(shí)會(huì)收集所有的符號(hào)定義以及引用信息,構(gòu)建一個(gè)全局的符號(hào)表。此時(shí),鏈接器已經(jīng)構(gòu)造好了最終的文件布局以及虛擬內(nèi)存布局,再根據(jù)符號(hào)表就能確定每個(gè)符號(hào)的虛擬地址。然后鏈接器會(huì)對(duì)整個(gè)文件進(jìn)行第二遍掃描,這一階段會(huì)利用第一遍掃描得到的符號(hào)表信息,依次對(duì)文件中每個(gè)符號(hào)引用的地方進(jìn)行地址替換。這個(gè)階段也就是對(duì)符號(hào)的解析以及重定位的過程。
以上4個(gè)過程是GCC編譯鏈接的全過程。其中,預(yù)處理、編譯以及匯編的過程,不管在哪個(gè)平臺(tái)(Windows、Linux、macOS)都是通用的。因?yàn)殡m然操作系統(tǒng)平臺(tái)不一樣,但是CPU的指令集是一樣的,有差異的地方主要在于鏈接的過程。
不同的操作系統(tǒng)平臺(tái)有著自己的二進(jìn)制文件格式,例如Windows下的PE格式、Linux下的ELF格式,以及macOS上的MachO格式。二進(jìn)制文件格式中定義了文件類型的魔數(shù)(Magic Number),代碼段、數(shù)據(jù)段的存儲(chǔ)位置以及一些其他程序相關(guān)的元數(shù)據(jù)等,因此當(dāng)你運(yùn)行對(duì)應(yīng)系統(tǒng)的可執(zhí)行文件時(shí),需要對(duì)應(yīng)系統(tǒng)的加載器(Loader)識(shí)別并加載對(duì)應(yīng)格式的可執(zhí)行文件,否則應(yīng)用程序就無法運(yùn)行,比如,ELF格式的可執(zhí)行文件就無法運(yùn)行在Windows系統(tǒng)中。
鏈接的過程就是生成對(duì)應(yīng)系統(tǒng)加載器可識(shí)別格式的文件,組織不同段的位置,設(shè)置魔數(shù),設(shè)置程序運(yùn)行起始地址等。由此可見,鏈接器與加載器的工作關(guān)系類似鏡像,鏈接器負(fù)責(zé)根據(jù)二進(jìn)制文件格式標(biāo)準(zhǔn)生成對(duì)應(yīng)格式的磁盤文件,而加載器則根據(jù)二進(jìn)制文件格式標(biāo)準(zhǔn)將對(duì)應(yīng)的磁盤文件讀取到內(nèi)存當(dāng)中并執(zhí)行。
運(yùn)行在操作系統(tǒng)上的應(yīng)用程序,是由系統(tǒng)的加載器進(jìn)行加載并運(yùn)行的。而操作系統(tǒng)內(nèi)核在開機(jī)上電的時(shí)候并沒有加載器來負(fù)責(zé)加載系統(tǒng)內(nèi)核,因此操作系統(tǒng)的引導(dǎo)程序就需要由自己負(fù)責(zé)加載。例如,Linux 0.11代碼中的bootsetct.S和setup.S這兩個(gè)匯編文件做的事情就是加載系統(tǒng)內(nèi)核。
同樣,對(duì)系統(tǒng)內(nèi)核的鏈接器而言,也不能生成ELF格式或者PE格式等,這里需要按照bootsetct.S和setup.S對(duì)初始引導(dǎo)過程中的內(nèi)存布局進(jìn)行設(shè)置,將對(duì)應(yīng)的代碼段生成在0x0位置,我們可以在內(nèi)核的構(gòu)建系統(tǒng)中看到ld的構(gòu)建選項(xiàng)中有“Ttext 0x0”這個(gè)選項(xiàng),表示將對(duì)應(yīng)的代碼段的虛擬內(nèi)存地址設(shè)置到0x0位置。
如果想查看經(jīng)過ld鏈接后的鏡像文件中的符號(hào)與虛擬內(nèi)存地址的對(duì)應(yīng)關(guān)系,可以通過“-M”選項(xiàng)將對(duì)應(yīng)關(guān)系輸出到文件中。在調(diào)試內(nèi)核代碼的時(shí)候可以方便查看內(nèi)存地址與符號(hào)的映射關(guān)系。
1.4.3 初識(shí)makefile
通常在對(duì)單文件或者比較少量的文件進(jìn)行編譯的時(shí)候,只需要通過GCC命令直接編譯就可以了。因?yàn)樵谖募?shù)目比較少的情況下,其編譯過程中的文件依賴關(guān)系還是很簡(jiǎn)單的,可以通過人工控制命令的順序來解決文件的依賴關(guān)系。
然而在大型項(xiàng)目的開發(fā)過程中,編譯過程面對(duì)的往往是成千上萬個(gè)源碼文件,而源碼的相互依賴關(guān)系又非常復(fù)雜,想通過人力來維護(hù)這種編譯順序幾乎是一件不可能的事情。當(dāng)然,可以通過維護(hù)一個(gè)shell腳本(用于構(gòu)建)文件來控制整個(gè)系統(tǒng)中所有文件的編譯過程。但是通過shell腳本來控制項(xiàng)目文件的編譯順序有3個(gè)問題。
1)shell腳本語言無法原生支持依賴關(guān)系的表達(dá),需要通過復(fù)雜的邏輯代碼來表達(dá)源碼的依賴關(guān)系。
2)shell腳本語言無法原生支持增量依賴編譯(即如果只修改項(xiàng)目中的一個(gè)文件,只需要重新編譯對(duì)該文件依賴的模塊即可),要想實(shí)現(xiàn)控制邏輯也非常復(fù)雜。
3)shell腳本的維護(hù)成本相對(duì)較高。
GNU make是一個(gè)收集文件依賴關(guān)系,并根據(jù)依賴關(guān)系自動(dòng)進(jìn)行項(xiàng)目構(gòu)建的工具。依賴定義在makefile文件中,make工具依據(jù)makefile的規(guī)則來按順序執(zhí)行對(duì)應(yīng)的命令。現(xiàn)在的大型項(xiàng)目都會(huì)使用更加智能的構(gòu)建工具cmake,cmake可以自動(dòng)分析文件中的依賴關(guān)系,從而生成對(duì)應(yīng)的makefile文件,使得項(xiàng)目的構(gòu)建更加簡(jiǎn)便。不過本書在實(shí)現(xiàn)Linux 0.11項(xiàng)目代碼的過程中還是采用make工具,因?yàn)檫@個(gè)工程結(jié)構(gòu)相對(duì)清晰、簡(jiǎn)單。
1.makefile基本規(guī)則
當(dāng)我們輸入make命令時(shí),make會(huì)到當(dāng)前目錄下去查找makefile文件。makefile文件由一系列的規(guī)則組成,每條規(guī)則的形式如下:
1 target…:prerequisites…
2 recipe…
第一行規(guī)定了目標(biāo)文件以及文件的依賴關(guān)系。在makefile里,target和冒號(hào)是必不可少的,prerequisites在這一行里邊可以沒有。第一行之后是一條或多條recipe命令,即要達(dá)成這個(gè)target需要執(zhí)行的命令。這里需要注意的是,這些recipe命令必須使用Tab分隔符來進(jìn)行縮進(jìn),相比第一行的target需要多縮進(jìn)一個(gè)制表符。
target一般是指該條規(guī)則下最終生成的文件名,如可執(zhí)行文件或者.o/.so文件等。一個(gè)target往往會(huì)依賴一個(gè)或多個(gè)文件,即規(guī)則中的prerequisites。多個(gè)依賴則用空格進(jìn)行分隔。例如:
1 foo.o:foo.c
2 GCC foo.c-c-o foo.o
3 bar.o:bar.c
4 GCC bar.c-c-o bar.o
5 a.out:foo.o bar.o
6 GCC bar.o foo.o-o a.out
這里第一條規(guī)則的target是foo.o文件,foo.o的生成依賴foo.c。生成foo.o的命令要通過GCC編譯。第二條規(guī)則表明了bar.o文件依賴bar.c。第三條規(guī)則給出a.out同時(shí)依賴foo.o以及bar.o。所以,在這個(gè)makefile文件里,我們可以通過執(zhí)行make a.out生成最終的可執(zhí)行文件。在構(gòu)建a.out的過程中,根據(jù)其依賴關(guān)系可知它同時(shí)依賴foo.o以及bar.o,make會(huì)先構(gòu)建出它的依賴文件foo.o和bar.o,也就是先執(zhí)行make foo.o以及make bar.o命令。由此可見,make是根據(jù)makefile文件定義的規(guī)則,并按照依賴的順序進(jìn)行構(gòu)建的。如果構(gòu)建完a.out以后又對(duì)bar.c文件進(jìn)行了修改,再次執(zhí)行make a.out時(shí),make只需要重新構(gòu)建bar.o以及a.out即可,不需要重新構(gòu)建foo.o,這樣在大型項(xiàng)目中可以大大加快構(gòu)建的速度。
一般情況下,make命令會(huì)將第一條規(guī)則作為默認(rèn)執(zhí)行的規(guī)則,這樣直接運(yùn)行make命令就等價(jià)于執(zhí)行make a.out。
除此之外,target可以用來指代一組命令的名稱,常用的target是clean。例如:
1 clean:
2 rm a.out *.o
這種情況下執(zhí)行make clean會(huì)刪除當(dāng)前目錄下的.o文件以及a.out文件。此時(shí),clean是一個(gè)偽目標(biāo)。如果當(dāng)前文件夾下恰好有一個(gè)clean文件,會(huì)干擾到make的執(zhí)行。因?yàn)閙ake發(fā)現(xiàn)這個(gè)target并沒有依賴文件,所以不需要重新構(gòu)建,這個(gè)target對(duì)應(yīng)的recipe也就不需要執(zhí)行了。為了消除這種影響,我們最好在這種偽目標(biāo)下做一些聲明:
1 .PHONY:clean
2 clean:
3 rm a.out *.o
這樣的話,make便不會(huì)認(rèn)為clean是一個(gè)需要生成的文件目標(biāo)了。
2.makefile的變量
在makefile中,還可以通過定義變量來避免在多處輸入重復(fù)的命令。如在上面a.out的例子中,可以通過變量進(jìn)行如下改寫:
1 OBJS:=foo.o bar.o
2 CC:=GCC
3 CFLAGS:=-O2
4
5 a.out: $(OBJS)
6 $(CC) $(CFLAGS) $(OBJS)-o a.out
7 foo.o:foo.c
8 $(CC) $(CFLAGS)foo.c-c-o foo.o
9 bar.o:bar.c
10 $(CC) $(CFLAGS)bar.c-c-o bar.o
11
12 .PHONY:clean
13 clean:
14 rm a.out$(OBJS)
這里將目標(biāo)文件的列表定義為變量OBJS,將編譯命令定義為變量CC,將編譯選項(xiàng)定義為CFLAGS。之后再修改目標(biāo)文件或者編譯器等只需要修改變量即可,不需要對(duì)每個(gè)規(guī)則下的命令進(jìn)行修改。
除了用戶定義的變量外,makefile中還有一系列自動(dòng)變量,這些自動(dòng)變量可以在規(guī)則執(zhí)行時(shí)根據(jù)規(guī)則的target以及prerequisites進(jìn)行刷新和計(jì)算。常用的自動(dòng)變量主要有以下幾個(gè)。
變量 $@:指代了當(dāng)前規(guī)則里的target。如果一個(gè)規(guī)則的target有多個(gè)的話,則指代第一個(gè)target。例如上述例子可以改為:
1 a.out: $(OBJS)
2 $(CC) $(CFLAGS) $(OBJS)-o$@
3 foo.o:foo.c
4 $(CC) $(CFLAGS)foo.c-c-o$@
5 bar.o:bar.c
6 $(CC) $(CFLAGS)bar.c-c-o$@
變量$<:指代了當(dāng)前規(guī)則里的第一個(gè)prerequisites。而$?變量則表示當(dāng)前規(guī)則中的所有prerequisites。由此,我們可以把上述例子繼續(xù)改寫為:
1 a.out: $(OBJS)
2 $(CC) $(CFLAGS)$^-o$@
3 foo.o:foo.c
4 $(CC) $(CFLAGS)$<-c-o$@
5 bar.o:bar.c
6 $(CC) $(CFLAGS)$<-c-o$@
變量$?:表示prerequisites列表中所有比target文件更新的文件。
至此,我們對(duì)makefile的規(guī)則就有了基本的了解。當(dāng)然makefile還有很多高級(jí)的用法,本節(jié)只是簡(jiǎn)單介紹在開發(fā)Linux 0.11過程中用到的一些知識(shí)。如果讀者對(duì)makefile的更多用法感興趣,可以通過man make命令查看GNU make的官方手冊(cè)。
- 零起點(diǎn)學(xué)Linux系統(tǒng)管理
- Windows Server 2012 Hyper-V:Deploying the Hyper-V Enterprise Server Virtualization Platform
- 構(gòu)建可擴(kuò)展分布式系統(tǒng):方法與實(shí)踐
- 嵌入式系統(tǒng)原理及開發(fā)
- 網(wǎng)絡(luò)操作系統(tǒng)教程:Windows Server 2016管理與配置
- Microsoft Operations Management Suite Cookbook
- Dreamweaver CS5.5 Mobile and Web Development with HTML5,CSS3,and jQuery
- Django Project Blueprints
- Cassandra 3.x High Availability(Second Edition)
- Windows 7使用詳解(修訂版)
- 應(yīng)急指揮信息系統(tǒng)設(shè)計(jì)
- VMware Horizon Mirage Essentials
- openEuler操作系統(tǒng)核心技術(shù)與行業(yè)應(yīng)用實(shí)踐
- 電腦辦公(Windows 7+Office 2016)入門與提高
- 不可不知的Windows技巧