X64處理器架構(翻譯的windbg幫助文檔)
X64處理器架構
X64架構是一個向後兼容的擴展的x86。提供了和x86相同的32位模式和一個新的64位模式。術語“x64”包括AMD 64 和Intel64,他們的指令集基本是相同的。
寄存器(Registers)
X64將x86的8個通用寄存器擴展爲64位,並且增加8個新的64位寄存器。64位寄存器命名以“r”開始,例如:eax擴展爲64位就是rax,8個新的64位寄存器命名爲r8到r15。
每個寄存器的低32位,16位,8位可以作爲操作數直接尋址,這包括向esi這樣的寄存器,以前他的低8位不可以直接尋址。下表說明了64位寄存器的地位部分在彙編語言中的命名。
64-bit register |
Lower 32 bits |
Lower 16 bits |
Lower 8 bits |
rax |
eax |
ax |
al |
rbx |
ebx |
bx |
bl |
rcx |
ecx |
cx |
cl |
rdx |
edx |
dx |
dl |
rsi |
esi |
si |
sil |
rdi |
edi |
di |
dil |
rbp |
ebp |
bp |
bpl |
rsp |
esp |
sp |
spl |
r8 |
r8d |
r8w |
r8b |
r9 |
r9d |
r9w |
r9b |
r10 |
r10d |
r10w |
r10b |
r11 |
r11d |
r11w |
r11b |
r12 |
r12d |
r12w |
r12b |
r13 |
r13d |
r13w |
r13b |
r14 |
r14d |
r14w |
r14b |
r15 |
r15d |
r15w |
r15b |
ax,bx,cx和dx的高8位ah,bh,ch,dh仍就是可以尋址的,但是不能用在所有類型的操作數。
指令指針寄存器eip和flags也被擴展到64位(分別爲 rip和rflags)。
X64處理器也提供幾個浮點寄存器:
·8個80位的x87寄存器
·8個64位的MMX寄存器
·以前的8個128位SSE寄存器增加到16個
調用約定(Calling Conventions)
跟x86不同,在x64下c/c++編譯器僅支持一種調用約定,這種調用約定利用了在x64下可用寄存器的增加。
·前四個整型值或指針參數傳給寄存器rcx,rdx,r8,和r9。調用函數在堆棧上保留空間爲這些參數。
·前四個浮點參數傳給前四個SSE寄存器xmm0-xmm3.
·調用函數在堆棧上保留空間爲傳遞給寄存器的參數。被調用函數利用這些空間將寄存器的內容存入堆棧。
·任何其他參數存入堆棧
·一個整型或指針返回值存在rax寄存器中,如果返回值是浮點則返回在xmm0中
·rax,rcx,rdx,r8-r11是要變化的
·rbx, rbp, rdi, rsi, r12-r15不變
這個調用約定跟c++是非常相似的:this指針作爲第一個隱含的參數被傳遞,後面三個參數傳遞給寄存器,剩下的存入堆棧。
尋址方式(Addressing Modes)
在64位模式下的尋址方式類似於x86但是不是完全相同。
·指令涉及到64位寄存器會自動執行64位精度。(例如mov rax,[rbx]是將rbx所指向的地址開始的8字節存入rax)
·一個特別的指令mov的立即數常量或常量地址已經增加爲64位,對於其他的指令立即數常量或常量指針仍就是32位。
·x64提供了一個新的rip相關的尋址模式。如果指令涉及到常量地址以rip爲偏移。例如mov rax,[addr]操作將地址addr+rip指向地址開始的8字節數存入rax。
Jmp,call,push和pop指令涉及到的指令指針和堆棧指針都爲64位在x64中。
x64 指令集
大多數x86指令在x64的64位模式下是有效的。在64位模式下一些很少用到的指令不再支持。例如:
·BCD碼算術指令:AAA,AAD,AAM,AAS,DAA,DAS
·BOUND
·PUSHAD
和
POPAD
·大多數的操作要處理段寄存器,例如PUSH DS 和 POP DS。(對FS和 GS段寄存器的操作仍然有效)
X64指令集包括最近增加的x86指令例如SSE2,程序中可以自由的使用這些指令。
數據傳送(Data Transfer)
X64提供新的MOV指令的變量來處理64位立即數常量或內存地址。
MOV |
r,#n |
r = #n |
MOV |
rax, m |
傳送64位地址處的內容到 rax. |
MOV |
m, rax |
傳送 rax |
X64也提供一個新的指令符號擴展32位到64位
MOVSXD |
r1, r/m |
傳送 DWORD 符號擴展到 QWORD. |
一般MOV操作32位子寄存器自動零擴展到64位,因此沒有MOVZXD指令。
兩個SSE指令可以用來傳送128位值(例如GUIDs)從內存到xmmn 寄存器或相反。
MOVDQA |
r1/m, r2/m |
傳送128位對齊值到xmmn 寄存器,或相反 |
MOVDQU |
r1/m, r2/m |
傳送128位值(不是必須對齊)到寄存器或相反 |
數據轉換(Data Conversion)
CDQE |
轉換 dword (eax) 爲 qword (rax). |
CQO |
轉換 qword (rax) 爲 oword (rdx:rax). |
字符串操作(String Manipulation)
MOVSQ |
將rsi指向的字符串傳送到rdi指向地址
|
CMPSQ |
比較rsi和rdi所指向地址的字符串 |
SCASQ |
掃描rdi指向的地址的qword並與rax比較
|
LODSQ |
將rsi指向的地址的qword傳入rax |
STOSQ |
將rax的值傳入rdi指向的地址
|
x64反彙編
下面一個非常簡單的函數來說明x64調用約定。int Simple(int i, int j)
{
return i*5 + j + 3;
}
編譯後的代碼是這樣:
01001080 lea eax,[rdx+rcx*4] ; eax = rdx+rcx*4
01001083 lea eax,[rcx+rax+0x3] ; eax = rcx+rax+3
01001087 ret
i和j參數被傳遞給ecx和edx寄存器,由於這僅有兩個參數這個函數根本沒用堆棧。
這段生成的代碼有三個地方值得注意,其中有一個事x64特有的:
1.
lea指令被用來執行一系列的簡單算術操作,第一個將j+i*4存入eax,第二個操作加上i+3存入結果中,最後爲j+i*5+3。
2.
許多操作例如加和乘,可以處理中用擴展精度,然後在舍入到正確精度。在這個例子中代碼用的是64位加和乘操作。我們可以安全的縮短結果到32位。
3.
在x64中,任何輸出到32位寄存器的操作會自動零擴展到64位,在這個例子中,輸出到eax中有效的縮短到32位。
返回值被傳送到rax寄存器,在這個例子中,結果已經在rax寄存器中,因此函數直接返回。
下面我們考慮一個更復雜的函數來說明典型的x64反彙編:
HRESULT Meaningless(IDispatch *pdisp, DISPID dispid, BOOL fUnique, LPCWSTR pszExe)
{
IQueryAssociations *pqa;
HRESULT hr = AssocCreate(CLSID_QueryAssociations, IID_IQueryAssociations, (void**)&pqa);
if (SUCCEEDED(hr)) {
hr = pqa->Init(ASSOCF_INIT_BYEXENAME, pszExe, NULL, NULL);
if (SUCCEEDED(hr)) {
WCHAR wszName[MAX_PATH];
DWORD cchName = MAX_PATH;
hr = pqa->GetString(0, ASSOCSTR_FRIENDLYAPPNAME, NULL, wszName, &cchName);
if (SUCCEEDED(hr)) {
VARIANTARG rgvarg[2] = { 0 };
V_VT(&rgvarg[0]) = VT_BSTR;
V_BSTR(&rgvarg[0]) = SysAllocString(wszName);
if (V_BSTR(&rgvarg[0])) {
DISPPARAMS dp;
LONG lUnique = InterlockedIncrement(&lCounter);
V_VT(&rgvarg[1]) = VT_I4;
V_I4(&rgvarg[1]) = fUnique ? lUnique : 0;
dp.rgvarg = rgvarg;
dp.cArgs = 2;
dp.rgdispidNamedArgs = NULL;
dp.cNamedArgs = 0;
hr = pdisp->Invoke(dispid, IID_NULL, 0, DISPATCH_METHOD, &dp, NULL, NULL, NULL);
VariantClear(&rgvarg[0]);
VariantClear(&rgvarg[1]);
} else {
hr = E_OUTOFMEMORY;
}
}
}
pqa->Release();
}
return hr;
}
我們將要進入這個函數並且對每行進行反彙編。
當進入的時候這個函數的參數被存儲爲下面這樣:
- rcx = pdisp.
- rdx = dispid.
- r8 = fUnique.
- r9 = pszExe.
前四個參數被傳入寄存器中,由於這個函數僅有四個參數,沒有一個被存入堆棧中。
下面開始彙編代碼:
Meaningless:010010e0 push rbx ; save
010010e1 push rsi ; save
010010e2 push rdi ; save
010010e3 push r12d ; save
010010e5 push r13d ; save
010010e7 push r14d ; save
010010e9 push r15d ; save
010010eb sub rsp,0x2c0 ; reserve stack
010010f2 mov rbx,r9 ; rbx = pszExe
010010f5 mov r12d,r8d ; r12 = fUnique (zero-extend)
010010f8 mov r13d,edx ; r13 = dispid (zero-extend)
010010fb mov rsi,rcx ; rsi = pdisp
這個函數開始保存不可變的寄存器,然後保留堆棧空間爲局部變量,再保存參數到不可變寄存器。注意中間兩個mov指令的目的操作數是32位寄存器,因此會隱含零擴展到64位。
IQueryAssociations *pqa;
HRESULT hr = AssocCreate(CLSID_QueryAssociations, IID_IQueryAssociations, (void**)&pqa);
AssocCreate的第一個參數是一個128位的CLSID值,由於寄存器是64位,這個CLSID被複制到堆棧裏,被傳遞的是指向堆棧地址的指針。
010010fe movdqu xmm0,oword ptr [CLSID_QueryAssociations (01001060)]
01001106 movdqu oword ptr [rsp+0x60],xmm0 ; temp buffer for first parameter
0100110c lea r8,[rsp+0x58] ; arg3 = &pqa
01001111 lea rdx,[IID_IQueryAssociations (01001070)] ; arg2 = &IID_IQueryAssociations
01001118 lea rcx,[rsp+0x60] ; arg1 = &temporary
0100111d call qword ptr [_imp_AssocCreate (01001028)] ; call
movdqu指令傳遞128位的值到xmmn寄存器或取128位值從xmmn寄存器,在這個例子的彙編代碼中它複製CLSID到堆棧裏。指向CLSID的指針傳遞到r8,其他兩個參數傳遞到rcx和rdx。
if (SUCCEEDED(hr)) {
01001123 test eax,eax
01001125 jl ReturnEAX (01001281)
hr = pqa->Init(ASSOCF_INIT_BYEXENAME, pszExe, NULL, NULL);
0100112b mov rcx,[rsp+0x58] ; arg1 = pqa
01001130 mov rax,[rcx] ; rax = pqa.vtbl
01001133 xor r14d,r14d ; r14 = 0
01001136 mov [rsp+0x20],r14 ; arg5 = 0
0100113b xor r9d,r9d ; arg4 = 0
0100113e mov r8,rbx ; arg3 = pszExe
01001141 mov r15d,0x2 ; r15 = 2 (for later)
01001147 mov edx,r15d ; arg2 = 2 (ASSOCF_INIT_BY_EXENAME)
0100114a call qword ptr [rax+0x18] ; call Init method
這是一個用c++虛函數表的間接調用。This指針傳遞到rcx作爲第一個參數。前三個參數傳遞到寄存器中,最後一個參數傳遞到堆棧上。這個函數保留16字節空間爲寄存器中參數的傳遞,因此第五個參數地址在rsp+0x20。
if (SUCCEEDED(hr)) {
0100114d mov ebx,eax ; ebx = hr
0100114f test ebx,ebx ; FAILED?
01001151 jl ReleasePQA (01001274) ; jump if so
這個彙編語言代碼保存返回值在ebx中,並且檢查返回值是否成功。
WCHAR wszName[MAX_PATH];
DWORD cchName = MAX_PATH;
hr = pqa->GetString(0, ASSOCSTR_FRIENDLYAPPNAME, NULL, wszName, &cchName);
if (SUCCEEDED(hr)) {
01001157 mov dword ptr [rsp+0x50],0x104 ; cchName = MAX_PATH
0100115f mov rcx,[rsp+0x58] ; arg1 = pqa
01001164 mov rax,[rcx] ; rax = pqa.vtbl
01001167 lea rdx,[rsp+0x50] ; rdx = &cchName
0100116c mov [rsp+0x28],rdx ; arg6 = cchName
01001171 lea rdx,[rsp+0xb0] ; rdx = &wszName[0]
01001179 mov [rsp+0x20],rdx ; arg5 = &wszName[0]
0100117e xor r9d,r9d ; arg4 = 0
01001181 mov r8d,0x4 ; arg3 = 4 (ASSOCSTR_FRIENDLYNAME)
01001187 xor edx,edx ; arg2 = 0
01001189 call qword ptr [rax+0x20] ; call GetString method
0100118c mov ebx,eax ; ebx = hr
0100118e test ebx,ebx ; FAILED?
01001190 jl ReleasePQA (01001274) ; jump if so
再次傳遞參數調用一個函數,並且測試返回值是否成功。
VARIANTARG rgvarg[2] = { 0 };
01001196 lea rdi,[rsp+0x82] ; rdi = &rgvarg
0100119e xor eax,eax ; rax = 0
010011a0 mov ecx,0x2e ; rcx = sizeof(rgvarg)
010011a5 rep stosb ; Zero it out
在x64下對一個緩衝區清零的方法和x86是相同的。
V_VT(&rgvarg[0]) = VT_BSTR;
V_BSTR(&rgvarg[0]) = SysAllocString(wszName);
if (V_BSTR(&rgvarg[0])) {
010011a7 mov word ptr [rsp+0x80],0x8 ; V_VT(&rgvarg[0]) = VT_BSTR
010011b1 lea rcx,[rsp+0xb0] ; arg1 = &wszName[0]
010011b9 call qword ptr [_imp_SysAllocString (01001010)] ; call
010011bf mov [rsp+0x88],rax ; V_BSTR(&rgvarg[0]) = result
010011c7 test rax,rax ; anything allocated?
010011ca je OutOfMemory (0100126f) ; jump if failed
DISPPARAMS dp;
LONG lUnique = InterlockedIncrement(&lCounter);
010011d0 lea rax,[lCounter (01002000)]
010011d7 mov ecx,0x1
010011dc lock xadd [rax],ecx ; interlocked exchange and add
010011e0 add ecx,0x1
InterlockedIncrement編譯爲機器碼,lock xadd指令執行自動交換數據並且相加,最後結果存入ecx中。
V_VT(&rgvarg[1]) = VT_I4;
V_I4(&rgvarg[1]) = fUnique ? lUnique : 0;
010011e3 mov word ptr [rsp+0x98],0x3 ; V_VT(&rgvarg[1]) = VT_I4;
010011ed mov eax,r14d ; rax = 0 (r14d is still zero)
010011f0 test r12d,r12d ; fUnique set?
010011f3 cmovne eax,ecx ; if so, then set rax=lCounter
010011f6 mov [rsp+0xa0],eax ; V_I4(&rgvarg[1]) = ...
由於x64支持cmov指令,所以?:結構被編譯後沒有用調轉指令。
dp.rgvarg = rgvarg;
dp.cArgs = 2;
dp.rgdispidNamedArgs = NULL;
dp.cNamedArgs = 0;
010011fd lea rax,[rsp+0x80] ; rax = &rgvarg[0]
01001205 mov [rsp+0x60],rax ; dp.rgvarg = rgvarg
0100120a mov [rsp+0x70],r15d ; dp.cArgs = 2 (r15 is still 2)
0100120f mov [rsp+0x68],r14 ; dp.rgdispidNamedArgs = NULL
01001214 mov [rsp+0x74],r14d ; dp.cNamedArgs = 0
這段代碼初始化DISPPARAMS結構剩下的成員。注意編譯器重用了先前被CLSID佔用的堆棧空間。
hr = pdisp->Invoke(dispid, IID_NULL, 0, DISPATCH_METHOD, &dp, NULL, NULL, NULL);
01001219 mov rax,[rsi] ; rax = pdisp.vtbl
0100121c mov [rsp+0x40],r14 ; arg9 = 0
01001221 mov [rsp+0x38],r14 ; arg8 = 0
01001226 mov [rsp+0x30],r14 ; arg7 = 0
0100122b lea rcx,[rsp+0x60] ; rcx = &dp
01001230 mov [rsp+0x28],rcx ; arg6 = &dp
01001235 mov word ptr [rsp+0x20],0x1 ; arg5 = 1 (DISPATCH_METHOD)
0100123c xor r9d,r9d ; arg4 = 0
0100123f lea r8,[GUID_NULL (01001080)] ; arg3 = &IID_NULL
01001246 mov edx,r13d ; arg2 = dispid
01001249 mov rcx,rsi ; arg1 = pdisp
0100124c call qword ptr [rax+0x30] ; call Invoke method
0100124f mov ebx,eax ; hr = result
這段代碼設置參數並且調用Invoke方法。
VariantClear(&rgvarg[0]);
VariantClear(&rgvarg[1]);
01001251 lea rcx,[rsp+0x80] ; arg1 = &rgvarg[0]
01001259 call qword ptr [_imp_VariantClear (01001018)]
0100125f lea rcx,[rsp+0x98] ; arg1 = &rgvarg[1]
01001267 call qword ptr [_imp_VariantClear (01001018)]
0100126d jmp ReleasePQA (01001274)
這段代碼完成當前的條件分支,並跳過else分支。
} else {
hr = E_OUTOFMEMORY;
}
}
OutOfMemory:
0100126f mov ebx,0x8007000e ; hr = E_OUTOFMEMORY
pqa->Release();
ReleasePQA:
01001274 mov rcx,[rsp+0x58] ; arg1 = pqa
01001279 mov rax,[rcx] ; rax = pqa.vtbl
0100127c call qword ptr [rax+0x10] ; release
else分支
return hr;
}
0100127f mov eax,ebx ; rax = hr (for return value)
ReturnEAX:
01001281 add rsp,0x2c0 ; clean up the stack
01001288 pop r15d ; restore
0100128a pop r14d ; restore
0100128c pop r13d ; restore
0100128e pop r12d ; restore
01001290 pop rdi ; restore
01001291 pop rsi ; restore
01001292 pop rbx ; restore
01001293 ret ; return (do not pop arguments)
返回值存儲在rax中,然後在返回前恢復保存的寄存器。