- 嵌入式Linux C語言程序設計基礎教程(微課版)
- 華清遠見嵌入式學院 劉洪濤 苗德行主編
- 3887字
- 2021-01-08 20:44:07
1.3 嵌入式Linux編譯器GCC的使用
1.3.1 GCC概述

GCC
作為自由軟件的旗艦項目,Richard Stallman在剛開始編寫GCC的時候,只是把它當作一個C程序的編譯器,GCC的意思也只是GNU C Compiler而已。
經過多年的發展,GCC除了能支持C語言,目前還支持Ada語言、C++語言、Java語言、Objective C語言、PASCAL語言、COBOL語言,以及支持函數式編程和邏輯編程的Mercury語言等。GCC也不再單指GNU C語言編譯器,而是變成了GNU編譯器家族。
GCC的編譯流程分為以下4個步驟。
① 預處理(pre-processing)。
② 編譯(compiling)。
③ 匯編(assembling)。
④ 鏈接(linking)。
編譯器通過程序的擴展名來分辨編寫源程序所用的語言。由于不同的程序所需要執行編譯的步驟是不同的,因此GCC根據不同的后綴名對它們進行相應的處理,表1-6所示為不同后綴名的處理方式。
表1-6 GCC所支持不同后綴名的處理方式

1.3.2 GCC編譯流程分析
GCC使用的基本語法為
gcc [option | filename]
這里的option是GCC使用時的一些選項,通過指定不同的選項GCC可以實現強大的功能。這里的filename 則是 GCC 要編譯的文件,GCC 會根據用戶所指定的編譯選項以及所識別的文件后綴名來對編譯文件進行相應的處理。
本小節從編譯流程的角度講解GCC的常見使用方法。
先來分析一段簡單的C語言程序。該程序由兩個文件組成,其中“hello.h”為頭文件,在“hello.c”中包含了“hello.h”,其源文件如下所示。
/*hello.h*/
#ifndef _HELLO_H_
#define _HELLO_H_
typedef unsigned long val32_t;
#endif
/*hello.c*/
#include <stdio.h>
#include <stdlib.h>
#include "hello.h"
int main()
{
val32_t i = 5;
printf("hello, embedded world %d\n",i);
}
1.預處理階段
GCC的選項“-E”可以使編譯器在預處理結束時就停止編譯,選項“-o”是指定GCC輸出的結果,其命令格式如下:
gcc–E–o [目標文件] [編譯文件]
表1-6指出后綴名為“.i”的文件是經過預處理的C原始程序。要注意,“hello.h”文件是不能進行編譯的,因此,使編譯器在預處理后停止的命令如下所示。
[root@localhost gcc]# gcc–E–o hello.i hello.c
在此處,選項“-o”是指目標文件,由表1-6可知,“.i”文件為已經過預處理的C原始程序。以下列出了“hello.i”文件的部分內容。
# 2 "hello.c" 2
# 1 "hello.h" 1
typedef unsigned long val32_t;
# 3 "hello.c" 2
int main()
{
val32_t i = 5;
printf("hello, embedded world %d\n",i);
}
由此可見,GCC確實進行了預處理,它把“hello.h”的內容插入到“hello.i”文件中了。
2.編譯階段
編譯器在預處理結束之后使用。GCC 首先要檢查代碼的規范性、是否有語法錯誤等,以確定代碼實際要做的工作,在檢查無誤后,就開始把代碼翻譯成匯編語言。GCC的選項“-S”能使編譯器在進行完編譯之后就停止。由表1-6可知,“.s”是匯編語言原始程序,因此,此處的目標文件就可設為“.s”類型。
[root@localhost gcc]# gcc–S–o hello.s hello.i
以下列出了“hello.s”的內容,可見GCC已經將其轉化為匯編語言了。感興趣的讀者可以分析一下這一行簡單的C語言小程序用匯編代碼是如何實現的。
.file "hello.c"
.section .rodata
.LC0:
.string "hello, embedded world %d\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $5, -4(%ebp)
subl $8, %esp
pushl -4(%ebp)
pushl $.LC0
call printf
addl $16, %esp
leave
ret
.size main, .-main
.section .note.GNU-stack,"",@progbits
. .ident "GCC: (GNU) 4.0.0 20050519 (Red Hat 4.0.0-8)"
可以看到,這一小段C語言的程序在匯編中已經復雜很多了,這也是C語言作為高級語言的優勢所在。
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”了,而這也就是鏈接的作用。
完成鏈接之后,GCC就可以生成可執行文件,其命令如下:
[root@localhost gcc]# gcc hello.o–o hello
運行該可執行文件,出現正確的結果。
[root@localhost gcc]# ./hello
hello, embedded world 5
1.3.3 GCC警告提示
本小節主要講解 GCC 的警告提示功能。GCC 包含完整的出錯檢查和警告提示功能,它們可以幫助Linux程序員寫出更加專業和高效的代碼。
讀者千萬不能小瞧這些警告信息,在很多情況下,運行含有警告信息的代碼往往會有意想不到的運行結果。
首先讀者可以先看一下以下這段代碼。
#include <stdio.h>
void main(void)
{
long long tmp = 1;
printf("This is a bad code!\n");
}
雖然這段代碼運行的結果是正確的,但還有以下問題。
① main函數的返回值被聲明為void,但實際上應該是int。
② 使用了GNU語法擴展,即使用long long來聲明64位整數,不符合ANSI/ISO C語言標準。
③ main函數在終止前沒有調用return語句。
GCC的警告提示選項有很多種類型,主要可分為“-Wall”類和非“-Wall”類。
1.Wall類警告提示
這一類警告提示選項占了GCC警告選項的90%以上,它不僅包含打開所有警告等功能,還可以單獨對常見錯誤分別指定警告。這些常見的警告選項如表 1-7 所示(這些選項可供讀者在實際操作時查閱使用)。
表1-7 GCC的Wall類警告提示選項

這些警告提示讀者可以根據自己的不同情況進行相應的選擇,這里最為常用的是“-Wall”,上面的這一小段程序使用該警告提示后的結果是:
[root@ft charpter2]# gcc -Wall wrong.c -o wrong
wrong.c:4: warning: return type of 'main' is not 'int'
wrong.c: In function 'main':
wrong.c:5: warning: unused variable 'tmp'
可以看出,使用“-Wall”選項找出了未使用的變量 tmp 以及返回值的問題,但沒有找出無效數據類型的錯誤。
2.非Wall類警告提示
非Wall類的警告提示中最為常用的有以下兩種:“-ansi”和“-pedantic”。
①“-ansi”。該選項強制GCC生成標準語法所要求的警告信息,盡管這還并不能保證所有沒有警告的程序都是符合ANSI C標準的。使用該選項的運行結果如下:
[root@ft charpter2]# gcc -ansi wrong.c -o wrong
wrong.c: In function 'main':
wrong.c:4: warning: return type of 'main' is not 'int'
可以看出,該選項并沒有發現“long long”這個無效數據類型的錯誤。
②“-pedantic”。該選項允許發出ANSI C標準所列的全部警告信息,同樣也保證所有沒有警告的程序都是符合ANSI C標準的。使用該選項的運行結果如下:
[root@ft charpter2]# gcc -pedantic wrong.c -o wrong
wrong.c: In function 'main':
wrong.c:5: warning: ISO C90 does not support 'long long'
wrong.c:4: warning: return type of 'main' is not 'int'
可以看出,使用該選項查看出了“long long”這個無效數據類型的錯誤。
1.3.4 GCC使用庫函數
1.Linux函數庫介紹
函數庫可以看作是事先編寫的函數集合,它可以與主函數分離,使程序模塊化,從而增加代碼的復用性。Linux中函數庫包括兩類:靜態庫和共享庫。
靜態庫的代碼在編譯時就已鏈接到開發人員開發的應用程序中,而共享庫是在程序開始運行時被加載。
由于在使用共享庫時程序中并不包括庫函數的實現代碼,只是包含了對庫函數的引用,因此程序代碼的規模比較小。
系統中可用的庫都安裝在“/usr/lib”和“/lib”目錄下。庫文件名由前綴“lib”和庫名以及后綴組成。庫的類型不同,后綴名也不一樣。
提示
共享庫的后綴名由“.so”和版本號組成。
靜態庫的后綴名為“.a”。
如數學共享庫的庫名為“libm.so.5”,這里的標識字符為m,版本號為5,“libm.a”則是靜態數學庫。
2.相關路徑選項
有些時候庫文件并不存放在系統默認的路徑下。因此,要通過路徑選項來指定相關的庫文件位置。這里首先介紹兩個常用選項的使用方法。
①“-I <dir>”。GCC使用缺省的路徑來搜索頭文件,如果想要改變搜索路徑,用戶可以使用“-I”選項。“-I<dir>”選項可以在頭文件的搜索路徑列表中添加<dir>目錄。這樣,GCC就會到指定的目錄去查找相應的頭文件。
比如在“/root/workplace/gcc”下有兩個文件。
hello.c
#include <my.h>
int main()
{
printf("Hello!!\n");
return 0;
}
my.h
#include <stdio.h>
這樣,就可在GCC命令行中加入“-I”選項,其命令如下:
[root@localhost gcc] gcc hello.c–I/root/workplace/gcc/ -o hello
這樣,GCC就能夠執行出正確結果。
技巧
在include語句中,“<>”表示在標準路徑中搜索頭文件,在Linux中默認為“/usr/include”。故在上例中,可把“hello.c”的“#include <my.h>”改為“#include "my.h"”,這樣就不需要加上“-I”選項了
② “-L <dir>”。選項“-L <dir>”的功能與“-I <dir>”類似,其區別就在于“-L”選項是用于指明庫文件的路徑。例如有程序“hello_sq.c”需要用到目錄“/root/workspace/gcc/lib”下的一個動態庫“libsunq.so”,則只需鍵入如下命令即可。
[root@localhost gcc] gcc hello_sq.c–L/root/workspace/gcc/lib–lsunq–o hello_sq
提示
“-I <dir>”和“-L< dir>”都只是指定了路徑,而沒有指定文件,因此不能在路徑中包含文件名。
3.使用不同類型鏈接庫
使用不同類型的鏈接庫的方法很相似,都是使用選項“-l”(注意這里是小寫的“L”)。該選項是用于指明具體使用的庫文件。由于在 Linux 中函數庫的命名規則都是以“lib”開頭的,因此,這里的庫文件只需填寫lib之后的內容即可。
如有靜態庫文件“libm.a”,在調用時只需寫作“-lm”;同樣對于共享庫文件“libm.so”,在調用時也只需寫作“-lm”即可,其整體調用命令類似如下。
[root@localhost gcc] gcc -o dynamic -L /root/lq/testc/lib/dynamic.o -lmydynamic
那么,若系統中同時存在庫名相同的靜態庫文件和共享庫文件時,該鏈接選項究竟會調用靜態庫文件還是共享庫文件呢?
經測試后可以發現,系統缺省鏈接的是共享庫,這是由于 Linux 系統中默認的是采用動態鏈接的方式。如果用戶要鏈接同名的靜態庫,則在“-l”之前需要添加選項“-static”。例如,鏈接“libm.a”庫文件的選項是“-static -lm”。
1.3.5 GCC代碼優化
GCC可以對代碼進行優化,它通過編譯選項“-On”來控制優化代碼的生成,其中n是一個代表優化級別的整數。對于不同版本的GCC來講,n的取值范圍及其對應的優化效果可能并不完全相同,比較典型的范圍是從0到2或3。
不同的優化級別對應不同的優化處理工作。如使用優化選項“-O”主要進行線程跳轉(thread jump)和延遲退棧(deferred stack pops)兩種優化;使用優化選項“-O2”除了完成所有“-O1”級別的優化之外,同時還要進行一些額外的調整工作,如處理器指令調度等;選項“-O3”則還包括循環展開和其他一些與處理器特性相關的優化工作。
雖然優化選項可以加快代碼的運行速度,但對于調試而言將是一個很大的挑戰。因為代碼在經過優化之后,原先在源程序中聲明和使用的變量很可能不再使用,控制流也可能會突然跳轉到其他的地方,循環語句也有可能因為循環展開而變得到處都有,所有這些都將使調試工作異常艱難。
建議開發人員在調試程序的時候不使用任何優化選項,只有當程序完成調試,最終發行的時候再考慮對其進行優化。
- Java語言程序設計
- Java虛擬機字節碼:從入門到實戰
- JavaScript前端開發與實例教程(微課視頻版)
- Oracle數據庫從入門到運維實戰
- 重學Java設計模式
- Mastering RStudio:Develop,Communicate,and Collaborate with R
- Python入門很輕松(微課超值版)
- Web Developer's Reference Guide
- Python大規模機器學習
- Modernizing Legacy Applications in PHP
- Learning Unreal Engine Game Development
- Shopify Application Development
- Spring Boot從入門到實戰
- Web前端開發精品課:HTML5 Canvas開發詳解
- Visual FoxPro數據庫程序設計