隨著 64 位體系結構的普及,針對 64 位系統准備好您的 Linux? 軟件已經變得比以前更為重要。在本文中,您將學習如何在進行語句聲明、賦值、位移、類型轉換、字符串格式化以及更多操作時,防止出現可移植性缺陷。
Linux 是可以使用 64 位處理器的跨平台操作系統之一,現在 64 位的系統在服務器和桌面端都已經非常常見了。很多開發人員現在都面臨著需要將自己的應用程序從 32 位環境移植到 64 位環境中。隨著 Intel? Itanium? 和其他 64 位處理器的引入,使軟件針對 64 位環境做好准備變得日益重要了。
與 UNIX? 和其他類 UNIX 操作系統一樣,Linux 使用了 LP64 標准,其中指針和長整數都是 64 位的,而普通的整數則依然是 32 位的。盡管有些高級語言並不會受到這種類型大小不同的影響,但是另外一些語言(例如 C 語言)卻的確會受到這種影響。
將應用程序從 32 位系統移植到 64 位系統上的工作可能會非常簡單,也可能會非常困難,這取決於這些應用程序是如何編寫和維護的。很多瑣碎的問題都可能導致產生問題,即使在一個編寫得非常好的高度可移植的應用程序中也是如此,因此本文將對這些問題進行歸納總結,並給出解決這些問題的一些方法建議。
64 位的優點
32 位平台有很多限制,這些限制正在阻礙大型應用程序(例如數據庫)開發人員的工作進展,尤其對那些希望充分利用計算機硬件優點的開發人員來說更是如此。科學計算通常要依賴於浮點計算,而有些應用程序(例如金融計算)則需要一個比較狹窄的數字范圍,但是卻要求更高的精度,其精度高於浮點數所提供的精度。64 位數學運算提供了這種更高精度的定點數學計算,同時還提供了足夠的數字范圍。現在在計算機業界中有很多關於 32 位地址空間所表示的地址空間的討論。32 位指針只能尋址 4GB 的虛擬地址空間。我們可以克服這種限制,但是應用程序開發就變得非常復雜了,其性能也會顯著降低。
在語言實現方面,目前的 C 語言標准要求 “long long” 數據類型至少是 64 位的。然而,其實現可能會將其定義為更大。
另外一個需要改進的地方是日期。在 Linux 中,日期是使用 32 位整數來表示的,該值所表示的是從 1970 年 1 月 1 日至今所經過的秒數。這在 2038 年就會失效。但是在 64 位的系統中,日期是使用有符號的 64 位整數表示的,這可以極大地擴充其可用范圍。
總之,64 位具有以下優點:
1. 64 位的應用程序可以直接訪問 4EB 的虛擬內存,Intel Itanium 處理器提供了連續的線性地址空間。
2. 64 位的 Linux 允許文件大小最大達到 4 EB(2 的 63 次冪),其重要的優點之一就是可以處理對大型數據庫的訪問。
Linux 64 位體系結構
不幸的是,C 編程語言並沒有提供一種機制來添加新的基本數據類型。因此,提供 64 位的尋址和整數運算能力必須要修改現有數據類型的綁定或映射,或者向 C 語言中添加新的數據類型。
表 1. 32 位和 64 位數據模型 ILP32 LP64 LLP64 ILP64 char 8 8 8 8 short 16 16 16 16 int 32 32 32 64 long 32 64 32 64 long long 64 64 64 64 指針 32 64 64 64 這 3 個 64 位模型(LP64、LLP64 和 ILP64)之間的區別在於非浮點數據類型。當一個或多個 C 數據類型的寬度從一種模型變換成另外一種模型時,應用程序可能會受到很多方面的影響。這些影響主要可以分為兩類:
數據對象的大小。編譯器按照自然邊界對數據類型進行對齊;換而言之,32 位的數據類型在 64 位系統上要按照 32 位邊界進行對齊,而 64 位的數據類型在 64 位系統上則要按照 64 位邊界進行對齊。這意味著諸如結構或聯合之類的數據對象的大小在 32 位和 64 位系統上是不同的。
基本數據類型的大小。通常關於基本數據類型之間關系的假設在 64 位數據模型上都已經無效了。依賴於這些關系的應用程序在 64 位平台上編譯也會失敗。例如,sizeof (int) = sizeof (long) = sizeof (pointer) 的假設對於 ILP32 數據模型有效,但是對於其他數據模型就無效了。
總之,編譯器要按照自然邊界對數據類型進行對齊,這意味著編譯器會進行 “填充”,從而強制進行這種方式的對齊,就像是在 C 結構和聯合中所做的一樣。結構或聯合的成員是根據最寬的成員進行對齊的。清單 1 對這個結構進行了解釋。
清單 1. C 結構
strUCt test { int i1; double d; int i2; long l; } 表 2 給出了這個結構中每個成員的大小,以及這個結構在 32 位系統和 64 位系統上的大小。
表 2. 結構和結構成員的大小
結構成員 在 32 位系統上的大小 在 64 位系統上的大小 struct test { int i1; 32 位 32 位 32 位填充 double d; 64 位 64 位 int i2; 32 位 32 位 32 位填充 long l; 32 位 64 位 }; 結構大小為 20 字節 結構大小為 32 字節 注意,在一個 32 位的系統上,編譯器可能並沒有對變量 d 進行對齊,盡管它是一個 64 位的對象,這是因為硬件會將其當作兩個 32 位的對象進行處理。然而,64 位的系統會對 d 和 l 都進行對齊,這樣會添加兩個 4 字節的填充。
從 32 位系統移植到 64 位系統
本節介紹如何解決一些常見的問題:
聲明表達式賦值數字常數Endianism類型定義位移字符串格式化函數參數
聲明
要想讓您的代碼在 32 位和 64 位系統上都可以工作,請注意以下有關聲明的用法:
根據需要適當地使用 “L” 或 “U” 來聲明整型常量。
確保使用無符號整數來防止符號擴展的問題。
如果有些變量在這兩個平台上都需要是 32 位的,請將其類型定義為 int.如果有些變量在 32 位系統上是 32 位的,在 64 位系統上是 64 位的,請將其類型定義為 long.為了對齊和性能的需要,請將數字變量聲明為 int 或 long 類型。不要試圖使用 char 或 short 類型來保存字節。
將字符指針和字符字節聲明為無符號類型的,這樣可以防止 8 位字符的符號擴展問題。
表達式
在 C/C++ 中,表達式是基於結合律、操作符的優先級和一組數學計算規則的。要想讓表達式在 32 位和 64 位系統上都可以正確工作,請注意以下規則:
兩個有符號整數相加的結果是一個有符號整數。
int 和 long 類型的兩個數相加,結果是一個 long 類型的數。
如果一個操作數是無符號整數,另外一個操作數是有符號整數,那麼表達式的結果就是無符號整數。
int 和 doubule 類型的兩個數相加,結果是一個 double 類型的數。此處 int 類型的數在執行加法運算之前轉換成 double 類型。
賦值
由於指針、int 和 long 在 64 位系統上大小不再相同了,因此根據這些變量是如何賦值和在應用程序中使用的,可能會出現問題。下面是有關賦值的一些技巧:
不要交換使用 int 和 long 類型,因為這可能會導致高位數字被截斷。例如,不要做下面的事情:
int i; long l; i = l; 不要使用 int 類型來存儲指針。下面這個例子在 32 位系統上可以很好地工作,但是在 64 位系統上會失敗,這是因為 32 位整數無法存放 64 位的指針。例如,不要做下面的事情:
unsigned int i, *ptr; i = (unsigned) ptr;不要使用指針來存放 int 類型的值。例如,不要做下面的事情;
int *ptr; int i; ptr = (int *) i; 如果在表達式中混合使用無符號和有符號的 32 位整數,並將其賦值給一個有符號的 long 類型,那麼將其中一個操作數轉換成 64 位的類型。這會導致其他操作數也被轉換成 64 位的類型,這樣在對表達式進行賦值時就不需要再進行轉換了。另外一種解決方案是對整個表達式進行轉換,這樣就可以在賦值時進行符號擴展。例如,考慮下面這種用法可能會出現的問題:
long n; int i = -2; unsigned k = 1; n = i + k;從數學計算上來說,上面這個黑體顯示的表達式的結果應該是 -1 。但是由於表達式是無符號的,因此不會進行符號擴展。解決方案是將一個操作數轉換成 64 位類型(下面的第一行就是這樣),或者對整個表達式進行轉換(下面第二行):
n = (long) i + k; n = (int) (i + k);
數字常量
16 進制的常量通常都用作掩碼或特殊位的值。如果一個沒有後綴的 16 進制的常量是 32 位的,並且其高位被置位了,那麼它就可以作為無符號整型進行定義。
例如,常數 OxFFFFFFFFL 是一個有符號的 long 類型。在 32 位系統上,這會將所有位都置位(每位全為 1),但是在 64 位系統上,只有低 32 位被置位了,結果是這個值是 0x00000000FFFFFFFF.
如果我們希望所有位全部置位,那麼一種可移植的方法是定義一個有符號的常數,其值為 -1.這會將所有位全部置位,因為它采用了二進制補碼算法。
long x = -1L; 可能產生的另外一個問題是最高位的設置。在 32 位系統上,我們使用的是常量 0x80000000。但是可移植性更好的方法是使用一個位移表達式:
1L << ((sizeof(long) * 8) - 1); Endianism
Endianism 是指用來存儲數據的方法,它定義了整數和浮點數據類型中是如何對字節進行尋址的。
Little-endian 是將低位字節存儲在內存的低地址中,將高位字節存儲在內存的高地址中。
Big-endian 是將高位字節存儲在內存的低地址中,將低位字節存儲在內存的高地址中。
表 3 給出了一個 64 位長整數的布局示例。
表 3. 64 位 long int 類型的布局
低地址 高地址 Little endian Byte 0 Byte 1 Byte 2 Byte 3 Byte 4 Byte 5 Byte 6 Byte 7 Big endian Byte 7 Byte 6 Byte 5 Byte 4 Byte 3 Byte 2 Byte 1 Byte 0 例如,32 位的字 0x12345678 在 big endian 機器上的布局如下:
表 4. 0x12345678 在 big-endian 系統上的布局
內存偏移量 0 1 2 3 內存內容 0x12 0x34 0x56 0x78
如果將 0x12345678 當作兩個半字來看待,分別是 0x1234 和 0x5678,那麼就會看到在 big endian 機器上是下面的情況:
表 5. 0x12345678 在 big-endian 系統上當作兩個半字來看待的情況
內存偏移量 0 2 內存內容 0x1234 0x5678 然而,在 little endian 機器上,字 0x12345678 的布局如下所示:
表 6. 0x12345678 在 little-endian 系統上的布局
內存偏移量 0 1 2 3 內存內容 0x78 0x56 0x34 0x12 類似地,兩個半字 0x1234 和 0x5678 如下所示:
表 7. 0x12345678 在 little-endian 系統上作為兩個半字看到的情況
內存偏移量 0 2 內存內容 0x3412 0x7856 下面這個例子解釋了 big endian 和 little endian 機器上字節順序之間的區別。
下面的 C 程序在一台 big endian 機器上進行編譯和運行時會打印 “Big endian”,在一台 little endian 機器上進行編譯和運行時會打印 “Little endian”。
清單 2. big endian 與 little endian
#include main () { int i = 0x12345678; if (*(char *)&i == 0x12) printf ("Big endian\n"); else if (*(char *)&i == 0x78) printf ("Little endian\n"); } Endianism 在以下情況中非常重要:
使用位掩碼時 對象的間接指針地址部分
在 C 和 C++ 中有位域來幫助處理 endian 的問題。我建議使用位域,而不要使用掩碼域或 16 進制的常量。有幾個函數可以用來將 16 位和 32 位數據從 “主機字節順序” 轉換成 “網絡字節順序”。例如,htonl (3)、ntohl (3) 用來轉換 32 位整數。類似地,htons (3)、ntohs (3) 用來轉換 16 位整數。然而,對於 64 位整數來說,並沒有標准的函數集。但是在 big endian 和 little endian 系統上,Linux 都提供了下面的幾個宏:
bswap_16 bswap_32 bswap_64
類型定義
建議您不要使用 C/C++ 中那些在 64 位系統上會改變大小的數據類型來編寫應用程序,而是使用一些類型定義或宏來顯式地說明變量中所包含的數據的大小和類型。有些定義可以使代碼的可移植性更好。
ptrdiff_t: 這是一個有符號整型,是兩個指針相減後的結果。
size_t: 這是一個無符號整型,是執行 sizeof 操作的結果。這在向一些函數(例如 malloc (3))傳遞參數時使用,也可以從一些函數(比如 fred (2))中返回。
int32_t、uint32_t 等: 定義具有預定義寬度的整型。
intptr_t 和 uintptr_t: 定義整型類型,任何有效指針都可以轉換成這個類型。
例 1:
在下面這條語句中,在對 bufferSize 進行賦值時,從 sizeof 返回的 64 位值被截斷成了 32 位。
int bufferSize = (int) sizeof (something);
解決方案是使用 size_t 對返回值進行類型轉換,並將其賦給聲明為 size_t 類型的 bufferSize,如下所示:
size_t bufferSize = (size_t) sizeof (something);
例 2:
在 32 位系統上,int 和 long 大小相同。由於這一點,有些開發人員會交換使用這兩種類型。這可能會導致指針被賦值給 int 類型,或者反之。但是在 64 位的系統上,將指針賦值給 int 類型會導致截斷高 32 位的值。
解決方案是將指針作為指針類型或為此而定義的特殊類型進行存儲,例如 intptr_t 和 uintptr_t.
位移
無類型的整數常量就是 (unsigned) int 類型的。這可能會導致在位移時出現被截斷的問題。
例如,在下面的代碼中,a 的最大值可以是 31.這是因為 1 << a 是 int 類型的。
long t = 1 << a;
要在 64 位系統上進行位移,應該使用 1L,如下所示:
long t = 1L << a;
字符串格式化
函數 printf (3) 及其相關函數都可能成為問題的根源。例如,在 32 位系統上,使用 %d 來打印 int 或 long 類型的值都可以,但是在 64 位平台上,這會導致將 long 類型的值截斷成低 32 位的值。對於 long 類型的變量來說,正確的用法是 %ld.
類似地,當一個小整數(char、short、int)被傳遞給 printf (3) 時,它會擴展成 64 位的,符號會適當地進行擴展。在下面的例子中,printf (3) 假設指針是 32 位的。
char *ptr = &something;printf (%x\n", ptr);
上面的代碼在 64 位系統上會失敗,它只會顯示低 4 字節的內容。
這個問題的解決方案是使用 %p,如下所示;這在 32 位和 64 位系統上都可以很好地工作:
char *ptr = &something;printf (%p\n", ptr);
函數參數
在向函數傳遞參數時需要記住幾件事情:
在參數的數據類型是由函數原型定義的情況中,參數應該根據標准規則轉換成這種類型。
在參數類型沒有指定的情況中,參數會被轉換成更大的類型。
在 64 位系統上,整型被轉換成 64 位的整型值,單精度的浮點類型被轉換成雙精度的浮點類型。
如果返回值沒有指定,那麼函數的缺省返回值是 int 類型的。
在將有符號整型和無符號整型的和作為 long 類型傳遞時就會出現問題。考慮下面的情況:
清單 3. 將有符號整型和無符號整型的和作為 long 類型傳遞
long function (long l); int main () { int i = -2; unsigned k = 1U; long n = function (i + k); }
上面這段代碼在 64 位系統上會失敗,因為表達式 (i + k) 是一個無符號的 32 位表達式,在將其轉換成 long 類型時,符號並沒有得到擴展。解決方案是將一個操作數強制轉換成 64 位的類型。
在基於寄存器的系統上還有一個問題:系統采用寄存器而不是堆棧來向函數傳遞參數。考慮下面的例子:
float f = 1.25;printf ("The hex value of %f is %x", f, f);
在基於堆棧的系統中,這會打印對應的 16 進制值。但是在基於寄存器的系統中,這個 16 進制的值會從一個整數寄存器中讀取,而不是從浮點寄存器中讀取。
解決方案是將浮點變量的地址強制轉換成一個指向整型類型的指針,如下所示:
printf ("The hex value of %f is %x", f, *(int *)&f);
結束語
主流的硬件供應商最近都在擴充自己的 64 位產品,這是因為 64 位平台可以提供更好的性能、價值和可伸縮性。32 位系統的限制,特別是 4GB 的虛擬內存上限,已經極大地刺激很多公司開始考慮遷移到 64 位平台上。了解如何將應用程序移植到 64 位體系結構上可以幫助我們編寫可移植性更好且效率更高的代碼。
關於作者
Harsha Adiga 就職於印度的 IBM Software Group,他參與了很多 Linux 和開放源碼社區、工作組的工作。