Désassemblement x86 annoté
La section suivante vous guide tout au long d’un exemple de désassemblement.
Source Code
Voici le code de la fonction qui sera analysée.
HRESULT CUserView::CloseView(void)
{
if (m_fDestroyed) return S_OK;
BOOL fViewObjectChanged = FALSE;
ReleaseAndNull(&m_pdtgt);
if (m_psv) {
m_psb->EnableModelessSB(FALSE);
if(m_pws) m_pws->ViewReleased();
IShellView* psv;
HWND hwndCapture = GetCapture();
if (hwndCapture && hwndCapture == m_hwnd) {
SendMessage(m_hwnd, WM_CANCELMODE, 0, 0);
}
m_fHandsOff = TRUE;
m_fRecursing = TRUE;
NotifyClients(m_psv, NOTIFY_CLOSING);
m_fRecursing = FALSE;
m_psv->UIActivate(SVUIA_DEACTIVATE);
psv = m_psv;
m_psv = NULL;
ReleaseAndNull(&_pctView);
if (m_pvo) {
IAdviseSink *pSink;
if (SUCCEEDED(m_pvo->GetAdvise(NULL, NULL, &pSink)) && pSink) {
if (pSink == (IAdviseSink *)this)
m_pvo->SetAdvise(0, 0, NULL);
pSink->Release();
}
fViewObjectChanged = TRUE;
ReleaseAndNull(&m_pvo);
}
if (psv) {
psv->SaveViewState();
psv->DestroyViewWindow();
psv->Release();
}
m_hwndView = NULL;
m_fHandsOff = FALSE;
if (m_pcache) {
GlobalFree(m_pcache);
m_pcache = NULL;
}
m_psb->EnableModelessSB(TRUE);
CancelPendingActions();
}
ReleaseAndNull(&_psf);
if (fViewObjectChanged)
NotifyViewClients(DVASPECT_CONTENT, -1);
if (m_pszTitle) {
LocalFree(m_pszTitle);
m_pszTitle = NULL;
}
SetRect(&m_rcBounds, 0, 0, 0, 0);
return S_OK;
}
Code d’assembly
Cette section contient l’exemple de désassemblement annoté.
Les fonctions qui utilisent le registre ebp comme pointeur d’image commencent comme suit :
HRESULT CUserView::CloseView(void)
SAMPLE!CUserView__CloseView:
71517134 55 push ebp
71517135 8bec mov ebp,esp
Cela configure l’image afin que la fonction puisse accéder à ses paramètres en tant que décalages positifs à partir d’ebp, et les variables locales en tant que décalages négatifs.
Il s’agit d’une méthode sur une interface COM privée, de sorte que la convention d’appel est __stdcall. Cela signifie que les paramètres sont poussés de droite à gauche (dans ce cas, il n’y en a pas), que le pointeur « ce » est envoyé, puis que la fonction est appelée. Ainsi, lors de l’entrée dans la fonction, la pile ressemble à ceci :
[esp+0] = return address
[esp+4] = this
Après les deux instructions précédentes, les paramètres sont accessibles comme suit :
[ebp+0] = previous ebp pushed on stack
[ebp+4] = return address
[ebp+8] = this
Pour une fonction qui utilise ebp comme pointeur d’image, le premier paramètre envoyé est accessible à [ebp+8] ; les paramètres suivants sont accessibles aux adresses DWORD supérieures consécutives.
71517137 51 push ecx
71517138 51 push ecx
Cette fonction ne nécessite que deux variables de pile locales, donc une instruction sous-esp, 8. Les valeurs poussées sont ensuite disponibles en tant que [ebp-4] et [ebp-8].
Pour une fonction qui utilise ebp comme pointeur d’image, les variables locales de pile sont accessibles à des décalages négatifs du registre ebp .
71517139 56 push esi
À présent, le compilateur enregistre les registres qui doivent être conservés entre les appels de fonction. En fait, il les enregistre en bits et en morceaux, entrelacés avec la première ligne de code réel.
7151713a 8b7508 mov esi,[ebp+0x8] ; esi = this
7151713d 57 push edi ; save another registers
Il se trouve que CloseView est une méthode sur ViewState, qui est au décalage 12 dans l’objet sous-jacent. Par conséquent, il s’agit d’un pointeur vers une classe ViewState, bien qu’en cas de confusion possible avec une autre classe de base, elle soit spécifiée avec plus de soin comme (ViewState*) ceci.
if (m_fDestroyed)
7151713e 33ff xor edi,edi ; edi = 0
XORing a register with lui-même est un moyen standard de le zéro.
71517140 39beac000000 cmp [esi+0xac],edi ; this->m_fDestroyed == 0?
71517146 7407 jz NotDestroyed (7151714f) ; jump if equal
L’instruction cmp compare deux valeurs (en les soustrayant). L’instruction jz vérifie si le résultat est égal à zéro, indiquant que les deux valeurs comparées sont égales.
L’instruction cmp compare deux valeurs ; une instruction j suivante saute en fonction du résultat de la comparaison.
return S_OK;
71517148 33c0 xor eax,eax ; eax = 0 = S_OK
7151714a e972010000 jmp ReturnNoEBX (715172c1) ; return, do not pop EBX
Le compilateur a retardé l’enregistrement du registre EBX jusqu’à plus tard dans la fonction. Par conséquent, si le programme va « sortir en début » sur ce test, le chemin de sortie doit être celui qui ne restaure pas EBX.
BOOL fViewObjectChanged = FALSE;
ReleaseAndNull(&m_pdtgt);
L’exécution de ces deux lignes de code est entrelacée, donc faites attention.
NotDestroyed:
7151714f 8d86c0000000 lea eax,[esi+0xc0] ; eax = &m_pdtgt
L’instruction lea calcule l’adresse d’effet d’un accès à la mémoire et la stocke dans la destination. L’adresse mémoire réelle n’est pas déréférencée.
L’instruction lea prend l’adresse d’une variable.
71517155 53 push ebx
Vous devez enregistrer ce registre EBX avant qu’il ne soit endommagé.
71517156 8b1d10195071 mov ebx,[_imp__ReleaseAndNull]
Étant donné que vous appelez fréquemment ReleaseAndNull , il est judicieux de mettre en cache son adresse dans EBX.
7151715c 50 push eax ; parameter to ReleaseAndNull
7151715d 897dfc mov [ebp-0x4],edi ; fViewObjectChanged = FALSE
71517160 ffd3 call ebx ; call ReleaseAndNull
if (m_psv) {
71517162 397e74 cmp [esi+0x74],edi ; this->m_psv == 0?
71517165 0f8411010000 je No_Psv (7151727c) ; jump if zero
N’oubliez pas que vous avez supprimé le registre EDI depuis un certain temps et qu’EDI est un registre conservé entre les appels de fonction (par conséquent, l’appel à ReleaseAndNull ne l’a pas modifié). Par conséquent, il contient toujours la valeur zéro et vous pouvez l’utiliser pour tester rapidement zéro.
m_psb->EnableModelessSB(FALSE);
7151716b 8b4638 mov eax,[esi+0x38] ; eax = this->m_psb
7151716e 57 push edi ; FALSE
7151716f 50 push eax ; "this" for callee
71517170 8b08 mov ecx,[eax] ; ecx = m_psb->lpVtbl
71517172 ff5124 call [ecx+0x24] ; __stdcall EnableModelessSB
Le modèle ci-dessus est un signe révélateur d’un appel de méthode COM.
Les appels de méthode COM étant assez populaires, il est judicieux d’apprendre à les reconnaître. En particulier, vous devez être en mesure de reconnaître les trois méthodes IUnknown directement à partir de leurs décalages de table virtuelle : QueryInterface=0, AddRef=4 et Release=8.
if(m_pws) m_pws->ViewReleased();
71517175 8b8614010000 mov eax,[esi+0x114] ; eax = this->m_pws
7151717b 3bc7 cmp eax,edi ; eax == 0?
7151717d 7406 jz NoWS (71517185) ; if so, then jump
7151717f 8b08 mov ecx,[eax] ; ecx = m_pws->lpVtbl
71517181 50 push eax ; "this" for callee
71517182 ff510c call [ecx+0xc] ; __stdcall ViewReleased
NoWS:
HWND hwndCapture = GetCapture();
71517185 ff15e01a5071 call [_imp__GetCapture] ; call GetCapture
Les appels indirects via des globals sont la façon dont les importations de fonctions sont implémentées dans Microsoft Win32. Le chargeur corrige les globals pour pointer vers l’adresse réelle de la cible. Il s’agit d’un moyen pratique d’obtenir vos repères lorsque vous examinez une machine en panne. Recherchez les appels aux fonctions importées et dans la cible. Vous aurez généralement le nom d’une fonction importée, que vous pouvez utiliser pour déterminer où vous vous trouvez dans le code source.
if (hwndCapture && hwndCapture == m_hwnd) {
SendMessage(m_hwnd, WM_CANCELMODE, 0, 0);
}
7151718b 3bc7 cmp eax,edi ; hwndCapture == 0?
7151718d 7412 jz No_Capture (715171a1) ; jump if zero
La valeur de retour de la fonction est placée dans le registre EAX.
7151718f 8b4e44 mov ecx,[esi+0x44] ; ecx = this->m_hwnd
71517192 3bc1 cmp eax,ecx ; hwndCapture = ecx?
71517194 750b jnz No_Capture (715171a1) ; jump if not
71517196 57 push edi ; 0
71517197 57 push edi ; 0
71517198 6a1f push 0x1f ; WM_CANCELMODE
7151719a 51 push ecx ; hwndCapture
7151719b ff1518195071 call [_imp__SendMessageW] ; SendMessage
No_Capture:
m_fHandsOff = TRUE;
m_fRecursing = TRUE;
715171a1 66818e0c0100000180 or word ptr [esi+0x10c],0x8001 ; set both flags at once
NotifyClients(m_psv, NOTIFY_CLOSING);
715171aa 8b4e20 mov ecx,[esi+0x20] ; ecx = (CNotifySource*)this.vtbl
715171ad 6a04 push 0x4 ; NOTIFY_CLOSING
715171af 8d4620 lea eax,[esi+0x20] ; eax = (CNotifySource*)this
715171b2 ff7674 push [esi+0x74] ; m_psv
715171b5 50 push eax ; "this" for callee
715171b6 ff510c call [ecx+0xc] ; __stdcall NotifyClients
Notez comment vous avez dû modifier votre pointeur « this » lors de l’appel d’une méthode sur une classe de base différente de la vôtre.
m_fRecursing = FALSE;
715171b9 80a60d0100007f and byte ptr [esi+0x10d],0x7f
m_psv->UIActivate(SVUIA_DEACTIVATE);
715171c0 8b4674 mov eax,[esi+0x74] ; eax = m_psv
715171c3 57 push edi ; SVUIA_DEACTIVATE = 0
715171c4 50 push eax ; "this" for callee
715171c5 8b08 mov ecx,[eax] ; ecx = vtbl
715171c7 ff511c call [ecx+0x1c] ; __stdcall UIActivate
psv = m_psv;
m_psv = NULL;
715171ca 8b4674 mov eax,[esi+0x74] ; eax = m_psv
715171cd 897e74 mov [esi+0x74],edi ; m_psv = NULL
715171d0 8945f8 mov [ebp-0x8],eax ; psv = eax
La première variable locale est psv.
ReleaseAndNull(&_pctView);
715171d3 8d466c lea eax,[esi+0x6c] ; eax = &_pctView
715171d6 50 push eax ; parameter
715171d7 ffd3 call ebx ; call ReleaseAndNull
if (m_pvo) {
715171d9 8b86a8000000 mov eax,[esi+0xa8] ; eax = m_pvo
715171df 8dbea8000000 lea edi,[esi+0xa8] ; edi = &m_pvo
715171e5 85c0 test eax,eax ; eax == 0?
715171e7 7448 jz No_Pvo (71517231) ; jump if zero
Notez que le compilateur a préparé spéculativement l’adresse du membre m_pvo , car vous allez l’utiliser fréquemment pendant un certain temps. Ainsi, le fait d’avoir l’adresse à portée de main entraîne un code plus petit.
if (SUCCEEDED(m_pvo->GetAdvise(NULL, NULL, &pSink)) && pSink) {
715171e9 8b08 mov ecx,[eax] ; ecx = m_pvo->lpVtbl
715171eb 8d5508 lea edx,[ebp+0x8] ; edx = &pSink
715171ee 52 push edx ; parameter
715171ef 6a00 push 0x0 ; NULL
715171f1 6a00 push 0x0 ; NULL
715171f3 50 push eax ; "this" for callee
715171f4 ff5120 call [ecx+0x20] ; __stdcall GetAdvise
715171f7 85c0 test eax,eax ; test bits of eax
715171f9 7c2c jl No_Advise (71517227) ; jump if less than zero
715171fb 33c9 xor ecx,ecx ; ecx = 0
715171fd 394d08 cmp [ebp+0x8],ecx ; _pSink == ecx?
71517200 7425 jz No_Advise (71517227)
Notez que le compilateur a conclu que le paramètre « this » entrant n’était pas requis (car il y a longtemps qu’il a été inscrit dans le registre ESI). Ainsi, il réutilise la mémoire en tant que variable locale pSink.
Si la fonction utilise une trame EBP, les paramètres entrants arrivent à des décalages positifs d’EBP et les variables locales sont placées à des décalages négatifs. Mais, comme dans ce cas, le compilateur est libre de réutiliser cette mémoire à n’importe quel usage.
Si vous y prêtez une attention particulière, vous verrez que le compilateur aurait pu optimiser ce code un peu mieux. Il aurait pu retarder l’instruction lea edi, [esi+0xa8] jusqu’à ce qu’après les deux instructions push 0x0 , en les remplaçant par push edi. Cela aurait enregistré 2 octets.
if (pSink == (IAdviseSink *)this)
Ces lignes suivantes doivent compenser le fait qu’en C++, (IAdviseSink *)NULL doit toujours avoir la valeur NULL. Par conséquent, si votre « this » est vraiment « (ViewState*)NULL », le résultat du cast doit être NULL et non la distance entre IAdviseSink et IBrowserService.
71517202 8d46ec lea eax,[esi-0x14] ; eax = -(IAdviseSink*)this
71517205 8d5614 lea edx,[esi+0x14] ; edx = (IAdviseSink*)this
71517208 f7d8 neg eax ; eax = -eax (sets carry if != 0)
7151720a 1bc0 sbb eax,eax ; eax = eax - eax - carry
7151720c 23c2 and eax,edx ; eax = NULL or edx
Bien que le Pentium ait une instruction de déplacement conditionnel, l’architecture i386 de base ne le fait pas. Le compilateur utilise donc des techniques spécifiques pour simuler une instruction de déplacement conditionnel sans effectuer de sauts.
Le modèle général d’une évaluation conditionnelle est le suivant :
neg r
sbb r, r
and r, (val1 - val2)
add r, val2
Le neg r définit l’indicateur de portage si r est différent de zéro, car neg annule la valeur en soustrayant de zéro. De plus, la soustraction de zéro génère un emprunt (définir le carry) si vous soustrayez une valeur différente de zéro. Cela endommage également la valeur dans le registre r , mais cela est acceptable, car vous êtes sur le point de le remplacer de toute façon.
Ensuite, l’instruction sbb r, r soustrait une valeur d’elle-même, ce qui aboutit toujours à zéro. Toutefois, il soustrait également le bit de portage (emprunt), de sorte que le résultat net est de définir r sur zéro ou -1, selon que le carry était clair ou défini, respectivement.
Par conséquent, sbb r, r définit r sur zéro si la valeur d’origine de r était zéro, ou sur -1 si la valeur d’origine était différente de zéro.
La troisième instruction exécute un masque. Étant donné que le registre r est égal à zéro ou à -1, « this » sert soit à laisser r zéro, soit à remplacer r de -1 en (val1 - val1), dans la mesure où ANDing toute valeur avec -1 laisse la valeur d’origine.
Par conséquent, le résultat de « et r, (val1 - val1) » est de définir r sur zéro si la valeur d’origine de r était zéro, ou sur « (val1 - val2) » si la valeur d’origine de r était différente de zéro.
Enfin, vous ajoutez val2 à r, ce qui donne val2 ou (val1 - val2) + val2 = val1.
Ainsi, le résultat final de cette série d’instructions est de définir r sur val2 s’il était à l’origine zéro ou sur val1 si elle n’était pas nulle. Il s’agit de l’équivalent de l’assembly r = r ? val1 : val2.
Dans ce instance particulier, vous pouvez voir que val2 = 0 et val1 = (IAdviseSink*)this. (Notez que le compilateur a supprimé l’instruction add final eax, 0 , car elle n’a aucun effet.)
7151720e 394508 cmp [ebp+0x8],eax ; pSink == (IAdviseSink*)this?
71517211 750b jnz No_SetAdvise (7151721e) ; jump if not equal
Plus haut dans cette section, vous définissez EDI sur l’adresse du membre m_pvo . Vous allez l’utiliser maintenant. Vous avez également annulé le registre ECX précédemment.
m_pvo->SetAdvise(0, 0, NULL);
71517213 8b07 mov eax,[edi] ; eax = m_pvo
71517215 51 push ecx ; NULL
71517216 51 push ecx ; 0
71517217 51 push ecx ; 0
71517218 8b10 mov edx,[eax] ; edx = m_pvo->lpVtbl
7151721a 50 push eax ; "this" for callee
7151721b ff521c call [edx+0x1c] ; __stdcall SetAdvise
No_SetAdvise:
pSink->Release();
7151721e 8b4508 mov eax,[ebp+0x8] ; eax = pSink
71517221 50 push eax ; "this" for callee
71517222 8b08 mov ecx,[eax] ; ecx = pSink->lpVtbl
71517224 ff5108 call [ecx+0x8] ; __stdcall Release
No_Advise:
Tous ces appels de méthode COM doivent sembler très familiers.
L’évaluation des deux instructions suivantes est entrelacée. N’oubliez pas qu’EBX contient l’adresse de ReleaseAndNull.
fViewObjectChanged = TRUE;
ReleaseAndNull(&m_pvo);
71517227 57 push edi ; &m_pvo
71517228 c745fc01000000 mov dword ptr [ebp-0x4],0x1 ; fViewObjectChanged = TRUE
7151722f ffd3 call ebx ; call ReleaseAndNull
No_Pvo:
if (psv) {
71517231 8b7df8 mov edi,[ebp-0x8] ; edi = psv
71517234 85ff test edi,edi ; edi == 0?
71517236 7412 jz No_Psv2 (7151724a) ; jump if zero
psv->SaveViewState();
71517238 8b07 mov eax,[edi] ; eax = psv->lpVtbl
7151723a 57 push edi ; "this" for callee
7151723b ff5034 call [eax+0x34] ; __stdcall SaveViewState
Voici d’autres appels de méthode COM.
psv->DestroyViewWindow();
7151723e 8b07 mov eax,[edi] ; eax = psv->lpVtbl
71517240 57 push edi ; "this" for callee
71517241 ff5028 call [eax+0x28] ; __stdcall DestroyViewWindow
psv->Release();
71517244 8b07 mov eax,[edi] ; eax = psv->lpVtbl
71517246 57 push edi ; "this" for callee
71517247 ff5008 call [eax+0x8] ; __stdcall Release
No_Psv2:
m_hwndView = NULL;
7151724a 83667c00 and dword ptr [esi+0x7c],0x0 ; m_hwndView = 0
L’ANDing d’un emplacement de mémoire avec zéro revient à le définir sur zéro, car tout ce qui est ET zéro est égal à zéro. Le compilateur utilise ce formulaire car, même s’il est plus lent, il est beaucoup plus court que l’instruction mov équivalente. (Ce code a été optimisé pour la taille et non pour la vitesse.)
m_fHandsOff = FALSE;
7151724e 83a60c010000fe and dword ptr [esi+0x10c],0xfe
if (m_pcache) {
71517255 8b4670 mov eax,[esi+0x70] ; eax = m_pcache
71517258 85c0 test eax,eax ; eax == 0?
7151725a 740b jz No_Cache (71517267) ; jump if zero
GlobalFree(m_pcache);
7151725c 50 push eax ; m_pcache
7151725d ff15b4135071 call [_imp__GlobalFree] ; call GlobalFree
m_pcache = NULL;
71517263 83667000 and dword ptr [esi+0x70],0x0 ; m_pcache = 0
No_Cache:
m_psb->EnableModelessSB(TRUE);
71517267 8b4638 mov eax,[esi+0x38] ; eax = this->m_psb
7151726a 6a01 push 0x1 ; TRUE
7151726c 50 push eax ; "this" for callee
7151726d 8b08 mov ecx,[eax] ; ecx = m_psb->lpVtbl
7151726f ff5124 call [ecx+0x24] ; __stdcall EnableModelessSB
CancelPendingActions();
Pour appeler CancelPendingActions, vous devez passer de (ViewState*)this à (CUserView*)this. Notez également que CancelPendingActions utilise la convention d’appel __thiscall au lieu de __stdcall. Selon __thiscall, le pointeur « this » est passé dans le registre ECX au lieu d’être transmis sur la pile.
71517272 8d4eec lea ecx,[esi-0x14] ; ecx = (CUserView*)this
71517275 e832fbffff call CUserView::CancelPendingActions (71516dac) ; __thiscall
ReleaseAndNull(&_psf);
7151727a 33ff xor edi,edi ; edi = 0 (for later)
No_Psv:
7151727c 8d4678 lea eax,[esi+0x78] ; eax = &_psf
7151727f 50 push eax ; parameter
71517280 ffd3 call ebx ; call ReleaseAndNull
if (fViewObjectChanged)
71517282 397dfc cmp [ebp-0x4],edi ; fViewObjectChanged == 0?
71517285 740d jz NoNotifyViewClients (71517294) ; jump if zero
NotifyViewClients(DVASPECT_CONTENT, -1);
71517287 8b46ec mov eax,[esi-0x14] ; eax = ((CUserView*)this)->lpVtbl
7151728a 8d4eec lea ecx,[esi-0x14] ; ecx = (CUserView*)this
7151728d 6aff push 0xff ; -1
7151728f 6a01 push 0x1 ; DVASPECT_CONTENT = 1
71517291 ff5024 call [eax+0x24] ; __thiscall NotifyViewClients
NoNotifyViewClients:
if (m_pszTitle)
71517294 8b8680000000 mov eax,[esi+0x80] ; eax = m_pszTitle
7151729a 8d9e80000000 lea ebx,[esi+0x80] ; ebx = &m_pszTitle (for later)
715172a0 3bc7 cmp eax,edi ; eax == 0?
715172a2 7409 jz No_Title (715172ad) ; jump if zero
LocalFree(m_pszTitle);
715172a4 50 push eax ; m_pszTitle
715172a5 ff1538125071 call [_imp__LocalFree]
m_pszTitle = NULL;
N’oubliez pas que EDI est toujours nul et QU’EBX est toujours &m_pszTitle, car ces registres sont conservés par les appels de fonction.
715172ab 893b mov [ebx],edi ; m_pszTitle = 0
No_Title:
SetRect(&m_rcBounds, 0, 0, 0, 0);
715172ad 57 push edi ; 0
715172ae 57 push edi ; 0
715172af 57 push edi ; 0
715172b0 81c6fc000000 add esi,0xfc ; esi = &this->m_rcBounds
715172b6 57 push edi ; 0
715172b7 56 push esi ; &m_rcBounds
715172b8 ff15e41a5071 call [_imp__SetRect]
Notez que vous n’avez plus besoin de la valeur « this », donc le compilateur utilise l’instruction d’ajout pour la modifier en place au lieu d’utiliser un autre registre pour contenir l’adresse. Il s’agit en fait d’une victoire des performances en raison du pipelining Pentium u/v, car le canal v peut effectuer des calculs arithmétiques, mais pas résoudre les calculs.
return S_OK;
715172be 33c0 xor eax,eax ; eax = S_OK
Enfin, vous restaurez les registres que vous devez conserver, propre dans la pile et retournez à votre appelant, en supprimant les paramètres entrants.
715172c0 5b pop ebx ; restore
ReturnNoEBX:
715172c1 5f pop edi ; restore
715172c2 5e pop esi ; restore
715172c3 c9 leave ; restores EBP and ESP simultaneously
715172c4 c20400 ret 0x4 ; return and clear parameters