32bit - 64bit machine attention

32bit-64bit porting work注意事項

64位服務器逐步普及,各條產品線對64位升級的需求也不斷加大。在本文中,主要討論向64位平臺移植現有32位代碼時,應注意的一些細小問題。

什麼樣的程序需要升級到64位?

理論上說,64位的操作系統,對32位的程序具有良好的兼容性,即使全部換成64位平臺,依然可以良好的運行32位的程序。因此,許多目前在32位平臺上運行良好的程序也許不必移植,有選擇,有甄別的進行模塊的升級,對我們工作的展開,是有幫助的。

什麼樣的程序需要升級到64位呢?

除非程序有以下要求:

l  需要多於4GB的內存。

l  使用的文件大小常大於2GB。

l  密集浮點運算,需要利用64位架構的優勢。

l  能從64位平臺的優化數學庫中受益。

ILP32和LP64數據模型

32位環境涉及"ILP32"數據模型,是因爲C數據類型爲32位的int、long、指針。而64位環境使用不同的數據模型,此時的long和指針已爲64位,故稱作"LP64"數據模型。下面的表中列出了常見的類型大小比較:

Data type

Data length(32bit)

Data length(64bit)

Signed

char

8

8

Y

unsigned char

8

8

N

short

16

16

Y

unsigned short

16

16

N

int

32

32

Y

unsigned int

32

32

N

long

32

64

Y

unsigned long

32

64

N

long long

64

64

Y

point

32

64

N

size_t

32

64

N

ssize_t

32

64

Y

off_t

32

64

Y

 

 

 

 

   由上表我們可以看出,32位到64位的porting工作,主要就是處理長度變化所引發的各種問題。在32位平臺上很多正確的操作,在64位平臺上都不再成立。例如:long->int等,會出現截斷問題等。下面將詳細闡述具體遇到的問題,並給出修改策略。

截斷問題

截斷問題是在32-64porting工作中最容易遇到的問題。

部分的截斷問題能夠被編譯器捕捉到,採用-Wall –W進行編譯,永遠沒有壞處。這種問題處理方法也非常簡單,舉個例子來說:

        long mylong;

        (void) scanf("%d",&mylong);// warning: int format, different type arg (arg 2)

        long mylong;

        (void) scanf("%ld",&mylong);// ok

但有很多情況下,一些截斷性問題並不能被良好的診斷出來。

例如:

long a;

int b;

b = a;

在這種情況下,編譯器會直接進行轉換(截斷處理),編譯階段不報任何警告。當a的數據範圍在2G範圍內時,不會出問題,但是超出範圍,數據將出現問題。

另外,採用了強制轉換的方式,使一些隱患被保留了下來,例如:

       long mylong;

        (void) scanf("%d",(int*)&mylong);//編譯成功,但mylong的高位未被賦值,有可能導致問題。

   採用pclint可以有效的檢查這種問題,但是,在繁多的warning 中,找到需要的warning,並不是一件容易的事情。

因此,在做平臺移植的時候,對於截斷問題,最根本的還是逐行閱讀代碼,詳細檢測。

在編碼設計的時候,儘量保持使用變量類型的一致性,避免發生截斷問題。

建議:在接口以及數據結構的定義中不要使用指針,long以及用long定義的類型(size_t, ssize_t, off_t, time_t)由於字長的變化這些類型不能32/64位兼容。

 

一個討厭的類型size_t:32bit平臺上它的原形是unsigned int,而在64bit平臺上它的原形式unsigned long。這導致在printf等使用時:無論使用%u或者%lu都會有一個平臺報warning。目前我們的解決辦法是:採用%lu打印,並且size_t強制轉換爲unsingedlong。在小尾字節序(Little-endian)的系統中,這種轉換是安全的。

常量有效性問題

那些以十六進制或二進制表示的常量,通常都是32位的。例如,無符號32位常量0xFFFFFFFF通常用來測試是否爲-1;   

#define INVALID_POINTER_VALUE 0xFFFFFFFF

然而,在64位系統中,這個值不是-1,而是4294967295;在64位系統中,-1正確的值應爲0xFFFFFFFFFFFFFFFF。要避免這個問題,在聲明常量時,使用const,並且帶上signed或unsigned。

例如:

const signed int INVALID_POINTER_VALUE =0xFFFFFFFF;

上面一行代碼將會在32位和64位系統上都運行正常。或者,根據需要適當地使用 “L” 或 “U” 來聲明整型常量。

又比如對最高位的設置,通常我們的做法是定義如下的常量0x80000000,但是可移植性更好的方法是使用一個位移表達式:1L << ((sizeof(long) * 8) - 1);

 

 

參數問題

在參數的數據類型是由函數原型定義的情況中,參數應該根據標準規則轉換成這種類型

。在參數類型沒有指定的情況中,參數會被轉換成更大的類型

。在 64 位系統上,整型被轉換成 64 位的整型值,單精度的浮點類型被轉換成雙精度的浮點類型

。如果返回值沒有指定,那麼函數的缺省返回值是 int 類型的

。避免將有符號整型和無符號整型的和作爲 long 類型傳遞(見符號擴展問題)

請看下面的例子:

long function (long l);
int main () {
         int i = -2;
         unsigned k = 1U;
         long n = function (i + k);
}

上面這段代碼在 64 位系統上會失敗,因爲表達式 (i + k) 是一個無符號的 32 位表達式,在將其轉換成long 類型時,符號並沒有得到擴展。解決方案是將一個操作數強制轉換成 64 位的類型。

擴充問題(指針範圍越界)

擴充問題與代碼截斷問題剛好相反。請看下面的例子:

int baidu_gunzip(char*inbuf,int len,char* outbuf,int* size)

{

   ……

ret=bd_uncompress((Byte*)outbuf,(uLongf*)size,

                       (Byte*)(inbuf+beginpos),inlen);

}

這是ullib庫中baidugz模塊的一段代碼。在這段代碼中,將int型的指針改爲long型的指針傳遞給了bd_uncompress函數。在32位系統中,由於int與long都是32bit,程序沒有任何問題,但在64位系統中,將導致指針控制的範圍在調用函數中擴展爲原來的2倍,這將有可能導致程序出core,或指示的值不正確。這種問題比較隱蔽,很難發現,但危害極大,需要嚴格注意。

解決方法:加強對指針型參數的檢查,看是否有範圍擴充的問題。

符號擴展問題

要避免有符號數與無符號數的算術運算。在把int與long數值作對比時,此時產生的數據提升在LP64和ILP32中是有差異的。因爲是符號位擴展,所以這個問題很難被發現,只有保證兩端的操作數均爲signed或均爲unsigned,才能從根本上防止此問題的發生。

例如:

long k;

int i = -2;

unsigned int j = 1;

k = i + j;

printf("Answer:%ld\n", k);

你無法期望例2中的答案是-1,然而,當你在LP64環境中編譯此程序時,答案會是4294967295。原因在於表達式(i+j)是一個unsigned int表達式,但把它賦值給k時,符號位沒有被擴展。要解決這個問題,兩端的操作數只要均爲signed或均爲unsigned就可。像如下所示:

k = i + (int) j

在 C/C++ 中,表達式是基於結合律、操作符的優先級和一組數學計算規則的。要想讓表達式在 32 位和 64 位系統上都可以正確工作,請注意以下規則:

兩個有符號整數相加的結果是一個有符號整數。

int 和 long類型的兩個數相加,結果是一個long 類型的數。

如果一個操作數是無符號整數,另外一個操作數是有符號整數,那麼表達式的結果就是無符號整數。   

int 和 doubule類型的兩個數相加,結果是一個 double 類型的數。此處 int 類型的數在執行加法運算之前轉換成 double 類型。

將字符指針和字符字節聲明爲無符號類型的,這樣可以防止 8 位字符的符號擴展問題。

聯合體問題(Union)

當聯合本中混有不同長度的數據類型時,如果單獨使用裏面定義的成員,一般沒有問題。但在一些複雜的操作中,例如幾種類型的混用,可能會導致問題。如例3是一個常見的開源代碼包,可在ILP32卻不可在LP64環境下運行。代碼假定長度爲2的unsigned short數組,佔用了與long同樣的空間,可這在LP64平臺上卻不正確。

union{

unsigned long bytes;

unsigned short len[2];

} size;

    正確的方法是檢查是否對結構體有特殊的應用,如果有,那麼需要在所有代碼中仔細檢查聯合體,以確認所有的數據成員在LP64中都爲同等長度。

對齊問題

現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定變量的時候經常在特定的內存地址訪問,這就需要各類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。

對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數據只能從某些特定地址開始存取。其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設爲32 位系統)如果存放在偶地址開始的地方,那麼一個讀週期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀週期,並對兩次讀出的結果的高低字節進行拼湊才能得到該int數據。顯然在讀取效率上下降很多。這也是空間和時間的博弈。

g++默認對齊方式是按結構中相臨數據類型最長的進行

在32位上,這些類型默認使用一個機器字(4字節)對齊。

在64位上,這些類型默認使用最大兩個機器字(8×2=16字節)對齊(按16字節對齊會有好處麼?估計於編譯器的具體實現相關)

舉兩個例子說明一下:

struct asdf {

     int a;

     long long b;

};

這個結構在32bit操作系統中,sizeof(asdf) =12,在64bit操作系統中,sizeof(asdf)=16

struct asdf {

     int a;

     long double b;

};

這個結構在32bit操作系統中,sizeof(asdf) =16,在64bit操作系統中,sizeof(asdf)=32.這裏需要說明的是,32位sizeof(long double) = 12, 64位sizeof(longdouble)=16

在跨平臺的網絡傳輸過程中,或者文件存取過程中,我們經常會遇到與數據對齊相關的問題,並且爲此煩惱不已。64位porting工作,同樣需要詳細處理這種問題。

幸好在我們的移植過程中,因爲平臺比較一致,字節對齊問題並不是非常突出。但需要注意字節長度改變帶來的問題,舉例說明如下:

struct t

{

        char b;

        short c;

        size_t a;

};

例如,我們經常需要把結構存儲到文件中去,常規的寫法會如下進行:

寫操作:

struct t st;

fwrite(&st, sizeof(struct t), 1, fp)

讀操作:

struct t st;

fread(&st,sizeof(struct t),1,fp)

這種操作,如果針對同一平臺,當然沒有問題,但是,如果在32位機器上存儲的文件,copy到64位系統中去,就會造成嚴重問題,因爲size_t在64位平臺上定義爲unsigned long型,導致結構按8字節對齊,並且sizeof(struct t)=16,不再是32位平臺上的8字節。在網絡傳輸中,問題同樣有可能發生,需要嚴格注意。

我們可以採用的解決問題的方法有如下幾種:

瞭解平臺對齊差異,以及長度變化,修改結構:

例如上例:

struct t

{

        char b;

        short c;

        u_int a;    //不使用size_t類型

};即可保證兩個平臺的一致性。

在複雜的機器環境以及網絡環境中,還需要進一步採用單字節對齊的方式避免出現其他問題:

struct t

{

        char b;

        short c;

        u_int a;

} __attribute__ ((packed));

此時sizeof(struct t) = 7(強制結構安1字節對齊)。

 

內存分配問題以及指針跳轉問題

通常我們寫程序的時候,容易引入一些不安全的內存分配方式,以及進行一些不安全的指針跳轉操作。例如下面的例子:

int **p; p = (int**)malloc(4 * NO_ELEMENTS);

在上面的代碼中,同樣只對ILP32有效,對LP64來講,將是致命的錯誤。在64位平臺上,指針已經是8字節,這將導致空間分配不足,越界操作等。正確的方法是嚴格使用sizeof()進行處理:int **p; p = (int**)malloc(sizeof(int*)* NO_ELEMENTS);

 

在比如:在處理網絡交互的報文過程中,經常要處理各種數據結構

typedef struct A{

long a;

int b;

}*pA;

A a;

char * p = &a;

long * p1 = (long*)p;

int * p2 = (int *)(p + 4);

……

在上面的例子裏,同樣是進行了錯誤的偏移計算,導致出錯,另外,上面的用法還有可能因爲字節對齊方式的不同,產生不同,堅決不推薦使用。

正確的使用方式如下:

long * p1 = &(pA)p->a;

int * p2 = &(pA)p->b;

或者如下:

long * p1 = p + (long)(((pA)NULL)->a);

int * p2 = p + (long)(((pA)NULL)->b);

內存消耗問題與性能

在升級到64位操作系統後,內存消耗也會隨之增加。例如複雜的結構中(b+tree dict等),會保存大量的指針類型,而這些指針類型在LP64模型中,自身佔用內存會大量增加。在內存消耗較多的程序中,升級時要評估升級代價。在必要的情況下,需要修改程序內部邏輯,採用局部偏移代替指針類型,以便節約空間消耗。

另外,由於字節對齊問題,也容易導致結構體的不正常膨脹。通過改變結構中數據排列的先後順序,能將此問題所帶來的影響降到最小,並能減少所需的存儲空間。例如把兩個32位int值放在一起,會因爲少了填充數據,存儲空間也隨之減少。

另外,由於64位環境中指針所佔用的字節更大,致使原來運行良好的32位代碼出現不同程度的緩存問題,具體表現爲執行效率降低。可使用工具來分析緩存命中率的變化,以確認性能降低是否由此引起。

字節序問題

字節序問題由來已久,不過,很幸運的是,我們採用的都是x86的結構,因此字節序問題並不會困擾我們的開發工作,簡單介紹如下:

一個機器的字長通常包含數個byte,在存儲數據的方法上出現了大端點(big endian)和小端點(little endian)兩種結構,前者如PowerPC和Sun Sparc,後者如Intel x86系列。大端點機使用機器字長的高字節存儲數字邏輯編碼的低字節,字節的屋裏順序和邏輯順序相反;小端點機使用機器字長的高字節存儲數字邏輯編碼的高字節,字節的屋裏順序和邏輯順序相同。TCP/IP等傳輸協議使用的都是大端點序。大端點機和小端點機實際上各有優點,   由於Little Endian提供了邏輯順序與物理順序的一致性,讓編程者擺脫了不一致性所帶來的困擾,C語言開發者可以無所顧忌的按照自己的意願進行強制類型轉換,所以現代體系結構幾乎都支持Little Endian。但Big Endian也有其優點,尤其對於彙編程序員:他們對於任意長度的整數,總是可以通過判斷Byte 0的bit-7來查看一個整數的正負;對於Little Endian則不得不首先知道當前整數的長度,然後查看最高byte的bit-7來判斷其正負。對於這種情況,big endian的開發者可以寫出非常高效的代碼。

這個差別卻給跨平臺的程序編寫和不同平臺主機間的通信帶來了相當的困擾。在c/c++中使用強制類型轉換,如int到char數組的轉換在有些時候可以寫出簡潔高效的程序,但字節序的不同確實這種寫法有些時候變得很困難,跨平臺的程序,以及處理網絡數據傳輸和文件數據轉換年的時候,必須要考慮字節序不同的問題。其實在C++中檢測和轉換字節序並不困難,寫出的程序可以多種多樣,基本的思想卻是相同的。

檢測平臺的Endian基本上都是用聯合體union來實現的,在union中的數據佔有的是最長的那個類型的長度,這樣在union中加入不同字節長度的數據並將較長的那個賦值爲1就可以判斷了:

typedef union uEndianTest{

struct

{

   boolflittle_endian;

   bool fill[3];

};

long value;

}EndianTest;

static const EndianTest __Endian_Test__ = { (long)1 };

const bool platform_little_endian = __Endian_Test__.flittle_endian;

這樣使用這個 platform_little_endian 就可以檢測到當前平臺是否是little_endian

Glibc升級以及內核升級帶來的問題

升級到64位平臺後,由於glibc的升級,以及內核版本的升級,導致個別的函數調用行爲與原函數不再一致,對於這種問題,需要不斷的總結和積累,慢慢克服。下面介紹一下目前在升級過程中已知的問題。

 

sendfile函數:在2.4的內核中,sendfile是可以支持文件間的相互copy的,但是在2.6以上的內核中,卻只支持文件向socket的copy。爲什麼去掉這樣一個優秀的特性,另人費解。

 

gethostbyname等函數:包括gethostbyname,gethostbyname_r,getservbyname_r,getservbyport_r等函數,在升級到64位平臺後,程序進行-static編譯的過程中,會報warning,並且無法消除。經過調查,在使用靜態連接的程序中,調用上述函數,的確是存在一定風險的,如果編譯機器和運行機器的glibc版本號不一致,會導致嚴重問題,例如程序crash等。因此,建議調用了上述函數的程序,最好不要使用-static進行編譯。事實上,在老版本的gcc上,同樣存在上述風險,只是沒有報出warning而已。

 

另外,在不同的版本中,gethostbyname_r的返回值也存在差異,在異常返回的時候,因此,這幾個函數在判斷是否成功的時候,需要注意,並不能單純的判斷返回值。這個問題在ul_gethostbyname_r中已經做了處理,可放心使用。

 

其他

如果在make的過程中,採用的是gcc的編譯器,而不是g++,可能遇到很多的錯誤,別緊張,這是由於gcc無法自動和c++使用的庫相連接而導致的。改成g++進行編譯就ok,或者在make的時候指定-lstdc++。

另外有一些編譯要求,請參考《技術部64位開發規範》

發佈了100 篇原創文章 · 獲贊 266 · 訪問量 69萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章