gcc 常用引數介紹

smilestone322發表於2015-01-05

轉自:http://blog.chinaunix.net/uid-13539494-id-1991081.html

 

1)       常見編譯引數介紹
gcc可以說是個偉大的編譯器集合。支援c, c++, object-c, java, fortran, pascal, ada等一大堆語言,同時支援幾乎所有32位以上的cpu和部分16位、8位cpu。因此幾乎所有開源作業系統、商業unix作業系統、嵌入式開發都使用gcc做編譯器。風河也使用gcc來編譯vxworks的程式碼。下面介紹些最常用的編譯引數(僅以gcc作為c編譯器使用時進行說明)。更多的引數請參考gcc的文件。或者開始選單裡的Tornado Online Manual。

-O 優化引數。後面可以跟數字表示優化級別-O0表示不優化,-O1,-O2,-O3優化程度依次遞增。大部分平臺最高階別是-O3,也就是說3以後的數字基本都沒用。另外建議大家最高只用到-O2。大部分開源作業系統經過千錘百煉都是使用-O2引數編譯的。優化級別開得太高對於除錯不利。為了優化,編譯器可能會打亂部分程式碼的順序,造成我們單步跟蹤的時候會發現在C原始碼裡亂跳。
 另外還有一個-Os表示為程式碼大小進行優化,用它可以生成儘量短小的機器碼。
-E 表示僅僅對程式碼進行預處理不編譯。也就是僅僅將標頭檔案包含和巨集定義展開。如果沒有用-o指定輸出檔案則將預處理結果輸出到控制檯上。
-c 表示僅僅彙編及編譯程式碼,不進行連結。也就是將原始碼編譯成.o檔案。
-S 表示僅僅彙編而不進行編譯及連結。也就是將原始碼翻譯成彙編指令。gcc -S生成的檔案閱讀起來不如用objdump反編譯.o檔案生成的結果舒服。建議想分析彙編指令採用objdump命令反編譯。
-o filename 指明輸出檔名。一般配合-E -c -S三個命令使用。比如:
 gcc -c a.c -o a.o
 gcc -E a.c -o a.e
-ansi 表示除了ANSI C標準之外其他特性都認為語法錯。比如//單行註釋。需要注意的是,這個選項只是嚴格執行大部分ansi標準。
-std= 後面可以跟c89 c99 gnu89等標準。表示編譯器使用哪個標準進行編譯。比如使用c89標準就用-std=c89,如果想一些gcc的擴充套件特性就用-std=gnu89。這個gnu89是預設值,也就是什麼都不指定的時候就是用c89標準+gcc的擴充套件。-ansi就相當於-std=c89
-pedantic 嚴格執行ANSI C標準。一般與-ansi配合使用可以讓原始碼嚴格遵循ANSI C標準。可以列印出-Wall以外更多的告警資訊。
-w 關閉所有告警提示
-Wall 雖然是-Wall但是不是開啟所有告警提示而是大部分,但下面的-W開頭的告警需要單獨開啟。
-W 對某些告警顯示更詳細的資訊
-Wfloat-equal 浮點數直接使用==判斷是否相等時告警
-Wtraditional 如果使用了原始C語言裡有而C標準化後被廢棄的特性就告警
-Wshadow 對shadow變數進行告警。比如:
 有個全域性變數nCount,這時你寫的函式有個引數也叫nCount。編譯器就會告警提示你,說區域性引數nCount起作用而全域性nCount不起作用。如果你用意不是如此就需要修改程式碼了。
-Werror 把所有的告警都轉化為編譯錯誤。只要有告警就停止編譯。
-g 生成除錯資訊。後面象-O一樣用數字表示資訊豐富程度。不跟數字預設為-g2,-g1是最基本的。-g3連巨集資訊都附加進去。有些偵錯程式可以在除錯時利用這些資訊來展開巨集。一般只要用-g。注意附加除錯資訊將顯著增加ELF檔案大小,但不會影響生成程式碼的大小。
-fstrict-aliasing  後面單獨介紹
-mlong-calls 對於arm平臺,如果想編譯出來的.o檔案能被shell下ld命令動態載入就需要加上此引數。不然會提示
 Relocation value does not fit in 26 bits.
 btw:這條是王璐瑋試出來的。我偷學來的。
-D 定義巨集。比如
 gcc -D_DEBUG=1 -Dver=”1.0” -c a.c -o a.o
-fvolatile 強制所有指標訪問都是volatile型別的,也就是強制去記憶體取資料。如果你不確定是否所有指向裝置地址空間的指標都是volatile型別宣告的建議加上這個引數。會損失優化效果。
-I dirname 指示前處理器把dirname加入到include檔案搜尋路徑裡來。比如
 gcc -c a.c -I../include -I./include -o a.o
-include filename 在原始檔前面包含filename作為標頭檔案。比如
 gcc -include ../common.h -c a.c -o a.o
 效果和a.c裡#include “../common.h”是一樣的。注意-include包含的標頭檔案將相當於在程式碼第一行就插入#include
推薦編譯引數-O2 -g -fno-strict-aliasing -Wall -fvolatile -I./

2)       怎樣生成依賴關係
手工寫Makefile的時候需要確定一個原始檔依賴哪些標頭檔案。每個檔案都開啟看一下include的標頭檔案的話會比較麻煩,並且有些用相對路徑include的標頭檔案的絕對路徑也不好確定。因此我們可以使用編譯器來給我們生成依賴關係。
C:\>ccarm -MM stone.c
stone.o: stone.c e://Tornado2.2/target/h/types/vxArch.h \
  e://Tornado2.2/target/h/arch/arm/archArm.h \
  e://Tornado2.2/target/h/arch/arm/arm.h \
  e://Tornado2.2/target/h/types/vxParams.h \
  e://Tornado2.2/target/h/types/vxTypesBase.h \
  e://Tornado2.2/target/h/types/vxTypesOld.h \
  e://Tornado2.2/target/h/sys/types.h \
  e://Tornado2.2/target/h/types/vxTypes.h \
  e://Tornado2.2/target/h/tool/gnu/toolMacros.h \
  e://Tornado2.2/target/h/vwModNum.h \
  e://Tornado2.2/target/h/objLib.h \
  e://Tornado2.2/target/h/errno.h \
e://Tornado2.2/target/h/memLib.h \
  e://Tornado2.2/target/h/private/objLibP.h \
  e://Tornado2.2/target/h/vxWorks.h \
e://Tornado2.2/target/h/objLib.h \
  e://Tornado2.2/target/h/classLib.h \
  e://Tornado2.2/target/h/private/classLibP.h \
  e://Tornado2.2/target/h/vwModNum.h \
e://Tornado2.2/target/h/memLib.h \
  e://Tornado2.2/target/h/private/objLibP.h \
e://Tornado2.2/host/x86-win32/lib/gcc-lib/arm-wrs-vxworks/2.9-010413/include/stdarg.h
把絕對路徑處理一下用變數代替tornado所在路徑就可以放到Makefile裡啦。注意的是生成依賴關係的時候最好保留完整的編譯命令列選項,只是將 -c替換成 -MM,比如ccarm -MM -DXXX -DYYY -fno-strict-aliasing -Dzzz=123 stone.c,這樣如果有些巨集定義會影響include的話我們還是可以生成正確的依賴關係。

3)       非常重要的strict aliasing規則
假設有人問你 (i++)+(++i)+(++i++)結果是多少,你也許會直接回答這和編譯器實現有關。不過如果有人問你
int abc()
{
  short a[2];
  int *b = (int *)a;
 
  a[0]=0x1111;
  a[1]=0x1111;
 
  *b = 0x22222222; 
 
  printf("%x %x\n", a[0], a[1]);
  return 0;
}
這個函式在單板上執行輸出結果是多少你可能就毫不猶豫的說是2222 2222了。
事實上,這段程式和(i++)+(++i)+(++i++)前面一樣是不一定的,它的結果與編譯器實現、編譯引數有關。不信的話你找個arm的單板(別的平臺我沒有試過),用ccarm -c -O2 引數編譯得到的結果就是1111 1111。如果用引數ccarm -c -O1編譯得到的結果就是2222 2222。造成兩種結果的引數不是-O2和-O1而是一個叫strict-aliasing的編譯引數。這個引數在-O2優化的時候預設是開啟的。剛才兩種不同結果是-fstrict-aliasing和-fno-strict-aliasing造成的。也就是說-O2結果就是1111 1111,-O2 -fno-strict-aliasing結果就是程式設計師預期的2222 2222。
    造成這個現象的原因是-fstrict-aliasing的時候編譯器會假設不同型別的指標指向的記憶體不會重疊來進行優化。
以上面的例子為例分析,由於a和b型別不同,當開啟-fstrict-aliasing,編譯器給*b賦值的時候就認為不會影響到a陣列裡面的值。編譯器在優化的時候分析:既然a陣列的值在賦值後沒被修改,下一次讀取a陣列值的時候就沒必要再從記憶體裡讀取了。因此printf這一行在取a[0],a[1]的時候就被優化成了從暫存器取值。結果列印出來的是1111 1111。
而如果使用了-fno-strict-aliasing,則編譯器認為任何指標都可能指向同一個記憶體區域。因此對*b賦值,編譯器認為有可能會影響a裡面的值了。所以編譯器給printf那一行傳遞引數的時候就認為暫存器裡的 a[0],a[1]值已經不一定正確了,只有記憶體裡的才可靠,於是只能老老實實從棧裡取值了。
以上的分析是在arm平臺上做的。我在x86下也做了比較,但是兩種引數編譯出來結果都是2222 2222。主要是因為x86下通用暫存器數量很少,優化的時候不一定總能把變數放在暫存器裡,所以這個例子影響不了x86。但在研發支撐體系上有人曾經提出過-O2生成的程式碼有問題,就是因為strict aliasing影響的。
2007-10-08 gcc編譯優化問題請教,牛人請進!
出現問題的程式碼如下:
XXXX_UINT32 xxxx_map_get_free
(
    XXXX_MAP_PTR        IO_map_p
)
{
    XXXX_VOID_PTR           cur_data_p;
    XXXX_BUFF_PTR           buff_p;
 
    buff_p = IO_map_p->buff_p;
 
    cur_data_p = buff_p->free_data_p;
    buff_p->free_data_p = *(XXXX_VOID_PTR*)cur_data_p;
    *(XXXX_UINT32*)cur_data_p = 0;
    buff_p->used ++;
 
    return XXXX_SUCCESS;
}
 
    知道了這個特性之後我們寫程式碼的時候就需要特別注意不要偷懶用強制指標轉化去給buf賦值。上面那個程式安全的版本是:
int abc()
{
    union
    {
       short a[2];
       int b;
    }U;
 
  U.a[0]=0x1111;
  U.a[1]=0x1111;
 
  U.b = 0x22222222; 
 
  printf("%x %x\n", U.a[0], U.a[1]);
  return 0;
}
這樣無論怎麼都不會出錯了。

    到這裡你也許會問,為何要有這個特性,既然-fno-strict-aliasing比較安全,那乾脆所有地方都這麼用算了。實際上出現-fstrict-aliasing是為了提供更好的優化效果。比如下面的程式:
void sum(int *array, short * num, int n)
{
    int i = 0;
 
    for (i = 0;i < n;i++)
    {
       array[i] += (int)num[0];
    }
}
如果使用-fno-strict-aliasing引數編譯,編譯器認為num和array有可能指向同一片區域。由於編譯器認為給array[i]賦值有可能會改變num[0],所以迴圈內部num[0]的值每次都是從記憶體裡取的。
而如果使用-fstrict-aliasing引數編譯。編譯器則認為num和array不會互相影響。這樣在迴圈外部就把num[0]的值讀取到暫存器中。迴圈內部每次都是暫存器的值去累加。減少了讀取記憶體的次數,從而優化了速度。
    要是一個函式內指標、陣列數量眾多,編譯器進行優化後行為就很可能會與程式設計師期望的。同時編譯引數是針對整個檔案起作用的,無法針對單個指標變數和單個函式。為了明確告訴編譯器究竟指標所指記憶體是否會互相覆蓋,在C99標準裡引入了新的關鍵字restrict。restrict使用的地方基本和const 差不多。它指明此指標指向的記憶體僅僅會被這個指標來修改,其他的指標不會修改這部分記憶體。這樣,明確告訴編譯器之後,編譯器就可以生成既符合程式設計師期望又高效的程式碼了。不過Tornado2.2裡所帶的gcc版本相當老,對C99標準支援非常差,不支援restrict關鍵字,在這裡就不多說了。更詳細的可以參考C99標準。建議在編譯命令列加上-fno-strict-aliasing,雖然優化效果會打折扣,但是安全。

4)       怎樣檢視前處理器預設定義的巨集
不同版本的gcc預設定義的巨集是不太一樣的。有時候為了相容不同平臺的gcc可能希望在程式碼裡用編譯巨集區分開。這樣就想知道編譯器與處理器預設定義了哪些巨集可以提供給我們使用。可以建立一個空檔案然後使用下面的命令
ccsimpc -dM -E emptyfilename
或者
ccsimpc -dM -E - < NUL
可以得到輸出
#define _stdcall __attribute__((__stdcall__))
#define CPU SIMNT
#define __i386__ 1
#define _X86_ 1
#define __i386 1
#define ___stdcall__ __attribute__((__stdcall__))
#define __GNUC_MINOR__ 96
#define __declspec(x) __attribute__((x))
#define __vxworks 1
#define i386 1
#define __stdcall __attribute__((__stdcall__))
#define __GNUC__ 2
#define __cdecl __attribute__((__cdecl__))
#define __STDC__ 1
#define ___stdcall __attribute__((__stdcall__))
 
ccarm -dM -E filename
或者
ccarm -dM -E - < NUL
可以得到輸出
#define __arm__ 1
#define __ARM_ARCH_4__ 1
#define arm 1
#define __GNUC__ 2
#define __APCS_32__ 1
#define __arm_elf__ 1
#define __arm_elf 1
#define __vxworks 1
#define __CHAR_UNSIGNED__ 1
#define __ARMEL__ 1
#define __ELF__ 1
#define arm_elf 1
#define __GNUC_MINOR__ 9
#define CPU ARMSA110

相關文章