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中,然后在返回前恢复保存的寄存器。