Désassemblement x64 annoté
La fonction très simple suivante illustre la convention d’appel x64.
int Simple(int i, int j)
{
return i*5 + j + 3;
}
Cette opération est compilée en code comme suit :
01001080 lea eax,[rdx+rcx*4] ; eax = rdx+rcx*4
01001083 lea eax,[rcx+rax+0x3] ; eax = rcx+rax+3
01001087 ret
Les paramètres i et j sont passés dans les registres ecx et edx , respectivement. Étant donné qu’il n’existe que deux paramètres, la routine n’utilise pas du tout la pile.
Le code particulier généré exploite trois astuces, dont l’une est spécifique au x64 :
L’opération lea peut être utilisée pour effectuer une série d’opérations arithmétiques simples en tant qu’opération unique. La première instruction stocke j+i*4 dans eax, et la deuxième instruction ajoute i+3 au résultat, pour un total de j+i*5+3.
De nombreuses opérations, telles que l’addition et la multiplication, peuvent être effectuées avec une précision supplémentaire, puis tronquées à la bonne précision. Dans cette instance, le code utilise l’ajout et la multiplication 64 bits. Nous pouvons tronquer le résultat en toute sécurité à 32 bits.
Sur le x64, toute opération qui génère une sortie vers un registre 32 bits supprime automatiquement le résultat. Dans ce cas, la sortie dans eax a pour effet de tronquer le résultat à 32 bits.
Les valeurs de retour sont transmises dans le registre rax . Dans ce cas, le résultat étant déjà dans le registre rax , la fonction retourne.
Ensuite, nous considérons une fonction plus compliquée pour illustrer le désassemblage x64 classique :
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;
}
Nous allons parcourir cette fonction et l’assembly équivalent ligne par ligne.
Une fois entrés, les paramètres de la fonction sont stockés comme suit :
rcx = pdisp.
Rdx = dispid.
r8 = fUnique.
r9 = pszExe.
Rappelez-vous que les quatre premiers paramètres sont passés dans des registres. Étant donné que cette fonction n’a que quatre paramètres, aucun n’est transmis sur la pile.
L’assembly commence comme suit :
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
La fonction commence par enregistrer des registres non volatiles, puis réserver de l’espace de pile pour les variables locales. Il enregistre ensuite les paramètres dans des registres non volatiles. Notez que la destination des deux instructions mov du milieu est un registre 32 bits, de sorte qu’elles sont implicitement étendues à 64 bits.
IQueryAssociations *pqa;
HRESULT hr = AssocCreate(CLSID_QueryAssociations, IID_IQueryAssociations, (void**)&pqa);
Le premier paramètre d’AssocCreate est un CLSID 128 bits passé par valeur. Étant donné que cela ne tient pas dans un registre 64 bits, le CLSID est copié dans la pile et un pointeur vers l’emplacement de la pile est passé à la place.
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
L’instruction movdqu transfère des valeurs de 128 bits vers et depuis les registres xmmn. Dans cette instance, le code d’assembly l’utilise pour copier le CLSID dans la pile. Le pointeur vers le CLSID est passé dans r8. Les deux autres arguments sont passés en rcx et rdx.
if (SUCCEEDED(hr)) {
01001123 test eax,eax
01001125 jl ReturnEAX (01001281)
Le code vérifie si la valeur de retour est une réussite.
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
Il s’agit d’un appel de fonction indirect utilisant une table virtuelle C++. Ce pointeur est passé dans rcx comme premier paramètre. Les trois premiers paramètres sont transmis dans les registres, tandis que le paramètre final est transmis sur la pile. La fonction réserve 16 octets pour les paramètres passés dans les registres, de sorte que le cinquième paramètre commence à rsp+0x20.
if (SUCCEEDED(hr)) {
0100114d mov ebx,eax ; ebx = hr
0100114f test ebx,ebx ; FAILED?
01001151 jl ReleasePQA (01001274) ; jump if so
Le code en langage assembly enregistre le résultat dans ebx et vérifie s’il s’agit d’un code de réussite.
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
Une fois de plus, nous avons configuré les paramètres et appelé une fonction, puis nous testons la valeur de retour pour la réussite.
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
La méthode idiomatique pour la mise à zéro d’une mémoire tampon sur x64 est la même que 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 est compilé directement sur le code de l’ordinateur. L’instruction lock xadd effectue un échange atomique et l’ajout. Le résultat final est stocké dans 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]) = ...
Étant donné que x64 prend en charge l’instruction cmov , la construction ?: peut être compilée sans utiliser de saut.
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
Ce code initialise le reste des membres de DISPPARAMS. Notez que le compilateur réutilise l’espace sur la pile précédemment utilisé par le 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
Le code configure ensuite les paramètres et appelle la méthode 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)
Le code termine la branche actuelle du conditionnel et passe par l’autre branche.
} 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
Branche 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)
La valeur de retour est stockée dans rax, puis les registres non volatiles sont restaurés avant le retour.