linux 32bit 改为64bit问题
32bit-64bit porting work注意事項
64位服務(wù)器逐步普及,各條產(chǎn)品線對64位升級的需求也不斷加大。在本文中,主要討論向64位平臺移植現(xiàn)有32位代碼時,應(yīng)注意的一些細小問題。
什么樣的程序需要升級到64位?
理論上說,64位的操作系統(tǒng),對32位的程序具有良好的兼容性,即使全部換成64位平臺,依然可以良好的運行32位的程序。因此,許多目前在32位平臺上運行良好的程序也許不必移植,有選擇,有甄別的進行模塊的升級,對我們工作的展開,是有幫助的。
什么樣的程序需要升級到64位呢?
除非程序有以下要求:
l? 需要多于4GB的內(nèi)存。
l? 使用的文件大小常大于2GB。
l? 密集浮點運算,需要利用64位架構(gòu)的優(yōu)勢。
l? 能從64位平臺的優(yōu)化數(shù)學(xué)庫中受益。
ILP32和LP64數(shù)據(jù)模型
32位環(huán)境涉及"ILP32"數(shù)據(jù)模型,是因為C數(shù)據(jù)類型為32位的int、long、指針。而64位環(huán)境使用不同的數(shù)據(jù)模型,此時的long和指針已為64位,故稱作"LP64"數(shù)據(jù)模型。下面的表中列出了常見的類型大小比較:
| 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工作,主要就是處理長度變化所引發(fā)的各種問題。在32位平臺上很多正確的操作,在64位平臺上都不再成立。例如:long->int等,會出現(xiàn)截斷問題等。下面將詳細闡述具體遇到的問題,并給出修改策略。
截斷問題
截斷問題是在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;
在這種情況下,編譯器會直接進行轉(zhuǎn)換(截斷處理),編譯階段不報任何警告。當(dāng)a的數(shù)據(jù)范圍在2G范圍內(nèi)時,不會出問題,但是超出范圍,數(shù)據(jù)將出現(xiàn)問題。
另外,采用了強制轉(zhuǎn)換的方式,使一些隱患被保留了下來,例如:
???????long mylong;
??????? (void) scanf("%d",(int*)&mylong);//編譯成功,但mylong的高位未被賦值,有可能導(dǎo)致問題。
?? 采用pclint可以有效的檢查這種問題,但是,在繁多的warning 中,找到需要的warning,并不是一件容易的事情。
因此,在做平臺移植的時候,對于截斷問題,最根本的還是逐行閱讀代碼,詳細檢測。
在編碼設(shè)計的時候,盡量保持使用變量類型的一致性,避免發(fā)生截斷問題。
建議:在接口以及數(shù)據(jù)結(jié)構(gòu)的定義中不要使用指針,long,以及用long定義的類型(size_t, ssize_t, off_t, time_t),由于字長的變化,這些類型不能32/64位兼容。
?
一個討厭的類型size_t:在32bit平臺上,它的原形是unsigned int,而在64bit平臺上,它的原形式unsigned long。這導(dǎo)致在printf等使用時:無論使用%u或者%lu都會有一個平臺報warning。目前我們的解決辦法是:采用%lu打印,并且size_t強制轉(zhuǎn)換為unsingedlong。在小尾字節(jié)序(Little-endian)的系統(tǒng)中,這種轉(zhuǎn)換是安全的。
常量有效性問題
那些以十六進制或二進制表示的常量,通常都是32位的。例如,無符號32位常量0xFFFFFFFF通常用來測試是否為-1;???
#define INVALID_POINTER_VALUE 0xFFFFFFFF
然而,在64位系統(tǒng)中,這個值不是-1,而是4294967295;在64位系統(tǒng)中,-1正確的值應(yīng)為0xFFFFFFFFFFFFFFFF。要避免這個問題,在聲明常量時,使用const,并且?guī)蟬igned或unsigned。
例如:
const signed int INVALID_POINTER_VALUE =0xFFFFFFFF;
上面一行代碼將會在32位和64位系統(tǒng)上都運行正常。或者,根據(jù)需要適當(dāng)?shù)厥褂?“L” 或 “U” 來聲明整型常量。
又比如對最高位的設(shè)置,通常我們的做法是定義如下的常量0x80000000,但是可移植性更好的方法是使用一個位移表達式:1L << ((sizeof(long) * 8) - 1);?
?
參數(shù)問題
在參數(shù)的數(shù)據(jù)類型是由函數(shù)原型定義的情況中,參數(shù)應(yīng)該根據(jù)標準規(guī)則轉(zhuǎn)換成這種類型
。在參數(shù)類型沒有指定的情況中,參數(shù)會被轉(zhuǎn)換成更大的類型
。在 64 位系統(tǒng)上,整型被轉(zhuǎn)換成 64 位的整型值,單精度的浮點類型被轉(zhuǎn)換成雙精度的浮點類型
。如果返回值沒有指定,那么函數(shù)的缺省返回值是 int 類型的
。避免將有符號整型和無符號整型的和作為 long 類型傳遞(見符號擴展問題)
請看下面的例子:
long function (long l); int main () { ???????? int i = -2; ???????? unsigned k = 1U; ???????? long n = function (i + k); }上面這段代碼在 64 位系統(tǒng)上會失敗,因為表達式?(i + k)?是一個無符號的 32 位表達式,在將其轉(zhuǎn)換成long 類型時,符號并沒有得到擴展。解決方案是將一個操作數(shù)強制轉(zhuǎn)換成 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函數(shù)。在32位系統(tǒng)中,由于int與long都是32bit,程序沒有任何問題,但在64位系統(tǒng)中,將導(dǎo)致指針控制的范圍在調(diào)用函數(shù)中擴展為原來的2倍,這將有可能導(dǎo)致程序出core,或指示的值不正確。這種問題比較隱蔽,很難發(fā)現(xiàn),但危害極大,需要嚴格注意。
解決方法:加強對指針型參數(shù)的檢查,看是否有范圍擴充的問題。
符號擴展問題
要避免有符號數(shù)與無符號數(shù)的算術(shù)運算。在把int與long數(shù)值作對比時,此時產(chǎn)生的數(shù)據(jù)提升在LP64和ILP32中是有差異的。因為是符號位擴展,所以這個問題很難被發(fā)現(xiàn),只有保證兩端的操作數(shù)均為signed或均為unsigned,才能從根本上防止此問題的發(fā)生。
例如:
long k;
int i = -2;
unsigned int j = 1;
k = i + j;
printf("Answer:%ld\n", k);
你無法期望例2中的答案是-1,然而,當(dāng)你在LP64環(huán)境中編譯此程序時,答案會是4294967295。原因在于表達式(i+j)是一個unsigned int表達式,但把它賦值給k時,符號位沒有被擴展。要解決這個問題,兩端的操作數(shù)只要均為signed或均為unsigned就可。像如下所示:
k = i + (int) j
在 C/C++?中,表達式是基于結(jié)合律、操作符的優(yōu)先級和一組數(shù)學(xué)計算規(guī)則的。要想讓表達式在 32 位和 64 位系統(tǒng)上都可以正確工作,請注意以下規(guī)則:
l?兩個有符號整數(shù)相加的結(jié)果是一個有符號整數(shù)。
l?int?和 long?類型的兩個數(shù)相加,結(jié)果是一個long 類型的數(shù)。
l?如果一個操作數(shù)是無符號整數(shù),另外一個操作數(shù)是有符號整數(shù),那么表達式的結(jié)果就是無符號整數(shù)。???
l??int?和 doubule?類型的兩個數(shù)相加,結(jié)果是一個 double 類型的數(shù)。此處 int 類型的數(shù)在執(zhí)行加法運算之前轉(zhuǎn)換成 double 類型。
將字符指針和字符字節(jié)聲明為無符號類型的,這樣可以防止 8?位字符的符號擴展問題。
聯(lián)合體問題(Union)
當(dāng)聯(lián)合本中混有不同長度的數(shù)據(jù)類型時,如果單獨使用里面定義的成員,一般沒有問題。但在一些復(fù)雜的操作中,例如幾種類型的混用,可能會導(dǎo)致問題。如例3是一個常見的開源代碼包,可在ILP32卻不可在LP64環(huán)境下運行。代碼假定長度為2的unsigned short數(shù)組,占用了與long同樣的空間,可這在LP64平臺上卻不正確。
union{
unsigned long bytes;
unsigned short len[2];
} size;
????正確的方法是檢查是否對結(jié)構(gòu)體有特殊的應(yīng)用,如果有,那么需要在所有代碼中仔細檢查聯(lián)合體,以確認所有的數(shù)據(jù)成員在LP64中都為同等長度。
對齊問題
現(xiàn)代計算機中內(nèi)存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定變量的時候經(jīng)常在特定的內(nèi)存地址訪問,這就需要各類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。
對齊的作用和原因:各個硬件平臺對存儲空間的處理上有很大的不同。一些平臺對某些特定類型的數(shù)據(jù)只能從某些特定地址開始存取。其他平臺可能沒有這種情況,但是最常見的是如果不按照適合其平臺要求對數(shù)據(jù)存放進行對齊,會在存取效率上帶來損失。比如有些平臺每次讀都是從偶地址開始,如果一個int型(假設(shè)為32 位系統(tǒng))如果存放在偶地址開始的地方,那么一個讀周期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀周期,并對兩次讀出的結(jié)果的高低字節(jié)進行拼湊才能得到該int數(shù)據(jù)。顯然在讀取效率上下降很多。這也是空間和時間的博弈。
g++默認對齊方式是按結(jié)構(gòu)中相臨數(shù)據(jù)類型最長的進行
在32位上,這些類型默認使用一個機器字(4字節(jié))對齊。
在64位上,這些類型默認使用最大兩個機器字(8×2=16字節(jié))對齊(按16字節(jié)對齊會有好處么?估計于編譯器的具體實現(xiàn)相關(guān))
舉兩個例子說明一下:
struct asdf {
???? int a;
???? long long b;
};
這個結(jié)構(gòu)在32bit操作系統(tǒng)中,sizeof(asdf) =12,在64bit操作系統(tǒng)中,sizeof(asdf)=16
struct asdf {
???? int a;
???? long double b;
};
這個結(jié)構(gòu)在32bit操作系統(tǒng)中,sizeof(asdf) =16,在64bit操作系統(tǒng)中,sizeof(asdf)=32.這里需要說明的是,32位sizeof(long double) = 12, 64位sizeof(longdouble)=16
在跨平臺的網(wǎng)絡(luò)傳輸過程中,或者文件存取過程中,我們經(jīng)常會遇到與數(shù)據(jù)對齊相關(guān)的問題,并且為此煩惱不已。64位porting工作,同樣需要詳細處理這種問題。
幸好在我們的移植過程中,因為平臺比較一致,字節(jié)對齊問題并不是非常突出。但需要注意字節(jié)長度改變帶來的問題,舉例說明如下:
struct t
{
??????? char b;
??????? short c;
??????? size_t a;
};
例如,我們經(jīng)常需要把結(jié)構(gòu)存儲到文件中去,常規(guī)的寫法會如下進行:
寫操作:
struct t st;
fwrite(&st, sizeof(struct t), 1, fp)
讀操作:
struct t st;
fread(&st,sizeof(struct t),1,fp)
這種操作,如果針對同一平臺,當(dāng)然沒有問題,但是,如果在32位機器上存儲的文件,copy到64位系統(tǒng)中去,就會造成嚴重問題,因為size_t在64位平臺上定義為unsigned long型,導(dǎo)致結(jié)構(gòu)按8字節(jié)對齊,并且sizeof(struct t)=16,不再是32位平臺上的8字節(jié)。在網(wǎng)絡(luò)傳輸中,問題同樣有可能發(fā)生,需要嚴格注意。
我們可以采用的解決問題的方法有如下幾種:
了解平臺對齊差異,以及長度變化,修改結(jié)構(gòu):
例如上例:
struct t
{
??????? char b;
??????? short c;
??????? u_int a;??? //不使用size_t類型
};即可保證兩個平臺的一致性。
在復(fù)雜的機器環(huán)境以及網(wǎng)絡(luò)環(huán)境中,還需要進一步采用單字節(jié)對齊的方式避免出現(xiàn)其他問題:
struct t
{
??????? char b;
??????? short c;
??????? u_int a;
} __attribute__ ((packed));
此時sizeof(struct t) = 7(強制結(jié)構(gòu)安1字節(jié)對齊)。
?
內(nèi)存分配問題以及指針跳轉(zhuǎn)問題
通常我們寫程序的時候,容易引入一些不安全的內(nèi)存分配方式,以及進行一些不安全的指針跳轉(zhuǎn)操作。例如下面的例子:
int **p; p = (int**)malloc(4 * NO_ELEMENTS);
在上面的代碼中,同樣只對ILP32有效,對LP64來講,將是致命的錯誤。在64位平臺上,指針已經(jīng)是8字節(jié),這將導(dǎo)致空間分配不足,越界操作等。正確的方法是嚴格使用sizeof()進行處理:int **p; p = (int**)malloc(sizeof(int*)* NO_ELEMENTS);
?
在比如:在處理網(wǎng)絡(luò)交互的報文過程中,經(jīng)常要處理各種數(shù)據(jù)結(jié)構(gòu)
typedef struct A{
long a;
int b;
}*pA;
A a;
char * p = &a;
long * p1 = (long*)p;
int * p2 = (int *)(p + 4);
……
在上面的例子里,同樣是進行了錯誤的偏移計算,導(dǎo)致出錯,另外,上面的用法還有可能因為字節(jié)對齊方式的不同,產(chǎn)生不同,堅決不推薦使用。
正確的使用方式如下:
long * p1 = &(pA)p->a;
int * p2 = &(pA)p->b;
或者如下:
long * p1 = p + (long)(((pA)NULL)->a);
int * p2 = p +?(long)(((pA)NULL)->b);
內(nèi)存消耗問題與性能
在升級到64位操作系統(tǒng)后,內(nèi)存消耗也會隨之增加。例如復(fù)雜的結(jié)構(gòu)中(b+tree dict等),會保存大量的指針類型,而這些指針類型在LP64模型中,自身占用內(nèi)存會大量增加。在內(nèi)存消耗較多的程序中,升級時要評估升級代價。在必要的情況下,需要修改程序內(nèi)部邏輯,采用局部偏移代替指針類型,以便節(jié)約空間消耗。
另外,由于字節(jié)對齊問題,也容易導(dǎo)致結(jié)構(gòu)體的不正常膨脹。通過改變結(jié)構(gòu)中數(shù)據(jù)排列的先后順序,能將此問題所帶來的影響降到最小,并能減少所需的存儲空間。例如把兩個32位int值放在一起,會因為少了填充數(shù)據(jù),存儲空間也隨之減少。
另外,由于64位環(huán)境中指針所占用的字節(jié)更大,致使原來運行良好的32位代碼出現(xiàn)不同程度的緩存問題,具體表現(xiàn)為執(zhí)行效率降低。可使用工具來分析緩存命中率的變化,以確認性能降低是否由此引起。
字節(jié)序問題
字節(jié)序問題由來已久,不過,很幸運的是,我們采用的都是x86的結(jié)構(gòu),因此字節(jié)序問題并不會困擾我們的開發(fā)工作,簡單介紹如下:
一個機器的字長通常包含數(shù)個byte,在存儲數(shù)據(jù)的方法上出現(xiàn)了大端點(big endian)和小端點(little endian)兩種結(jié)構(gòu),前者如PowerPC和Sun Sparc,后者如Intel x86系列。大端點機使用機器字長的高字節(jié)存儲數(shù)字邏輯編碼的低字節(jié),字節(jié)的屋里順序和邏輯順序相反;小端點機使用機器字長的高字節(jié)存儲數(shù)字邏輯編碼的高字節(jié),字節(jié)的屋里順序和邏輯順序相同。TCP/IP等傳輸協(xié)議使用的都是大端點序。大端點機和小端點機實際上各有優(yōu)點,?? 由于Little Endian提供了邏輯順序與物理順序的一致性,讓編程者擺脫了不一致性所帶來的困擾,C語言開發(fā)者可以無所顧忌的按照自己的意愿進行強制類型轉(zhuǎn)換,所以現(xiàn)代體系結(jié)構(gòu)幾乎都支持Little Endian。但Big Endian也有其優(yōu)點,尤其對于匯編程序員:他們對于任意長度的整數(shù),總是可以通過判斷Byte 0的bit-7來查看一個整數(shù)的正負;對于Little Endian則不得不首先知道當(dāng)前整數(shù)的長度,然后查看最高byte的bit-7來判斷其正負。對于這種情況,big endian的開發(fā)者可以寫出非常高效的代碼。
這個差別卻給跨平臺的程序編寫和不同平臺主機間的通信帶來了相當(dāng)?shù)睦_。在c/c++中使用強制類型轉(zhuǎn)換,如int到char數(shù)組的轉(zhuǎn)換在有些時候可以寫出簡潔高效的程序,但字節(jié)序的不同確實這種寫法有些時候變得很困難,跨平臺的程序,以及處理網(wǎng)絡(luò)數(shù)據(jù)傳輸和文件數(shù)據(jù)轉(zhuǎn)換年的時候,必須要考慮字節(jié)序不同的問題。其實在C++中檢測和轉(zhuǎn)換字節(jié)序并不困難,寫出的程序可以多種多樣,基本的思想?yún)s是相同的。
檢測平臺的Endian基本上都是用聯(lián)合體union來實現(xiàn)的,在union中的數(shù)據(jù)占有的是最長的那個類型的長度,這樣在union中加入不同字節(jié)長度的數(shù)據(jù)并將較長的那個賦值為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 就可以檢測到當(dāng)前平臺是否是little_endian
Glibc升級以及內(nèi)核升級帶來的問題
升級到64位平臺后,由于glibc的升級,以及內(nèi)核版本的升級,導(dǎo)致個別的函數(shù)調(diào)用行為與原函數(shù)不再一致,對于這種問題,需要不斷的總結(jié)和積累,慢慢克服。下面介紹一下目前在升級過程中已知的問題。
?
sendfile函數(shù):在2.4的內(nèi)核中,sendfile是可以支持文件間的相互copy的,但是在2.6以上的內(nèi)核中,卻只支持文件向socket的copy。為什么去掉這樣一個優(yōu)秀的特性,另人費解。
?
gethostbyname等函數(shù):包括gethostbyname,gethostbyname_r,getservbyname_r,getservbyport_r等函數(shù),在升級到64位平臺后,程序進行-static編譯的過程中,會報warning,并且無法消除。經(jīng)過調(diào)查,在使用靜態(tài)連接的程序中,調(diào)用上述函數(shù),的確是存在一定風(fēng)險的,如果編譯機器和運行機器的glibc版本號不一致,會導(dǎo)致嚴重問題,例如程序crash等。因此,建議調(diào)用了上述函數(shù)的程序,最好不要使用-static進行編譯。事實上,在老版本的gcc上,同樣存在上述風(fēng)險,只是沒有報出warning而已。
?
另外,在不同的版本中,gethostbyname_r的返回值也存在差異,在異常返回的時候,因此,這幾個函數(shù)在判斷是否成功的時候,需要注意,并不能單純的判斷返回值。這個問題在ul_gethostbyname_r中已經(jīng)做了處理,可放心使用。
?
其他
如果在make的過程中,采用的是gcc的編譯器,而不是g++,可能遇到很多的錯誤,別緊張,這是由于gcc無法自動和c++使用的庫相連接而導(dǎo)致的。改成g++進行編譯就ok,或者在make的時候指定-lstdc++。
另外有一些編譯要求,請參考《技術(shù)部64位開發(fā)規(guī)范》
總結(jié)
以上是生活随笔為你收集整理的linux 32bit 改为64bit问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Qt之系统托盘(QSystemTrayI
- 下一篇: Linux学习笔记 -- 文本编辑器之