GNU CC(簡稱為Gcc)是GNU項目中符合ANSI C標准的編譯系統,能夠編譯用C、C++和Object C等語言編寫的程序。Gcc不僅功能強大,而且可以編譯如C、C++、Object C、Java、Fortran、Pascal、Modula-3和Ada等多種語言,而且Gcc又是一個交叉平台編譯器,它能夠在當前CPU平台上為多種不同體系結構的硬件平台開發軟件,因此尤其適合在嵌入式領域的開發編譯。本章中的示例,除非特別注明,否則均采用Gcc版本為4.0.0。
GCC入門基礎
表3.6 Gcc所支持后綴名解釋
后 綴 名 |
所對應的語言 |
后 綴 名 |
所對應的語言 |
.c |
C原始程序 |
.s/.S |
匯編語言原始程序 |
.C/.cc/.cxx |
C++原始程序 |
.h |
預處理文件(頭文件) |
.m |
Objective-C原始程序 |
.o |
目標文件 |
.i |
已經過預處理的C原始程序 |
.a/.so |
編譯后的庫文件 |
.ii |
已經過預處理的C++原始程序 |
如本章開頭提到的,Gcc的編譯流程分為了四個步驟,分別為:
· 預處理(Pre-Processing)
· 編譯(Compiling)
· 匯編(Assembling)
· 鏈接(Linking)
下面就具體來查看一下Gcc是如何完成四個步驟的。
首先,有以下hello.c源代碼
#include<stdio.h>
int main()
{
printf("Hello! This is our embedded world!n");
return 0;
}
(1)預處理階段
在該階段,編譯器將上述代碼中的stdio.h編譯進來,並且用戶可以使用Gcc的選項”-E”進行查看,該選項的作用是讓Gcc在預處理結束后停止編譯過程。
注意 |
Gcc指令的一般格式為:Gcc [選項] 要編譯的文件 [選項] [目標文件] 其中,目標文件可缺省,Gcc默認生成可執行的文件,命為:編譯文件.out |
[root@localhost Gcc]# Gcc –E hello.c –o hello.i
在此處,選項”-o”是指目標文件,由表3.6可知,”.i”文件為已經過預處理的C原始程序。以下列出了hello.i文件的部分內容:
typedef int (*__gconv_trans_fct) (struct __gconv_step *,
struct __gconv_step_data *, void *,
__const unsigned char *,
__const unsigned char **,
__const unsigned char *, unsigned char **,
size_t *);
…
# 2 "hello.c" 2
int main()
{
printf("Hello! This is our embedded world!n");
return 0;
}
由此可見,Gcc確實進行了預處理,它把”stdio.h”的內容插入到hello.i文件中。
(2)編譯階段
接下來進行的是編譯階段,在這個階段中,Gcc首先要檢查代碼的規范性、是否有語法錯誤等,以確定代碼的實際要做的工作,在檢查無誤后,Gcc把代碼翻譯成匯編語言。用戶可以使用”-S”選項來進行查看,該選項只進行編譯而不進行匯編,生成匯編代碼。
[root@localhost Gcc]# Gcc –S hello.i –o hello.s
以下列出了hello.s的內容,可見Gcc已經將其轉化為匯編了,感興趣的讀者可以分析一下這一行簡單的C語言小程序是如何用匯編代碼實現的。
.file "hello.c"
.section .rodata
.align 4
.LC0:
.string"Hello! This is our embedded world!"
.text
.globl main
.type main, @function
main:
pushl �p
movl %esp, �p
subl $8, %esp
andl $-16, %esp
movl $0, �x
addl $15, �x
addl $15, �x
shrl $4, �x
sall $4, �x
subl �x, %esp
subl $12, %esp
pushl $.LC0
call puts
addl $16, %esp
movl $0, �x
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.0.0 20050519 (Red Hat 4.0.0-8)"
.section .note.GNU-stack,"",@progbits
(3)匯編階段
匯編階段是把編譯階段生成的”.s”文件轉成目標文件,讀者在此可使用選項”-c”就可看到匯編代碼已轉化為”.o”的二進制目標代碼了。如下所示:
[root@localhost Gcc]# Gcc –c hello.s –o hello.o
(4)鏈接階段
在成功編譯之后,就進入了鏈接階段。在這里涉及到一個重要的概念:函數庫。
讀者可以重新查看這個小程序,在這個程序中並沒有定義”printf”的函數實現,且在預編譯中包含進的”stdio.h”中也只有該函數的聲明,而沒有定義函數的實現,那么,是在哪里實現”printf”函數的呢?最后的答案是:系統把這些函數實現都被做到名為libc.so.6的庫文件中去了,在沒有特別指定時,Gcc會到系統默認的搜索路徑”/usr/lib”下進行查找,也就是鏈接到libc.so.6庫函數中去,這樣就能實現函數”printf”了,而這也就是鏈接的作用。
函數庫一般分為靜態庫和動態庫兩種。靜態庫是指編譯鏈接時,把庫文件的代碼全部加入到可執行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了。其后綴名一般為”.a”。動態庫與之相反,在編譯鏈接時並沒有把庫文件的代碼加入到可執行文件中,而是在程序執行時由運行時鏈接文件加載庫,這樣可以節省系統的開銷。動態庫一般后綴名為”.so”,如前面所述的libc.so.6就是動態庫。Gcc在編譯時默認使用動態庫。
完成了鏈接之后,Gcc就可以生成可執行文件,如下所示。
[root@localhost Gcc]# Gcc hello.o –o hello
運行該可執行文件,出現正確的結果如下。
[root@localhost Gcc]# ./hello
Hello! This is our embedded world!
Gcc編譯選項分析
Gcc有超過100個的可用選項,主要包括總體選項、告警和出錯選項、優化選項和體系結構相關選項。以下對每一類中最常用的選項進行講解。
(1)總體選項
Gcc的總結選項如表3.7所示,很多在前面的示例中已經有所涉及。
表3.7 Gcc總體選項列表
后綴名 |
所對應的語言 |
-c |
只是編譯不鏈接,生成目標文件“.o” |
-S |
只是編譯不匯編,生成匯編代碼 |
-E |
只進行預編譯,不做其他處理 |
-g |
在可執行程序中包含標准調試信息 |
-o file |
把輸出文件輸出到file里 |
-v |
打印出編譯器內部編譯各過程的命令行信息和編譯器的版本 |
-I dir |
在頭文件的搜索路徑列表中添加dir目錄 |
-L dir |
在庫文件的搜索路徑列表中添加dir目錄 |
-static |
鏈接靜態庫 |
-llibrary |
連接名為library的庫文件 |
對於“-c”、“-E”、“-o”、“-S”選項在前一小節中已經講解了其使用方法,在此主要講解另外兩個非常常用的庫依賴選項“-I dir”和“-L dir”。
· “-I dir”
正如上表中所述,“-I dir”選項可以在頭文件的搜索路徑列表中添加dir目錄。由於Linux中頭文件都默認放到了“/usr/include/”目錄下,因此,當用戶希望添加放置在其他位置的頭文件時,就可以通過“-I dir”選項來指定,這樣,Gcc就會到相應的位置查找對應的目錄。
比如在“/root/workplace/Gcc”下有兩個文件:
#include<my.h>
int main()
{
printf(“Hello!!n”);
return 0;
}
#include<stdio.h>
這樣,就可在Gcc命令行中加入“-I”選項:
[root@localhost Gcc] Gcc hello1.c –I /root/workplace/Gcc/ -o hello1
這樣,Gcc就能夠執行出正確結果。
小知識 |
在include語句中,“<>”表示在標准路徑中搜索頭文件,““””表示在本目錄中搜索。故在上例中,可把hello1.c的“#include<my.h>”改為“#include “my.h””,就不需要加上“-I”選項了。 |
· “-L dir”
選項“-L dir”的功能與“-I dir”類似,能夠在庫文件的搜索路徑列表中添加dir目錄。例如有程序hello_sq.c需要用到目錄“/root/workplace/Gcc/lib”下的一個動態庫libsunq.so,則只需鍵入如下命令即可:
[root@localhost Gcc] Gcc hello_sq.c –L /root/workplace/Gcc/lib –lsunq –o hello_sq
需要注意的是,“-I dir”和“-L dir”都只是指定了路徑,而沒有指定文件,因此不能在路徑中包含文件名。
另外值得詳細解釋一下的是“-l”選項,它指示Gcc去連接庫文件libsunq.so。由於在Linux下的庫文件命名時有一個規定:必須以lib三個字母開頭。因此在用-l選項指定鏈接的庫文件名時可以省去lib三個字母。也就是說Gcc在對”-lsunq”進行處理時,會自動去鏈接名為libsunq.so的文件。
(2)告警和出錯選項
Gcc的告警和出錯選項如表3.8所示。
表3.8 Gcc總體選項列表
選項 |
含義 |
-ansi |
支持符合ANSI標准的C程序 |
-pedantic |
允許發出ANSI C標准所列的全部警告信息 |
選項 |
含義 |
-pedantic-error |
允許發出ANSI C標准所列的全部錯誤信息 |
-w |
關閉所有告警 |
-Wall |
允許發出Gcc提供的所有有用的報警信息 |
-werror |
把所有的告警信息轉化為錯誤信息,並在告警發生時終止編譯過程 |
下面結合實例對這幾個告警和出錯選項進行簡單的講解。
如有以下程序段:
#include<stdio.h>
void main()
{
long long tmp = 1;
printf(“This is a bad code!n”);
return 0;
}
這是一個很糟糕的程序,讀者可以考慮一下有哪些問題?
· “-ansi”
該選項強制Gcc生成標准語法所要求的告警信息,盡管這還並不能保證所有沒有警告的程序都是符合ANSI C標准的。運行結果如下所示:
[root@localhost Gcc]# Gcc –ansi warning.c –o warning
warning.c: 在函數“main”中:
warning.c:7 警告:在無返回值的函數中,“return”帶返回值
warning.c:4 警告:“main”的返回類型不是“int”
可以看出,該選項並沒有發現”long long”這個無效數據類型的錯誤。
· “-pedantic”
允許發出ANSI C標准所列的全部警告信息,同樣也保證所有沒有警告的程序都是符合ANSI C標准的。其運行結果如下所示:
[root@localhost Gcc]# Gcc –pedantic warning.c –o warning
warning.c: 在函數“main”中:
warning.c:5 警告:ISO C90不支持“long long”
warning.c:7 警告:在無返回值的函數中,“return”帶返回值
warning.c:4 警告:“main”的返回類型不是“int”
可以看出,使用該選項查看出了”long long”這個無效數據類型的錯誤。
· “-Wall”
允許發出Gcc能夠提供的所有有用的報警信息。該選項的運行結果如下所示:
[root@localhost Gcc]# Gcc –Wall warning.c –o warning
warning.c:4 警告:“main”的返回類型不是“int”
warning.c: 在函數”main”中:
warning.c:7 警告:在無返回值的函數中,”return”帶返回值
warning.c:5 警告:未使用的變量“tmp”
使用“-Wall”選項找出了未使用的變量tmp,但它並沒有找出無效數據類型的錯誤。
另外,Gcc還可以利用選項對單獨的常見錯誤分別指定警告,有關具體選項的含義感興趣的讀者可以查看Gcc手冊進行學習。
(3)優化選項
Gcc可以對代碼進行優化,它通過編譯選項“-On”來控制優化代碼的生成,其中n是一個代表優化級別的整數。對於不同版本的Gcc來講,n的取值范圍及其對應的優化效果可能並不完全相同,比較典型的范圍是從0變化到2或3。
不同的優化級別對應不同的優化處理工作。如使用優化選項“-O”主要進行線程跳轉(Thread Jump)和延遲退棧(Deferred Stack Pops)兩種優化。使用優化選項“-O2”除了完成所有“-O1”級別的優化之外,同時還要進行一些額外的調整工作,如處理器指令調度等。選項“-O3”則還包括循環展開和其他一些與處理器特性相關的優化工作。
雖然優化選項可以加速代碼的運行速度,但對於調試而言將是一個很大的挑戰。因為代碼在經過優化之后,原先在源程序中聲明和使用的變量很可能不再使用,控制流也可能會突然跳轉到意外的地方,循環語句也有可能因為循環展開而變得到處都有,所有這些對調試來講都將是一場噩夢。所以筆者建議在調試的時候最好不使用任何優化選項,只有當程序在最終發行的時候才考慮對其進行優化。
(4)體系結構相關選項
Gcc的體系結構相關選項如表3.9所示。
表3.9Gcc體系結構相關選項列表
選項 |
含義 |
-mcpu=type |
針對不同的CPU使用相應的CPU指令。可選擇的type有i386、i486、pentium及i686等 |
-mieee-fp |
使用IEEE標准進行浮點數的比較 |
-mno-ieee-fp |
不使用IEEE標准進行浮點數的比較 |
-msoft-float |
輸出包含浮點庫調用的目標代碼 |
-mshort |
把int類型作為16位處理,相當於short int |
-mrtd |
強行將函數參數個數固定的函數用ret NUM返回,節省調用函數的一條指令 |
這些體系結構相關選項在嵌入式的設計中會有較多的應用,讀者需根據不同體系結構將對應的選項進行組合處理。在本書后面涉及到具體實例會有針對性的講解。
Gdb調試器
調試是所有程序員都會面臨的問題。如何提高程序員的調試效率,更好更快地定位程序中的問題從而加快程序開發的進度,是大家共同面對的。就如讀者熟知的Windows下的一些調試工具,如VC自帶的如設置斷點、單步跟蹤等,都受到了廣大用戶的贊賞。那么,在Linux下有什么很好的調試工具呢?
本文所介紹的Gdb調試器是一款GNU開發組織並發布的UNIX/Linux下的程序調試工具。雖然,它沒有圖形化的友好界面,但是它強大的功能也足以與微軟的VC工具等媲美。下面就請跟隨筆者一步步學習Gdb調試器。
Gdb使用流程
首先,筆者給出了一個短小的程序,由此帶領讀者熟悉一下Gdb的使用流程。強烈建議讀者能夠實際動手操作。
首先,打開Linux下的編輯器Vi或者Emacs,編輯如下代碼。(由於為了更好地熟悉Gdb的操作,筆者在此使用Vi編輯,希望讀者能夠參見3.3節中對Vi的介紹,並熟練使用Vi)。
#include <stdio.h>
int sum(int m);
int main()
{
int i,n=0;
sum(50);
for(i=1; i<=50; i++)
{
n += i;
}
printf("The sum of 1-50 is %d n", n );
}
int sum(int m)
{
int i,n=0;
for(i=1; i<=m;i++)
n += i;
printf("The sum of 1-m is %dn", n);
}
在保存退出后首先使用Gcc對test.c進行編譯,注意一定要加上選項”-g”,這樣編譯出的可執行代碼中才包含調試信息,否則之后Gdb無法載入該可執行文件。
[root@localhost Gdb]# gcc -g test.c -o test
雖然這段程序沒有錯誤,但調試完全正確的程序可以更加了解Gdb的使用流程。接下來就啟動Gdb進行調試。注意,Gdb進行調試的是可執行文件,而不是如”.c”的源代碼,因此,需要先通過Gcc編譯生成可執行文件才能用Gdb進行調試。
[root@localhost Gdb]# gdb test
GNU Gdb Red Hat Linux (6.3.0.0-1.21rh)
Copyright 2004 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"...Using host libthread_db library "/lib/libthread_db.so.1".
(gdb)
可以看出,在Gdb的啟動畫面中指出了Gdb的版本號、使用的庫文件等信息,接下來就進入了由“(gdb)”開頭的命令行界面了。
(1)查看文件
在Gdb中鍵入”l”(list)就可以查看所載入的文件,如下所示:
注意 |
在Gdb的命令中都可使用縮略形式的命令,如“l”代便“list”,“b”代表“breakpoint”,“p”代表“print”等,讀者也可使用“help”命令查看幫助信息。 |
(Gdb) l
1 #include <stdio.h>
2 int sum(int m);
3 int main()
4 {
5 int i,n=0;
6 sum(50);
7 for(i=1; i<=50; i++)
8 {
9 n += i;
10 }
(Gdb) l
11 printf("The sum of 1~50 is %d n", n );
12
13 }
14 int sum(int m)
15 {
16 int i,n=0;
17 for(i=1; i<=m;i++)
18 n += i;
19 printf("The sum of 1~m is = %dn", n);
20 }
可以看出,Gdb列出的源代碼中明確地給出了對應的行號,這樣就可以大大地方便代碼的定位。
(2)設置斷點
設置斷點是調試程序中是一個非常重要的手段,它可以使程序到一定位置暫停它的運行。因此,程序員在該位置處可以方便地查看變量的值、堆棧情況等,從而找出代碼的症結所在。
在Gdb中設置斷點非常簡單,只需在”b”后加入對應的行號即可(這是最常用的方式,另外還有其他方式設置斷點)。如下所示:
(Gdb) b 6
Breakpoint 1 at 0x804846d: file test.c, line 6.
要注意的是,在Gdb中利用行號設置斷點是指代碼運行到對應行之前將其停止,如上例中,代碼運行到第五行之前暫停(並沒有運行第五行)。
(3)查看斷點情況
在設置完斷點之后,用戶可以鍵入”info b”來查看設置斷點情況,在Gdb中可以設置多個斷點。
(Gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804846d in main at test.c:6
(4)運行代碼
接下來就可運行代碼了,Gdb默認從首行開始運行代碼,可鍵入”r”(run)即可(若想從程序中指定行開始運行,可在r后面加上行號)。
(Gdb) r
Starting program: /root/workplace/Gdb/test
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0x5fb000
Breakpoint 1, main () at test.c:6
6 sum(50);
可以看到,程序運行到斷點處就停止了。
(5)查看變量值
在程序停止運行之后,程序員所要做的工作是查看斷點處的相關變量值。在Gdb中只需鍵入”p”+變量值即可,如下所示:
(Gdb) p n
$1 = 0
(Gdb) p i
$2 = 134518440
在此處,為什么變量”i”的值為如此奇怪的一個數字呢?原因就在於程序是在斷點設置的對應行之前停止的,那么在此時,並沒有把”i”的數值賦為零,而只是一個隨機的數字。但變量”n”是在第四行賦值的,故在此時已經為零。
小技巧 |
Gdb在顯示變量值時都會在對應值之前加上”$N”標記,它是當前變量值的引用標記,所以以后若想再次引用此變量就可以直接寫作”$N”,而無需寫冗長的變量名。 |
(6)單步運行
單步運行可以使用命令”n”(next)或”s”(step),它們之間的區別在於:若有函數調用的時候,”s”會進入該函數而”n”不會進入該函數。因此,”s”就類似於VC等工具中的”step in”,”n”類似與VC等工具中的”step over”。它們的使用如下所示:
(Gdb) n
The sum of 1-m is 1275
7 for(i=1; i<=50; i++)
(Gdb) s
sum (m=50) at test.c:16
16 int i,n=0;
可見,使用”n”后,程序顯示函數sum的運行結果並向下執行,而使用”s”后則進入到sum函數之中單步運行。
(7)恢復程序運行
在查看完所需變量及堆棧情況后,就可以使用命令”c”(continue)恢復程序的正常運行了。這時,它會把剩余還未執行的程序執行完,並顯示剩余程序中的執行結果。以下是之前使用”n”命令恢復后的執行結果:
(Gdb) c
Continuing.
The sum of 1-50 is :1275
Program exited with code 031.
可以看出,程序在運行完后退出,之后程序處於“停止狀態”。
小知識 |
在Gdb中,程序的運行狀態有“運行”、“暫停”和“停止”三種,其中“暫停”狀態為程序遇到了斷點或觀察點之類的,程序暫時停止運行,而此時函數的地址、函數參數、函數內的局部變量都會被壓入“棧”(Stack)中。故在這種狀態下可以查看函數的變量值等各種屬性。但在函數處於“停止”狀態之后,“棧”就會自動撤銷,它也就無法查看各種信息了。 |
Gdb基本命令
Gdb的命令可以通過查看help進行查找,由於Gdb的命令很多,因此Gdb的help將其分成了很多種類(class),用戶可以通過進一步查看相關class找到相應命令。如下所示:
(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
…
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) help data
Examining data.
List of commands:
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
…
Type "help" followed by command name for full documentation.
Command name abbreViations are allowed if unambiguous.
至此,若用戶想要查找call命令,就可鍵入“help call”。
(gdb) help call
Call a function in the program.
The argument is the function name and arguments, in the notation of the
current working language. The result is printed and saved in the value
history, if it is not void.
當然,若用戶已知命令名,直接鍵入“help [command]”也是可以的。
Gdb中的命令主要分為以下幾類:工作環境相關命令、設置斷點與恢復命令、源代碼查看命令、查看運行數據相關命令及修改運行參數命令。以下就分別對這幾類的命令進行講解。
1.工作環境相關命令
Gdb中不僅可以調試所運行的程序,而且還可以對程序相關的工作環境進行相應的設定,甚至還可以使用shell中的命令進行相關的操作,其功能極其強大。表3.10所示列出了Gdb常見工作環境相關命令。
表3.10 Gdb工作環境相關命令
命 令 格 式 |
含義 |
set args運行時的參數 |
指定運行時參數,如:set args 2 |
show args |
查看設置好的運行參數 |
path dir |
設定程序的運行路徑 |
show paths |
查看程序的運行路徑 |
set enVironment var [=value] |
設置環境變量 |
show enVironment [var] |
查看環境變量 |
cd dir |
進入到dir目錄,相當於shell中的cd命令 |
pwd |
顯示當前工作目錄 |
shell command |
運行shell的command命令 |
2.設置斷點與恢復命令
Gdb中設置斷點與恢復的常見命令如表3.11所示。
表3.11 Gdb設置斷點與恢復相關命令
命 令 格 式 |
含義 |
bnfo b |
查看所設斷點 |
break 行號或函數名 <條件表達式> |
設置斷點 |
tbreak 行號或函數名 <條件表達式> |
設置臨時斷點,到達后被自動刪除 |
delete [斷點號] |
刪除指定斷點,其斷點號為”info b”中的第一欄。若缺省斷點號則刪除所有斷點 |
disable [斷點號]] |
停止指定斷點,使用”info b”仍能查看此斷點。同delete一樣,省斷點號則停止所有斷點 |
enable [斷點號] |
激活指定斷點,即激活被disable停止的斷點 |
condition [斷點號] <條件表達式> |
修改對應斷點的條件 |
ignore [斷點號]<num> |
在程序執行中,忽略對應斷點num次 |
step |
單步恢復程序運行,且進入函數調用 |
next |
單步恢復程序運行,但不進入函數調用 |
finish |
運行程序,直到當前函數完成返回 |
c |
繼續執行函數,直到函數結束或遇到新的斷點 |
由於設置斷點在Gdb的調試中非常重要,所以在此再着重講解一下Gdb中設置斷點的方法。
Gdb中設置斷點有多種方式:其一是按行設置斷點,設置方法在3.5.1節已經指出,在此就不重復了。另外還可以設置函數斷點和條件斷點,在此結合上一小節的代碼,具體介紹后兩種設置斷點的方法。
① 函數斷點
Gdb中按函數設置斷點只需把函數名列在命令”b”之后,如下所示:
(gdb) b sum
Breakpoint 1 at 0x80484ba: file test.c, line 16.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x080484ba in sum at test.c:16
要注意的是,此時的斷點實際是在函數的定義處,也就是在16行處(注意第16行還未執行)。
② 條件斷點
Gdb中設置條件斷點的格式為:b 行數或函數名 if 表達式。具體實例如下所示:
(gdb) b 8 if i==10
Breakpoint 1 at 0x804848c: file test.c, line 8.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804848c in main at test.c:8
stop only if i == 10
(gdb) r
Starting program: /home/yul/test
The sum of 1-m is 1275
Breakpoint 1, main () at test.c:9
9 n += i;
(gdb) p i
$1 = 10
可以看到,該例中在第8行(也就是運行完第7行的for循環)設置了一個“i==0”的條件斷點,在程序運行之后可以看出,程序確實在i為10時暫停運行。
3.Gdb中源碼查看相關命令
在Gdb中可以查看源碼以方便其他操作,它的常見相關命令如表3.12所示:
表3.12 Gdb源碼查看相關相關命令
命 令 格 式 |
含義 |
list <行號>|<函數名> |
查看指定位置代碼 |
file [文件名] |
加載指定文件 |
forward-search 正則表達式 |
源代碼前向搜索 |
reverse-search 正則表達式 |
源代碼后向搜索 |
dir dir |
停止路徑名 |
show directories |
顯示定義了的源文件搜索路徑 |
info line |
顯示加載到Gdb內存中的代碼 |
4.Gdb中查看運行數據相關命令
Gdb中查看運行數據是指當程序處於“運行”或“暫停”狀態時,可以查看的變量及表達式的信息,其常見命令如表3.13所示:
表3.13 Gdb查看運行數據相關命令
命 令 格 式 |
含義 |
print 表達式|變量 |
查看程序運行時對應表達式和變量的值 |
x <n/f/u> |
查看內存變量內容。其中n為整數表示顯示內存的長度,f表示顯示的格式,u表示從當前地址往后請求顯示的字節數 |
display 表達式 |
設定在單步運行或其他情況中,自動顯示的對應表達式的內容 |
5.Gdb中修改運行參數相關命令
Gdb還可以修改運行時的參數,並使該變量按照用戶當前輸入的值繼續運行。它的設置方法為:在單步執行的過程中,鍵入命令“set 變量=設定值”。這樣,在此之后,程序就會按照該設定的值運行了。下面,筆者結合上一節的代碼將n的初始值設為4,其代碼如下所示:
(Gdb) b 7
Breakpoint 5 at 0x804847a: file test.c, line 7.
(Gdb) r
Starting program: /home/yul/test
The sum of 1-m is 1275
Breakpoint 5, main () at test.c:7
7 for(i=1; i<=50; i++)
(Gdb) set n=4
(Gdb) c
Continuing.
The sum of 1-50 is 1279
Program exited with code 031.
可以看到,最后的運行結果確實比之前的值大了4。
Gdb的使用切記點: · 在Gcc編譯選項中一定要加入”-g”。 · 只有在代碼處於“運行”或“暫停”狀態時才能查看變量值。 · 設置斷點后程序在指定行之前停止。 |
Make工程管理器
到此為止,讀者已經了解了如何在Linux下使用編輯器編寫代碼,如何使用Gcc把代碼編譯成可執行文件,還學習了如何使用Gdb來調試程序,那么,所有的工作看似已經完成了,為什么還需要Make這個工程管理器呢?
所謂工程管理器,顧名思義,是指管理較多的文件的。讀者可以試想一下,有一個上百個文件的代碼構成的項目,如果其中只有一個或少數幾個文件進行了修改,按照之前所學的Gcc編譯工具,就不得不把這所有的文件重新編譯一遍,因為編譯器並不知道哪些文件是最近更新的,而只知道需要包含這些文件才能把源代碼編譯成可執行文件,於是,程序員就不能不再重新輸入數目如此龐大的文件名以完成最后的編譯工作。
但是,請讀者仔細回想一下本書在3.1.2節中所闡述的編譯過程,編譯過程是分為編譯、匯編、鏈接不同階段的,其中編譯階段僅檢查語法錯誤以及函數與變量的聲明是否正確聲明了,在鏈接階段則主要完成是函數鏈接和全局變量的鏈接。因此,那些沒有改動的源代碼根本不需要重新編譯,而只要把它們重新鏈接進去就可以了。所以,人們就希望有一個工程管理器能夠自動識別更新了的文件代碼,同時又不需要重復輸入冗長的命令行,這樣,Make工程管理器也就應運而生了。
實際上,Make工程管理器也就是個“自動編譯管理器”,這里的“自動”是指它能夠根據文件時間戳自動發現更新過的文件而減少編譯的工作量,同時,它通過讀入Makefile文件的內容來執行大量的編譯工作。用戶只需編寫一次簡單的編譯語句就可以了。它大大提高了實際項目的工作效率,而且幾乎所有Linux下的項目編程均會涉及到它,希望讀者能夠認真學習本節內容。
Makefile基本結構
Makefile是Make讀入的惟一配置文件,因此本節的內容實際就是講述Makefile的編寫規則。在一個Makefile中通常包含如下內容:
· 需要由make工具創建的目標體(target),通常是目標文件或可執行文件;
· 要創建的目標體所依賴的文件(dependency_file);
· 創建每個目標體時需要運行的命令(command)。
它的格式為:
target: dependency_files
command
例如,有兩個文件分別為hello.c和hello.h,創建的目標體為hello.o,執行的命令為gcc編譯指令:gcc –c hello.c,那么,對應的Makefile就可以寫為:
#The simplest example
hello.o: hello.c hello.h
gcc –c hello.c –o hello.o
接着就可以使用make了。使用make的格式為:make target,這樣make就會自動讀入Makefile(也可以是首字母小寫makefile)並執行對應target的command語句,並會找到相應的依賴文件。如下所示:
[root@localhost makefile]# make hello.o
gcc –c hello.c –o hello.o
[root@localhost makefile]# ls
hello.c hello.h hello.o Makefile
可以看到,Makefile執行了“hello.o”對應的命令語句,並生成了“hello.o”目標體。
注意 |
在Makefile中的每一個command前必須有“Tab”符,否則在運行make命令時會出錯。 |
Makefile變量
上面示例的Makefile在實際中是幾乎不存在的,因為它過於簡單,僅包含兩個文件和一個命令,在這種情況下完全不必要編寫Makefile而只需在Shell中直接輸入即可,在實際中使用的Makefile往往是包含很多的文件和命令的,這也是Makefile產生的原因。下面就可給出稍微復雜一些的Makefile進行講解:
sunq:kang.o yul.o
Gcc kang.o bar.o -o myprog
kang.o : kang.c kang.h head.h
Gcc –Wall –O -g –c kang.c -o kang.o
yul.o : bar.c head.h
Gcc - Wall –O -g –c yul.c -o yul.o
在這個Makefile中有三個目標體(target),分別為sunq、kang.o和yul.o,其中第一個目標體的依賴文件就是后兩個目標體。如果用戶使用命令“make sunq”,則make管理器就是找到sunq目標體開始執行。
這時,make會自動檢查相關文件的時間戳。首先,在檢查“kang.o”、“yul.o”和“sunq”三個文件的時間戳之前,它會向下查找那些把“kang.o”或“yul.o”做為目標文件的時間戳。比如,“kang.o”的依賴文件為:“kang.c”、“kang.h”、“head.h”。如果這些文件中任何一個的時間戳比“kang.o”新,則命令“gcc –Wall –O -g –c kang.c -o kang.o”將會執行,從而更新文件“kang.o”。在更新完“kang.o”或“yul.o”之后,make會檢查最初的“kang.o”、“yul.o”和“sunq”三個文件,只要文件“kang.o”或“yul.o”中的任比文件時間戳比“sunq”新,則第二行命令就會被執行。這樣,make就完成了自動檢查時間戳的工作,開始執行編譯工作。這也就是Make工作的基本流程。
接下來,為了進一步簡化編輯和維護Makefile,make允許在Makefile中創建和使用變量。變量是在Makefile中定義的名字,用來代替一個文本字符串,該文本字符串稱為該變量的值。在具體要求下,這些值可以代替目標體、依賴文件、命令以及makefile文件中其它部分。在Makefile中的變量定義有兩種方式:一種是遞歸展開方式,另一種是簡單方式。
遞歸展開方式定義的變量是在引用在該變量時進行替換的,即如果該變量包含了對其他變量的應用,則在引用該變量時一次性將內嵌的變量全部展開,雖然這種類型的變量能夠很好地完成用戶的指令,但是它也有嚴重的缺點,如不能在變量后追加內容(因為語句:CFLAGS = $(CFLAGS) -O在變量擴展過程中可能導致無窮循環)。
為了避免上述問題,簡單擴展型變量的值在定義處展開,並且只展開一次,因此它不包含任何對其它變量的引用,從而消除變量的嵌套引用。
遞歸展開方式的定義格式為:VAR=var
簡單擴展方式的定義格式為:VAR:=var
Make中的變量使用均使用格式為:$(VAR)
注意 |
變量名是不包括“:”、“#”、“=”結尾空格的任何字符串。同時,變量名中包含字母、數字以及下划線以外的情況應盡量避免,因為它們可能在將來被賦予特別的含義。 變量名是大小寫敏感的,例如變量名“foo”、“FOO”、和“Foo”代表不同的變量。 推薦在makefile內部使用小寫字母作為變量名,預留大寫字母作為控制隱含規則參數或用戶重載命令選項參數的變量名。 |
下面給出了上例中用變量替換修改后的Makefile,這里用OBJS代替kang.o和yul.o,用CC代替Gcc,用CFLAGS代替“-Wall -O –g”。這樣在以后修改時,就可以只修改變量定義,而不需要修改下面的定義實體,從而大大簡化了Makefile維護的工作量。
經變量替換后的Makefile如下所示:
OBJS = kang.o yul.o
CC = Gcc
CFLAGS = -Wall -O -g
sunq : $(OBJS)
$(CC) $(OBJS) -o sunq
kang.o : kang.c kang.h
$(CC) $(CFLAGS) -c kang.c -o kang.o
yul.o : yul.c yul.h
$(CC) $(CFLAGS) -c yul.c -o yul.o
可以看到,此處變量是以遞歸展開方式定義的。
Makefile中的變量分為用戶自定義變量、預定義變量、自動變量及環境變量。如上例中的OBJS就是用戶自定義變量,自定義變量的值由用戶自行設定,而預定義變量和自動變量為通常在Makefile都會出現的變量,其中部分有默認值,也就是常見的設定值,當然用戶可以對其進行修改。
預定義變量包含了常見編譯器、匯編器的名稱及其編譯選項。下表3.14列出了Makefile中常見預定義變量及其部分默認值。
表3.14 Makefile中常見預定義變量
命 令 格 式 |
含義 |
AR |
庫文件維護程序的名稱,默認值為ar |
AS |
匯編程序的名稱,默認值為as |
CC |
C編譯器的名稱,默認值為cc |
CPP |
C預編譯器的名稱,默認值為$(CC) –E |
CXX |
C++編譯器的名稱,默認值為g++ |
FC |
FORTRAN編譯器的名稱,默認值為f77 |
RM |
文件刪除程序的名稱,默認值為rm –f |
ARFLAGS |
庫文件維護程序的選項,無默認值 |
ASFLAGS |
匯編程序的選項,無默認值 |
CFLAGS |
C編譯器的選項,無默認值 |
CPPFLAGS |
C預編譯的選項,無默認值 |
CXXFLAGS |
C++編譯器的選項,無默認值 |
FFLAGS |
FORTRAN編譯器的選項,無默認值 |
可以看出,上例中的CC和CFLAGS是預定義變量,其中由於CC沒有采用默認值,因此,需要把“CC=Gcc”明確列出來。
由於常見的Gcc編譯語句中通常包含了目標文件和依賴文件,而這些文件在Makefile文件中目標體的一行已經有所體現,因此,為了進一步簡化Makefile的編寫,就引入了自動變量。自動變量通常可以代表編譯語句中出現目標文件和依賴文件等,並且具有本地含義(即下一語句中出現的相同變量代表的是下一語句的目標文件和依賴文件)。下表3.15列出了Makefile中常見自動變量。
表3.15Makefile中常見自動變量
命令格式 |
含義 |
$* |
不包含擴展名的目標文件名稱 |
$+ |
所有的依賴文件,以空格分開,並以出現的先后為序,可能包含重復的依賴文件 |
$< |
第一個依賴文件的名稱 |
$? |
所有時間戳比目標文件晚的依賴文件,並以空格分開 |
命令格式 |
含義 |
$@ |
目標文件的完整名稱 |
$^ |
所有不重復的依賴文件,以空格分開 |
$% |
如果目標是歸檔成員,則該變量表示目標的歸檔成員名稱 |
自動變量的書寫比較難記,但是在熟練了之后會非常的方便,請讀者結合下例中的自動變量改寫的Makefile進行記憶。
OBJS = kang.o yul.o
CC = Gcc
CFLAGS = -Wall -O -g
sunq : $(OBJS)
$(CC) $^ -o $@
kang.o : kang.c kang.h
$(CC) $(CFLAGS) -c $< -o $@
yul.o : yul.c yul.h
$(CC) $(CFLAGS) -c $< -o $@
另外,在Makefile中還可以使用環境變量。使用環境變量的方法相對比較簡單,make在啟動時會自動讀取系統當前已經定義了的環境變量,並且會創建與之具有相同名稱和數值的變量。但是,如果用戶在Makefile中定義了相同名稱的變量,那么用戶自定義變量將會覆蓋同名的環境變量。
Makefile規則
Makefile的規則是Make進行處理的依據,它包括了目標體、依賴文件及其之間的命令語句。一般的,Makefile中的一條語句就是一個規則。在上面的例子中,都顯示地指出了Makefile中的規則關系,如“$(CC) $(CFLAGS) -c $< -o $@”,但為了簡化Makefile的編寫,make還定義了隱式規則和模式規則,下面就分別對其進行講解。
1.隱式規則
隱含規則能夠告訴make怎樣使用傳統的技術完成任務,這樣,當用戶使用它們時就不必詳細指定編譯的具體細節,而只需把目標文件列出即可。Make會自動搜索隱式規則目錄來確定如何生成目標文件。如上例就可以寫成:
OBJS = kang.o yul.o
CC = Gcc
CFLAGS = -Wall -O -g
sunq : $(OBJS)
$(CC) $^ -o $@
為什么可以省略后兩句呢?因為Make的隱式規則指出:所有“.o”文件都可自動由“.c”文件使用命令“$(CC) $(CPPFLAGS) $(CFLAGS) -c file.c –o file.o”生成。這樣“kang.o”和“yul.o”就會分別調用“$(CC) $(CFLAGS) -c kang.c -o kang.o”和“$(CC) $(CFLAGS) -c yul.c -o yul.o”生成。
注意 |
在隱式規則只能查找到相同文件名的不同后綴名文件,如”kang.o”文件必須由”kang.c”文件生成。 |
下表3.16給出了常見的隱式規則目錄:
表3.16 Makefile中常見隱式規則目錄
對應語言后綴名 |
規則 |
C編譯:.c變為.o |
$(CC) –c $(CPPFLAGS) $(CFLAGS) |
C++編譯:.cc或.C變為.o |
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) |
Pascal編譯:.p變為.o |
$(PC) -c $(PFLAGS) |
Fortran編譯:.r變為-o |
$(FC) -c $(FFLAGS) |
2.模式規則
模式規則是用來定義相同處理規則的多個文件的。它不同於隱式規則,隱式規則僅僅能夠用make默認的變量來進行操作,而模式規則還能引入用戶自定義變量,為多個文件建立相同的規則,從而簡化Makefile的編寫。
模式規則的格式類似於普通規則,這個規則中的相關文件前必須用“%”標明。使用模式規則修改后的Makefile的編寫如下:
OBJS = kang.o yul.o
CC = Gcc
CFLAGS = -Wall -O -g
sunq : $(OBJS)
$(CC) $^ -o $@
%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@
Make使用
使用make管理器非常簡單,只需在make命令的后面鍵入目標名即可建立指定的目標,如果直接運行make,則建立Makefile中的第一個目標。
此外make還有豐富的命令行選項,可以完成各種不同的功能。下表3.17列出了常用的make命令行選項。
表3.17 make的命令行選項
命令格式 |
含 義 |
-C dir |
讀入指定目錄下的Makefile |
-f file |
讀入當前目錄下的file文件作為Makefile |
命令格式 |
含 義 |
-i |
忽略所有的命令執行錯誤 |
-I dir |
指定被包含的Makefile所在目錄 |
-n |
只打印要執行的命令,但不執行這些命令 |
-p |
顯示make變量數據庫和隱含規則 |
-s |
在執行命令時不顯示命令 |
-w |
如果make在執行過程中改變目錄,則打印當前目錄名 |
使用autotools
在上一小節,讀者已經了解到了make項目管理器的強大功能。的確,Makefile可以幫助make完成它的使命,但要承認的是,編寫Makefile確實不是一件輕松的事,尤其對於一個較大的項目而言更是如此。那么,有沒有一種輕松的手段生成Makefile而同時又能讓用戶享受make的優越性呢?本節要講的autotools系列工具正是為此而設的,它只需用戶輸入簡單的目標文件、依賴文件、文件目錄等就可以輕松地生成Makefile了,這無疑是廣大用戶的所希望的。另外,這些工具還可以完成系統配置信息的收集,從而可以方便地處理各種移植性的問題。也正是基於此,現在Linux上的軟件開發一般都用autotools來制作Makefile,讀者在后面的講述中就會了解到。
autotools使用流程
正如前面所言,autotools是系列工具,讀者首先要確認系統是否裝了以下工具(可以用which命令進行查看)。
· aclocal
· autoscan
· autoconf
· autoheader
· automake
使用autotools主要就是利用各個工具的腳本文件以生成最后的Makefile。其總體流程是這樣的:
· 使用aclocal生成一個“aclocal.m4”文件,該文件主要處理本地的宏定義;
· 改寫“configure.scan”文件,並將其重命名為“configure.in”,並使用autoconf文件生成configure文件。
接下來,筆者將通過一個簡單的hello.c例子帶領讀者熟悉autotools生成makefile的過程,由於在這過程中有涉及到較多的腳本文件,為了更清楚地了解相互之間的關系,強烈建議讀者實際動手操作以體會其整個過程。
1.autoscan
它會在給定目錄及其子目錄樹中檢查源文件,若沒有給出目錄,就在當前目錄及其子目錄樹中進行檢查。它會搜索源文件以尋找一般的移植性問題並創建一個文件“configure.scan”,該文件就是接下來autoconf要用到的“configure.in”原型。如下所示:
[root@localhost automake]# autoscan
autom4te: configure.ac: no such file or directory
autoscan: /usr/bin/autom4te failed with exit status: 1
[root@localhost automake]# ls
autoscan.log configure.scan hello.c
如上所示,autoscan首先會嘗試去讀入“configure.ac”(同configure.in的配置文件)文件,此時還沒有創建該配置文件,於是它會自動生成一個“configure.in”的原型文件“configure.scan”。
2.autoconf
configure.in是autoconf的腳本配置文件,它的原型文件“configure.scan”如下所示:
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ(2.59)
#The next one is modified by sunq
#AC_INIT(FULL-PACKAGE-NAME,VERSION,BUG-REPORT-ADDRESS)
AC_INIT(hello,1.0)
# The next one is added by sunq
AM_INIT_AUTOMAKE(hello,1.0)
AC_CONFIG_SRCDIR([hello.c])
AC_CONFIG_HEADER([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
下面對這個腳本文件進行解釋:
· 以“#”號開始的行為注釋。
· AC_PREREQ宏聲明本文件要求的autoconf版本,如本例使用的版本2.59。
· AC_INIT宏用來定義軟件的名稱和版本等信息,在本例中省略了BUG-REPORT-ADDRESS,一般為作者的e-mail。
· AM_INIT_AUTOMAKE是筆者另加的,它是automake所必備的宏,也同前面一樣,PACKAGE是所要產生軟件套件的名稱,VERSION是版本編號。
· AC_CONFIG_SRCDIR宏用來偵測所指定的源碼文件是否存在,來確定源碼目錄的有
效性。在此處為當前目錄下的hello.c。
· AC_CONFIG_HEADER宏用於生成config.h文件,以便autoheader使用。
· AC_CONFIG_FILES宏用於生成相應的Makefile文件。
· 中間的注釋間可以添加分別用戶測試程序、測試函數庫、測試頭文件等宏定義。
接下來首先運行aclocal,生成一個“aclocal.m4”文件,該文件主要處理本地的宏定義。如下所示:
[root@localhost automake]# aclocal
再接着運行autoconf,生成“configure”可執行文件。如下所示:
[root@localhost automake]# autoconf
[root@localhost automake]# ls
aclocal.m4 autom4te.cache autoscan.log configure configure.in hello.c
3.autoheader
接着使用autoheader命令,它負責生成config.h.in文件。該工具通常會從“acconfig.h”文件中復制用戶附加的符號定義,因此此處沒有附加符號定義,所以不需要創建“acconfig.h”文件。如下所示:
[root@localhost automake]# autoheader
4.automake
這一步是創建Makefile很重要的一步,automake要用的腳本配置文件是Makefile.am,用戶需要自己創建相應的文件。之后,automake工具轉換成Makefile.in。在該例中,筆者創建的文件為Makefile.am如下所示:
AUTOMAKE_OPTIONS=foreign
bin_PROGRAMS= hello
hello_SOURCES= hello.c
下面對該腳本文件的對應項進行解釋。
· 其中的AUTOMAKE_OPTIONS為設置automake的選項。由於GNU(在第1章中已經有所介紹)對自己發布的軟件有嚴格的規范,比如必須附帶許可證聲明文件COPYING等,否則automake執行時會報錯。automake提供了三種軟件等級:foreign、gnu和gnits,讓用戶選擇采用,默認等級為gnu。在本例使用foreign等級,它只檢測必須的文件。
· bin_PROGRAMS定義要產生的執行文件名。如果要產生多個執行文件,每個文件名用空格隔開。
· hello_SOURCES定義“hello”這個執行程序所需要的原始文件。如果”hello”這個程序是由多個原始文件所產生的,則必須把它所用到的所有原始文件都列出來,並用空格隔開。例如:若目標體“hello”需要“hello.c”、“sunq.c”、“hello.h”三個依賴文件,則定義hello_SOURCES=hello.c sunq.c hello.h。要注意的是,如果要定義多個執行文件,則對每個執行程序都要定義相應的file_SOURCES。
接下來可以使用automake對其生成“configure.in”文件,在這里使用選項“—adding-missing”可以讓automake自動添加有一些必需的腳本文件。如下所示:
[root@localhost automake]# automake --add-missing
configure.in: installing './install-sh'
configure.in: installing './missing'
Makefile.am: installing 'depcomp'
[root@localhost automake]# ls
aclocal.m4 autoscan.log configure.in hello.c Makefile.am missing
autom4te.cache configure depcomp install-sh Makefile.in config.h.in
可以看到,在automake之后就可以生成configure.in文件。
5.運行configure
在這一步中,通過運行自動配置設置文件configure,把Makefile.in變成了最終的Makefile。如下所示:
[root@localhost automake]# ./configure
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build enVironment is sane... yes
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking for Gcc... Gcc
checking for C compiler default output file name... a.out
checking whether the C compiler works... yes
checking whether we are cross compiling... no
checking for suffix of executables...
checking for suffix of object files... o
checking whether we are using the GNU C compiler... yes
checking whether Gcc accepts -g... yes
checking for Gcc option to accept ANSI C... none needed
checking for style of include used by make... GNU
checking dependency style of Gcc... Gcc3
configure: creating ./config.status
config.status: creating Makefile
config.status: executing depfiles commands
可以看到,在運行configure時收集了系統的信息,用戶可以在configure命令中對其進行方便地配置。在./configure的自定義參數有兩種,一種是開關式(--enable-XXX或--disable-XXX),另一種是開放式,即后面要填入一串字符(--with-XXX=yyyy)參數。讀者可以自行嘗試其使用方法。另外,讀者可以查看同一目錄下的”config.log”文件,以方便調試之用。
到此為止,makefile就可以自動生成了。回憶整個步驟,用戶不再需要定制不同的規則,而只需要輸入簡單的文件及目錄名即可,這樣就大大方便了用戶的使用。下面的圖3.9總結了上述過程:
圖3.9 autotools生成Makefile流程圖
使用autotools所生成的Makefile
autotools生成的Makefile除具有普通的編譯功能外,還具有以下主要功能(感興趣的讀者可以查看這個簡單的hello.c程序的makefile):
1.make
鍵入make默認執行”make all”命令,即目標體為all,其執行情況如下所示:
[root@localhost automake]# make
if Gcc -DPACKAGE_NAME="" -DPACKAGE_TARNAME="" -DPACKAGE_VERSION="" -DPACKAGE_STRING="" -DPACKAGE_BUGREPORT="" -DPACKAGE="hello" -DVERSION="1.0" -I. -I. -g -O2 -MT hello.o -MD -MP -MF ".deps/hello.Tpo" -c -o hello.o hello.c;
then mv -f ".deps/hello.Tpo" ".deps/hello.Po"; else rm -f ".deps/hello.Tpo"; exit 1; fi
Gcc -g -O2 -o hello hello.o
此時在本目錄下就生成了可執行文件“hello”,運行“./hello”能出現正常結果,如下所示:
[root@localhost automake]# ./hello
Hello!Autoconf!
2.make install
此時,會把該程序安裝到系統目錄中去,如下所示:
[root@localhost automake]# make install
if Gcc -DPACKAGE_NAME="" -DPACKAGE_TARNAME="" -DPACKAGE_VERSION="" -DPACKAGE_STRING="" -DPACKAGE_BUGREPORT="" -DPACKAGE="hello" -DVERSION="1.0" -I. -I. -g -O2 -MT hello.o -MD -MP -MF ".deps/hello.Tpo" -c -o hello.o hello.c;
then mv -f ".deps/hello.Tpo" ".deps/hello.Po"; else rm -f ".deps/hello.Tpo"; exit 1; fi
Gcc -g -O2 -o hello hello.o
make[1]: Entering directory '/root/workplace/automake'
test -z "/usr/local/bin" || mkdir -p -- "/usr/local/bin"
/usr/bin/install -c 'hello' '/usr/local/bin/hello'
make[1]: Nothing to be done for 'install-data-am'.
make[1]: LeaVing directory '/root/workplace/automake'
此時,若直接運行hello,也能出現正確結果,如下所示:
[root@localhost automake]# hello
Hello!Autoconf!
3.make clean
此時,make會清除之前所編譯的可執行文件及目標文件(object file, *.o),如下所示:
[root@localhost automake]# make clean
test -z "hello" || rm -f hello
rm -f *.o
4.make dist
此時,make將程序和相關的文檔打包為一個壓縮文檔以供發布,如下所示:
[root@localhost automake]# make dist
[root@localhost automake]# ls hello-1.0-tar.gz
hello-1.0-tar.gz
可見該命令生成了一個hello-1.0-tar.gz的壓縮文件。
由上面的講述讀者不難看出,autotools確實是軟件維護與發布的必備工具,也鑒於此,如今GUN的軟件一般都是由automake來制作的。
想一想 |
對於automake制作的這類軟件,應如何安裝呢? |
Vi使用練習
1.實驗目的
通過指定指令的Vi操作練習,使讀者能夠熟練使用Vi中的常見操作,並且熟悉Vi的三種模式,如果讀者能夠熟練掌握實驗內容中所要求的內容,則表明對Vi的操作已經很熟練了。
2.實驗內容
(1)在“/root”目錄下建一個名為“/Vi”的目錄。
(2)進入“/Vi”目錄。
(3)將文件“/etc/inittab”復制到“/Vi”目錄下。
(4)使用Vi打開“/Vi”目錄下的inittab。
(5)設定行號,指出設定initdefault(類似於“id:5:initdefault”)的所在行號。
(6)將光標移到該行。
(7)復制該行內容。
(8)將光標移到最后一行行首。
(9)粘貼復制行的內容。
(10)撤銷第9步的動作。
(11)將光標移動到最后一行的行尾。
(12)粘貼復制行的內容。
(13)光標移到“si::sysinit:/etc/rc.d/rc.sysinit”。
(14)刪除該行。
(15)存盤但不退出。
(16)將光標移到首行。
(17)插入模式下輸入“Hello,this is Vi world!”。
(18)返回命令行模式。
(19)向下查找字符串“0:wait”。
(20)再向上查找字符串“halt”。
(21)強制退出Vi,不存盤。
分別指出每個命令處於何種模式下?
3.實驗步驟
(1)mkdir /root/Vi
(2)cd /root/Vi
(3)cp /etc/inittab ./
(4)Vi ./inittab
(5):set nu(底行模式)
(6)17<enter>(命令行模式)
(7)yy
(8)G
(9)p
(10)u
(11)$
(12)p
(13)21G
(14)dd
(15):w(底行模式)
(16)1G
(17)i 並輸入“Hello,this is Vi world!”(插入模式)
(18)Esc
(19)/0:wait(命令行模式)
(20)?halt
(21):q!(底行模式)
4.實驗結果
該實驗最后的結果只對“/root/inittab”增加了一行復制的內容:“id:5:initdefault”。
用Gdb調試有問題的程序
1.實驗目的
通過調試一個有問題的程序,使讀者進一步熟練使用Vi操作,而且熟練掌握Gcc編譯命令及Gdb的調試命令,通過對有問題程序的跟蹤調試,進一步提高發現問題和解決問題的能力。這是一個很小的程序,只有35行,希望讀者認真調試。
2.實驗內容
(1)使用Vi編輯器,將以下代碼輸入到名為greet.c的文件中。此代碼的原意為輸出倒序main函數中定義的字符串,但結果顯示沒有輸出。代碼如下所示:
#include <stdio.h>
int display1(char *string);
int display2(char *string);
int main ()
{
char string[] = "Embedded Linux";
display1 (string);
display2 (string);
}
int display1 (char *string)
{
printf ("The original string is %s n", string);
}
int display2 (char *string1)
{
char *string2;
int size,i;
size = strlen (string1);
string2 = (char *) malloc (size + 1);
for (i = 0; i < size; i++)
string2[size - i] = string1[i];
string2[size+1] = ' ';
printf("The string afterward is %sn",string2);
}
(2)使用Gcc編譯這段代碼,注意要加上“-g”選項以方便之后的調試。
(3)運行生成的可執行文件,觀察運行結果。
(4)使用Gdb調試程序,通過設置斷點、單步跟蹤,一步步找出錯誤所在。
(5)糾正錯誤,更改源程序並得到正確的結果。
3.實驗步驟
(1)在工作目錄上新建文件greet.c,並用Vi啟動:vi greet.c。
(2)在Vi中輸入以上代碼。
(3)在Vi中保存並退出:wq。
(4)用Gcc編譯:gcc -g greet.c -o greet。
(5)運行greet:./greet,輸出為:
The original string is Embedded Linux
The string afterward is
可見,該程序沒有能夠倒序輸出。
(6)啟動Gdb調試:gdb greet。
(7)查看源代碼,使用命令“l”。
(8)在30行(for循環處)設置斷點,使用命令“b 30”。
(9)在33行(printf函數處)設置斷點,使用命令“b 33”。
(10)查看斷點設置情況,使用命令“info b”。
(11)運行代碼,使用命令“r”。
(12)單步運行代碼,使用命令“n”。
(13)查看暫停點變量值,使用命令“p string2[size - i]”。
(14)繼續單步運行代碼數次,並使用命令查看,發現string2[size-1]的值正確。
(15)繼續程序的運行,使用命令“c”。
(16)程序在printf前停止運行,此時依次查看string2[0]、string2[1]…,發現string[0]沒有被正確賦值,而后面的復制都是正確的,這時,定位程序第31行,發現程序運行結果錯誤的原因在於“size-1”。由於i只能增到“size-1”,這樣string2[0]就永遠不能被賦值而保持NULL,故輸不出任何結果。
(17)退出Gdb,使用命令q。
(18)重新編輯greet.c,把其中的“string2[size - i] = string1[i]”改為“string2[size – i - 1] = string1[i];”即可。
(19)使用Gcc重新編譯:gcc -g greet.c -o greet。
(20)查看運行結果:./greet
The original string is Embedded Linux
The string afterward is xuniL deddedbmE
這時,輸入結果正確。
4.實驗結果
將原來有錯的程序經過Gdb調試,找出問題所在,並修改源代碼,輸出正確的倒序顯示字符串的結果。
編寫包含多文件的Makefile
1.實驗目的
通過對包含多文件的Makefile的編寫,熟悉各種形式的Makefile,並且進一步加深對Makefile中用戶自定義變量、自動變量及預定義變量的理解。
2.實驗過程
(1)用Vi在同一目錄下編輯兩個簡單的Hello程序,如下所示:
#hello.c
#include "hello.h"
int main()
{
printf("Hello everyone!n");
}
#hello.h
#include <stdio.h>
(2)仍在同一目錄下用Vi編輯Makefile,且不使用變量替換,用一個目標體實現(即直接將hello.c和hello.h編譯成hello目標體)。然后用make驗證所編寫的Makefile是否正確。
(3)將上述Makefile使用變量替換實現。同樣用make驗證所編寫的Makefile是否正確
(4)用編輯另一Makefile,取名為Makefile1,不使用變量替換,但用兩個目標體實現(也就是首先將hello.c和hello.h編譯為hello.o,再將hello.o編譯為hello),再用make的”-f”選項驗證這個Makefile1的正確性。
(5)將上述Makefile1使用變量替換實現。
3.實驗步驟
(1)用Vi打開上述兩個代碼文件“hello.c”和“hello.h”。
(2)在shell命令行中用Gcc嘗試編譯,使用命令:”Gcc hello.c –o hello”,並運行hello可執行文件查看結果。
(3)刪除此次編譯的可執行文件:rm hello。
(4)用Vi編輯Makefile,如下所示:
hello:hello.c hello.h
Gcc hello.c -o hello
(5)退出保存,在shell中鍵入:make,查看結果。
(6)再次用Vi打開Makefile,用變量進行替換,如下所示:
OBJS :=hello.o
CC :=Gcc
hello:$(OBJS)
$(CC) $^ -o $@
(7)退出保存,在shell中鍵入:make,查看結果。
(8)用Vi編輯Makefile1,如下所示:
hello:hello.o
Gcc hello.o -o hello
hello.o:hello.c hello.h
Gcc -c hello.c -o hello.o
(9)退出保存,在shell中鍵入:make -f Makefile1,查看結果。
(10)再次用Vi編輯Makefile1,如下所示:
OBJS1 :=hello.o
OBJS2 :=hello.c hello.h
CC :=Gcc
hello:$(OBJS1)
$(CC) $^ -o $@
$(OBJS1):$(OBJS2)
$(CC) -c $< -o $@
在這里請注意區別“$^”和“$<”。
(11)退出保存,在shell中鍵入:make -f Makefile1,查看結果
4.實驗結果
各種不同形式的makefile都能完成其正確的功能。
使用autotools生成包含多文件的Makefile
1.實驗目的
通過使用autotools生成包含多文件的Makefile,進一步掌握autotools的正確使用方法。同時,掌握Linux下安裝軟件的常用方法。
2.實驗過程
(1)在原目錄下新建文件夾auto。
(2)利用上例的兩個代碼文件“hello.c”和“hello.h”,並將它們復制到該目錄下。
(3)使用autoscan生成configure.scan。
(4)編輯configure.scan,修改相關內容,並將其重命名為configure.in。
(5)使用aclocal生成aclocal.m4。
(6)使用autoconf生成configure。
(7)使用autoheader生成config.in.h。
(8)編輯Makefile.am。
(9)使用automake生成Makefile.in。
(10)使用configure生成Makefile。
(11)使用make生成hello可執行文件,並在當前目錄下運行hello查看結果。
(12)使用make install將hello安裝到系統目錄下,並運行,查看結果。
(13)使用make dist生成hello壓縮包。
(14)解壓hello壓縮包。
(15)進入解壓目錄。
(16)在該目錄下安裝hello軟件。
3.實驗步驟
(1)mkdir ./auto。
(2)cp hello.* ./auto(假定原先在“hello.c”文件目錄下)。
(3)命令:autoscan。
(4)使用Vi編輯configure.scan為:
# -*- Autoconf -*-
# Process this file with autoconf to produce a configure script.
AC_PREREQ(2.59)
AC_INIT(hello, 1.0)
AM_INIT_AUTOMAKE(hello,1.0)
AC_CONFIG_SRCDIR([hello.h])
AC_CONFIG_HEADER([config.h])
# Checks for programs.
AC_PROG_CC
# Checks for libraries.
# Checks for header files.
# Checks for typedefs, structures, and compiler characteristics.
# Checks for library functions.
AC_OUTPUT(Makefile)
(5)保存退出,並重命名為configure.in。
(6)運行:aclocal。
(7)運行:autoconf,並用ls查看是否生成了configure可執行文件。
(8)運行:autoheader。
(9)用Vi編輯Makefile.am文件為:
AUTOMAKE_OPTIONS=foreign
bin_PROGRAMS=hello
hello_SOURCES=hello.c hello.h
(10)運行:automake。
(11)運行:./configure。
(12)運行:make。
(13)運行:./hello,查看結果是否正確。
(14)運行:make install。
(15)運行:hello,查看結果是否正確。
(16)運行:make dist。
(17)在當前目錄下解壓hello-1.0.tar.gz:tar –zxvf hello-1.0.tar.gz。
(18)進入解壓目錄:cd ./hello-1.0。
(19)下面開始Linux下常見的安裝軟件步驟:./configure。
(20)運行:make。
(21)運行:./hello(在正常安裝時這一步可省略)。
(22)運行:make install。
(23)運行:hello,查看結果是否正確。
4.實驗結果
能夠正確使用autotools生成Makefile,並且能夠安裝成功短小的Hello軟件。