1. 程式人生 > >用VC進行64位程式設計

用VC進行64位程式設計

本文討論:
64 位版本 Windows 的背景資訊
適當地利用 x64 體系結構
使用 Visual C++ 2005 進行 x64 開發
針對 x64 版本的除錯技術
本文使用以下技術:
Windows、Win64、Visual Studio 2005



本頁內容
使用 Windows® 先鋒產品的樂趣之一是能夠探究新技術以瞭解它的工作方式。實際上,我不太喜歡使用作業系統,直到對其內部結構有了一點深入瞭解之後。因此,當 Windows XP 64 位版本和 Windows Server® 2003 出現時,我簡直快完蛋了。

Win64 和 x64 CPU 體系結構的優點是:它們與其前任完全不同,但不需要很長的學習過程。儘管開發人員認為遷移到 x64 只是一個重新編譯的過程,但事實是我們仍然要在偵錯程式中花費很多時間。擁有 OS 和 CPU 的應用知識十分寶貴。
本文,我將本人在 Win64 和 x64 體系結構方面的經驗歸結為一個高手 Win32® 程式設計師遷移到 x64 必備的幾個要點。我假設您瞭解基本的 Win32 概念、基本的 x86 概念以及為什麼程式碼應該在 Win64 上執行。這使我可以將關注的重點放在更重要的內容上。通過本概述,您可以在已經理解的 Win32 和 x86 體系結構基礎上了解到一些重要差異。

有關 x64 系統的一個優點是:與基於 Itanium 的系統不同,您可以在同一臺計算機上使用 Win32 或 Win64,而不會導致嚴重的效能損失。此外,除了 Intel 和 AMD x64 實現之間的幾個模糊差異,與 x64 相容的同一個 Windows 版本應該能夠在這兩個系統上執行。您不需要在 AMD x64 系統上使用一個 Windows 版本,在 Intel x64 系統上使用另一個版本。
我將討論分為三大領域:OS 實現細節、適當地利用 x64 CPU 體系結構以及使用 Visual C++® 進行 x64 開發。
x64 作業系統
在 Windows 體系結構的所有概述中,我一般喜歡從記憶體和地址空間開始。儘管 64 位處理器在理論上定址 16 EB 的記憶體 (264),但 Win64 目前支援 16 TB(由 44 位表示)。為什麼不能在計算機中載入到 16 EB 以使用全部 64 位呢?原因有很多。

對初級使用者而言,當前的 x64 CPU 通常只允許訪問 40 位(1 TB)的實體記憶體。體系結構(不包括當前硬體)可以將其擴充套件到 52 位(4 PB)。即使沒有該限制,對映如此大記憶體的頁表大小也是巨大的。
與 Win32 中一樣,可定址範圍分為使用者模式區和核心模式區。每個程序都在底部獲得其唯一的 8 TB,而核心模式的程式碼存在於頂部的 8 TB 中,並由所有程序共享。不同版本的 64 位 Windows 具有不同的實體記憶體限制,如圖 1 和圖 2 所示。
同樣,與 Win32 中一樣,x64 頁大小為 4 KB。前 64 KB 的地址空間始終不對映,因此您看到的最低有效地址應該是 0x10000。與在 Win32 中不同,系統 DLL 在使用者模式的地址範圍頂部附近沒有預設的載入地址。相反,它們在 4 GB 記憶體以上載入,通常在 0x7FF00000000 附近的地址上載入。
許多較新的 x64 處理器的一個出色功能是:支援 Windows 用於實現硬體資料執行保護 (DEP) 的 CPU No Execute 位。x86 平臺上存在許多錯誤和病毒,這是因為 CPU 可以將資料當作合法程式碼位元組執行。CPU 在供資料儲存使用的記憶體中執行從而可終止緩衝區溢位(有意或無意)。通過 DEP,OS 可以在有效程式碼區域周圍設定更清晰的邊界,從而使 CPU 在執行超出這些預期邊界時捕獲到該事件。這推動著為使 Windows 減少受到的攻擊而付出的不懈努力。
在為捕獲錯誤而設計的活動中,x64 連結器將可執行檔案預設的載入地址指定為在 32 位 (4 GB) 之上。這可以幫助在程式碼遷移到 Win64 之後能夠在現有程式碼中快速找到這些區域。具體說,如果將指標儲存為一個 32 位大小的值(如 DWORD),那麼在 Win64 版本中執行時,它將被有效地截斷,從而導致指標無效,進而觸發訪問衝突。該技巧使查詢這些令人討厭的指標錯誤變得非常簡單。
有關指標和 DWORD 的主題將在 Win64 型別系統中繼續討論。指標有多大?LONG 怎麼樣?那麼控制代碼(如 HWND)呢?幸好,Microsoft 在進行從 Win16 到 Win32 的複雜轉換時,使新的型別模型能夠輕鬆地進一步擴充套件到 64 位。一般地,除了個別幾種情況外,新的 64 位環境中的所有型別(除了指標和 size_t)均與 Win32 中的完全相同。也就是說,64 位指標是 8 位元組,而 int、long、DWORD 和 HANDLE 仍然是 4 位元組。在隨後討論進行 Win64 開發時,我將討論更多有關型別的內容。
Win64 的檔案格式稱為 PE32+。幾乎從每個角度看,該格式在結構上都與 Win32 PE 檔案完全相同。只是擴充套件了少數幾個欄位(例如,頭結構中的 ImageBase),刪除了一個欄位,並更改了一個欄位以反映不同的 CPU 型別。圖 3 顯示已更改的欄位。
除 PE 頭之外,沒有太多的更改。有幾個結構(例如,IMAGE_LOAD_CONFIG 和 IMAGE_THUNK_DATA)只是將某些欄位擴充套件到 64 位。新增的 PDATA 區段很有趣,因為它突出了 Win32 和 Win64 實現之間的一個主要差異:異常處理。
在 x86 環境中,異常處理是基於堆疊的。如果 Win32 函式包含 try/catch 或 try/finally 程式碼,則編譯器將發出在堆疊上建立小型資料塊的指令。此外,每個 try 資料塊指向先前的 try 資料結構,從而形成了一個連結串列,其中最新新增的結構位於表頭。隨著函式的呼叫和退出,該連結串列頭會不斷更新。如果發生異常,OS 將遍歷堆疊上的資料塊連結串列,以查詢相應的處理程式。我在 1997 年 1 月的 MSJ 文章中非常詳細地描述了該過程,因此這裡只做簡要說明。
與 Win32 異常處理相比,Win64(包括 x64 和 Itanium 版本)使用了基於表的異常處理。它不會在堆疊上生成任何 try 資料塊連結串列。相反,每個 Win64 可執行檔案都包含一個執行時函式表。每個函式表項都包含函式的起始和終結地址,以及一組豐富資料(有關函式中異常處理程式碼)的位置和函式的堆疊幀佈局。請參見 WINNT.H 和 x64 SDK 中的 IMAGE_RUNTIME_FUNCTION_ENTRY 結構,瞭解這些結構的實質。
當異常發生時,OS 會遍歷常規的執行緒堆疊。當堆疊稽核遇到每個幀和儲存的指令指標時,OS 會確定該指令指標屬於哪一個可執行的模組。隨後,OS 會在該模組中搜索執行時函式表,查詢相應的執行時函式項,並根據這些資料制定適當的異常處理決策。
如果您是一位火箭科學家,並直接在記憶體中生成了程式碼而沒有使用基本的 PE32+ 模組,該怎麼辦呢?這種情況也包含在內。Win64 有一個 RtlAddFunctionTable API,它可讓您告訴 OS 有關動態生成的程式碼的資訊。
基於表的異常處理的缺點(相對於基於堆疊的 x86 模型)是:在程式碼地址中查詢函式表項所需的時間比遍歷連結串列的時間要長。但優點是:函式沒有在每次執行時設定 try 資料塊的開銷。
請記住,這只是一個簡要介紹,而不是 x64 異常處理的完整描述,但是很令人激動,不是嗎?有關 x64 異常模型的進一步概述,請參閱 Kevin Frei 的網路日記項
與 x64 相容的 Windows 版本不包含最新 API 的具體數量;大部分新的 Win64 API 都新增到針對 Itanium 處理器的 Windows 版本。簡言之,現有的兩個重要的 API,分別是 IsWow64Process 和 GetNativeSystemInfo。它們允許 Win32 應用程式確定是否在 Win64 上執行,如果是,則可以看到系統的真正功能。否則,呼叫 GetSystemInfo 的 32 位程序只能看到 32 位系統的系統功能。例如,GetSystemInfo 只會報告 32 位程序的地址範圍。圖 4 顯示的 API 以前在 x86 上不可用,但可用於 x64。
儘管執行完全的 64 位 Windows 系統聽起來很不錯,但事實是,在某些情況下,您很可能需要執行 Win32 程式碼。為此,x64 版本的 Windows 包含 WOW64 子系統,以允許 Win32 和 Win64 程序在同一個系統上並行執行。但是,將 32 位 DLL 載入 64 位程序(反之亦然)則不受支援。(相信我,這是件好事。)您終於可以向 16 位舊式程式碼吻別了!
在 x64 版本的 Windows 中,從 64 位可執行檔案啟動的程序(如 Explorer.exe)只能載入 Win64 DLL,而從 32 位可執行檔案啟動的程序只能載入 Win32 DLL。當 Win32 程序呼叫核心模式(例如,讀取檔案)時,WOW64 程式碼會安靜地截斷該呼叫,並在適當的位置呼叫正確的 x64 等效程式碼。
當然,不同系統(32 位與 64 位)的程序需要能夠互相通訊。幸運的是,Win32 中您知道並喜愛的所有常規程序間通訊機制也可以在 Win64 中工作,包括共享記憶體、命名管道以及命名同步物件。
您可能在想,"那麼系統目錄呢?同一個目錄不能同時儲存 32 位和 64 位版本的系統 DLL(例如,KERNEL32 或 USER32),不是嗎"?通過執行可選擇的檔案系統重定向,WOW64 魔法般地為您解決了這個問題。來自 Win32 程序的檔案活動通常轉到 System32 目錄,而不是在名為 SysWow64 的目錄中。在內部,WOW64 會默默地更改這些請求以指向 SysWow64 目錄。Win64 系統實際上有兩個 \Windows\System32 目錄 - 一個用於 x64 二進位制檔案,另一個用於 Win32 等效檔案。
這看上去沒什麼,但會令人混淆。例如,我在某一點上使用了 32 位命令列提示(我自己並不知道)。當我針對 System32 目錄中的 Kernel32.dll 執行 DIR 時,所得到的結果與我在 SysWow64 目錄中執行相同操作後所得到的結果完全相同。我絞盡腦汁後才發現,檔案系統重定向的工作方式就是這樣。也就是說,即使我認為是在 \Windows\System32 目錄中工作,但 WOW64 實際上已將呼叫重定向到 SysWow64 目錄。順便說一下,如果您確實希望從 x64 應用程式訪問 32 位 \Windows\System32 目錄,則 GetSystemWow64Directory API 會提供正確的路徑。請一定閱讀 MSDN® 文件,瞭解完整的資訊。
除了檔案系統重定向之外,WOW64 施加的另一個小魔法是登錄檔重定向。請考慮我前面提到的 Win32 DLL 不能載入 Win64 程序的內容,然後再考慮一下 COM 及其使用登錄檔載入程序內伺服器 DLL 的情況。如果 64 位應用程式要使用 CoCreateInstance 建立一個在 Win32 DLL 中實現的物件,該怎麼辦呢?該 DLL 不能載入,對嗎?WOW64 通過將來自 32 位應用程式的訪問重定向到 \Software\Classes(以及相關的)註冊節點,再一次節省了時間。實際結果是,Win32 應用程式的登錄檔檢視與 x64 應用程式的不同(但大部分是相同的)。如您所料,OS 通過在呼叫 RegOpenKey 及友元時指定新的標記值,為 32 位應用程式提供了一個讀取實際 64 位登錄檔值的應急方法。
更進一步說,後幾個正中我下懷的 OS 差異涉及執行緒的區域性資料。在 x86 版本的 Windows 中,FS 暫存器用於指向每個執行緒的記憶體區域,包括"最後一個錯誤"和執行緒的本地儲存(分別是 GetLastError 和 TlsGetValue)。在 x64 版本的 Windows 中,FS 暫存器由 GS 暫存器取代。另外,它們的工作方式幾乎完全相同。
雖然本文主要從使用者模式角度討論 x64,但有一項重要的核心模式體系結構附加內容需要說明。針對 x64 的 Windows 中有一項稱為 PatchGuard 的新技術,該技術主要針對安全性和健壯性。簡言之,能夠更改關鍵核心資料結構(例如,系統呼叫表和中斷排程表 (IDT))的使用者模式程式或驅動程式會導致安全漏洞和潛在的穩定性問題。對於 x64 體系結構而言,Windows 家族決定不允許以不受支援的方式修改核心記憶體。強制該操作的技術是 PatchGuard。它使用核心模式執行緒監視對關鍵核心記憶體位置的更改。如果該記憶體被更改,則錯誤檢測時系統將停止。
總之,如果您熟悉 Win32 體系結構,並且瞭解如何編寫在它上面執行的本機程式碼,那麼在遷移到 Win64 的過程中您就不會感到很驚奇了。您可以在很大程度上將其視為一個更廣闊的環境。
返回頁首
適當利用 x64
現在,我們看一下 CPU 體系結構本身,因為對 CPU 指令集有一個基本的瞭解可以使開發(特別是除錯)工作更輕鬆。在編譯器生成的 x64 程式碼中,您將注意到的第一件事是,它與您瞭解並喜愛的 x86 程式碼是多麼地相似。這對於瞭解 Intel IA64 編碼的人們則完全不同。
隨後您將注意到的第二件事是,註冊名稱與您所熟悉的略有不同,並且有很多名稱。通用 x64 暫存器的名稱以 R 開頭,如 RAX、RBX 等等。這是針對 32 位 x86 暫存器的基於 E 的舊命名方案的發展演化。就像過去一樣,16 位 AX 暫存器變為 32 位 EAX,16 位 BX 變為 32 位 EBX,以此類推。如果從 32 位版本轉換,所有 E 暫存器都會變為其 64 位形態的 R 暫存器。因此,RAX 是 EAX 的繼承者,RBX 超越 EBX,RSI 取代 ESI,以此類推。
此外,還添加了 8 個新的通用暫存器 (R8-R15)。主要的 64 位通用暫存器清單如圖 5 所示。
此外,32 位 EIP 暫存器也會變為 RIP 暫存器。當然,32 位指令必須繼續執行,以便這些暫存器(EAX、AX、AL、AH 等)的原始、較小型別的版本仍然可用。
為了照顧到圖形和科學程式設計人員,x64 CPU 還有 16 個 128 位 SSE2 暫存器,分別以 XMM0 到 XMM15 命名。由 Windows 儲存的 x64 暫存器的完整集合位於 WINNT.H 中定義的相應 #ifdef'ed _CONTEXT 結構中。
在任何時候,x64 CPU 不是以舊式的 32 位模式操作,就是以 64 位模式操作。在 32 位模式中,CPU 與任何其他 x86 類別的 CPU 一樣對指令進行解碼和操作。在 64 位模式中,CPU 對某些指令編碼進行了少量調整,以支援新的暫存器和指令。
如果您熟悉 CPU 操作碼編碼模型,就會記得為新的指令編碼提供的空間會很快消失,並且在 8 個新暫存器中擠出空間也不是一項輕鬆的任務。為此,一種方法是刪除一些極少使用的指令。到目前為止,我留下的指令只有 64 位版本的 PUSHAD 和 POPAD,它們用於在堆疊上儲存和恢復所有通用暫存器。釋放指令編碼空間的另一種方法是,在 64 位模式中完全消除區段。這樣,CS、DS、ES、SS、FS 和 GS 的生命週期就結束了。沒有太多人會想念它們的。
由於地址是 64 位的,您可能會擔心程式碼大小。例如,下面是一個常見的 32 位指令:
[pre]CALL DWORD PTR [XXXXXXXX][/pre]這裡,用 X 表示的部分是一個 32 位地址。在 64 位模式中,這會變為 64 位地址,從而將 5 位元組的指令變為 9 位元組嗎?幸運的是,答案是"否"。指令大小保持不變。在 64 位模式中,指令的 32 位運算元部分被視為相對於當前指令的資料偏移。一個示例可以更清楚地說明這一點。在 32 位模式中,以下是呼叫地址 00020000h 中儲存的 32 位指標值的指令:
[pre]00401000: CALL DWORD PTR [00020000h][/pre]在 64 位模式中,相同的操作碼位元組呼叫地址 00421000h (4010000h + 20000h) 中儲存的 64 位指標值。這可以使您聯想到,如果是自己生成程式碼,則這種相對定址模式會造成重大分歧。您不能僅在指令中指定 8 位元組的指標值,而是需要為實際 64 位目標地址駐留的記憶體位置指定一個 32 位相對地址。因而,有一個未提出的假設是:64 位目標指標必須在使用它的指令的 2GB 空間中。對大多數人而言,這並不是一個大問題,但如果您要生成動態程式碼或者修改記憶體中的現有程式碼,就會出現問題!
所有 x64 暫存器的一個主要優勢是,編譯器能夠最終生成在暫存器中(而非堆疊上)傳遞大部分引數的程式碼。將引數推入堆疊會引發記憶體訪問。我們都需要牢記,在 CPU 快取中找不到的記憶體訪問會導致 CPU 延遲許多個週期,以等待可用的常規 RAM 記憶體。
在設計呼叫約定時,x64 體系結構利用機會清除了現有 Win32 呼叫約定(如 __stdcall、__cdecl、__fastcall、_thiscall 等)的混亂。在 Win64 中,只有一個本機呼叫約定和 __cdecl 之類的修飾符被編譯器忽略。除此之外,減少呼叫約定行為還為可除錯性帶來了好處。
您需要了解的有關 x64 呼叫約定的主要內容是:它與 x86 fastcall 約定的相似之處。使用 x64 約定,會將前 4 個整數引數(從左至右)傳入指定的 64 位暫存器:
[pre]RCX: 1st integer argumentRDX: 2nd integer argumentR8: 3rd integer argumentR9: 4th integer argument[/pre]前 4 個以外的整數引數將傳遞到堆疊。該指標被視為整數引數,因此始終位於 RCX 暫存器內。對於浮點引數,前 4 個引數將傳入 XMM0 到 XMM3 的暫存器,後續的浮點引數將放置到執行緒堆疊上。
更進一步探究呼叫約定,即使引數可以傳入暫存器,編譯器仍然可以通過消耗 RSP 暫存器在堆疊上為其預留空間。至少,每個函式必須在堆疊上預留 32 個位元組(4 個 64 位值)。該空間允許將傳入函式的暫存器輕鬆地複製到已知的堆疊位置。不要求被呼叫函式將輸入暫存器引數溢位至堆疊,但需要時,堆疊空間預留確保它可以這樣做。當然,如果要傳遞 4 個以上的整數引數,則必須預留相應的額外堆疊空間。
讓我們看一個示例。請考慮一個將兩個整數引數傳遞給子函式的函式。編譯器不僅會將值賦給 RCX 和 RDX,還會從 RSP 堆疊指標暫存器中減去 32 個位元組。在被呼叫函式中,可以在暫存器(RCX 和 RDX)中訪問引數。如果被呼叫程式碼因其他目的而需要暫存器,可將暫存器複製到預留的 32 位元組堆疊區域中。圖 6 顯示在傳遞 6 個整數引數之後的暫存器和堆疊。

圖 6 傳遞整數


x64 系統上的引數堆疊清除比較有趣。從技術上說,呼叫方(而非被呼叫方)負責清除堆疊。但是,您很少看到在起始程式碼和結束程式碼之外的位置調整 RSP。與通過 PUSH 和 POP 指令在堆疊中顯式新增和移除引數的 x86 編譯器不同,x64 程式碼生成器會預留足夠的堆疊空間,以呼叫最大目標函式(引數方法)所使用的任何內容。隨後,在呼叫子函式時,它重複使用相同的堆疊區域來設定引數。
另一方面,RSP 很少更改。這與 x86 程式碼大不相同,在 x86 程式碼中,ESP 值隨著引數在堆疊中的新增和移除而不斷變化。
有一個示例可幫助說明這一點。請考慮一個呼叫三個其他函式的 x64 函式。第一個函式接受 4 個引數(0x20 個位元組),第二個接受 12 個引數(0x60 個位元組),第三個接受 8 個引數(0x40 個位元組)。在起始程式碼中,生成的程式碼只需在堆疊上預留 0x60 個位元組,並將引數值複製到 0x60 位元組中的適當位置,以便目標函式能夠找到它們。
您可以在 Raymond Chen 的網路日記中看到一個有關 x64 呼叫約定更詳細的描述。我不會過多地討論所有細節,僅在這裡強調一些要點。首先,小於 64 位的整數引數進行了符號擴充套件,然後仍然通過相應的暫存器傳遞(如果在前 4 個整數引數內)。其次,任何引數所處的堆疊位置都應該是 8 位元組的倍數,從而保持 64 位對齊。不是 1、2、4 或 8 位元組的任何引數(包括結構)都是通過引用傳遞的。最後,8、16、32 或 64 位的結構和聯合作為相同長度的整數傳遞。
函式的返回值儲存在 RAX 暫存器中。如果返回到 XMM0 中的是浮點型別,就會引發異常。在所有呼叫中,以下暫存器必須保留:RBX、RBP、RDI、RSI、R12、R13、R14 和 R15。以下暫存器不穩定,可能會被毀壞:RAX、RCX、RDX、R8、R9、R10 和 R11。
我在前面提到過,作為異常處理機制的一部分,OS 會遍歷堆疊幀。如果您曾經編寫過堆疊遍歷程式碼,就會知道 Win32 幀佈局的這一特性可巧妙處理該過程。這種情況在 x64 系統上要好得多。如果某個函式需要分配堆疊空間,呼叫其他函式,保留任何暫存器或者使用異常處理,則該函式必須使用一組定義良好的指令來生成標準的起始程式碼和結束程式碼。
實行建立函式堆疊幀的標準方法是 OS 確保(在理論上)能夠始終遍歷堆疊的一種方法。除了一致、標準的起始程式碼,編譯器和連結器還必須建立關聯的函式表資料項。奇怪的是,所有這些函式項都在 IMAGE_FUNCTION_ENTRY64 的陣列表(在 winnt.h 中定義)中結束。如何找到這個表呢?它由 PE 頭的 DataDirectory 欄位中的 IMAGE_DIRECTORY_ENTRY_EXCEPTION 項指出。
我在短短的一段中討論了許多體系結構內容。但是,通過大體瞭解這些概念以及 32 位程式集語言的現有知識,您應該能夠在一段較短的時間內瞭解偵錯程式中的 x64 指令。總是實踐出真知。
返回頁首
使用 Visual C++ 進行 x64 開發
儘管可以使用 Visual Studio® 2005 之前的 Microsoft® C++ 編譯器編寫 x64 程式碼,但這在 IDE 中是一項沉悶的體驗。因此,在本文中,我假定您使用的是 Visual Studio 2005,並選擇在預設安裝中未啟用的 x64 工具。我還假定您在 C++ 中擁有要為 x86 和 x64 平臺構建的現有 Win32 使用者模式專案。
針對 x64 構建的第一步是建立 64 位生成配置。作為一個優秀的 Visual Studio 使用者,您應該已經知道專案在預設情況下有兩種配置:Debug 和 Retail。這裡,您只需建立另外兩個配置:x64 形態下的 Debug 和 Retail。
首先,載入現有專案/解決方案。在 Build 選單上,選擇 Configuration Manager。在 Configuration Manager 對話方塊中,從 Active solution platform 下拉選單中選擇 New(參見圖 7)。現在,您應該看到另一個標題為 New Solution Platform 的對話方塊。

圖 7 建立新的生成配置


選擇 x64 作為您的新平臺(參見圖 8),並將另一個配置保留為預設狀態;然後單擊 OK。就這麼簡單!現在,您應該擁有四個可能的生成配置:Win32 Debug、Win32 Retail、x64 Debug 和 x64 Retail。使用 Configuration Manager,您可以輕鬆地在它們之間切換。
現在,我們看一下您的程式碼與 x64 的相容性。將 x64 Debug 配置設為預設值,然後生成專案。除非程式碼不重要,否則可能會收到一些不會在 Win32 配置中發生的編譯器錯誤。除非您已經完全摒棄了編寫可移植 C++ 程式碼的所有原則,否則修正這些問題以使程式碼能夠隨時用於 Win32 和 x64 相對比較輕鬆,而無需大量的條件編譯程式碼。

圖 8 選擇生成平臺


返回頁首
使程式碼與 Win64 相容
將 Win32 程式碼轉換為 x64,所需的最重要的工作可能是確保型別定義正確。還記得先前討論的 Win64 型別系統嗎?通過使用 Windows typedef 型別而非 C++ 編譯器的本機型別(int、long 等),Windows 頭使得編寫乾淨的 Win32 x64 程式碼很輕鬆。您應該在自己的程式碼中繼續保持這一點。例如,如果 Windows 將一個 HWND 傳遞給您,請不要僅僅為了方便就將其儲存在 FARPROC 中。
升級完許多程式碼之後,我看到的最常見而簡單的錯誤可能就是:假定指標值可以儲存或傳遞到 32 位型別(如 int 和 long)甚至 DWORD 中。Win32 和 Win64 中的指標長度視需要而不同,而整數型別長度保持不變。但是,讓編譯器不允許指標儲存在整數型別中也是不現實的。這是一個根深蒂固的 C++ 習慣。
解救方法是 Windows 頭中定義的 _PTR 型別。DWORD_PTR、INT_PTR 和 LONG_PTR 之類的型別可讓您宣告整數型別的變數,並且這些變數始終足夠長以便在目標平臺上儲存指標。例如,定義為 DWORD_PTR 型別的變數在針對 Win32 編譯時是 32 位整數,在針對 Win64 編譯時是 64 位整數。經過實踐,我已經習慣了宣告型別以詢問"這裡是否需要 DWORD 或者實際是指 DWORD_PTR 嗎?"。
正如您期望的,可能有機會明確指定整數型別需要多少位元組。定義 DWORD_PTR 及其友元的同一標頭檔案 (Basetsd.h) 還可以定義特定長度的整數,如 INT32、INT64、INT16、UINT32 和 DWORD64。
與型別大小差異相關的另一個問題是 printf 和 sprintf 格式化。我對於在過去使用 %X 或 %08X 格式化指標值感到懊悔萬分,並且在 x64 系統上執行該程式碼時還遇到了阻礙。正確的方法是使用 %p,%p 可以在目標平臺上自動考慮指標大小。此外,對於與大小相關的型別,printf 和 sprintf 還具有 I 字首。例如,您可能使用 %Iu 來列印 UINT_PTR 變數。同樣,如果您知道該變數始終是 64 位標記值,則可以使用 %I64d。
在清除了無法用於 Win64 的型別定義所導致的錯誤之後,可能還有隻能在 x86 模式下執行的程式碼。或者,您可能需要編寫函式的兩個版本,一個用於 Win32,另一個用於 x64。這就是一組前處理器巨集的用武之地:
[pre]_M_IX86_M_AMD64_WIN64[/pre]正確使用前處理器巨集對於編寫正確的跨平臺程式碼而言至關重要。_M_IX86 和 _M_AMD64 僅在針對特定處理器編譯時進行定義。_WIN64 在針對任何 64 位版本的 Windows(包括 Itanium 版)編譯時定義。
在使用前處理器巨集時,請仔細考慮您的需要。例如,只需要程式碼真正特定於 x64 處理器,沒有別的需要了嗎?然後,使用與以下類似的程式碼:
[pre]#ifdef _M_AMD64[/pre]另一方面,如果同一程式碼既可以在 x64 又可以在 Itanium 上工作,則使用如下所示的程式碼可能更好:
[pre]#ifdef _WIN64[/pre]我發現一個有用的習慣是:只要使用其中一個巨集,就始終顯式建立 #else 情況,以便提前知道是否忘記了某些情況。請考慮以下編寫錯誤的程式碼:
[pre]#ifdef _M_AMD64// My x64 code here#else// My x86 code here#endif[/pre]如果現在針對第三個 CPU 體系結構編譯該程式碼,會發生什麼情況?系統將無意識地編譯我的 x86 程式碼。上面程式碼的一個更好的表達方式如下:
[pre]#ifdef _M_AMD64// My x64 code here#elif defined (_M_IX86)// My x86 code here#else#error !!! Need to write code for this architecture#endif[/pre]在我的 Win32 程式碼中無法輕鬆移植到 x64 的一部分程式碼是內聯彙編,Visual C++ 不支援它的 x64 目標。不要害怕,彙編有辦法。它提供了一個 64 位 MASM (ML64.exe),這在 MSDN 中有所說明。ML64.exe 和其他 x64 工具(包括 CL.EXE 和 LINK.EXE)可以從命令列呼叫。您可以只執行 VCVARS64.BAT 檔案,該檔案可以將它們新增到您的路徑中。
返回頁首
除錯
最後,您需要在 Win32 和 x64 版本上乾淨地編譯程式碼。最後一個難題是執行和除錯程式碼。無論是否在 x64 盒上生成 x64 版本,您都需要使用 Visual Studio 遠端除錯功能在 x64 模式下進行除錯。幸運的是,如果您在 64 位計算機上執行 Visual Studio IDE,則 IDE 將為您執行以下所有步驟。如果您出於某些原因無法使用遠端除錯,則另一個選項是使用 x64 版本的 WinDbg。但是,您會失去 Visual Studio 偵錯程式提供的許多除錯優勢。
如果您從未使用過遠端除錯,也不需要過於擔心。一旦設定好,遠端除錯就可以像在本地一樣無縫使用。
第一步是在目標計算機上安裝 64 位 MSVSMON。這通常是通過執行 Visual Studio 隨附的 RdbgSetup 程式來完成的。一旦 MSVSMON 執行,請使用 Tools 選單為 32 位 Visual Studio 和 MSVSMON 例項之間的連線配置適當的安全設定(或者缺失)。
接下來,您需要在 Visual Studio 中將專案配置為針對 x64 程式碼使用遠端除錯,而不是嘗試進行本地除錯。您可以從除錯專案的屬性開始啟動這個過程(參見圖 9)。

圖 9 除錯屬性


確定 64 位配置是當前配置,然後選擇 Configuration Properties 下面的 Debugging。靠近頂端是標題為 Debugger to launch 的下拉選單。通常,它設定為 Local Windows Debugger。將其更改為 Remote Windows Debugger。在下面,您可以指定在啟動除錯時要執