第009課 gcc和arm-linux-gcc和Makefile

從 百问网嵌入式Linux wiki
跳到: 導覽搜尋

第001節_gcc編譯器1_gcc常用選項__gcc編譯過程詳解

gcc的使用方法

gcc  [选项]   文件名

gcc常用選項

選項 功能
-v 查看gcc編譯器的版本,顯示gcc執行時的詳細過程
-o <file> 指定輸出文件名為file,這個名稱不能跟源文件名同名
-E 只預處理,不會編譯、匯編、連結t
-S 只編譯,不會匯編、連結
-c 編譯和匯編,不會連結

一個c/c++文件要經過預處理、編譯、匯編和連結才能變成可執行文件。

  • (1)預處理

C/C++源文件中,以#開頭的命令被稱為預處理命令,如包含命令#include、宏定義命令#define、條件編譯命令#if#ifdef等。預處理就是將要包含(include)的文件插入原文件中、將宏定義展開、根據條件編譯命令選擇要使用的代碼,最後將這些東西輸出到一個.i文件中等待進一步處理。

  • (2)編譯

編譯就是把C/C++代碼(比如上述的.i文件)翻譯成匯編代碼。

  • (3)匯編

匯編就是將第二步輸出的匯編代碼翻譯成符合一定格式的機器代碼,在Linux系統上一般表現為ELF目標文件(OBJ文件)。反匯編是指將機器代碼轉換為匯編代碼,這在調試程序時常常用到。

  • (4)連結

連結就是將上步生成的OBJ文件和系統庫的OBJ文件、庫文件連結起來,最終生成了可以在特定平台運行的可執行文件。

hello.c(預處理)->hello.i(編譯)->hello.s(匯編)->hello.o(連結)->hello

詳細的每一步命令如下:

gcc -E -o hello.i hello.c
gcc -S -o hello.s hello.i
gcc -c -o hello.o hello.s
gcc -o hello hello.o

上面一連串命令比較麻煩,gcc會對.c文件默認進行預處理操作,使用-c再來指明了編譯、匯編,從而得到.o文件, 再將.o文件進行連結,得到可執行應用程式。簡化如下:

gcc -c -o hello.o hello.c
gcc -o hello hello.o

第002節_gcc編譯器2_深入講解連結過程

前面編譯出來的可執行文件比原始碼大了很多,這是什麼原因呢?


我們從連結過程來分析,連結將匯編生成的OBJ文件、系統庫的OBJ文件、庫文件連結起來,crt1.o、crti.o、crtbegin.o、crtend.o、crtn.o這些都是gcc加入的系統標準啟動文件,它們的加入使最後出來的可執行文件相原來大了很多。

-lc:链接libc库文件,其中libc库文件中就实现了printf等函数。
gcc -v -nostdlib -o hello hello.o:

會提示因為沒有連結系統標準啟動文件和標準庫文件,而連結失敗。

這個-nostdlib選項常用於裸機bootloader、linux內核等程序,因為它們不需要啟動文件、標準庫文件。

一般應用程式才需要系統標準啟動文件和標準庫文件。 裸機/bootloader、linux內核等程序不需要啟動文件、標準庫文件。


  • 動態連結使用動態連結庫進行連結,生成的程序在執行的時候需要加載所需的動態庫才能運行。

動態連結生成的程序體積較小,但是必須依賴所需的動態庫,否則無法執行。

gcc -c -o hello.o hello.c
gcc -o hello_shared  hello.o


  • 靜態連結使用靜態庫進行連結,生成的程序包含程序運行所需要的全部庫,可以直接運行,

不過靜態連結生成的程序體積較大。

gcc -c -o hello.o hello.c
gcc -static -o hello_static hello.o

第003節_c語言指針複習1__指向char和int的指針

日常中,我們把筆記寫到記事本中,記事本就相當於一個載體(存儲筆記的內容)。
C語言中有些變量,例如,char、int類型的變量,它們也需要一個載體,來存儲這些變量的值,這個載體就是內存。
比如我們的電腦內存有4GB內存,也就是4*1024*1024*1024=4294967296字節。

我們可以把整個內存想像成一串連續格子,每個格子(字節)都可以放入一個數據,如下圖所示。
Chapter9 lesson3 001.jpg

每一個小格子都有一個編號,小格子的編號從0開始,我們可以通過讀取格子的編號,得到格子裏面的內容。同理,我們根據內存的變量的地址,來獲得其中的數據。
下面寫個小程序進行測試,實例:

point_test.c

#include <stdio.h>

int main(int argc, char *argv[])
{
	printf("sizeof(char   ) = %d\n",sizeof(char   ));
	printf("sizeof(int    ) = %d\n",sizeof(int     ));
	printf("sizeof(char  *) = %d\n",sizeof(char  *));	
	printf("sizeof(char  **) = %d\n",sizeof(char **));	
	
	return 0;
}

根據程序可以看出來,函數的功能是輸出,char,int,char **類型所佔據的字節數;

編譯

gcc -o pointer_test pointer_test.c

運行應用程式:

./pointer_test

結果:(我用的是64位的編譯器)

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 8
sizeof(char **) = 8

可以看出在64位的機器中,用8個字節表示指針,我們可以測試一下用32位的機器編譯

編譯:

gcc -m32 -o pointer_test pointer_test.c   //加上-m32:编译成32位的机器码

編譯可能會出現下面提示錯誤:

/usr/include/features.h:374:25: fatal error: sys/cdefs.h: No such file or directory

解決錯誤,安裝lib32readline-gplv2-dev,執行:

sudo apt-get install lib32readline-gplv2-dev

重新編譯

gcc -m32 -o pointer_test pointer_test.c    //没有错误

運行生成的應用程式

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4

可以看出編譯成32位的機器碼,指針就是用4個字節來存儲的,

總結:

1. 所用變量不論是普通變量(char,int)還是指針變量,都存在內存中。

2. 所用變量都可以保存某些值。

3. 怎麼使用指針?

取值

移動指針


實例0

  • 步驟一
#include <stdio.h>

void test0()
{
	char c;
	char *pc;
	
	/*第一步 : 所有变量都保存在内存中,我们打印一下变量的存储地址*/
	printf("&c  =%p\n",&c);
	printf("&pc =%p\n",&pc);
	
}

int main(int argc, char *argv[])
{
	printf("sizeof(char   ) = %d\n",sizeof(char   ));
	printf("sizeof(int    ) = %d\n",sizeof(int    ));	
	printf("sizeof(char  *) = %d\n",sizeof(char  *));
	printf("sizeof(char **) = %d\n",sizeof(char **));	
	printf("//==============\n");
	test0();
	
	return 0;
}
  • 編譯:
gcc -m32 -o pointer_test pointer_test.c
  • 運行:
./pointer_test
  • 結果:
sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
&c  =0xffaaa2b7
&pc =0xffaaa2b8

從運行的結果我們可知,變量c的地址編號(即地址)是0xffaaa2b7,指針變量pc的地址編號是0xffaaa2b8,如下圖所示,編譯成32位的機器碼,字符類型佔用一個字節,指針類型就是用4個字節來存儲的。
Chapter9 lesson3 002.jpg


  • 步驟二

我們把test0()函數裏面的變量保存(賦予)一些值,假如這些變量不保存數據的話,那麼存儲該變量的地址空間就會白白浪費,就相當於買個房子不住,就會白白浪費掉。


我們把上面程序中的test0()函數裏面的字符變量c,指針變量pc進行賦值。

c  = ‘A’;  //把字符‘A’赋值给字符变量c
pc = &c;  //把字符变量c的地址赋值给指针变量pc

然後把賦值後變量的值打印出來

printf("c  =%c\n",c);
printf("pc =%p\n",pc)

編譯:

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
&c  = 0xffb009b7
&pc = 0xffb009b8
c  =  A
pc = 0xffb009b7 


從運行的結構來看字符變量和指針變量的地址編號發成了變化,所以在程序重新運行時,變量的地址,具有不確定性,字符變量c存儲的內容是字符『A』,指針變量pc存儲的內容是0xffb009b7(用四個字節來存儲)。

由於內存的存儲方式是,小端模式:低字節的數據放在低地址,高字節的數據放在高地址。在內存中的存儲格式如下圖所示。
Chapter9 lesson3 003.jpg

  • 步驟三

我們辛辛苦苦定義的指針類型變量,我們要把他用起來了,下面我們來分析一下,用指針來取值,『*』:表示取指針變量存儲地址的數據。

我們在test0()函數裏面添加如下代碼:

printf("*pc =%c\n",*pc);	
printf("//=================\n");

編譯:

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
&c  =0xfff59ea7
&pc =0xfff59ea8
c  =A
pc =0xfff59ea7
*pc =A
//=================

指針變量pc存儲的內容是是字符變量c的地址,所以*pc就想相當於取字符變量c的內容。如圖 Chapter9 lesson3 004.jpg


實例1

  • 步驟一

我們在上面函數的基礎上,寫一個函數test1()

void test1()
{
	int  ia;
	int  *pi;
	char *pc;

	/*第一步 : 所有变量都保存在内存中,我们打印一下变量的存储地址*/
	printf("&ia =%p\n",&ia);
	printf("&pi =%p\n",&pi);	
	printf("&pc =%p\n",&pc);
}

main.c

int main(int argc, char *argv[])
{
	printf("sizeof(char   ) = %d\n",sizeof(char   ));
	printf("sizeof(int    ) = %d\n",sizeof(int    ));	
	printf("sizeof(char  *) = %d\n",sizeof(char  *));
	printf("sizeof(char **) = %d\n",sizeof(char **));	
	printf("//==============\n");
	//test0();
	test1();
	return 0;
}

我們在test1()函數中定義了一個整型變量ia,定義了一個指向整型的指針變量pi,定義了一個指向字符型的指針變量pc。然後打印出這些變量的地址。

編譯

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
&ia =0xffc936e4
&pi =0xffc936e8
&pc =0xffc936ec

在32位的系統中int類型變量在內存中佔用4個字節,指針型變量在內存中佔用4個字節如圖:
Chapter9 lesson3 005.jpg

  • 步驟二

在test1()的函數中對定義的變量進行賦值,然後把賦值的結果打印出來。

/*第二步:所有变量都可以保存某些值,接着赋值并打印*/	
	ia = 0x12345678;
	pi = &ia;
	pc = (char *)&ia;
	printf("ia =0x%x\n",ia);	
	printf("pi =%p\n",pi);		
	printf("pc =%p\n",pc);

編譯

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
&ia = 0xffb6f724
&pi = 0xffb6f728
&pc = 0xffb6f72c
ia = 0x12345678
pi = 0xffb6f724
pc = 0xffb6f724

從結果可以看出來,變量pi和pc的值都等於變量ia的地址。

  • 步驟三

我們使用指針並且對其進行取值,然後移動指針,在test1中添加如下代碼,完成所述要求

/*第三步:使用指针:1)取值  2)移动指针*/

printf("*pi =0x%x\n",*pi); printf("pc =%p\t",pc); printf("*pc =0x%x\n",*pc); pc=pc+1; printf("pc =%p\t",pc); printf("*pc =0x%x\n",*pc); pc=pc+1; printf("pc =%p\t",pc); printf("*pc =0x%x\n",*pc); pc=pc+1; printf("pc =%p\t",pc); printf("*pc =0x%x\n",*pc); printf("//=================\n");

編譯

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
&ia =0xffee0930
&pi =0xffee0934
&pc =0xffee0938
ia =0x12345678
pi =0xffee0930
pc =0xffee0930
*pi =0x12345678
pc =0xffee0930  *pc =0x78
pc =0xffee0931  *pc =0x56
pc =0xffee0932  *pc =0x34
pc =0xffee0933  *pc =0x12
 

由於pi指向了ia,所以*pi的值為0x12345678。由於pc也指向了ia,但是由於pc是字符型指針變量,一次只能訪問一個字節,需要四次才能訪問完。如圖所示:

Chapter9 lesson3 002.jpg

結論:

1. 指針變量所存儲的內容是所指向的變量在內存中的起始地址。

2. &變量:

目的:獲得變量在內存中的地址; 返回:變量在內存中起始地址;

第004節_c語言指針複習2_指向數組和字符串的指針

實例2

我們在pointer_test.c的文件中寫一個test2()函數,我們定義一個有3個元素的字符數組初始化值分別為,』A』, 』B』, 』C』,然後定義一個字符指針pc,把數組ca的首地址複製給字符指針pc,然後通過訪問指針變量pc,來讀取指針變量pc所指向地址的數據,代碼如下:

void test2()
{
	char ca[3]={'A','B','C'};
	char *pc;

	/*第一步 : 所有变量都保存在内存中,我们打印一下变量的存储地址*/
	printf("ca  =%p\n",ca);
	printf("&pc =%p\n",&pc);

	/*第二步:所有变量都可以保存某些值,接着赋值并打印*/
	//前面已经有ca[3]={'A','B','C'};
	pc = ca;
	printf("pc =%p\n",pc);

	/*第三步:使用指针:1)取值  2)移动指针*/
	printf("pc =%p\t",pc);	printf("*pc =0x%x\n",*pc); pc=pc+1;
	printf("pc =%p\t",pc);	printf("*pc =0x%x\n",*pc); pc=pc+1;
	printf("pc =%p\t",pc);	printf("*pc =0x%x\n",*pc);	
	printf("//=================\n");			
}

main()函數

int main(int argc,char **argv)
{
	printf("sizeof(char   )=%d\n",sizeof(char   ));
	printf("sizeof(int    )=%d\n",sizeof(int    ));
	printf("sizeof(char  *)=%d\n",sizeof(char  *));
	printf("sizeof(char **)=%d\n",sizeof(char **));	
	printf("//=================\n");
	//test0();
	//test1();
	test2();
	return 0;
}

編譯

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
ca  =0xffb946b9
&pc =0xffb946b4
pc =0xffb946b9
pc =0xffb946b9  *pc =0x41
pc =0xffb946ba  *pc =0x42
pc =0xffb946bb  *pc =0x43
//=================

分析:

  • 第一步:

首先定義一個3個元素的字符數組ca(數組名表示該數組存儲的首地址),然後定義一個字符指針pc,然後通過printf()函數把定義這兩個變量在內存中的地址打印出來。


  • 第二步:

執行pc = ca;就是把數組ca的首地址複製給指針變量pc,然後通過printf()函數打印pc的值可以看出pc的值就是字符數組ca的首地址0xffb946b9。


  • 第三步:

通過移動指針我們可以發現數組所佔用的內存是連續的,0x41(的ascii值『A『),0x42(的ascii值『B『),0x43(的ascii值『C『)。
如圖
Chapter9 lesson4 001.jpg


實例3

我們在pointer_test.c的文件中寫一個test3()函數,我們定義一個有3個元素的整型數組ia,初始化值分別為,0x12345678, 0x87654321, 0x13572468,然後定義一個整型指針pi,把數組ia的首地址複製給整型指針pi,然後通過訪問指針變量pi,來讀取指針變量pi所指向地址的數據,代碼如下:

void test3()
{
	int ia[3]={0x12345678,0x87654321,0x13572468};
	int *pi;

	/*第一步 : 所有变量都保存在内存中,我们打印一下变量的存储地址*/
	printf("ia  =%p\n",i);
	printf("&pi =%p\n",&pi);

	/*第二步:所有变量都可以保存某些值,接着赋值并打印*/
	//前面已经有ia[3]={0x12345678,0x87654321,0x13572468};
	pi = ia;
	printf("pi =%p\n",pi);

	/*第三步:使用指针:1)取值  2)移动指针*/
	printf("pi =%p\t",pi);	printf("*pi =0x%x\n",*pi); pi=pi+1;
	printf("pi =%p\t",pi);	printf("*pi =0x%x\n",*pi); pi=pi+1;
	printf("pi =%p\t",pi);	printf("*pi =0x%x\n",*pi); 
	printf("//=================\n");	
}

把main()函數test2()修改為test3().

編譯

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
ia  =0xff91c060
&pi =0xff91c05c
pi =0xff91c060
pi =0xff91c060  *pi =0x12345678
pi =0xff91c064  *pi =0x87654321
pi =0xff91c068  *pi =0x13572468

分析:

  • 第一步:

我們定義一個有3個元素的整型數組ia數組名表示該數組存儲的首地址),初始化值分別為,0x12345678, 0x87654321, 0x13572468, 然後定義一個整型指針pi,然後通過printf()函數把定義這兩個變量在內存中的地址打印出來。


  • 第二步:

執行pi = ia; 就是把數組ia的首地址複製給指針變量pi,然後通過printf()函數打印pi的值可以看出pi的值就是整型數組ia的首地址0xff91c060。


  • 第三步:

我們知道 pi是整型指針變量,並且整型變量佔用四個字節,所以整型指針變量pi是以四字節為單元進行訪問的,所以pi和pi+1之間的差是一個整型變量的大小(4個字節)。


Chapter9 lesson4 002.jpg

實例4

定義一個指向字符串的指針pc,然後對字符串指針進行初始化設置為abc,代碼如下:

void test4()
{
 	char *pc="abc";
	/*第一步 : 所有变量都保存在内存中,我们打印一下变量的存储地址*/
	printf("&pc =%p\n",&pc);

	/*第二步:所有变量都可以保存某些值,接着赋值并打印*/
	//前面已经有pc="abc";
	
	/*第三步:使用指针:1)取值  2)移动指针*/
	printf("pc    =%p\n", pc);	
	printf("*pc   =%c\n",*pc);
	printf("pc str=%s\n", pc);	
}

把main()函數test3()修改為test4(). 編譯

gcc -m32 -o pointer_test pointer_test.c

運行:

./pointer_test

結果:

sizeof(char   ) = 1
sizeof(int    ) = 4
sizeof(char  *) = 4
sizeof(char **) = 4
//==============
&pc   =0xfff49a68
pc    =0x08048b4b
*pc   =a
pc str=abc

分析:

  • 第一步:

定義一個指向字符串的指針pc,然後對字符串指針進行初始化設置為abc,此時,指針變量pc的值就是字符串abc的首地址,然後通過printf()函數把指針pc的地址打印出來為0xfff49a68


  • 第二步:

首先通過printf()函數打印出指針變量pc的值(字符串abc的首地址),pc的值為0x08048b4b,然後通過pc指針訪問第一個字符(pc的就是字符串的首地址),所以pc的值就是字符『a『的地址,所以*pc的值就是』a『, 如圖所示:
Chapter9 lesson4 003.jpg

下面分析一下指向數組的指針和指向字符串的指針:

char ca[3]={'A','B','C'};
char *pc0 = ca;

pc0是指向字符數組的字符指針,pc0就是數組首元素的地址,pc0=&a[0]

char *pc11="abc";

pc是指向字符串的字符指針,pc1就是字符串"abc"的首字符'a'的地址。

第005節_Makefile的引入及規則

使用keil, mdk, avr等工具開發程序時點擊鼠標就可以編譯了,它的內部機制是什麼?它怎麼組織管理程序?怎麼決定編譯哪一個文件?

答:實際上windows工具管理程序的內部機制,也是Makefile,我們在linux下來開發裸板程序的時候,使用Makefile組織管理這些程序,本節我們來講解Makefile最基本的規則。Makefile要做什麼事情呢? 組織管理程序,組織管理文件,我們寫一個程序來實驗一下:

文件a.c

02	#include <stdio.h>
03
04	int main()
05	{
06	func_b();
07	return 0;
08}

文件b.c

2	#include <stdio.h>
3
4	void func_b()
5	{
6		printf("This is B\n");
7	}

編譯:

gcc -o test a.c b.c

運行:

./test

結果:

This is B

gcc -o test a.c b.c這條命令雖然簡單,但是它完成的功能不簡單。 我們來看看它做了哪些事情。

我們知道.c程序 --> 得到可執行程序

它們之間要經過四個步驟:

1.預處理

2.編譯

3.匯編

4.連結

我們經常把前三個步驟統稱為編譯了。我們具體分析:gcc -o test a.c b.c這條命令 它們要經過下面幾個步驟:

1).對於a.c執行:預處理 編譯 匯編 的過程,a.c -->xxx.s -->xxx.o 文件。

2).對於b.c執行:預處理 編譯 匯編 的過程,b.c -->yyy.s -->yyy.o 文件。

3).最後:xxx.o和yyy.o連結在一起得到一個test應用程式。

提示:gcc -o test a.c b.c -v :加上一個『-v』選項可以看到它們的處理過程,

第一次編譯a.c得到xxx.o文件,這是很合乎情理的, 執行完第一次之後,如果修改a.c 又再次執行:gcc -o test a.c b.c,對於a.c應該重新生成xxx.o,但是對於b.c又會重新編譯一次,這完全沒有必要,b.c根本沒有修改,直接使用第一次生成的yyy.o文件就可以了。

缺點:對所有的文件都會再處理一次,即使b.c沒有經過修改,b.c也會重新編譯一次, 當文件比較少時,這沒有沒有什麼問題,當文件非常多的時候,就會帶來非常多的效率問題。

如果文件非常多的時候,我們,只是修改了一個文件,所用的文件就會重新處理一次,編譯的時候就會等待很長時間。

對於這些源文件,我們應該分別處理,執行:預處理 編譯 匯編 ,先分別編譯它們,最後再把它們連結在一次,比如:

編譯:

gcc -o a.o a.c
gcc -o b.o b.c

連結:

gcc -o test a.o b.o

比如:上面的例子,當我們修改a.c之後,a.c會重現編譯然後再把它們連結在一起就可以了。,b.c 就不需要重新編譯。

那麼問題又來了,怎麼知道哪些文件被更新了/被修改了?

比較時間:比較a.o和a.c的時間,如果a.c的時間比a.o的時間更加新的話,就表明a.c被修改了,同理b.o和b.c也會進行同樣的比較。比較test和a.o, b.o的時間,如果a.o或者b.o的時間比test更加新的話,就表明應該重新生成test。Makefile 就是這樣做的。

我們現在來寫出一個簡單的Makefile: makefie最基本的語法是規則,規則:

目标 :   依赖1   依赖2  ...
[TAB]命令

當「依賴」比「目標」新,執行它們下面的命令。我們要把上面三個命令寫成makefile規則,如下:

test :a.o b.o  //test是目标,它依赖于a.o b.o文件,一旦a.o或者b.o比test新的时候,

就需要執行下面的命令,重新生成test可執行程序。

gcc -o test a.o b.o
a.o : a.c  //a.o依赖于a.c,当a.c更加新的话,执行下面的命令来生成a.o
gcc -c -o a.o a.c
b.o : b.c  //b.o依赖于b.c,当b.c更加新的话,执行下面的命令,来生成b.o
gcc -c -o b.o b.c

我們來作一下實驗:

在改目錄下我們寫一個Makefile文件:

文件:Makefile

1	test:a.o b.o
2		gcc -o test a.o b.o
3	
4	a.o : a.c
5		gcc -c -o a.o a.c
6
7	b.o : b.c
8		gcc -c -o b.o b.c

上面是makefile中的三條規則。makefile,就是名字為「makefile」的文件。當我們想編譯程序時,直接執行make命令就可以了,一執行make命令它想生成第一個目標test可執行程序, 如果發現a.o 或者b.o沒有,就要先生成a.o或者b.o,發現a.o依賴a.c,有a.c

但是沒有a.o,他就會認為a.c比a.o新,就會執行它們下面的命令來生成a.o,同理b.o和b.c的處理關係也是這樣的。

如果修改a.c ,我們再次執行make,它的本意是想生成第一個目標test應用程式,

它需要先生成a.o, 發現a.o依賴a.c(執行我們修改了a.c)發現a.c比a.o更加新,就會執行gcc -c -o a.o a.c命令來生成a.o文件。b.o依賴b.c,發現b.c並沒有修改,就不會執行gcc -c -o b.o b.c來重新生成b.o文件。現在a.o b.o都有了,其中的a.o比test更加新,就會執行gcc -o test a.o b.o來重新連結得到test可執行程序。所以當執行make命令時候就會執行下面兩條執行:

	gcc -c -o a.o a.c
	gcc -o test a.o b.o

我們第一次執行make的時候,會執行下面三條命令(三條命令都執行): gcc -c -o a.o a.c gcc -c -o b.o b.c gcc -o test a.o b.o


再次執行make 就會顯示下面的提示:

make: `test' is up to date.

我們再次執行make 就會判斷Makefile文件中的依賴,發現依賴沒有更新,所以目標文件就不會重現生成,就會有上面的提示。當我們修改a.c後,重新執行make,

就會執行下面兩條指令:

gcc -c -o a.o a.c
gcc -o test a.o b.o

我們同時修改a.c b.c,執行make就會執行下面三條指令。

gcc -c -o a.o a.c
gcc -c -o b.o b.c
gcc -o test a.o b.o

a.c文件修改了,重新編譯生成a.o, b.c修改了重新編譯生成b.o,a.o, b.o都更新了重新連結生成test可執行程序,makefile的規則其實還是比較簡單的。

規則是Makefie的核心,執行make命令的時候,就會在當前目錄下面找到名字為:Makefile的文件,根據裏面的內容來執行裏面的判斷/命令。

第006節_Makefile的語法

本節我們只是簡單的講解Makefile的語法,如果想比較深入學習Makefile的話可以:

a. 百度搜 "gnu make 於鳳昌"。

b. 查看官方文檔: http://www.gnu.org/software/make/manual/

通配符

假如一個目標文件所依賴的依賴文件很多,那樣豈不是我們要寫很多規則,這顯然是不合乎常理的。

我們可以使用通配符,來解決這些問題。

我們對上節程序進行修改代碼如下:

test: a.o b.o 
	gcc -o test $^
	
%.o : %.c
	gcc -c -o $@ $<
%.o:表示所用的.o文件
%.c:表示所有的.c文件
$@:表示目标
$<:表示第1个依赖文件
$^:表示所有依赖文件

我們來在該目錄下增加一個c.c文件,代碼如下:

#include <stdio.h>

void func_c()
{
	printf("This is C\n");
}

然後在main函數中調用修改Makefile,修改後的代碼如下:

test: a.o b.o c.o
	gcc -o test $^
	
%.o : %.c
	gcc -c -o $@ $<

執行:

make

結果:

gcc -c -o a.o a.c
gcc -c -o b.o b.c
gcc -c -o c.o c.c
gcc -o test a.o b.o c.o

運行:

./test

結果:

This is B
This is C

假想目標: .PHONY

1.我們想清除文件,我們在Makefile的結尾添加如下代碼就可以了:

clean:
	rm *.o test

1).執行make:生成第一個可執行文件。

2).執行make clean: 清除所有文件,即執行:rm *.o test。

make後面可以帶上目標名,也可以不帶,如果不帶目標名的話它就想生成第一個規則裏面的第一個目標。


2.使用Makefile

執行:make [目標]

也可以不跟目標名,若無目標默認第一個目標。我們直接執行make的時候,會在makefile裏面找到第一個目標然後執行下面的指令生成第一個目標。當我們執行make clean的時候,就會在Makefile裏面找到clean這個目標,然後執行裏面的命令,這個寫法有些問題,原因是我們的目錄裏面沒有clean這個文件,這個規則執行的條件成立,他就會執行下面的命令來刪除文件。

如果:該目錄下面有名為clean文件怎麼辦呢?

我們在該目錄下創建一個名為「clean」的文件,然後重新執行:make然後make clean,結果(會有下面的提示:):

make: `clean' is up to date.

它根本沒有執行我們的刪除操作,這是為什麼呢?

我們之前說,一個規則能過執行的條件:

1).目標文件不存在

2).依賴文件比目標新。


現在我們的目錄裏面有名為「clean」的文件,目標文件是有的,並且沒有依賴文件,沒有辦法判斷依賴文件的時間。這種寫法會導致:有同名的"clean"文件時,就沒有辦法執行make clean操作。解決辦法:我們需要把目標定義為假想目標,用關鍵字PHONY。

.PHONY: clean	  //把clean定义为假想目标。他就不会判断名为“clean”的文件是否存在,

然後在Makfile結尾添加.PHONY: clean語句,重新執行:make clean,就會執行刪除操作。

變量

在makefile中有兩種變量:

  • 1)簡單變量(即時變量):
A := xxx   # A的值即刻确定,在定义时即确定

對於即時變量使用「:=」表示,它的值在定義的時候已經被確定了


  • 2)延時變量
B = xxx    # B的值使用到时才确定 

對於延時變量使用「=」表示。它只有在使用到的時候才確定,在定義/等於時並沒有 確定下來。

想使用變量的時候使用「$」來引用,如果不想看到命令是,可以在命令的前面加上"@"符號,就不會顯示命令本身。當我們執行make命令的時候,make這個指令本身,會把整個Makefile讀進去,進行全部分析,然後解析裏面的變量。常用的變量的定義如下:

:=   # 即时变量
=    # 延时变量
?=   # 延时变量, 如果是第1次定义才起效, 如果在前面该变量已定义则忽略这句
+=   # 附加, 它是即时变量还是延时变量取决于前面的定义
?=:如果这个变量在前面已经被定义了,这句话就不会起效果,

實例:

A := $(C)
B = $(C)
C = abc

#D = 100ask
D ?= weidongshan

all:
	@echo A = $(A)
	@echo B = $(B)
	@echo D = $(D)

C += 123

執行:

make

結果:

A =
B = abc 123
D = weidongshan

分析:

1) A := $(C): A為即時變量,在定義時即確定,由於剛開始C的值為空,所以A的值也為空。

2) B = $(C): B為延時變量,只有使用到時它的值才確定,當執行make時,會解析Makefile裏面的所用變量,所以先解析C = abc,然後解析C += 123,此時,C = abc 123,當執行:@echo B = $(B) B的值為 abc 123。

3) D ?= weidongshan: D變量在前面沒有定義,所以D的值為weidongshan,如果在前面添加D = 100ask,最後D的值為100ask。

我們還可以通過命令行存入變量的值 例如:

執行:

make D=123456

裏面的D ?= weidongshan這句話就不起作用了。

結果:

A =
B = abc 123
D = 123456

第007節_Makefile函數

makefile 裏面可以包含很多函數,這些函數都是make本身實現的,下面我們來幾個常用的函數。

引用一個函數用「$」。

函數foreach

函數foreach語法如下:

$(foreach var,list,text)

前兩個參數,『var』和『list』,將首先擴展,注意最後一個參數『text』此時不擴展;接着,對每一個『list』擴展產生的字,將用來為『var』擴展後命名的變量賦值;然後『text』引用該變量擴展;因此它每次擴展都不相同。結果是由空格隔開的『text』 在『list』中多次擴展的字組成的新的『list』。『text』多次擴展的字串聯起來,字與字之間由空格隔開,如此就產生了函數foreach的返回值。

實例:

A = a b c
B = $(foreach f, &(A), $(f).o)

all:
	@echo B = $(B)

結果:

B = a.o b.o c.o

函數filter/filter-out

函數filter/filter-out語法如下:

$(filter pattern...,text)      # text中取出符合patten格式的值
$(filter-out pattern...,text)  # text中取出不符合patten格式的值

實例:

C = a b c d/

D = $(filter %/, $(C))
E = $(filter-out %/, $(C))

all:
        @echo D = $(D)
        @echo E = $(E)

結果:

D = d/
E = a b c

Wildcard

函數Wildcard語法如下:

$(wildcard pattern)   # pattern定义了文件名的格式, wildcard取出其中存在的文件

這個函數wildcard會以pattern這個格式,去尋找存在的文件,返回存在文件的名字。

實例:

在該目錄下創建三個文件:a.c b.c c.c

files = $(wildcard *.c)

all:
        @echo files = $(files)

結果:

files = a.c b.c c.c

我們也可以用wildcard函數來判斷,真實存在的文件 實例:

files2 = a.c b.c c.c d.c e.c  abc
files3 = $(wildcard $(files2))

all:
        @echo files3 = $(files3)

結果:

files3 = a.c b.c c.c

patsubst函數

函數patsubst語法如下:

$(patsubst pattern,replacement,$(var))

patsubst函數是從var變量裏面取出每一個值,如果這個符合pattern格式,把它替換成replacement格式。

實例:

files2  = a.c b.c c.c d.c e.c abc

dep_files = $(patsubst %.c,%.d,$(files2))

all:
        @echo dep_files = $(dep_files)

結果:

dep_files = a.d b.d c.d d.d e.d abc

第008節_Makefile實例

前面講了那麼多Makefile的知識,現在開始做一個實例。

之前編譯的程序002_syntax,有個缺陷,將其複製出來,新建一個003_example文件夾,放在裏面。 在c.c裏面,包含一個頭文件c.h,在c.h裏面定義一個宏,把這個宏打印出來。

c.c:

#include <stdio.h>
#include <c.h>

void func_c()
{
	printf("This is C = %d\n", C);
}

c.h:

#define C 1

然後上傳編譯,執行./test,打印出:

This is B
This is C =1

測試沒有問題,然後修改c.h:

#define C 2

重新編譯,發現沒有更新程序,運行,結果不變,說明現在的Makefile存在問題。

為什麼會出現這個問題呢, 首先我們test依賴c.o,c.o依賴c.c,如果我們更新c.c,會重新更新整個程序。 但c.o也依賴c.h,我們更新了c.h,並沒有在Makefile上體現出來,導致c.h的更新,Makefile無法檢測到。 因此需要添加:

c.o : c.c c.h

現在每次修改c.h,Makefile都能識別到更新操作,從而更新最後輸出文件。

這樣又冒出了一個新的問題,我們怎麼為每個.c文件添加.h文件呢?對於內核,有幾萬個文件,不可能為每個文件依次寫出其頭文件。 因此需要做出改進,讓其自動生成頭文件依賴,可以參考這篇文章:http://blog.csdn.net/qq1452008/article/details/50855810

gcc -M c.c // 打印出依赖

gcc -M -MF c.d c.c  // 把依赖写入文件c.d

gcc -c -o c.o c.c -MD -MF c.d  // 编译c.o, 把依赖写入文件c.d

修改Makefile如下:

objs = a.o b.o c.o

dep_files := $(patsubst %,.%.d, $(objs))
dep_files := $(wildcard $(dep_files))

test: $(objs)
	gcc -o test $^

ifneq ($(dep_files),)
include $(dep_files)
endif

%.o : %.c
	gcc -c -o $@ $< -MD -MF .$@.d

clean:
	rm *.o test

distclean:
	rm $(dep_files)
	
.PHONY: clean

首先用obj變量將.o文件放在一塊。 利用前面講到的函數,把obj里所有文件都變為.%.d格式,並用變量dep_files表示。 利用前面介紹的wildcard函數,判斷dep_files是否存在。 然後是目標文件test依賴所有的.o文件。 如果dep_files變量不為空,就將其包含進來。 然後就是所有的.o文件都依賴.c文件,且通過-MD -MF生成.d依賴文件。 清理所有的.o文件和目標文件 清理依賴.d文件。

現在我門修改了任何.h文件,最終都會影響最後生成的文件,也沒任何手工添加.h、.c、.o文件,完成了支持頭文件依賴。

下面再添加CFLAGS,即編譯參數。比如加上編譯參數-Werror,把所有的警告當成錯誤。

CFLAGS = -Werror -Iinclude

…………


%.o : %.c
	gcc $(CFLAGS) -c -o $@ $< -MD -MF .$@.d

現在重新make,發現以前的警告就變成了錯誤,必須要解決這些錯誤編譯才能進行。在a.c裏面聲明一下函數:

void func_b();
void func_c();

重新make,錯誤就沒有了。

除了編譯參數-Werror,還可以加上-I參數,指定頭文件路徑,-Iinclude表示當前的inclue文件夾下。 此時就可以把c.c文件里的#include ".h"改為#include <c.h>,前者表示當前目錄,後者表示編譯器指定的路徑和GCC路徑。

《《所有章節目錄》》