下一節將逐步引導您完成反組譯範例。
原始程式碼
以下是將分析之函式的程序代碼。
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;
}
組合語言程式碼
本節包含批註反組譯碼範例。
使用 ebp 暫存器作為框架指標的函式起始如下:
HRESULT CUserView::CloseView(void)
SAMPLE!CUserView__CloseView:
71517134 55 push ebp
71517135 8bec mov ebp,esp
這會設定框架,讓函式可以透過 ebp 的正位移來存取其參數,並將局部變數當做負位移來存取。
這是私人 COM 介面上的方法,因此呼叫慣例 __stdcall。 這表示參數會由右至左推入(在這種情況下,沒有參數),將 "this" 指標推入,然後呼叫函式。 因此,在進入函式時,堆疊看起來像這樣:
[esp+0] = return address
[esp+4] = this
在上述兩個指示之後,參數可存取為:
[ebp+0] = previous ebp pushed on stack
[ebp+4] = return address
[ebp+8] = this
對於使用 ebp 作為框架指標的函式,第一個推送的參數可在 [ebp+8] 存取;後續參數可在連續較高的 DWORD 位址存取。
71517137 51 push ecx
71517138 51 push ecx
此函式只需要兩個局部堆疊變數,因此 sub esp, 8 指令。 推送的值接著會以 [ebp-4] 和 [ebp-8] 的形式提供。
對於使用 ebp 做為框架指標的函式,堆疊局部變數可從 ebp 緩存器進行負位移存取。
71517139 56 push esi
現在編譯程式會儲存需要在函數調用中保留的暫存器。 實際上,它會將它們以零碎片段儲存,與實際程式碼的第一行交錯。
7151713a 8b7508 mov esi,[ebp+0x8] ; esi = this
7151713d 57 push edi ; save another registers
因此,CloseView 是 ViewState 上的一個方法,並位於底層物件的偏移量 12 處。 因此,此是指向 ViewState 類別的指標,但當可能與另一個基底類別混淆時,會更加仔細地指定為 (ViewState*)此。
if (m_fDestroyed)
7151713e 33ff xor edi,edi ; edi = 0
將寄存器與它本身進行XOR運算是將其歸零的標準方式。
71517140 39beac000000 cmp [esi+0xac],edi ; this->m_fDestroyed == 0?
71517146 7407 jz NotDestroyed (7151714f) ; jump if equal
cmp 指令會比較兩個值(藉由減去它們)。 jz 指令會檢查結果是否為零,表示兩個比較值相等。
cmp 指令會比較兩個值;後續 j 指令會根據比較的結果跳躍。
return S_OK;
71517148 33c0 xor eax,eax ; eax = 0 = S_OK
7151714a e972010000 jmp ReturnNoEBX (715172c1) ; return, do not pop EBX
編譯器延遲儲存 EBX 暫存器,直到函式稍後才儲存,因此如果程式要在此測試上提前結束,則結束路径必須是不會還原 EBX 的暫存器。
BOOL fViewObjectChanged = FALSE;
ReleaseAndNull(&m_pdtgt);
這兩行程式代碼的執行是交錯的,因此請注意。
NotDestroyed:
7151714f 8d86c0000000 lea eax,[esi+0xc0] ; eax = &m_pdtgt
lea 指令會計算記憶體存取的效果位址,並將其儲存在目的地中。 實際的記憶體位址未被解除參照。
lea 指令會採用變數的位址。
71517155 53 push ebx
您應該先儲存該 EBX 暫存器,以避免損毀。
71517156 8b1d10195071 mov ebx,[_imp__ReleaseAndNull]
因為您會經常呼叫 ReleaseAndNull ,所以最好在 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
請記住,您之前已將 EDI 暫存器清零,而且 EDI 是在函式呼叫期間保留的暫存器(所以呼叫 ReleaseAndNull 並未改變它)。 因此,它仍然保留零值,而且您可以使用它快速測試零。
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
上述模式是 COM 方法呼叫的指示符號。
COM 方法呼叫相當受歡迎,因此最好學會辨識它們。 特別是,您應該能夠直接從其 Vtable 位移辨識三個 IUnknown 方法:QueryInterface=0、AddRef=4 和 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
在 Microsoft 的 Win32 中,函式匯入是透過全域變數的間接呼叫來實作的。 載入器會修正全域,以指向目標的實際位址。 這是在調查故障的機器時,取得方向的便利方式。 尋找對匯入函式和目標中的呼叫。 您通常會有一些匯入的函式名稱,可用來判斷您在原始程式碼中的位置。
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
函式傳回值放在 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
請注意,當您在不同於自己基類的另一個基類上呼叫方法時,必須如何變更 "this" 指標。
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
第一個局部變數是 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
請注意,編譯程式推測地準備 m_pvo 成員的位址,因為您將經常使用它一段時間。 因此,擁有隨身可用的位址,將會產生較小的程式碼。
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)
請注意,編譯器認為傳入的「this」參數是不需要的(因為很久以前已經將它存放在 ESI 暫存器中)。 因此,它會重複使用記憶體作為局部變數 pSink。
如果函式使用 EBP 堆疊框架,則傳入參數會位於 EBP 的正位移處,而局部變數則位於 EBP 的負位移處。 但是,在此情況下,編譯程式可以任意重複使用該記憶體。
如果您要密切關注,您會看到編譯程式可能已將此程式代碼優化一點。 它可能會將 lea edi, [esi+0xa8] 指令延遲到兩個 push 0x0 指令之後,而用 push edi 取代它們。 這會節省 2 個位元組。
if (pSink == (IAdviseSink *)this)
接下來的幾行是為了補償在 C++ 中,(IAdviseSink *)NULL 依然必須是 NULL 的情況。 因此,如果您的 “this” 真的是 “(ViewState*)NULL”,則轉換的結果應該是 NULL ,而不是 IAdviseSink 與 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
雖然 Pentium 具有條件式移動指令,但基底 i386 架構不會,因此編譯程式會使用特定技術來模擬條件式移動指令,而不需要進行任何跳躍。
條件式評估的一般模式如下:
neg r
sbb r, r
and r, (val1 - val2)
add r, val2
如果 r 為非零值,則 neg r 會設定攜帶旗標,因為 neg 會從零減去來否定值。 而且,如果從零減去非零值,則會產生借位(設定進位)。 它也會損害 r 緩存器中的值,但這是可以接受的,因為您還是要覆寫它。
接下來 ,sbb r、r 指令會從本身減去值,這一律會產生零。 不過,它也會減去進位(借位)位,因此淨結果是將 r 設定為零或 -1,這取決於進位位是清除或設置的。
因此,如果 r 的原始值為零,則 sbb r、r 會將 r 設定為零,如果原始值不是零,則設定為 -1。
第三個指令會執行遮罩。 因為 r 暫存器是零或 -1,因此要麼將 r 保持為零,要麼將 r 從 -1 變為 (val1 - val1),在該 AND 運算中,任何與 -1 進行 AND 運算的值將保持為原始值。
因此,如果 r 的原始值為零,則 “and r, (val1 - val1)” 的結果是將 r 設定為零,如果 r 的原始值為零,則為 “(val1 - val2)”。
最後,您會將 val2 新增至 r,導致 val2 或 (val1 - val2) + val2 = val1。
因此,如果 r 原本為零,則此系列指令的最終結果是將 r 設定為 val2 ,如果它不是零,則設定為 val1 。 這是等同於組合語言的表達式:r = r ? val1 : val2。
在此特定實例中,您可以看到 val2 = 0 和 val1 = (IAdviseSink*)這個。 (請注意,編譯程式已省略最終 add eax, 0 指令,因為它沒有任何效果。)
7151720e 394508 cmp [ebp+0x8],eax ; pSink == (IAdviseSink*)this?
71517211 750b jnz No_SetAdvise (7151721e) ; jump if not equal
本節稍早,您會將 EDI 設定為 m_pvo 成員的位址。 您現在將開始使用它。 您先前也將 ECX 暫存器清零。
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:
所有這些 COM 方法的呼叫應該都看起來非常熟悉。
接下來兩個陳述的評估會被交錯執行。 別忘了 EBX 包含 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
以下是更多的 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
將記憶體位置與零進行 AND 和將其設定為零是相同的,因為任何與零進行 AND 的結果都是零。 編譯程式會使用此窗體,因為即使速度較慢,也比對等 的mov 指令短得多。 (此程式代碼已針對大小優化,而非速度。
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();
若要呼叫 CancelPendingActions,您必須將 (ViewState*)this 移至 (CUserView*)this。 另請注意, CancelPendingActions 會使用__thiscall呼叫慣例,而不是__stdcall。 根據 __thiscall,「this」指標會透過 ECX 寄存器傳遞,而不是在堆疊上傳遞。
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;
請記住,EDI 仍然為零,EBX 仍然是 &m_pszTitle,因為函數調用會保存這些暫存器。
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]
請注意,您不再需要 「this」 的值,因此編譯程式會使用 add 指令來就地修改它,而不是使用另一個緩存器來保存位址。 這實際上是因為 Pentium u/v 管線的緣故,帶來了效能提升,因為 v 管線可以進行算術運算,但無法處理地址計算。
return S_OK;
715172be 33c0 xor eax,eax ; eax = S_OK
最後,您要還原必須保留的緩存器、清除堆疊,然後返回呼叫端,移除傳入參數。
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