32位程序移植到64位平台前的准備工作


我們最近完成的一個項目是移植一個大型的32位應用程序,它可在64位環境中支持11個操作系統平台,並且程序的源代碼超過了30萬行。由於此32位程序是在幾年前分成幾部分開發而成,所以極有可能代碼是由不同的開發者編寫。鑒於此,我們有理由懷疑,在64位移植中導致問題的類型不匹配,很有可能是在這幾年中隨着程序模塊的添加與刪除而引入的。

  我們移植此32位程序到64位平台,是為了利用64位技術的先進之處--支持更大的文件、支持更大的內存、及64位計算,大體使用的方法是一個反復迭代的過程,不斷地在一些細節問題上來來回回,如字節序、調整編譯器選項等等,並時不時停下來查看是否達到了總體目標--遵從ANSI標准及源代碼將來的可移植性。第一步,我們研究了64位的系統資源,以充分了解11個操作系統平台上每一個的編譯器選項、內存模型和編碼方面的考慮。作為全部工作的起點,我們在其中一個平台上打開了所有編譯器警告,進行第一次構建,並仔細檢查構建日志信息。通過這些最初的構建、使用本地調試器、及之后使用如Parasoft's Insure++ (http://www.parasoft.com/)這樣的工具,我們確定了一個開發藍圖,接下來,編制了一個清晰徹底的源代碼目錄清單,並在每次配置構建之后進行相應的檢查。

  經過最初的代碼修改、調試、查閱構建日志,已經有足夠的信息對現實中可能碰到的事件進行排序以確定優先次序。在一個擁有所有基本功能的程序成功地通過我們的自動測試案例之后,移植工作總算到了一個轉折點;此測試除了測試64位功能,也包含了向后兼容性測試。如果你所移植的項目中有幾個不同的64位平台,很可能要在其上一一測試,一旦程序可在第一個平台上正確運行,接下來就要測試下一個平台,如此這般下去。然而,我們卻發現了一個非常好的方法,可在同一時間,在所有的平台上進行工作,這是因為: 

  ·每一個編譯器都在它的警告信息中都提供了不同的信息,仔細查看幾個編譯器產生的錯誤,可有助於我們定位問題區域。

  ·不同平台上,錯誤也不同。同一個問題,在另一個平台上看上去無任何跡象,很可能會在當前平台上導致程序崩潰。

  在此項目中,最后需要考慮的一點是為最終發布的測試階段作提前計划。因為最近修改的代碼會被32位及64位的多平台共享,所以在每一個32位平台上,必須重新進行徹底地測試,這要花上雙倍的測試時間和資源。

   跨平台問題

  期間還有許多的問題,在把32位程序移植到多個64位操作系統平台時,這些問題涵蓋了從編譯器警告到讀寫二進制數據等等。幸運的是,編譯器能幫助我們確定大多數64位移植問題,可在所有平台上把編譯器警告級別設為最嚴,要更多地關注那些指示出數據截斷及把64位數據賦給32位數據的警告。不管怎樣,把編譯器警告設為更嚴的級別,將會導致多得數不清的警告信息,當然其中大部分能被編譯器自身自動解決;此處最主要的問題是,很可能最重要的警告信息被大量的次要信息淹沒了,沒辦法做出區分。為了解決這個問題,我們在不同平台上同時進行構建,因為不同的編譯器能給出不同詳細級別的警告信息,這將幫助我們做出區分,以過濾掉無用的信息,找出真正需要修正的問題所在。

  某些應用程序需要訪問那些64位與32位共享的二進制數據或文件,在這種情況下,必須仔細檢查long與指針的二進制格式,可能需要修改相關的讀寫函數以轉換不同的大小,並在多平台間處理大字節序與小字節序問題。為得到正確的機器字節序,在64位程序中更大的數據尺寸需要更多的字節交換。

  例如,一個32位的long:

Big Endian(大字節序)= (B0, B1, B2, B3)

  轉換為:

Little Endian(小字節序)= (B3, B2, B1, B0)

  而一個64位的long:

Big Endian(大字節序)= (B0, B1, B2, B3, B4, B5, B6, B7)

  轉換為:

Little Endian(小字節序)= (B7, B6, B5, B4, B3, B2, B1, B0)

  大多數的編譯器能發現類型的不匹配,並能在構建期間糾正它們,這對如傳遞給函數的參數這樣大多數的簡單賦值來說是正確的。而真正的問題在於,對int、long、指針之間的不匹配,編譯器在編譯期間卻視而不見,或者說編譯器在編譯期間所做的假設,導致了這種不匹配問題依然存在。前者涉及指針參數及函數指針,而后者主要是有關於函數原型。

  傳遞一個int和long指針作為函數參數時,如果這個指針之后被解引用為一個不同的、不兼容的類型,也會導致問題。這種情況不是32位代碼的問題,而是因為ing與long是不可互換的。但是,在64位代碼中,卻因為指針與生具有的可伸縮性,這種情況將導致運行時錯誤,大多數編譯器假定你正在做的事情,就是你想要做的事情,所以會悄無聲息地對此問題放行,除非打開更多的警告信息。而通常只有在程序運行時,此問題才會浮出水面。 

  舉例來說,例1可同時在Solaris和AIX(Frote7、VAC 6)的32位與64位模式中通過編譯,而沒有任何警告,然而,64位的版本運行時卻輸出不正確的值。在如此短的示例中,這種問題很容易被發現,如果在更大型的代碼中呢,恐怕會難多了吧。這種類型的問題可隱藏在現實中實際的代碼里,而大多數的編譯器卻不能發現它們。

  例1:

#include <stdlib.h>
#include <stdio.h>

int Func1(char *);

int main()
{
 long arg, ret;
 arg = 247;
 ret = Func1((char *)&arg);
 printf("%ldn", ret);
 return(0);
}
int Func1(char * input)
{
 int *tmp;
 tmp = (int *)input;
 return(*tmp);
}

  當作為64位可執行程序運行於小字節序機器上時,例1顯得一切正常,因為arg的值完全包含在long的四個最少有意義的字節中。然而,甚至在小字節序的x86機器上,當arg的值超出了四個最少有意義的字節時,64位的版本也會在運行期間產生一個錯誤。

  正是因為函數指針,編譯器無法獲取信息,以確定哪一個函數將會調用,因此它不能糾正或警告你有關可能存在的類型不匹配問題,所有通過特定函數指針調用的函數參數與返回類型都在此范圍之內。如果想從根本上糾正此問題,你必須提供一種分離的方案,在函數調用時對參數及返回值進行適當的類型轉換。 

  第二個問題涉及隱式函數聲明,如果沒有對代碼中調用的每一個函數提供一個原型,編譯器就會做出假設。編譯器產生的類似警告信息"Implicit function declaration: assuming extern returning int"在32位構建時通常是無關緊要的;但在64位構建時,這種返回值是int的假設,當函數實際上是返回long或指針(例如malloc)時,就會導致真正的問題了。為了不讓編譯器做出這種假設,必須確保所有需要的系統頭文件都包含或提供了外部函數的函數原型。

隱藏的問題

  當然,也有一些問題不會在一開始就很容易地被發現,例如,在64位程序中,long與指針尺寸更大了,隨之也會帶來包含它們的結構大小上的增長。結構元素的排列方式決定了結構將占用多大的空間,舉例來說,一個包含了int其后跟着一個long的結構,在32位程序中占用8字節,但64位程序在結構的第一個元素上加入了4個字節的填充數據,以使第二個元素在邊界上排列得更自然,見圖1:


圖1:32位與64位結構中數據的排列方式

  為最小化填充數據所帶來的影響,可把結構中的數據元素按從大到小重新排列。但是,如果數據元素是通過字節流訪問的,還必須調整代碼中的邏輯部分,以適應結構中的新的數據排列方式。

  在那些重新排列結構數據不起作用,或數據元素是通過字節流訪問的情況中,就要小心計算填充數據,我們對此的解決方案是,實現一個幫助函數,在把數據寫到字節流之前,從結構中消去多余的填充數據;而由此帶來的另一個好處是,在讀取數據時就不需要作任何修改了,參見例2:

  例2:

typdef struct demo{
 int i;
 long j;
} DEMO;

DEMO test;
/*pout_raw輸出原始數據到一個文件中*/
/*輸出結構的每一個元素並避免了填充數據*/ 
pout_raw ((int) file_unit, (char *) test.i, sizeof (test.i)); 
pout_raw ((int) file_unit, (char *) test.j, sizeof (test.j));

/*下行包含了填充數據*/ 
pout_raw ((int) file_unit, (char *) test,sizeof(test));

   數組問題

  64位上的long型數組或結構中的數組,不只是比它們32位的對等體包含更大的數值,而且可包含更多的元素。回頭看一下前面用來定義數組邊界和分配數組大小的4字節變量,它們可被轉換成long。如果為了讓64程序獲得更好的性能,需決定現存的long數組是否應轉換成int類型,請參考http://developers.sun.com/prodtech/cc/articles/ILP32toLP64Issues.html。


編碼慣例與移植考慮

  除了遵循操作系統編譯器文檔所推薦的標准64位編碼慣例之外,以下還有一些意見及小提示,也許在計划向64位移植時,可幫得上忙:

  ·如果可能且現實,把源代碼轉換為ANSI C/C++。這將簡化64位移植過程,甚至將來的移植也會受益。

  ·你的目標操作系統同時支持32位與64位應用程序嗎?應及早知道答案,因為它會對是否移植產生影響。例如:在Solaris上,使用系統命令isainfo檢查32位與64位程序的兼容性。

% isainfo -v
64-bit sparcv9 applications
32-bit sparc applications

  ·如果你的源代碼已經被如CVS之類的版本控制系統管理,在移植之前,將會有助於實現第一個程序。由於為了移植,我們常常需要對全局代碼作大量的改動,而有時需要回返到前一個版本的代碼,版本控制系統正好是這方面的能手。

  ·你的應用程序使用或加載32位的第三方庫嗎?如果有,在計划期間就最好決定,是否這些庫也應該升級為64位。如果long與指針不能在你的主程序與第三方庫之間傳遞,那么只要操作系統可以同時運行32位與64位程序,就沒有必要移植為64位了;如果操作系統不支持,那么就要計划把第三方程序也移植為64位了。

  ·如果你的程序在運行時動態地加載庫,並仍然使用對load()的老式調用,那么應使用dlopen()來糾正在主程序與第三方模塊之間的數據傳遞問題,尤其是在dlopen()出現之前的那些老式AIX程序更應如此。為在AIX上打開運行時鏈接,鏈接器中必須加上 -brtl選項,並帶上 -L ":"以指定庫的位置。為提高兼容性,你的主程序和所有通過dlopen()加載的庫,都必須使用運行時鏈接重新編譯。

  ·考慮向后兼容性。當移植到64位平台時,向后兼容性問題變得尤為重要,必須對現有測試方案進行改進,以包含老式32位測試和新式64位測試。

   工具

  對大型的源代碼編制一個詳細目錄清單,特別是共享於跨32位與64位平台的,而且要對每一處修改進行評估,不管它是多么微不足道,都是一項令人畏懼的任務,因為忽略轉換問題的潛在性及引入新錯誤的概率都非常高。可是,使用一些64位工具及技巧,許多的這種潛在性問題能在預編譯階段、編譯階段、和運行時捕捉到。下面是一些可用的工具:

  ·預編譯階段。在編譯器中使用 -errchk=longptr64選項,可啟用lint,對於捕捉類型轉換的不匹配、隱式函數聲明、及參數不匹配等非常有效,例3演示了典型的lint警告,當然也有其他lint類型的程序,如FlexLint(http://www.gimpel.com/html/products.htm)。

  ·編譯時技巧。調整編譯器警告級別,以保證至少在項目的初始階段,沒有抑制任何警告信息。對多平台環境而言,在不同的操作系統上,編譯同一源代碼,將會產生不同的問題,要充分利用這一點,對比分析出問題所在。

  ·編譯時和運行時工具。先進的工具,如 Insure++ 或 Purify for 64-bit,都針對至少一個平台,在任何針對運行時和編譯時的開發環境中,都派得上用場。

  ·運行時工具。試一下dbx,其由每一個Unix編譯器提供;或者ddd(數據顯示調試器),它是Unix上的dbx和gdb的圖形接口(http://www.gnu.org/software/ddd/)。

  例3:典型lint警告

warning: implicit function declaration: main
warning: argument does not match remembered type: arg #1
warning: passing 64-bit integer arg, expecting 32-bit integer: 
MyProc(arg 7)
warning: assignment of 64-bit integer to 32-bit integer
warning: function argument ( number ) used inconsistently
warning: comparing 32-bit integer with 64-bit integer

   結論

  花一些時間來做前期的計划和調查,雖說不一定能達到事半功倍的效果,但從獲得的結果來看還是值得的。而當程序中沒有任何一處工作正常時,也不要氣餒,此時,應仔細並系統地檢查代碼並找出問題所在。隨着內存及數據數量的逐年高速增長,我們有理由相信,跟64位程序所帶來的好處相比,那些移植轉換時的艱難過程根本算不了什么。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com