注釈付き x86 逆アセンブリ

次のセクションでは、逆アセンブリの例について説明します。

ソース コード

分析対象関数のコードを次に示します。

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

上の 2 つの命令の後、パラメーターには次のようにアクセスできます。

[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

この関数は、2 つのローカル スタック変数のみを必要とするので、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 にあります。 したがって、this は ViewState クラスへのポインターですが、別の基底クラスと混乱する可能性がある場合は、(ViewState*)this としてより慎重に指定されます。

    if (m_fDestroyed)
7151713e 33ff             xor     edi,edi           ; edi = 0

レジスタをそれ自体で XORing することは、それをゼロにする標準的な方法です。

71517140 39beac000000     cmp     [esi+0xac],edi    ; this->m_fDestroyed == 0?
71517146 7407             jz      NotDestroyed (7151714f)  ; jump if equal

cmp 命令では、2 つの値を (減算して) 比較します。 jz 命令は、結果が 0 かどうかをチェックします。ゼロなら、比較された 2 つの値が等しいことになります。

cmp 命令は、2 つの値を比較します。後続の 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);

これら 2 つのコード行の実行はインターリーブされるので、注意してください。

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 の呼び出しでは変更されませんでした)。 したがって、値「0」が保持されるので、それを使用して「0」の有無をすばやくテストできます。

        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 メソッド呼び出しは非常に一般的であるため、それらを学習して認識できるようにしておくことをお勧めします。 具体的には、次の 3 つの IUnknown メソッドを Vtable オフセットから直接認識できる必要があります: 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

globals を介した間接呼び出しは、関数のインポートを Microsoft Win32 で実装する方法です。 ローダーは、ターゲットの実際のアドレスを指すように globals を修正します。 これは、クラッシュしたマシンを調査するときに何をすべきなのかを知るための便利な方法です。 インポートされた関数の呼び出しとターゲット内の呼び出しを探します。 通常、インポートされた関数の名前はわかっているはずです。これを使用して、ソース コード内のどこにいるのかを判断できます。

        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 からの正のオフセットに到達し、ローカル変数は負のオフセットに配置されます。 ただし、この場合と同様に、コンパイラはそのメモリを任意の目的に自由に再利用できます。

プログラマーが、細心の注意を払っている場合は、コンパイラがこのコードをもう少し改善できた可能性があることがわかるでしょう。 コンパイラは、lea edi、[esi+0xa8] 命令を 2 つの 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

neg r は、r が 0 以外の場合は、キャリー フラグを設定しますが、これは neg が 0 から減算して値をネゲートするためです。 また、ゼロから減算すると、0 以外の値を減算した場合に、ボローが生成されます (または キャリー フラグを設定します)。 また、r レジスタの値が破損されますが、いずれにしろ上書きしようとしている値なので許容されます。

次に、sbb r, r 命令は、それ自体から値を減算し、結果は常にゼロになります。 ただし、また、キャリー (ボロー) ビットも減算されるため、実際の結果は r を「0」または「-1」に設定することになります。どちらになるのかは、それぞれ、キャリーがクリアされるのかそれとも設定されるのかで決まります。

したがって、sbb r, r は、r の元の値が 0 の場合は r を「0」に設定し、元の値が 0 以外の場合は「-1」に設定します。

3 番目の命令はマスクを実行します。 r レジスタは 0 または -1 であるため、"this" は r をゼロのままにするか、r を -1 から (val1 - val1) に変更します。つまり、任意の値を -1 と AND すると、元の値が残ることになります。

したがって、"and r, (val1 - val1)" の結果は、r の元の値が 0 の場合は r を「0」に設定し、r の元の値が 0 以外の場合は 「(val1 - val2)」に設定します。

最後に、val2r に追加すると、val2 または (val1 - val2) + val2 = val1 になります。

したがって、この一連の命令の最終的な結果は、r の値が元々 0 の場合は r が val2 に設定され、0 以外の場合は val1 に設定されることになります。 これは、r = r ? val1 : val2 に相当するアセンブリです。

この特定のインスタンスでは、val2 = 0val1 = (IAdviseSink*)this を確認できます。 (効果がないため、コンパイラが最後の 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 メソッド呼び出しは、よく知られているはずです。

次の 2 つのステートメントの評価は、インターリーブされます。 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 では、__stdcall ではなく __thiscall 呼び出し規則が使用されることにも注意してください。 __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 は 0 であり、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 命令を使用して "this" の値を変更します。 これは実際には 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