張喜俊
(中國電子科技集團(tuán)公司 第四十一研究所,青島266555)
程序員都希望盡可能地重用自己的代碼,即不需要任何修改,只是簡單地重新編譯就可以在其他系統(tǒng)上運(yùn)行。但是,處理器架構(gòu)、匯編器語法、C編譯器實(shí)現(xiàn)、操作系統(tǒng)接口都會(huì)對(duì)代碼的可移植性產(chǎn)生不同程度的影響。首先,匯編代碼是不可移植的,例如ARM匯編語言編寫的代碼不可能直接運(yùn)行在x86處理器上,這是因?yàn)锳RM和x86的指令/機(jī)器碼不同。其次,雖然MASM和NASM匯編器都可以生成x86機(jī)器碼,但是由于它們的語法并不相同,因此也不能直接重用。最后,不同操作系統(tǒng)的系統(tǒng)調(diào)用/應(yīng)用程序編程接口相差甚遠(yuǎn),也嚴(yán)重地阻礙了代碼重用。
C標(biāo)準(zhǔn)通過規(guī)定C編譯器的行為為最大化代碼重用提供了條件,但這并不等于說C代碼就是可移植的,操作系統(tǒng)的差異、代碼質(zhì)量、以及編譯器的實(shí)現(xiàn)和擴(kuò)展都會(huì)對(duì)可移植性產(chǎn)生影響。本文主要討論影響C代碼可移植性的因素,以及如何編寫可移植的C代碼。
C語言標(biāo)準(zhǔn)可以看作是C語言使用者和C編譯器實(shí)現(xiàn)者之間的協(xié)議。如果使用者遵守標(biāo)準(zhǔn)規(guī)定的語法,而且編譯器實(shí)現(xiàn)了標(biāo)準(zhǔn)規(guī)定的行為,那么使用者可以得到期望的輸出。這樣,C程序員就能夠在不了解底層硬件和操作系統(tǒng)等細(xì)節(jié)的情況下編寫出具有指定行為的程序。
C語言標(biāo)準(zhǔn)定義了C語言的語法和語義、運(yùn)行時(shí)環(huán)境、預(yù)處理器和標(biāo)準(zhǔn)庫等。為了提高C代碼的執(zhí)行效率,C標(biāo)準(zhǔn)并沒有試圖定義C語言的每個(gè)實(shí)現(xiàn)細(xì)節(jié),而是為編譯器實(shí)現(xiàn)者提供了一定的自由,由此導(dǎo)致了可移植性問題。
C語言標(biāo)準(zhǔn)只是規(guī)定了每種內(nèi)置數(shù)據(jù)類型的最小尺寸,而沒有定義它的確切尺寸。例如,規(guī)定int類型至少16位,但是通常為32位;long類型至少32位,但在某些系統(tǒng)上卻為64位。因此一定不要假設(shè)int或long具有一個(gè)特定的尺寸,否則它們會(huì)在某個(gè)時(shí)刻突然溢出。應(yīng)該使用typedef類型而不是int或long類型,例如:
或者使用宏定義:
雖然這兩種方法的效果相同,但是推薦使用 typedef類型,這樣可以充分利用編譯器的靜態(tài)檢查功能。
C99標(biāo)準(zhǔn)在頭文件inttypes.h中提供了一系列固定尺寸類型,表1列出了其中的幾個(gè)。只是到目前為止支持C99標(biāo)準(zhǔn)的編譯器仍然很少,于是程序員不得不自己定義它們。
表1 C99固定尺寸類型
除了自定義的typedef類型以外,C標(biāo)準(zhǔn)也定義了一些特殊用途的typedef類型,如表2所列。在編碼過程中應(yīng)該盡可能使用這些typedef類型,而不是int等。
表2 特殊的typedef類型
1.2.1 編譯器行為
為了給編譯器實(shí)現(xiàn)者提供靈活性以生成效率更高的代碼,C標(biāo)準(zhǔn)故意定義了3種與可移植性密切相關(guān)的行為,它們分別為未指定行為、實(shí)現(xiàn)定義行為和未定義行為。
①未指定行為。實(shí)現(xiàn)者需要從標(biāo)準(zhǔn)規(guī)定的幾種選項(xiàng)中選擇一種,但是不必在文檔中說明所選擇的行為,例如函數(shù)調(diào)用時(shí)實(shí)參的計(jì)算次序、宏替換時(shí)預(yù)處理器連接操作符#和##的計(jì)算次序等。
該語句依賴于n在調(diào)用power之前還是之后遞增,不同的編譯器可能產(chǎn)生不同的結(jié)果,而下面的代碼則并不存在二義性:
②實(shí)現(xiàn)定義行為。實(shí)現(xiàn)者需要從幾種選項(xiàng)中選擇一種,而且必須在文檔中說明所選擇的行為。例如,char類型可能是signed char也可能是unsigned char,因此不能對(duì)char的符號(hào)性做任何假設(shè)。如果需要特定類型的char變量,則必須顯式地使用signed char或者unsigned char,但是更好的方法是使用自定義的typedef類型。編譯器實(shí)現(xiàn)者一般在編譯器手冊(cè)中詳細(xì)說明它的行為。以ARM公司的RealView編譯工具為例,在編譯程序和庫指南的附錄B(標(biāo)準(zhǔn)C實(shí)現(xiàn)方法定義)中描述了其實(shí)現(xiàn)定義行為。
③未定義行為。標(biāo)準(zhǔn)不對(duì)其施加任何要求,程序既可以立即崩潰,也可以好像什么事情都沒有發(fā)生過一樣繼續(xù)運(yùn)行。例如,使用不可移植或錯(cuò)誤的程序構(gòu)造,或者使用錯(cuò)誤的數(shù)據(jù)(如溢出或除數(shù)為零)。
在ISO/IEC 9899:1999文檔的附錄J(移植問題)中詳細(xì)地描述了這3種行為。顯然,任何依賴于這3種行為之一的程序本質(zhì)上都是不可移植的。如果必須依賴它們,那么應(yīng)該將這部分代碼隔離起來,并且在項(xiàng)目文檔中說明。
1.2.2 編譯器擴(kuò)展
C標(biāo)準(zhǔn)允許編譯器實(shí)現(xiàn)者對(duì)C語言進(jìn)行擴(kuò)展,這在嵌入式系統(tǒng)編程中尤為明顯。以 RealView為例,它使用__irq將一個(gè)C函數(shù)聲明為中斷處理器,使用__asm在C函數(shù)體中嵌入一段匯編代碼,使用__packed聲明壓縮的結(jié)構(gòu)體,以及支持“//”注釋等。雖然這些擴(kuò)展為程序員提供了一定的便利,但是與此同時(shí)也引入了可移植性問題,這是因?yàn)椴煌木幾g器有不同的擴(kuò)展,即使相同的擴(kuò)展也極少是兼容的。
許多編譯器都提供一些編譯選項(xiàng)用來檢查代碼是否嚴(yán)格符合ISO標(biāo)準(zhǔn),例如 RealView提供了-strict選項(xiàng),GCC提供了-ansi選項(xiàng)。打開嚴(yán)格編譯選項(xiàng)后,如果使用了任何特定于編譯器的特性,那么編譯器將會(huì)給出相應(yīng)的警告/錯(cuò)誤。
在字節(jié)尋址的存儲(chǔ)器中,存在小端字節(jié)序和大端字節(jié)序兩種方式存儲(chǔ)多字節(jié)數(shù)據(jù)。大端字節(jié)序(big endian)也稱為“網(wǎng)絡(luò)字節(jié)序”,在最低地址處存儲(chǔ)最高有效字節(jié),而小端字節(jié)序(little endian)則在最低地址處存儲(chǔ)最低有效字節(jié)。假設(shè)一個(gè)32位整型數(shù)據(jù)存儲(chǔ)在自然對(duì)齊的地址A處,如圖1所示。如果為小端格式,那么地址A處的字為0x78563412,地址A+2處的半字為0x7856;相反,如果為大端格式,那么地址A處的字為0x12345678,地址A+2處的半字為0x5678。
圖1 字節(jié)序
不同的處理器的字節(jié)序可能并不相同,例如x86使用小端字節(jié)序,PowerPC使用大端字節(jié)序,而ARM同時(shí)支持大端格式和小端格式。如果不清楚處理器的字節(jié)序,那么可以使用下面的is_big_endian函數(shù)判斷它是否為大端字節(jié)序。
一般情況下,程序員并不需要考慮處理器的字節(jié)序,但是當(dāng)編寫需要在計(jì)算機(jī)間交換數(shù)據(jù)時(shí)的應(yīng)用程序(特別是網(wǎng)絡(luò)應(yīng)用程序),則需要特別關(guān)注字節(jié)序問題。例如,sockaddr_in結(jié)構(gòu)體中的端口成員就要求使用網(wǎng)絡(luò)字節(jié)序。為了增強(qiáng)網(wǎng)絡(luò)應(yīng)用程序的可移植性,定義了兩類在本地字節(jié)序和網(wǎng)絡(luò)字節(jié)序之間進(jìn)行轉(zhuǎn)換的函數(shù):操作32位整數(shù)的 htonl和 ntohl,以及操作16位整數(shù)的 htons和ntohs。ntoh函數(shù)將網(wǎng)絡(luò)字節(jié)序轉(zhuǎn)換為本機(jī)字節(jié)序,而hton函數(shù)將主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序。
Linux支持?jǐn)?shù)目眾多的處理器,與處理器字節(jié)序相關(guān)的操作都定義在各自的byteorder.h中。以PowerPC處理器為例,在linux-2.6.24includeasm-powerpcyteorder.h中包含了位于linux-2.6.24includelinuxyteorder中的big_endian.h。它定義了__BIG_ENDIAN宏(表明PowerPC處理器為大端字節(jié)序),以及一些用來在本機(jī)字節(jié)序和大/小端字節(jié)序之間進(jìn)行轉(zhuǎn)換的宏。例如下面的宏專門用來操作32位數(shù):
對(duì)齊的目的是為了提高處理器的執(zhí)行效率。不同的處理器有不同的存儲(chǔ)器訪問特點(diǎn),Intel x86處理器允許非對(duì)齊的存儲(chǔ)器訪問,但是這會(huì)造成一定的性能損失,而ARM處理器進(jìn)行非對(duì)齊訪問時(shí)竟然得到一個(gè)不正確的數(shù)據(jù)。為了保證可移植性,應(yīng)該確保數(shù)據(jù)的對(duì)齊性,同時(shí)避免濫用指針操作以避免非對(duì)齊的存儲(chǔ)器訪問。對(duì)于ARM處理器,執(zhí)行下面的語句后,變量l將等于奇怪的0x01040302。
如果一個(gè)C語言原生類型T的變量在存儲(chǔ)器中的地址為sizeof(T)的整數(shù)倍,那么稱它是自然對(duì)齊的。一般情況下,編譯器通過自然對(duì)齊所有數(shù)據(jù)類型以解決對(duì)齊問題。對(duì)C原生類型來說,這不存在任何問題;而struct類型的對(duì)齊要求與對(duì)齊要求最嚴(yán)格的成員一致,這可能導(dǎo)致結(jié)構(gòu)體中兩個(gè)相鄰的、不同尺寸數(shù)據(jù)類型的成員之間存在填充。例如,對(duì)結(jié)構(gòu)體foo來說,它的對(duì)齊要求與y一致,隨著處理器字長的不同,x和y之間可能存在1個(gè)或3個(gè)甚至7個(gè)字節(jié)的填充。
C標(biāo)準(zhǔn)在stddef.h中定義了offsetof宏,用來返回結(jié)構(gòu)體成員的偏移值。為了查看成員y的偏移值可以使用下面的語句:
GCC編譯器為此提供了更多的擴(kuò)展。關(guān)鍵字__alignof__返回對(duì)象的對(duì)齊需求,例如如果目標(biāo)機(jī)器要求double值對(duì)齊于8字節(jié)邊界,那么__alignof__(double)返回8。關(guān)鍵字__attribute__可以用來指定變量或者結(jié)構(gòu)體成員的最低對(duì)齊需求(以字節(jié)為單位),例如int x__attribute__((aligned(16)))=0;可以通知編譯器為變量x分配一個(gè)16字節(jié)對(duì)齊的地址。如果打開了-Wpadded開關(guān),那么當(dāng)結(jié)構(gòu)體存在填充時(shí),GCC編譯器還會(huì)給出警告。如果編譯器沒有提供類似GCC那樣的擴(kuò)展,那么另一個(gè)常用的技巧是使用union提升較低數(shù)據(jù)類型的對(duì)齊要求。例如對(duì)于下面的聯(lián)合u,假設(shè)sizeof(int)=4,那么可以保證c也是4字節(jié)對(duì)齊的。
總之,在實(shí)踐中應(yīng)該編寫符合標(biāo)準(zhǔn)的代碼,隔離與特定處理器或者操作系統(tǒng)相關(guān)的代碼,甚至嘗試使用不同的編譯器編譯你的代碼,只有這樣才能確保代碼的可移植性。
[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.程序設(shè)計(jì)實(shí)踐[M].裘宗燕 ,譯 .北京 :機(jī)械工業(yè)出版社,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.