天天看點

32位平台代碼向64位平台移植

1背景描述

    從蘋果A7處理器開始,就支援着兩種不同的指令集:第一種為原有處理器所支援的32-bit ARM指令集,第二種為嶄新的64-bit ARM體系結構。這種64-bit體系結構擁有更大的位址空間,最大支援16GB記憶體,同時它一次性可提取64位資料,比32-bit體系提高了一倍。現如今,蘋果的LLVM編譯器已經能夠充分支援64-bit指令集。

    正如蘋果A7處理器一樣,支援64-bit指令集的處理器已經很普遍了,如AMD公司的AMD-64、Intel公司的EM64T及IA-64。處理器屬于硬體,那麼硬體的更新必然引起軟體的更新。微軟的Windows 7就提供了64位版本的作業系統以支援64位處理器,類Unix作業系統方面也有相應的64位版本,如Ubuntu 12.04。除了作業系統外,我們在作業系統中運作的軟體也需要更新,以充分利用64位處理給我們帶來的便利。

    接下來,我們主要研究一些C類語言代碼從32位平台移植到64位平台的案例,分析其中存在的問題,再給出解決方案。

2分析及實作

    不同平台之間的代碼移植通常需要滿足以下幾點:首先,相容原來的平台;其次,能在新的平台上運作良好;再次,不同平台之間的程式能夠正常互動。

我們現在的任務是要實作32位平台的代碼向64位平台移植,上述要求也是移植後需要達到的性能目标。我們移植的步驟如下:

編譯生成64位代碼;

消除編譯錯誤與警告;

性能目标測試。

    32位平台使用的是ILP32資料模型,而64位平台使用的是LP64資料模型。由于32位平台上的C資料類型int、long及指針的長度為32位,是以稱為ILP32資料模型;而64位平台上的C資料類型long及指針的長度是64位,故稱為LP64資料模型,64位的Windows上稱為LLP64(long long、pointer)資料模型。

    現如今,所有64位的Unix-like平台均使用LP64資料模型,64位Windows使用的是LLP64資料模型。ILP32與LP64(或LLP64)之間的差異是實作代碼移植需要關注的焦點之一。

表1-1  ILP32、LP64(或LLP64)資料模型的類型長度與對齊差異

類型

ILP32長度(byte)

ILP32對齊(byte)

LP64長度(byte)

LP64對齊(byte)

char

1

short

2

int

4

long

8

long long

pinter

    從上表可以看出,從32平台到64平台主要變化在于long、long long及指針這三種類型。它們可能帶來的影響集中在類型截斷、格式化輸出及類型對齊這幾方面。

    在一些情況中,32位和64位程式在源代碼級别的接口上很難區分。不少頭檔案中,都是通過一些測試宏來區分它們,不幸的是,這些特定的宏依賴于特定的平台、特定的編譯器或特定的編譯器版本。舉例來說,GCC 3.4或之後的版本都定義了__LP64__,以便為所有的64位平台通過選項-m64編譯産生64位代碼。然而,GCC 3.4之前的版本卻是特定于平台和作業系統的。 也許你的編譯器使用了不同于__LP64__的宏,例如IBM XL的編譯器當用-q64編譯程式時,使用了__64bit__宏,而另一些平台使用_LP64,具體情況可用__WORDSIZE來測試一下。請檢視相關編譯器文檔,以便找出最适合的宏。例2.1可适用于多種平台和編譯器:

#if defined (__LP64__) || defined (__64BIT__) || defined (_LP64) || (__WORDSIZE == 64)

printf("I am LP64\n");

#else

printf("I am ILP32 \n");

#endif

上例使用的測試宏并不适用于Windows平台,Windows上的64位測試宏使用的是_WIN64,見例2.2。

#ifdef _WIN64

printf("I am LLP64\n");

    此外,打開一些編譯警告,有助于我們發現移植過程中的潛在問題。以gcc編譯器為例,-Wall可以發現大部分的警告資訊;-Wconversion可以發現不同尺寸的資料類型之間的轉換警告資訊,這些資訊對我們的移植過程是很有幫助的。

    最後,消除編譯時期産生的警告也有助于我們檢視編譯資訊,尤其是當我們修改了代碼以後産生的新的編譯資訊。

    大端位元組序:高位位元組存放在記憶體的低位址段,低位位元組存放在記憶體的高位址端;

    小端位元組序:低位位元組存放在記憶體的低位址段,高位位元組存放在記憶體的高位址端;

    網絡位元組序:TCP/IP各層協定将位元組序定義為Big-Endian,是以TCP/IP協定中使用的位元組序通常稱之為網絡位元組序。

    以 int = 0x12345678為例,則Big-Endian與Little-Endian在記憶體中的存儲方式可表示為:

    位元組号:             3               2            1              0

    大端位元組序:       0x78         0x56       0x34         0x12

    小端位元組序:       0x12         0x34       0x56         0x78

    由此可知,小端位元組序是符合我們通常二進制運算習慣的(從右往左位址遞增),而大端位元組序剛好相反(從右往左位址遞減)。

    因64位平台的差異,在移植32位程式時,可能會失敗,原因可歸咎于機器上位元組序的不同。Intel、IBM PC等CISC晶片使用的是小端位元組序,而Apple之類的RISC晶片使用的是Big-endian;小端位元組序通常會隐藏移植過程中的截斷bug。

int main(void)

{

 long k = 2;

   int *ptr = (int*)&k;

 printf("k has the value %lx, value pointed to by ptr is %x\n", k, *ptr);

 return 0;

}

    一個聲明指向int的指針,卻不經意間指向了long。在ILP32模型中,這段代碼列印出2,因為int與long長度一樣。但在LP64模型中,由于int與long的長度不一樣,導緻指針被截斷。在小端位元組序的系統中,代碼依舊會給出k的正确答案2,但在大端位元組序系統中,k的值卻是0。

    看到上面的例子,可能會給我們帶來很大的疑惑:在大端位元組序機器上的LP64資料模型中,若将long型變量(值小于231-1)賦給int型變量,結果會不會是0呢?我們可以将上述代碼改成下面這樣:

   int a = (int)k;

 printf(“k has the value %ld, value pointed to by a is %d\n”, k, a);

    幸運的是,a的值是2不是0。為什麼說是幸運?為什麼a的值不向前一個例子那樣為0?這就涉及到算術運算與記憶體操作的問題。

    算術運算是不改變被運算資料的位元組序的,此時我們不用關心所操作的資料到底是什麼位元組序的。但記憶體操作就需要注意了,若我們将一個大類型指針強制轉換為一個小類型的指針(如例1.1中的long*轉換為int*),這時候我們就必須關注位元組序問題。不然寫的代碼要麼是錯誤的,要麼一到其他機器上就不能正常運作。

    常見的算術運算有:+、-、*、/、%、&、|、~、<<、>>、=等,請注意位運算符(&、|、~、<<及>>等)是屬于算術運算的範疇而不是記憶體操作,是以他們進行混合運算時不用關心位元組序問題。指派運算符隻有在資料類型相容的時候才不涉及位元組序問題,才屬于算術運算範疇。

    常見的記憶體操作有:強制轉換後對碎片資料的算術運算、記憶體讀寫及檔案讀寫等。

3解決結果

    準備工作已就緒,接下來可以開始移植了。移植過程的主要内容就是消除編譯錯誤與告警資訊,真正需要修改的地方其實較少。這些問題中,又以截斷問題最為突出。

    截斷問題的常見情況是把類型相容的長類型值經過算術運算後賦給短類型值,我把它稱為類型相容的截斷,這類截斷在編譯時已警告形式給出,如果結果值沒有超出結果類型所能表達的範圍,那麼這類截斷是安全的,我們可以通過在指派前将其強制轉化為結果類型以消除警告(見例3.1)。

    char str[] = “Hello World”;

    unsigned int a = strlen(str) ;             // 在LP64模型中截斷告警

    unsigned int a1 = (unsigned int)strlen(str) ;  // 消除警告(不推薦)

    size_t a2 = strlen(str);                   // 消除警告(推薦)

    int nRet = recv(hSock, &str, sizeof(str), 0);   // 在LP64模型中截斷警告

    int nRet1 = (int)recv(hSock, &str, sizeof(str), 0); // 消除浸膏(不推薦)

    ssize_t nRet2 = recv(hSock, &str, sizeof(str), 0); // 消除警告(推薦)

    例3.1中strlen的傳回值是size_t型,其在ILP32模型中是unsigned int型,但在LP64(或LLP64)模型中是unsigned long(或unsigned long long)型。由于我們知道str的長度不會超過unsigned int型所能表達的最大值232-1,是以可以直接在指派前現将strlen的傳回值強制轉化為結果類型unsigned int以消除警告,這種消除警告的方式在我們不知道結果值的長度時(如STL中的size()函數)就不推薦了。

    例3.1中還出現了一種ssize_t型轉化為int型的截斷,這類截斷通常發生在LP64模型中的fwrite/fread,recv/send,recvfrom/sendto之類的操作中出現。ssize_t意為signed size_t,其在ILP32模型中是int型,在LP64模型中為long型。

    另一類截斷是将指針類型賦給int型變量引起的截斷,可把它稱為類型不相容的轉換截斷(見例3.2)。

    char  str[] = “Hello world”;

    int para1 = (int)str;           // 錯誤,在LP64(或LLP64)模型中編譯報錯

    intptr_t para2 = (intptr_t)str; // 正确

    由于str為指針類型,強制轉換操作屬于類型不相容的,是以需要考慮位元組序問題。在代碼中我們應該避免将指針類型轉化為int型變量,因為在ILP32模型中,指針占4個位元組與int型長度相同;但在LP64模型中指針占8位元組,我們把8位元組的指針位址轉化為4位元組的整形值是類型不相容的,幸好編譯器能夠發現這一錯誤。

    是以,在擷取指針位址值的時候我們應當使用多态類型intptr_t或uintptr_t,它們兩個的尺寸分别與ssize_t與size_t相同,隻是名稱賦給了它們特殊的意義。值得注意的是,intptr_t與uintptr_t不是指針類型,不能按字面意思去了解,它們通常的作用就是擷取指針的位址。

    // 參數類型與原型不符的截斷

    long a,b ;

    scanf(“%d”, &a);  // 編譯告警

    scanf(“%ld”, &b);  //正确

    // 傳回值與原型不符的截斷

    char str[128] = {0};

    int nRet = recv(hSock, &str, sizeof(str), 0);

    上述代碼中scanf函數試圖在變量a中插入一個32位的值,剩下的32位就不管了,這在LP64模型中編譯會告警。如果格式化字元串與實際類型不比對的話,類似的告警資訊也會出現在printf()等格式化輸入輸出函數中。在格式化輸出多态類型(如size_t等)時,我們需要針對不同資料模型進行不同的格式化輸出(見例3.4)。

缺少原型的截斷這類情況主要發生在函數調用時,傳遞的參數或傳回值類型與原型不符所緻。

    所謂符号擴充是指,短資料類型的符号位填充到長資料類型的高位元組位(比短資料類型多出的那部分),以保證擴充後的數值大小不變。

    零擴充是指,用零來填充長資料類型的高位元組位。

    當将短資料類型值賦給長資料類型變量時,C系列的語言使用一些符号擴充規則來決定目标是否有符号位。擴充規則如下:

相同尺寸的有符号與無符号值相加為無符号值;

無符号值轉化為長類型後是零擴充的;

符号值轉化為長類型後是符号擴充的,即使結果值是無符号類型的;

常量(除非有字尾修飾,如0x8L)作為能容納其值的最小尺寸類型對待。以16進制書寫的值将被編譯為signed或unsigned的int、long與long long類型,而十進制數字常被編譯為有符号類型。

當資料類型轉換時,同時需要在不同資料大小,以及無符号和有符号之間轉換時,C語言标準要求先進行資料大小的轉換,之後再進行無符号和有符号之間的轉換。

char,short,枚舉類及位域等短類型的提升結果都是int型;

short a = 2;

short b = a – 1; // 編譯告警:int 型轉short會損失精度

int a = -2;

unsigned int b = 1;

long c = a + b;  // 警告,無符号擴充

long long d = c;

printf(“%lld\n”, d);

    這段代碼在32位平台上的輸出結果為-1(0xffffffff),而在64位平台輸出的結果将是4294967295(0x00000000ffffffff)。為什麼會這樣呢?首先,a+b得到的結果是無符号擴充的(規則1);随後,無符号結果提升為long型,是無符号擴充的(規則2),是以c是無符号擴充的。針對此類問題,我們的解決方法是,在進行指派運算前将b強制轉化為符号擴充的long型(即 long c = a + (long)b;)。

unsigned short a = 1;

unsigned long b = (a << 31);

unsigned long long c = b;

printf(“%llx\n”, c);

    這段代碼在32位平台上運作的結果是0x80000000,而在64位平台上的結果将是0xffffffff80000000。這一過程是這樣的:首先,根據規則5,a<<31運算值範圍在int型以内,是以結果為符号擴充的;随後,符号值結果值提升為類型較長的unsigned long型,根據規則3,結果依然是符号擴充的,即b是有符号值,同理,c也是符号擴充的。

    由于ILP32與LP64模型中類型對齊不一緻,是以若果在聯合體中出現long型資料,在代碼移植時就會存在問題。

union{

       unsigned long bytes;

       unsigned short len[2];

}size;

    由于long型資料在64位平台上占用8個位元組,而len數組占用的空間為4個位元組,這會引起錯誤。要修正這一問題,隻需把bytes的類型改為unsigned int。

    類似的對齊問題也會出現在結構體中,見例3.8。

struct {

        int foo0;

        int foo1;

        int foo2;

       long long bar;

};

    當例2.8中的代碼在32位平台中編譯時,成員bar的起始位址在結構體首位址偏移12位元組處;而在64位平台上編譯時,偏移值變成了16。這是由于long long型變量在32位于64位平台上的對齊方式不一樣所緻(分别為4位元組與8位元組)。

    要修正這一問題,我們可以将結構體的對齊方式通過#pragma pack(4)強制指定為按4位元組對齊,這隻有在需要的情況下才應該這樣做。

    共享資料主要是指程序間的通信資料,如套接字通信。這其中的主要問題就是位元組序問題及資料對齊問題。

    程序間通信資料應當避免使用long型,以免造成32位平台與64位平台通信時産生不正确的結果。

    在套接字通訊時,發送整形資料前需要調用htonl()或htons()函數将主機序轉化為網絡序,接收整形資料時需要調用ntohl()或ntohs()函數将網絡序轉化為主機序。

    檔案操作問題類似于共享資料問題,向檔案中讀寫的内容中不應該包含long型,如果非得這麼做的話,可以通過将32位平台與64位平台的資料分别存放在不同的檔案中。

4總結

    通過前面的分析,并結合自身實踐,我們可以總結出以下一些有助于代碼移的經驗規則。

1.不要将指針轉化為整形;

2.避免使用long型;

3.使用相同的符号類型(signed或unsigned)來進行邏輯運算;

4.使用固定的尺寸及對齊方式來建立資料結構;

5.使用sizeof來計算變量或類型的長度;

6.更新格式化字元串的輸入輸出;

7.使用函數原型;

8.確定代碼在32位平台運作良好。

繼續閱讀