本篇原文為 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-64
和64
位Intel® Itanium®
體系結構並集。
本篇介紹不會涉及硬件的相關細節,比如緩存、分支預測和其他高級話題。有一些參考將會在本文章末尾處給出來幫助大家以后深入這些領域。
匯編一般用於編寫應用程序對性能極其苛刻要求的部分,盡管對於大多數開發者來說做到比C++
編譯器更好是非常困難的。匯編知識對於調試代碼來說十分有用——有時編譯器會生成錯誤的匯編代碼或者對在調試器中單步調試代碼確認錯誤原因有更好的幫助。代碼優化者們有時候會犯錯。當你沒有源代碼的時候,匯編就可以派上用場,提供修復代碼的接口。匯編可以讓你修改/修復當前已經存在的可執行文件。如果你想知道你所用的編程語言在底層的實現,匯編是必需品。學會它你就可以知道為什么有時候它運行的慢或者為什么其他運行的快。最后一點,匯編代碼知識在逆向分析惡意程序是不可或缺的。
架構
當在一個現有使用的平台學習匯編的時候,首先要學習寄存器組。
大體架構
目前64位的寄存器允許訪問各種大小和位置,我們定義一個字節8個位,一個字16個位,一個雙字32個位,一個四字64位,一個雙四字為128位。Intel
使用小端存儲,意味着低地址存低字節。
圖一展示了16個64位通用寄存器,第一組8個寄存器被命名(因為歷史原因)為RAX
、RBX
、RCX
、RDX
、RBP
、RSI
、RDI
和RSP
。第二組8個寄存器用R8 - R15
命名。將字母E
替換用開頭的字母R
,能夠訪問低32位(RAX
的EAX
)。類似的RAX
、RBX
、RCX
和RDX
可以通過去掉首字母R
來訪問低16個字節(RAX
的AX
),通過再把X
替換成L
可以訪問低16位(AX
的AL
)或通過再把X
換成H
訪問較高的16位(AX
的AH
)。R8
到R15
也可以用相同的方式進行訪問,像這樣: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++
這類高級語言傳遞參數,在調用約定中存儲“影子空間”。
RFLAGS
寄存器存儲標志位,用來表示操作結果或者寄存器的控制。這通過在X86
的32位寄存器EFLAGS
高位擴展保留目前不使用的32個位得到的。表1列舉了最有用的標志,而沒列出的大部分標志位中有被用於操作系統級別的任務,並且應當總是設置為之前讀取過的值。
浮點數處理單元FPU
包含了八個FPR0
到FPR7
寄存器,狀態和控制寄存器和其他一些特殊寄存器。FPR0-7
每一個都能夠存儲如表2所示的類型的一個值。浮點值操作遵守IEEE 754
標准。注意大多數C/C++
編譯器支持32位和64位的float
和double
類型,但並不支持匯編中提供的80位的類型。這些寄存器和8個64位的MMX
寄存器共享空間。
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)指令對多條數據並行執行單個命令,是匯編例程的常見用法。MMX
和SSE
命令(分別使用MMX
和XMM
寄存器)支持SIMD
操作,這些操作可並行執行多達八條數據的指令。例如,可以使用MMX
在一條指令中將其中一個八個字節與另一個八個字節進行加法運算。
八個64
位MMX
寄存器MMX0-MMX7
在FPR0-7
之上有別名,這意味着任何混合FP
和MMX
操作的代碼都必須小心不要覆蓋所需的值。MMX
指令對整數類型進行操作,允許對MMX
寄存器中的值並行執行字節、字和雙字操作。大多數MMX
指令以P
開頭,表示打包
。算術、移位/循環移位、比較,例如:PCMPGTB
意為比較壓縮有符號字節整數是否大於。
十六個128
位XMM
寄存器允許每條指令對四個單精度或兩個雙精度值進行並行操作。一些指令也適用於壓縮字節、字、雙字和四字整數。這些指令稱為Streaming SIMD Extensions
(SSE),有多種形式:SSE
、SSE2
、SSE3
、SSSE3
、SSE4
,可能在打印數值時會使用更多。英特爾已經宣布了更多類似的擴展,稱為英特爾高級矢量擴展。
(Intel® Advanced Vector Extensions,Intel® AVX),具有新的256
位寬數據路徑。SSE
指令包含浮點和整數類型的移動、算術、比較、混洗和解包以及按位運算。指令名稱包括諸如PMULHUW
和RSQRTPS
之類的美。最后,SSE
引入了一些內存預取指令(為了性能)和內存柵欄(為了多線程安全)。
表3
列出了一些命令集、操作的寄存器類型、並行操作的項目數以及項目類型。例如,使用SSE3
和128
位XMM
寄存器,您可以並行處理2
個(必須是64
位)浮點值,甚至可以並行處理16
個(必須是字節大小的)整數值。
要查找給定芯片支持的技術,有一條CPUID
指令會返回特定於處理器的信息。
工具
匯編器
Internet
搜索支持x64
的匯編器,例如Netwide Assembler NASM
、稱為YASM
的NASM
重構版本、快速平面匯編器FASM
和傳統的Microsoft MASM
。甚至還有一個免費的用於x86
和x64
程序集的IDE
,稱為WinASM
。每個匯編器
對其他匯編器的宏和語法有不同的支持,但匯編代碼與C++
或各版本是Java
等匯編器的源代碼不兼容。
對於下面的示例,我使用平台SDK
中免費提供的64
位版本的MASM
,ML64.EXE
。對於下面的示例,請注意MASM
語法的形式為:
指令 目標操作數, 源操作數
有些匯編器可能將源操作數和目標操作數位置對調,故請你認真閱讀文檔。
C/C++ 編譯器
C/C++
編譯器通常允許使用內聯匯編在代碼中嵌入匯編,但Microsoft Visual Studio
各版本的64位C/C++
代碼不再支持,這可能會簡化代碼優化器的任務。 這留下了兩個選擇:使用單獨的匯編文件和外部匯編器,或使用頭文件intrn.h
中的內在函數(參見Birtolo
和MSDN
)。 其他編譯器具有類似的選項。
使用內部函數(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
- 間接尋址:
這種尋址方式允許使用8
、16
或32
位的大小,任何用於基址和索引的通用寄存器,以及1
、2
、4
或8
的比例來乘以索引。從技術上講,這些也可以以段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
不允許這種形式的操作碼,但其他匯編程序如FASM
和YASM
允許。相反,MASM
隱式嵌入RIP
相對尋址。
MOV EAX, TABLE ; uses RIP- relative addressing to get table address
- 特殊情況
一些操作碼基於操作碼以獨特的方式使用寄存器。 例如,64
位操作數值的有符號整數除法IDIV
將RDX:RAX
中的128
位值除以該值,將結果存儲在RAX
中,余數存儲在RDX
中。
指令集
表4
列出了一些常用指令。*
表示此條目是多個操作碼,其中*
表示后綴。
常見的指令是LOOP
指令,根據使用情況遞減RCX、ECX
或CX
,如果結果不為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
位系統理論上允許尋址 \(2^{64}\) 字節的數據,但目前沒有芯片允許訪問所有16
艾字節(exabytes)8,446,744,073,709,551,616
字節)。例如,AMD
架構僅使用地址的低48
位,並且第48
到63
位必須是第47
位的副本,否則處理器會引發異常。因此,地址是0
到00007FFF'FFFFFFFF
,從FFFF8000'00000000
到FFFFFFFF'FFFFFFFF
,總共256 TB
(281,474,976,710,656
字節)的可用虛擬地址空間。另一個缺點是尋址所有64
位內存需要更多的分頁表供操作系統存儲,對於安裝的系統少於所有16
艾字節的系統使用寶貴的內存。請注意,這些是虛擬地址,而不是物理地址。
從結果上說,許多操作系統將這個空間的高半部分用於操作系統,從頂部開始向下增長,而用戶程序使用下半部分,從底部開始向上增長。當前的Windows
各版本使用44
位尋址(16 TB
=17,592,186,044,416
字節)。生成的尋址如圖2
所示。由於地址是由操作系統分配的,因此生成的地址對用戶程序不太重要,但用戶地址和內核地址之間的區別對於調試很有用。
最后一個與操作系統相關的項目與多線程編程有關,但是這個話題太大了,無法在這里討論。唯一值得一提的是,內存屏障操作碼有助於保持共享資源不受損壞。
調用約定
與操作系統庫交互需要知道如何傳遞參數和管理堆棧。平台上的這些細節稱為調用約定。
一個常見的x64
調用約定是用於C
風格函數調用的Microsoft
的64
調用約定(請參閱MSDN
、Chen
和Pietrek
)。在各版本Linux
下,這稱為應用程序二進制接口 (Application Binary Interface,ABI)。請注意,此處介紹的調用約定不同於x64
的各版本Linux
系統上使用的調用約定。
對於Microsoft
各版本操作系統的x64
調用約定,額外的寄存器空間讓fastcall
成為唯一的調用約定(在x86
下有很多:stdcall
、thiscall
、fastcall
、cdecl
等)。與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
的版本。然后、
-
創建一個新的空 C++ 控制台項目。 創建一個你想移植到的函數程序集,並從 main 調用它。
-
要更改默認的 32 位構建,請選擇構建/配置管理器。
-
在活動平台下,選擇新建。
-
在平台下,選擇 x64。 如果沒有出現,請弄清楚如何添加 64 位 SDK 工具並重復該操作。
-
編譯並單步執行代碼。 在 調試-窗體——匯編窗體 下查看以查看匯編函數所需的生成代碼和接口。
-
創建一個程序集文件,並將其添加到項目中。它默認是 32 位匯編器,這是正常的。
-
打開程序集文件屬性,選擇所有配置,編輯自定義構建步驟。
-
輸入命令行
ml64.exe /DWIN_X64 /Zi /c /Cp /Fl /Fo $(IntDir)\$(InputName).obj $(InputName).asm
並設置輸出為
$(IntDir)\$(InputName).obj
。 -
構建並運行。
舉個例子,在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
的論壇以獲取大量資料。
參考
- "AMD64 Architecture Tech Docs", 在線版地址: https://www.amd.com/system/files/TechDocs/24592.pdf
- NASM: http://www.nasm.us/
- YASM: http://www.tortall.net/projects/yasm/
- Flat Assembler(FASM): http://www.flatassembler.net/
- Dylan Birtolo, "New Intrinsic Support in Visual Studio 2008", 在線版地址: https://devblogs.microsoft.com/cppblog/new-intrinsic-support-in-visual-studio-2008/
- Raymond Chen, "The history of calling conventions, part5: amd64", 在線版地址: https://devblogs.microsoft.com/oldnewthing/20040114-00/?p=41053
- Intel 64 and IA-32 Architecture Software Developer's Manuals, 在線版地址: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- Compiler Intrinsics, 在線版地址: https://learn.microsoft.com/en-us/cpp/intrinsics/compiler-intrinsics?view=msvc-170
- Calling Conventions, 在線版地址: https://learn.microsoft.com/en-us/cpp/cpp/calling-conventions?view=msvc-170
- Everything You Need to Know To Start Programming 64-bit Windows Systems, 在線版地址: https://learn.microsoft.com/en-us/archive/msdn-magazine/2006/may/x64-starting-out-in-64-bit-windows-systems-with-visual-c