將 Linux 應用程式移植到 64 位系統上

工程師WWW發表於2013-12-07

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 位具有以下優點:

  • 64 位的應用程式可以直接訪問 4EB 的虛擬記憶體,Intel Itanium 處理器提供了連續的線性地址空間。
  • 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 <stdio.h>
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_tuint32_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 位體系結構上可以幫助我們編寫可移植性更好且效率更高的程式碼。


參考資料

學習

獲得產品和技術

  • 在您的下一個 Linux 開發專案中採用 IBM 試用版軟體,這可以從 developerWorks 上直接下載。 

討論

相關文章