x64 簡介

寂靜的羽夏發表於2022-03-31

  本篇原文為 introduction to x64 assembly ,如果有良好的英文基礎,可以點選該連結進行下載閱讀。本文為我個人:寂靜的羽夏(wingsummer) 中文翻譯,非機翻,著作權歸原作者所有。
  本篇不算太長,是來自Intel的官方下載的介紹性文件,如有翻譯不得當的地方,歡迎批評指正。翻譯不易,如有閒錢,歡迎支援。注意在轉載文章時注意保留原文的作者連結,我(譯者)的相關資訊。話不多說,正文開始:

簡介

  很多年了,PC端程式設計師使用x86彙編來編寫高效能的程式碼。但是32位的PC已經正在被64位的替代,並且底層的彙編程式碼也已經改變了。這個是對x64彙編難得可貴的介紹。閱讀該篇文章不需要x86彙編前置知識,但如果你會它會讓你更快更容易的學會x64彙編。
  x64是英特爾和AMD的32位x86指令集體系結構ISA的64位擴充套件的通用名稱。AMD推出了x64的第一個版本,最初名為x86-64,後來改名為AMD64。英特爾將其實現命名為IA-32e,之後命名為EMT64。兩個版本之間有一些輕微的不相容,但大多數程式碼在兩個版本上都可以正常工作;有關詳細資訊,請參閱《Intel®64 and IA-32 Architectures Software Developer's Manuals》和《AMD64 Architecture Tech Docs》。我們統稱之為x64。請不要把IA-6464位Intel® Itanium®體系結構相混淆。
  本篇介紹不會涉及硬體的相關細節,比如快取、分支預測和其他高階話題。有一些參考將會在本文章末尾處給出來幫助大家以後深入這些領域。
  彙編一般用於白編寫應用程式對效能極其苛刻要求的部分,儘管對於大多數開發者來說做到比C++編譯器更好是非常困難的。彙編知識對於除錯程式碼來說十分有用——有時編譯器會生成錯誤的彙編程式碼或者對在偵錯程式中單步除錯程式碼確認錯誤原因有更好的幫助。程式碼優化者們有時候會犯錯。當你沒有原始碼的時候,彙編就可以派上用場,提供修復程式碼的介面。彙編可以讓你改變修改當前已經存在的可執行檔案。如果你想知道你所用的程式語言在底層的實現,彙編是必需品。學會它你就可以知道為什麼有時候它執行的慢或者為什麼其他執行的快。最後一點,彙編程式碼知識在逆向分析惡意程式是不可或缺的。

架構

  當在一個現有使用的平臺學習彙編的時候,首先要學習暫存器組。

大體架構

  目前64位的暫存器允許訪問各種大小和位置,我們定義一個位元組8個位,一個字16個位,一個雙字32個位,一個四字64位,一個雙四字為128位。Intel使用小端儲存,意味著低地址存低位元組。

x64 簡介

  圖一展示了16個64位通用暫存器,第一組8個暫存器被命名(因為歷史原因)為RAXRBXRCXRDXRBPRSIRDIRSP。第二組8個暫存器用R8 - R15命名。將字母E替換用開頭的字母R,能夠訪問低32位(RAXEAX)。類似的RAXRBXRCXRDX可以通過去掉首字母R來訪問低16個位元組(RAXAX),通過再把X替換成L可以訪問低16位(AXAL)或通過再把X快取H訪問較高的16位(AXAH)。R8R15也可以用相同的方式進行訪問,像這樣:R8四字,R8D低雙字,R8W低字,R8B低單位元組(MASM表示方式,Intel 表示方式為 R8L)。注意這裡沒有R8H
  由於用於新暫存器的REX操作碼字首中的編碼問題,訪問位元組暫存器時存在一些奇怪的限制:一條指令不能同時引用一箇舊的高位位元組(AH、BH、CH、DH)和一個新的位元組暫存器(如R11B),但它可以使用低位位元組(AL、BL、CL、DL)。這是通過將使用REX字首的指令(AH、BH、CH、DH)更改為(BPL、SPL、DIL、SIL)來實現的。
  64位的指令指標RIP指向下一個將要執行的指令,並且支援64位平坦記憶體模型。後面將介紹當前作業系統中的記憶體地址佈局。棧指標RSP指向最後一個被壓入棧的地址,棧是向小地址增長的,被用來儲存函式呼叫流程的返回地址,在像C/C++這類高階語言傳遞引數,在呼叫約定中儲存“影子空間”。
  RFLAG暫存器儲存標誌位,用來表示操作結果或者暫存器的控制。這通過在X86的32位暫存器EFLAG高位擴充套件保留目前不使用的32個位得到的。表1列舉了最有用的標誌。絕大多數其他標誌提供給作業系統級別的任務並且經常用來設定值為之前讀到的數值。

x64 簡介

  浮點數處理單元FPU包含了八個FPR0FPR7暫存器,狀態和控制暫存器和其他一些特殊暫存器。FPR0-7每一個都能夠儲存如表2所示的型別的一個值。浮點值操作遵守IEEE 754標準。注意大多數C/C++編譯器支援32位和64位的floatdouble型別,但並不是80位的數值能夠從彙編獲取。這些暫存器和8個64位的MMX暫存器共享空間。

x64 簡介

  BCD編碼通過一些8位的指令支援,並且浮點暫存器支援的奇數格式提供了一種80位、17位的BCD型別。
  16個128位XMM暫存器(比x86多8個)可以包含更多的細節。
  最後的暫存器包含段暫存器(大多數在X64未被使用),控制暫存器,記憶體管理暫存器,除錯暫存器,虛擬化暫存器,跟蹤各種內部引數(快取命中/未命中、分支命中/未命中、執行的微操作、計時等)的效能暫存器。最值得關注的效能操作碼是RDTSC,它用於計算處理器週期以分析小程式碼段。
  全部細節都可以在 http://www.intel.com/products/processor/manuals/ 上獲取的《Intel® 64 and IA-32 Architectures Software Developer's Manuals》的第五卷中找到。它們可以以PDF格式免費下載,在CD上訂購,並且通常可以在列出時作為精裝集免費訂購。

SIMD 架構

  單指令多資料(SIMD)指令對多條資料並行執行單個命令,是彙編例程的常見用法。MMXSSE命令(分別使用MMXXMM暫存器)支援SIMD操作,這些操作可並行執行多達八條資料的指令。例如,可以使用MMX在一條指令中向八個位元組新增八個位元組。
  八個64MMX暫存器MMX0-MMX7FPR0-7之上有別名,這意味著任何混合FPMMX操作的程式碼都必須小心不要覆蓋所需的值。MMX指令對整數型別進行操作,允許對MMX暫存器中的值並行執行位元組、字和雙字操作。大多數MMX指令以P開頭,表示打包。算術、移位/迴圈移位、比較,例如:PCMPGTB意為比較壓縮有符號位元組整數是否大於。
  十六個128XMM暫存器允許每條指令對四個單精度或兩個雙精度值進行並行操作。一些指令也適用於壓縮位元組、字、雙字和四字整數。這些指令稱為Streaming SIMD Extensions(SSE),有多種形式:SSESSE2SSE3SSSE3SSE4,可能在列印數值時會使用更多。英特爾
已經宣佈了更多類似的擴充套件,稱為英特爾高階向量擴充套件
(Intel® Advanced Vector Extensions,Intel® AVX),具有新的256位寬資料路徑。SSE指令包含浮點和整數型別的移動、算術、比較、混洗和解包以及按位運算。指令名稱包括諸如PMULHUWRSQRTPS之類的美。最後,SSE引入了一些記憶體預取指令(為了效能)和記憶體柵欄(為了多執行緒安全)。
  表3列出了一些命令集、操作的暫存器型別、並行操作的專案數以及專案型別。例如,使用SSE3128XMM暫存器,您可以並行處理2個(必須是64位)浮點值,甚至可以並行處理16個(必須是位元組大小的)整數值。
  要查詢給定晶片支援的技術,有一條CPUID指令會返回特定於處理器的資訊。

x64 簡介

工具

彙編器

  Internet搜尋支援x64的彙編器,例如Netwide Assembler NASM、稱為YASMNASM重構版本、快速平面彙編器FASM和傳統的Microsoft MASM。甚至還有一個免費的用於x86x64程式集的IDE,稱為WinASM。每個彙編器
對其他彙編器的巨集和語法有不同的支援,但彙編程式碼與C++或各版本是Java等彙編器的原始碼不相容。
  對於下面的示例,我使用平臺SDK中免費提供的64位版本的MASMML64.EXE。對於下面的示例,請注意MASM語法的形式為:
  指令 目標運算元, 源運算元
  有些彙編器可能將源運算元和目標運算元位置對調,故請你認真閱讀文件。

C/C++ 編譯器

  C/C++編譯器通常允許使用內聯彙編在程式碼中嵌入彙編,但Microsoft Visual Studio各版本的64位C/C++程式碼不再支援,這可能會簡化程式碼優化器的任務。 這留下了兩個選擇:使用單獨的彙編檔案和外部彙編器,或使用標頭檔案intrn.h中的內在函式(參見BirtoloMSDN)。 其他編譯器具有類似的選項。
  使用內部函式(intrinsics)的一些原因:

  • x64 不支援內聯彙編。
  • 易於使用:您可以使用變數名,而不必處理暫存器手動分配。
  • 比彙編更跨平臺:編譯器製造商可以將內在函式移植到各種架構。
  • 優化器更適用於內部函式。

  例如,Microsoft Visual Studio 2008各版本有一個內部函式,它將16位值中的位向右迴圈移位b位並返回結果。在C中這樣給出:

unsigned short a1 = (b>>c)|(b<<(16-c));

  它將其擴充套件到十五個彙編指令(在Debug版本中與在Release版本中構建整個程式優化使其更難區分,但長度相似),於此同時如果使用一樣的內部函式:

unsigned short a2 = _rotr16(b,c);

  上式就會擴充套件為四個指令。有關更多資訊,請閱讀標頭檔案和文件。

彙編指令基礎

定址方式

  在介紹一些基本指令之前,您需要了解定址方式,即指令可以訪問暫存器或記憶體的方式。以下是常見的定址方式和示例:

  • 立即定址:值存在指令當中。
ADD EAX, 14 ; add 14 into 32-bit EAX
  • 暫存器到暫存器定址:
ADD R8L, AL ; add 8 bit AL into R8L
  • 間接定址:

  這種定址方式允許使用81632位的大小,任何用於基址和索引的通用暫存器,以及1248的比例來乘以索引。從技術上講,這些也可以以段FS:GS:為字首,但這很少用到。

MOV R8W, 1234[8*RAX+RCX] ; move word at address 8*RAX+RCX+1234 into R8W

  有很多合法的寫法。以下指令是等價的:

MOV ECX, dword ptr table[RBX][RDI]
MOV ECX, dword ptr table[RDI][RBX]
MOV ECX, dword ptr table[RBX+RDI]
MOV ECX, dword ptr [table+RBX+RDI]

  dword ptr告訴彙編器如何編碼MOV指令。

  • RIP 間接定址

  這是x64的新功能,允許在相對於當前指令指標的程式碼中訪問資料表等,使與位置無關的程式碼更易於實現。

MOV AL, [RIP] ; RIP points to the next instruction aka NOP
NOP

  不幸的是,MASM不允許這種形式的操作碼,但其他彙編程式如FASMYASM允許。相反,MASM隱式嵌入RIP相對定址。

MOV EAX, TABLE ; uses RIP- relative addressing to get table address
  • 特殊情況

  一些操作碼基於操作碼以獨特的方式使用暫存器。 例如,64位運算元值的有符號整數除法IDIVRDX:RAX中的128位值除以該值,將結果儲存在RAX中,餘數儲存在RDX中。

指令集

  表4列出了一些常用指令。*表示此條目是多個操作碼,其中*表示字尾。

x64 簡介

  常見的指令是LOOP指令,根據使用情況遞減RCX、ECXCX,如果結果不為0則跳轉。例如:

 XOR EAX, EAX ; zero out eax
 MOV ECX, 10 ; loop 10 times
Label: ; this is a label in assembly
 INX EAX ; increment eax
 LOOP Label ; decrement ECX, loop if not 0

  不太常見的操作碼實現字串操作、重複指令字首、埠I/O指令、標誌設定/清除/測試、浮點操作(通常以F開頭,並支援移動、轉為整數/從整數轉、算術、比較、超出、代數和控制函式)、用於多執行緒和效能問題的快取和記憶體操作碼等。《The Intel® 64 and IA-32 Architectures Software Developer’s Manual》第2卷分為兩部分詳細介紹了每個操作碼。

作業系統

  64位系統理論上允許定址264位元組的資料,但目前沒有晶片允許訪問所有16艾位元組(exabytes)8,446,744,073,709,551,616位元組)。例如,AMD架構僅使用地址的低48位,並且第4863位必須是第47位的副本,否則處理器會引發異常。因此,地址是000007FFF'FFFFFFFF,從FFFF8000'00000000FFFFFFFF'FFFFFFFF,總共256 TB281,474,976,710,656位元組)的可用虛擬地址空間。另一個缺點是定址所有64位記憶體需要更多的分頁表供作業系統儲存,對於安裝的系統少於所有16艾位元組的系統使用寶貴的記憶體。請注意,這些是虛擬地址,而不是實體地址。
  從結果上說,許多作業系統將這個空間的高半部分用於作業系統,從頂部開始向下增長,而使用者程式使用下半部分,從底部開始向上增長。當前的Windows各版本使用44位定址(16 TB =17,592,186,044,416位元組)。生成的定址如圖2所示。由於地址是由作業系統分配的,因此生成的地址對使用者程式不太重要,但使用者地址和核心地址之間的區別對於除錯很有用。
  最後一個與作業系統相關的專案與多執行緒程式設計有關,但是這個話題太大了,無法在這裡討論。唯一值得一提的是,記憶體屏障操作碼有助於保持共享資源不受損壞。

x64 簡介

呼叫約定

  與作業系統庫互動需要知道如何傳遞引數和管理堆疊。平臺上的這些細節稱為呼叫約定。
  一個常見的x64呼叫約定是用於C風格函式呼叫的Microsoft64呼叫約定(請參閱MSDNChenPietrek)。在各版本Linux下,這稱為應用程式二進位制介面 (Application Binary Interface,ABI)。請注意,此處介紹的呼叫約定不同於x64的各版本Linux系統上使用的呼叫約定。
  對於Microsoft各版本作業系統的x64呼叫約定,額外的暫存器空間讓fastcall成為唯一的呼叫約定(在x86下有很多:stdcallthiscallfastcallcdecl等)。與C/C++風格函式互動的規則:

  • RCX、RDX、R8、R9 按從左到右的順序用於整數和指標引數。
  • XMM0、XMM1、XMM2 和 XMM3 用於浮點引數。
  • 附加引數從左到右壓入堆疊。
  • 長度小於 64 位的引數不進行零擴充套件;剩餘的高位元組為垃圾資料。
  • 在呼叫函式之前,呼叫者有責任分配 32 位元組的預留空間(如果需要,用於儲存 RCX、RDX、R8 和 R9)。
  • 呼叫者負責平衡清理棧空間。
  • 如果 64 位或更小的資料,則在 RAX 中返回整數返回值(類似於 x86)。
  • 浮點返回值在 XMM0 中返回。
  • 較大的返回值(結構體)具有由呼叫者在堆疊上分配的空間,然後 RCX 在呼叫被呼叫者時包含指向返回空間的指標。然後將整數引數的暫存器使用推到右邊。 RAX 將此地址返回給呼叫者。
  • 堆疊是 16 位元組對齊的。 call 指令壓入一個 8 位元組的返回值,因此所有非葉函式在分配堆疊空間時必須將堆疊調整為 16n+8 形式的值。
  • 暫存器 RAX、RCX、RDX、R8、R9、R10 和 R11 被認為是易失的,並且必須在函式呼叫時被銷燬。
  • RBX、RBP、RDI、RSI、R12、R14、R14 和 R15 必須儲存在任何使用它們的函式中。
  • 請注意,浮點(以及 MMX)暫存器沒有呼叫約定。
  • 更多詳細資訊(可變引數、異常處理、堆疊展開)在 Microsoft 的網站上。

例子

  有了以上內容,這裡有幾個例子展示了x64的使用。第一個例子是一個簡單的x64獨立彙編程式,它彈出一個Windows的資訊框。

; Sample x64 Assembly Program
; Chris Lomont 2009 www.lomont.org
extrn ExitProcess: PROC ; external functions in system libraries
extrn MessageBoxA: PROC
.data
caption db '64-bit hello!', 0
message db 'Hello World!', 0
.code
Start PROC
    sub rsp,28h ; shadow space, aligns stack
    mov rcx, 0 ; hWnd = HWND_DESKTOP
    lea rdx, message ; LPCSTR lpText
    lea r8, caption ; LPCSTR lpCaption
    mov r9d, 0 ; uType = MB_OK
    call MessageBoxA ; call MessageBox API function
    mov ecx, eax ; uExitCode = MessageBox(...)
    call ExitProcess
Start ENDP
End

  將其儲存為hello.asm,使用ML64編譯,可在Microsoft Windows各種64位版本的SDK中使用
如下:

ml64 hello.asm /link /subsystem:windows /defaultlib:kernel32.lib /defaultlib:user32.lib /entry:Start

  這使得Windows可執行並與適當的庫連結。執行生成的可執行檔案hello.exe,應該會彈出訊息框。
  第二個示例將程式集檔案與各版本的Microsoft Visual Studio 2008下的C/C++檔案連結。其他編譯器系統類似。首先確保您的編譯器是支援x64的版本。然後、

  1. 建立一個新的空 C++ 控制檯專案。 建立一個你想移植到的函式程式集,並從 main 呼叫它。

  2. 要更改預設的 32 位構建,請選擇構建/配置管理器。

  3. 在活動平臺下,選擇新建。

  4. 在平臺下,選擇 x64。 如果沒有出現,請弄清楚如何新增 64 位 SDK 工具並重復該操作。

  5. 編譯並單步執行程式碼。 在 除錯-窗體——彙編窗體 下檢視以檢視彙編函式所需的生成程式碼和介面。

  6. 建立一個程式集檔案,並將其新增到專案中。它預設是 32 位彙編器,這是正常的。

  7. 開啟程式集檔案屬性,選擇所有配置,編輯自定義構建步驟。

  8. 輸入命令列

    ml64.exe /DWIN_X64 /Zi /c /Cp /Fl /Fo $(IntDir)\$(InputName).obj $(InputName).asm
    

    並設定輸出為$(IntDir)\$(InputName).obj

  9. 構建並執行。

  舉個例子,在main.cpp中,我們放置了一個函式CombineC,它對五個整數引數和一個雙精度引數進行一些簡單的數學運算,並返回一個雙精度答案。我們在一個單獨的檔案CombineA.asm中的一個名為CombineA的函式中複製該功能。C++檔案是:

// C++ code to demonstrate x64 assembly file linking
#include <iostream>
using namespace std;
double CombineC(int a, int b, int c, int d, int e, double f)
{
    return (a+b+c+d+e)/(f+1.5);
}

// NOTE: extern “C” needed to prevent C++ name mangling
extern "C" double CombineA(int a, int b, int c, int d, int e, double 
f);

int main(void)
{
    cout << "CombineC: " << CombineC(1,2,3,4, 5, 6.1) << endl;
    cout << "CombineA: " << CombineA(1,2,3,4, 5, 6.1) << endl;
    return 0;
}

  確保使函式外部C連結以防止C++名稱混淆。程式集檔案CombineA.asm內容為:

; Sample x64 Assembly Program
.data
realVal REAL8 +1.5 ; this stores a real number in 8 bytes

.code
PUBLIC CombineA
CombineA PROC
    ADD ECX, DWORD PTR [RSP+28H] ; add overflow parameter to first parameter
    ADD ECX, R9D ; add other three register parameters
    ADD ECX, R8D ;
    ADD ECX, EDX ;
    MOVD XMM0, ECX ; move doubleword ECX into XMM0
    CVTDQ2PD XMM0, XMM0 ; convert doubleword to floating point
    MOVSD XMM1, realVal ; load 1.5
    ADDSD XMM1, MMWORD PTR [RSP+30H] ; add parameter
    DIVSD XMM0, XMM1 ; do division, answer in xmm0
    RET ; return
CombineA ENDP

End

  執行這個應該導致值1.97368被輸出兩次。

結論

  這是對x64彙編程式設計的簡要介紹。下一步是瀏覽《Intel® 64 and IA-32 Architectures Software Developer‟s Manuals》。第1捲包含架構詳細資訊,如果您瞭解彙編,這是一個好的開始。其他地方是彙編書籍或線上彙編教程。要了解程式碼是如何執行的,在偵錯程式中單步除錯程式碼,檢視反彙編,直到您可以閱讀彙編程式碼以及您喜歡的語言,這很有指導意義。對於C/C++編譯器,除錯版本比釋出版本更容易閱讀,因此請務必從那裡開始。最後,逛一下masm32.com的論壇以獲取大量資料。

參考