張喜俊
(中國電子科技集團公司 第四十一研究所,青島266555)
程序員都希望盡可能地重用自己的代碼,即不需要任何修改,只是簡單地重新編譯就可以在其他系統上運行。但是,處理器架構、匯編器語法、C編譯器實現、操作系統接口都會對代碼的可移植性產生不同程度的影響。首先,匯編代碼是不可移植的,例如ARM匯編語言編寫的代碼不可能直接運行在x86處理器上,這是因為ARM和x86的指令/機器碼不同。其次,雖然MASM和NASM匯編器都可以生成x86機器碼,但是由于它們的語法并不相同,因此也不能直接重用。最后,不同操作系統的系統調用/應用程序編程接口相差甚遠,也嚴重地阻礙了代碼重用。
C標準通過規定C編譯器的行為為最大化代碼重用提供了條件,但這并不等于說C代碼就是可移植的,操作系統的差異、代碼質量、以及編譯器的實現和擴展都會對可移植性產生影響。本文主要討論影響C代碼可移植性的因素,以及如何編寫可移植的C代碼。
C語言標準可以看作是C語言使用者和C編譯器實現者之間的協議。如果使用者遵守標準規定的語法,而且編譯器實現了標準規定的行為,那么使用者可以得到期望的輸出。這樣,C程序員就能夠在不了解底層硬件和操作系統等細節的情況下編寫出具有指定行為的程序。
C語言標準定義了C語言的語法和語義、運行時環境、預處理器和標準庫等。為了提高C代碼的執行效率,C標準并沒有試圖定義C語言的每個實現細節,而是為編譯器實現者提供了一定的自由,由此導致了可移植性問題。
C語言標準只是規定了每種內置數據類型的最小尺寸,而沒有定義它的確切尺寸。例如,規定int類型至少16位,但是通常為32位;long類型至少32位,但在某些系統上卻為64位。因此一定不要假設int或long具有一個特定的尺寸,否則它們會在某個時刻突然溢出。應該使用typedef類型而不是int或long類型,例如:

或者使用宏定義:

雖然這兩種方法的效果相同,但是推薦使用 typedef類型,這樣可以充分利用編譯器的靜態檢查功能。
C99標準在頭文件inttypes.h中提供了一系列固定尺寸類型,表1列出了其中的幾個。只是到目前為止支持C99標準的編譯器仍然很少,于是程序員不得不自己定義它們。

表1 C99固定尺寸類型
除了自定義的typedef類型以外,C標準也定義了一些特殊用途的typedef類型,如表2所列。在編碼過程中應該盡可能使用這些typedef類型,而不是int等。

表2 特殊的typedef類型
1.2.1 編譯器行為
為了給編譯器實現者提供靈活性以生成效率更高的代碼,C標準故意定義了3種與可移植性密切相關的行為,它們分別為未指定行為、實現定義行為和未定義行為。
①未指定行為。實現者需要從標準規定的幾種選項中選擇一種,但是不必在文檔中說明所選擇的行為,例如函數調用時實參的計算次序、宏替換時預處理器連接操作符#和##的計算次序等。

該語句依賴于n在調用power之前還是之后遞增,不同的編譯器可能產生不同的結果,而下面的代碼則并不存在二義性:

②實現定義行為。實現者需要從幾種選項中選擇一種,而且必須在文檔中說明所選擇的行為。例如,char類型可能是signed char也可能是unsigned char,因此不能對char的符號性做任何假設。如果需要特定類型的char變量,則必須顯式地使用signed char或者unsigned char,但是更好的方法是使用自定義的typedef類型。編譯器實現者一般在編譯器手冊中詳細說明它的行為。以ARM公司的RealView編譯工具為例,在編譯程序和庫指南的附錄B(標準C實現方法定義)中描述了其實現定義行為。
③未定義行為。標準不對其施加任何要求,程序既可以立即崩潰,也可以好像什么事情都沒有發生過一樣繼續運行。例如,使用不可移植或錯誤的程序構造,或者使用錯誤的數據(如溢出或除數為零)。
在ISO/IEC 9899:1999文檔的附錄J(移植問題)中詳細地描述了這3種行為。顯然,任何依賴于這3種行為之一的程序本質上都是不可移植的。如果必須依賴它們,那么應該將這部分代碼隔離起來,并且在項目文檔中說明。
1.2.2 編譯器擴展
C標準允許編譯器實現者對C語言進行擴展,這在嵌入式系統編程中尤為明顯。以 RealView為例,它使用__irq將一個C函數聲明為中斷處理器,使用__asm在C函數體中嵌入一段匯編代碼,使用__packed聲明壓縮的結構體,以及支持“//”注釋等。雖然這些擴展為程序員提供了一定的便利,但是與此同時也引入了可移植性問題,這是因為不同的編譯器有不同的擴展,即使相同的擴展也極少是兼容的。
許多編譯器都提供一些編譯選項用來檢查代碼是否嚴格符合ISO標準,例如 RealView提供了-strict選項,GCC提供了-ansi選項。打開嚴格編譯選項后,如果使用了任何特定于編譯器的特性,那么編譯器將會給出相應的警告/錯誤。
在字節尋址的存儲器中,存在小端字節序和大端字節序兩種方式存儲多字節數據。大端字節序(big endian)也稱為“網絡字節序”,在最低地址處存儲最高有效字節,而小端字節序(little endian)則在最低地址處存儲最低有效字節。假設一個32位整型數據存儲在自然對齊的地址A處,如圖1所示。如果為小端格式,那么地址A處的字為0x78563412,地址A+2處的半字為0x7856;相反,如果為大端格式,那么地址A處的字為0x12345678,地址A+2處的半字為0x5678。

圖1 字節序
不同的處理器的字節序可能并不相同,例如x86使用小端字節序,PowerPC使用大端字節序,而ARM同時支持大端格式和小端格式。如果不清楚處理器的字節序,那么可以使用下面的is_big_endian函數判斷它是否為大端字節序。

一般情況下,程序員并不需要考慮處理器的字節序,但是當編寫需要在計算機間交換數據時的應用程序(特別是網絡應用程序),則需要特別關注字節序問題。例如,sockaddr_in結構體中的端口成員就要求使用網絡字節序。為了增強網絡應用程序的可移植性,定義了兩類在本地字節序和網絡字節序之間進行轉換的函數:操作32位整數的 htonl和 ntohl,以及操作16位整數的 htons和ntohs。ntoh函數將網絡字節序轉換為本機字節序,而hton函數將主機字節序轉換為網絡字節序。
Linux支持數目眾多的處理器,與處理器字節序相關的操作都定義在各自的byteorder.h中。以PowerPC處理器為例,在linux-2.6.24includeasm-powerpcyteorder.h中包含了位于linux-2.6.24includelinuxyteorder中的big_endian.h。它定義了__BIG_ENDIAN宏(表明PowerPC處理器為大端字節序),以及一些用來在本機字節序和大/小端字節序之間進行轉換的宏。例如下面的宏專門用來操作32位數:

對齊的目的是為了提高處理器的執行效率。不同的處理器有不同的存儲器訪問特點,Intel x86處理器允許非對齊的存儲器訪問,但是這會造成一定的性能損失,而ARM處理器進行非對齊訪問時竟然得到一個不正確的數據。為了保證可移植性,應該確保數據的對齊性,同時避免濫用指針操作以避免非對齊的存儲器訪問。對于ARM處理器,執行下面的語句后,變量l將等于奇怪的0x01040302。

如果一個C語言原生類型T的變量在存儲器中的地址為sizeof(T)的整數倍,那么稱它是自然對齊的。一般情況下,編譯器通過自然對齊所有數據類型以解決對齊問題。對C原生類型來說,這不存在任何問題;而struct類型的對齊要求與對齊要求最嚴格的成員一致,這可能導致結構體中兩個相鄰的、不同尺寸數據類型的成員之間存在填充。例如,對結構體foo來說,它的對齊要求與y一致,隨著處理器字長的不同,x和y之間可能存在1個或3個甚至7個字節的填充。

C標準在stddef.h中定義了offsetof宏,用來返回結構體成員的偏移值。為了查看成員y的偏移值可以使用下面的語句:

GCC編譯器為此提供了更多的擴展。關鍵字__alignof__返回對象的對齊需求,例如如果目標機器要求double值對齊于8字節邊界,那么__alignof__(double)返回8。關鍵字__attribute__可以用來指定變量或者結構體成員的最低對齊需求(以字節為單位),例如int x__attribute__((aligned(16)))=0;可以通知編譯器為變量x分配一個16字節對齊的地址。如果打開了-Wpadded開關,那么當結構體存在填充時,GCC編譯器還會給出警告。如果編譯器沒有提供類似GCC那樣的擴展,那么另一個常用的技巧是使用union提升較低數據類型的對齊要求。例如對于下面的聯合u,假設sizeof(int)=4,那么可以保證c也是4字節對齊的。

總之,在實踐中應該編寫符合標準的代碼,隔離與特定處理器或者操作系統相關的代碼,甚至嘗試使用不同的編譯器編譯你的代碼,只有這樣才能確保代碼的可移植性。
[1]Horton M ark.Portable C Software[M].London:Prentice Hall,1990.
[2]Jones Derek M.The New C Standard an Economic and Cultural Commentary[M].NewYork:Addison-Wesley Professional,2003.
[3]ISO/IEC 9899:1999[EB/OL].[2010-03].www.open-std.org/JTC1/SC22/wg14/www/docs/n1124.pdf.
[4]Brian Kernighan W.程序設計實踐[M].裘宗燕 ,譯 .北京 :機械工業出版社,2003.
[5]Harbison Samuel P,Steele Guy L.C:A Reference Manual[M].5版.北京:人民郵電出版社,2007.
[6]Stallman Richard M,the GCC Developer Community.Using the GNU Compiler Collection:For GCC version 4.4.1[EB/OL].[2010-03].http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc.pdf.