The Linux GCC HOWTO中譯版V0.1 : 移植(Porting)與編譯(Compiling)程式
Previous: GCC的安裝(installation)與啟用(setup)
Next: Debugging and Profiling

4. 移植(Porting)與編譯(Compiling)程式

4.1. gcc自行定義的符號

只要執行gcc時,附加 -v這個參數(switch),就能找出你所用的這版gcc,自動幫你定義了什麼符號(symbols).例如,我的機器看起來會像這樣:

$ echo 'main(){printf("hello world\n");}' | gcc -E -v -
Reading specs from /usr/lib/gcc-lib/i486-box-linux/2.7.2/specs
gcc version 2.7.2
 /usr/lib/gcc-lib/i486-box-linux/2.7.2/cpp -lang-c -v -undef
-D__GNUC__=2 -D__GNUC_MINOR__=7 -D__ELF__ -Dunix -Di386 -Dlinux
-D__ELF__ -D__unix__ -D__i386__ -D__linux__ -D__unix -D__i386
-D__linux -Asystem(unix) -Asystem(posix) -Acpu(i386)
-Amachine(i386) -D__i486__ -

假若目前你正在寫的程式碼,會用到一些Linux獨有的特性(Linux-specific features),那麼把那些無法移植的程式碼(non-portable bits),以條件式編譯(conditional compilation)的前置命令封括(enclose in)起來,可是個不錯的主意呢!

#ifdef __linux__
/* ... funky stuff ... */
#endif /* linux */

__linux__即可達成目的;看仔細一點,不是linux啊.僅管後者也有定義,畢竟,仍然不是POSIX的標準(not POSIX compliant).

4.2. 線上求助說明(invocation)

gcc編譯器參數(switches)的說明文件是gcc info page(在Emacs內,按下C-h i,然後選'gcc'的選項).要是弄不出來,不是賣你CD-ROM的人,沒把這個東東壓給你,不然就是你現在用的是舊版的.這種情況下,最好的方法是移動尊臀到archiveftp://prep.ai.mit.edu/pub/gnu或是它的mirrors站台上,把gcc的原始檔案抓回家,重新烹飪一番.

gcc manual page (gcc.1) 可以說是已經過時了.一旦你吃飽撐著沒事幹要去看看它的話,它就會告訴你這件事,叫你別無聊了.

4.2.1. 旗正飄飄~(flags)

在命令列(command line)上執行gcc時,只要在它的屁股後面加上-On的選項,就能讓gcc乖乖的替你生出最佳化後的機器碼(output code).這裡的n是一個可有可無的小整數.不同的gcc版本,n的意義與其正確的(exact)功效都不一樣;不過,典型的範圍是從0(不要雞婆,我不要最佳化)變化到2(最佳化要多一點),再到3(最佳化要再多一點,多一點).

gcc在其內部會將這些轉譯成一系列的-f-m選項(options).執行gcc時帶上旗號(flags)-v-Q,你就能很清楚的看出每一種等級的-O是對應(maps)到那些選項(options).例如,就-O2來講,我的gcc告訴我說:

enabled: -fdefer-pop -fcse-follow-jumps -fcse-skip-blocks
-fexpensive-optimizations
         -fthread-jumps -fpeephole -fforce-mem -ffunction-cse -finline
         -fcaller-saves -fpcc-struct-return -frerun-cse-after-loop
         -fcommon -fgnu-linker -m80387 -mhard-float -mno-soft-float
         -mno-386 -m486 -mieee-fp -mfp-ret-in-387

要是你用的最佳化等級(optimization level)高於你的編譯器所能支援的(e.g. -O6),那麼它的效果,就跟你用你的編譯器所能提供的最高等級的,是一樣的結果.說實在的,發行出去的gcc程式碼,用在編譯時竟是如此處理這等問題,實非什麼好的構想.日後若是有更進步的最佳化方法具體整合到新的版本裡,而你(或你的users)還是試著這樣做的話,可能就會發現,gcc會中斷你的程式(break your code)了.

從gcc 2.7.0到2.7.2的users應該注意到,使用時-O2會有一個bug存在.更糟糕的是,強度折減(strength reduction)居然沒有用(doesn't work)!要是你喜歡重新編譯gcc的話,是有那麼一個修正的版本(patch)可以更正這項錯誤;不然的話,一定要確定每次編譯時都會加上-fno-strength-reduce喔!

11/12/97譯

4.2.1.1. 有個性的微處理器(Processor-specific)

有一些-m的旗號無法藉由各種等級的-O來打開,然而卻是十分有用的.這之中最主要的是-m386-m486兩種,用來告訴gcc該把正在編譯的程式碼視作專為386或是486機器所寫的.不論是用哪一種來編譯程式碼,都可以在彼此的機器上執行,-m486編譯出來的碼會比較大,可是拿來在386的機器上跑也不會比較慢就是了.

目前尚無-mpentium或是-m586的旗號.Linus建議我們,可以用-m486 -malign-loops=2 -malign-jumps=2 -malign-functions=2,來得到最佳化的486程式碼(486 code optimizations),而這樣做正好就可以避免alignment(Pentium並不需要)有過大的gaps發生. Michael Meissner說:

我的第六感(hunch)告訴我, -mno-strength-reduce(嘿!我可不是在談強度折減的bug啊,那已經是另外一個爭論的戰場了.)一樣也可以在x86的機器上,產生較快的程式碼,這是因為x86的機器對暫存器(register)有著不可磨滅的饑渴在(and GCC's method of grouping registers into spill registers vs. other registers doesn't help either).傳統上,強度折減的結果會使得編譯器利用加法暫存器(additional registers)以加法運算(addition)來取代乘法運算(multiplication).而且,我也在懷疑(suspect)-fcaller-saves,可能也只是個漏洞(loss)也說不定.
而我的第七感則再度的告訴我, -fomit-frame-pointer可能會,也可能不會有任何的賺頭.從這點來看,即意謂著有另一個暫存器可用來處理記憶體分配(allocation)的問題.另方面,若純粹從x86的機器在轉換(encodes)它的指令集(instruction set)成為機器碼的方法上來看,便意謂著堆疊(stack)所用到的記憶體空間要比frame所用到的還要來的多;換句話說,Icache對程式碼而言並沒有實質上的益處.若是閣下用了-fomit-frame-pointer的話,同時,也就是告訴編譯器在每次呼叫函數(calls)之後,就必須修正堆疊的指標(stack pointer);然而,就frame來講,若呼叫的次數不多的話,則允許堆疊暫時堆積(accumulate)起來.

有關這方面主題的最後一段話仍是來自於Linus:

要注意的是,如果你想要得到最佳狀況的執行成果(optimal performance),可千萬別相信我的話.無論如何,一定要進行測試.gcc編譯器還有許多的參數(switches)可用,其中可能就有一種最特別的組合(set),可以給你最佳化的結果喔.

11/14/97譯

4.2.2. Internal compiler error: cc1 got fatal signal 11

Signal 11是指 SIGSEGV, 或者 `segmentation violation'.通常這是指 說gcc對自己所用的指標感到困惑起來,而且還嘗試著寫入不屬於它的記憶體.所以,這可能是一個gcc的bug.

然而,大部份而言,gcc是一件經過嚴密測試且可靠度佳的軟體佳作.它也用了大量複雜的資料結構與驚人的指標數量.簡言之,若是要評選本世紀最挑惕與最一絲不茍的RAM測試程式(RAM tester)的話,gcc絕對可以一摘后冠的.假如你無法重新複製這隻bug---當你重新開始編譯時,錯誤的訊息並沒有一直出現在同一個地方---那幾乎可以確定,是你的硬體本身有問題(CPU,記憶體,主機板或是快取記憶體).千萬不要因為你的電腦可以通過開機程序的測試(power-on checks),或者Windows可以跑得很順,或者其它什麼的,就回過頭來大肆宣傳說這是gcc的一個bug;你所做的這些測試動作,通常沒有什麼實際上的價值,而且沒有價值也是很合理的結論.另外,也不要因為編譯核心時,總是停留在`make zImage'的階段,就要大罵這是gcc的bug---當然它會停在那兒啊!做`make zImage'時,需要編譯的檔案可能超過200檔案;我們正在研擬一個比較小的地方來取代.

如果你可以重覆產生這個bug,而且(最好是這樣啦)可以寫一個短小的程式來展示這隻bug的話,你就可以把它做成bug報表(bug report),然後email給FSF,或者是linux-gcc郵件表列(linux-gcc mailing list).你可以去參考gcc的說明文件,看看有什麼詳細的資訊,是他們所需要的.

4.3. 移植能力(Portability)

據報,近日來許多正面消息指出,若有某件東東到現在都還沒移植到Linux上去,那麼可以肯定的是,它一定一點價值也沒有.:-)

嗯!正經一點.一般而言,原始碼只需要做一些局部的修改(minor changes),就可以克服(get over)Linux 100%與POSIX相容的特質(compliance).如果你做了任何的修改,而將此部份傳回(passing back)給原作者,會是很有建設性的舉動(worthwhile).這樣日後就只需要用到'make',就能得到一個可執行的檔案了.

4.3.1. BSD教派(BSDisms) (有 bsd_ioctl, daemon<sgtty.h>)

編譯程式時,可以配合-I/usr/include/bsd與連結-lbsd的程式庫.(例如:在你的Makefile檔內,把-I/usr/include/bsd加到CFLAGS那一行;把-lbsd加到LDFLAGS那一行).如果你真的那麼想要BSD型態的信號行為(BSD type signal behavior),也再需要加上-D__USE_BSD_SIGNAL了.那是因為當你用了-I/usr/include/bsd與含括了標頭檔<signal.h>之後,make就自動會把它加入了.

4.3.2. 失落的封印(`Missing' signals)(SIGBUS, SIGEMT, SIGIOT, SIGTRAP, SIGSYS etc)

Linux與POSIX是完全相容的.不過,有些信號並不是POSIX定義的---ISO/IEC 9945-1:1990 (IEEE Std 1003.1-1990), paragraph B.3.3.1.1 sez:

"在POSIX.1中省略了SIGBUS, SIGEMT, SIGIOT, SIGTRAP, 與SIGSYS信號,那是因為它們的行為(behavior)與實作方式是息息相關的(implementations dependent),而且也無法進行適當的分門別類(adequately categorized).確認實作方式後(conforming implementations),便可以生產出(deliver)這些信號,可以必須以文件說明(document)它們是在什麼樣的環境(circumstances)下生產出來的,以及指出與它們的發展相關的任何限制(any restrictions concerning their delivery)".

如欲修正此點,最簡單,也是最笨的(cheesy)方法就是以SIGUNUSED重新定義這些信號.而正確的方法應是以條件式的編譯#ifdef來處理這些問題才對:

#ifdef SIGSYS
/* ... non-posix SIGSYS code here .... */
#endif

11/15/97譯

4.3.3. K & R

gcc是個與ANSI相容的編譯器;奇怪的是,目前大多數的程式碼都不符合ANSI所定的標準.如果你熱愛ANSI,喜歡用ANSI提供的標準來撰寫C程式,似乎除了在編譯器的旗號上加上-traditional之外,就沒有什麼其它的可以多談的了.There is a certain amount of finer-grained control over which varieties of brain damage to emulate;請自行查閱gcc info page.

要注意的是,儘管你用了-traditional來改變語言,它的效果也僅侷限在gcc所能夠接受的範圍.例如, -traditional會打開(turn on)-fwritable-strings,使得字串常數(string constants)移至資料記憶體空間(data space)內(從程式碼記憶體空間(text space),這地方是不能任意寫入的).這樣做會讓程式碼的記憶體空間無形中增加的.

4.3.4. 前置處理器(Preprocessor)的符號卯上函數原型宣告(prototypes)

最常見的問題是,如眾所皆知,Linux中有許多常用的函數都定義成巨集(macros)存放在標頭檔(header files)內,此時若有相似的函數原型宣告出現在程式碼內,前置處理器會拒絕進行語法分析(parse)的前置作業.常見的有atoi()atol().

4.3.5. sprintf()

在大部份的Unix系統上, sprintf(string, fmt, ...)傳回的是string的指標,然而,這方面Linux(遵循ANSI)傳回的卻是放入string內的字元數目.進行移植時,尤其是針對SunOS,需有警覺的心.

4.3.6. fcntl 與相關的函數; FD_*家族的定義到底擺在哪裡?

就在 <sys/time.h>裡頭. 為了真正的原型宣告,當你用了fcntl,可能你也想含括標頭檔<unistd.h>進來.

一般而言,函數的manual page會在SYNOPSIS章節內列出需要的標頭檔.

4.3.7. select() 的計時(time-out)---程式執行時會處於忙碌-等待的狀態(busy-waiting).

很久很久以前, select()的計時參數(time-out parameter)只有讀的屬性(read-only)而已.即使到了最近,manual pages仍然有下面這段的警告:

select()照理講應該是藉由適當的修正時間的數值,再傳回自原始計時(original time-out)開始後所剩餘的時間.未來的版本可能會使這項功能實現.因此,就目前而言,若假定在呼叫select()之後,計時指標(time-out pointer)仍然不會讓人給修正過,可是一種非常不明智的想法喔!

未來就在我們的眼前了!至少,在這兒你絕對可以看到. 函數select()傳回的,是扣除等待尚未到達的資料所耗費的時間後,其剩餘的時間值.如果在計時結束時,都沒有資料傳送進來,計時引數(time-out argument)便會設為0;如果接著還有任何的select(),以同樣的time-out structure來呼叫,那麼select()便會立刻結束.

若要修正這項問題,只要每次呼叫select()前,都把計時數值(time-out value)放到time-out structure內,就沒有問題了.把下面的程式碼,

      struct timeval timeout;
      timeout.tv_sec = 1; timeout.tv_usec = 0;
      while (some_condition)
            select(n,readfds,writefds,exceptfds,&timeout); 
改成,
      struct timeval timeout;
      while (some_condition) {
            timeout.tv_sec = 1; timeout.tv_usec = 0;
            select(n,readfds,writefds,exceptfds,&timeout);
      }

這個問題,在有些版本的Mosaic裡是相當著名的,只消一次的等待,Mosaic就掛了.Mosaic的螢幕右上角,是不是有個圓圓的,會旋轉的地球動畫.那顆球轉得愈快,就表示資料從網路上傳送過來的速率愈慢!

4.3.8. 產生中斷的系統呼叫(Interrupted system calls)

4.3.8.1. 徵兆(Symptom):

當一支程式以Ctrl-Z中止(stop),然後再重新執行(restart)時--或者是其它可以產生Ctrl-C中斷(interruption)信號的情況,如子程序(child process)終結(termination)等--系統就會抱怨說"interrupted system call"或是"write: unknown error",或者諸如此類的訊息.

4.3.8.2. 問題點:

POSIX的系統檢查信號的次數,比起一些舊版的Unix是要多那麼一點.如果是Linux,可能就會執行signal handlers了--

就其它的作業系統而言,你需要的可能就是下面這些系統呼叫(system calls)了: creat(), close(), getmsg(), putmsg(), msgrcv(), msgsnd(), recv(), send(), wait(), waitpid(), wait3(), tcdrain(), sigpause(), semop() to this list.

在系統呼叫期間,若有一信號(那支程式本身應準備好handler因應了)產生,handler就會被呼叫.當handler將控制權轉移回系統呼叫時,它會偵測出它已經產生中斷,而且傳回值會立刻設定成-1,errno設定成EINTR.程式並沒有想到會發生這種事,所以就會bottles out了.

有兩種修正的方法可以選擇:

(1) 對每個你自行安裝(install)的signal handler,都須在sigaction旗號加上SA_RESTART.例如,把下列的程式,

  signal (sig_nr, my_signal_handler);
改成,
  signal (sig_nr, my_signal_handler);
  { struct sigaction sa;
    sigaction (sig_nr, (struct sigaction *)0, &sa);
#ifdef SA_RESTART
    sa.sa_flags |= SA_RESTART;
#endif
#ifdef SA_INTERRUPT
    sa.sa_flags &= ~ SA_INTERRUPT;
#endif
    sigaction (sig_nr, &sa, (struct sigaction *)0);
  }

要注意的是,當這部份的變更大量應用到系統呼叫之後,呼叫read(), write(),ioctl(), select(), pause()connect()時,你仍然得自行檢查(check for)EINTR.如下所示.

(2) 你自己得很明確地(explicitly)檢查EINTR:

這裡有兩個針對read()ioctl()的例子.

原始的程式片段,使用read().

int result;
while (len > 0) { 
  result = read(fd,buffer,len);
  if (result < 0) break;
  buffer += result; len -= result;
}
修改成,
int result;
while (len > 0) { 
  result = read(fd,buffer,len);
  if (result < 0) { if (errno != EINTR) break; }
  else { buffer += result; len -= result; }
}
原始的程式片段,使用ioctl().

int result;
result = ioctl(fd,cmd,addr);
修改成,
int result;
do { result = ioctl(fd,cmd,addr); }
while ((result == -1) && (errno == EINTR));

注意一點,有些版本的BSD Unix,其內定的行為(default behaviour)是重新執行系統呼叫.若要讓系統呼叫中斷,得使用 SV_INTERRUPTSA_INTERRUPT旗號.

4.3.9. 可以寫入的字串(Writable strings)

gcc對其users總懷抱著樂觀的想法(optimistic view),相信當他們打算讓某個字串當作常數來用時---那它就真的只是字串常數而已.因此,這種字串常數會儲存在程式碼的記憶體區段內(in the code area of the program).這塊區域可以page到磁碟機的image上,避免耗掉swap的記憶體空間,而且任何嘗試寫入的舉動都會造成分頁的錯誤(segmentation fault).這可是一種特色呢!

對老舊一點的程式而言, 這可能會產生一個問題.例如,呼叫mktemp(),傳遞引數(arguments)是字串常數. mktemp()會嘗試著在*適當的位置(in place)*重新寫入它的引數.

修正的方法不外乎(a)以-fwritable-strings編譯,迫使gcc將此常數置放在資料記憶體空間(data space)內.或者(b)將侵犯地權的部份(offending parts)重新改寫,配置一個不為常數的字串(non-constant string),在呼叫前,先以strcpy()將資料拷貝進去.

4.3.10. 為什麼呼叫execl()會失敗?

那是因為你呼叫的方式不對.execl的第一個引數是你想要執行的程式名.第二個與接續的引數會變成你所呼叫的程式的argv陣列(array).記住:傳統上,argv[0]是只有當程式沒有帶著引數執行時,才會有設定值.所以囉,你應該這樣寫:

execl("/bin/ls","ls",NULL);
而不是只有,
execl("/bin/ls", NULL);

執行程式而不帶任何引數(with no arguments),可解釋成(construe)是一種邀請函(invitation),目的是把此程式的動態程式庫獨立(dynamic library dependencies)的特性印出來(print out).至少,a.out是這樣的.就ELF而言,事情就不是這樣了.

(如果你想得知此程式庫的資訊,有一些更簡單的介面可用;參考動態載入(dynamic loading)那一章節,或是ldd的manual page.)

11/16/97譯


The Linux GCC HOWTO中譯版V0.1 : 移植(Porting)與編譯(Compiling)程式
Previous: GCC的安裝(installation)與啟用(setup)
Next: Debugging and Profiling