青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

兔子的技術(shù)博客

兔子

   :: 首頁(yè) :: 聯(lián)系 :: 聚合  :: 管理
  202 Posts :: 0 Stories :: 43 Comments :: 0 Trackbacks

留言簿(10)

最新評(píng)論

閱讀排行榜

評(píng)論排行榜

一種實(shí)現(xiàn)Win32窗口過(guò)程函數(shù)(Window Procedure)的新方法

基于Thunk實(shí)現(xiàn)的類成員消息處理函數(shù)

JERKII.SHANG (JERKII@HOTMAIL.COM)

MAR.10th - 31st, 2006

Windows是一個(gè)消息驅(qū)動(dòng)的操作系統(tǒng),在系統(tǒng)中發(fā)生的所有消息均需要通過(guò)消息處理過(guò)程(或叫窗口過(guò)程)進(jìn)行處理。由于C++給我們?cè)诔绦蛟O(shè)計(jì)中帶來(lái)更多的靈活性(如繼承、重載、多態(tài)等),所以我們都希望能夠使用C++的類來(lái)封裝Windows中的窗口過(guò)程函數(shù),但是Windows規(guī)定了窗口過(guò)程函數(shù)必須定義為一個(gè)全局函數(shù),也就是說(shuō)需要使用面向過(guò)程的方法來(lái)實(shí)現(xiàn),為了使用面向?qū)ο蟮募夹g(shù)來(lái)實(shí)現(xiàn)消息處理,我們必須另辟它徑。目前我們?cè)诰W(wǎng)絡(luò)上見得比較多的方式是使用Thunk將即將傳遞給窗口過(guò)程的第一個(gè)參數(shù)(HWND hWnd)的值使用類對(duì)象的內(nèi)存地址(即this指針)進(jìn)行替換(ATL使用的也是這種方法)。這樣,在相應(yīng)的窗口過(guò)程中通過(guò)將hWnd強(qiáng)制轉(zhuǎn)換成類對(duì)象的指針,這樣就可以通過(guò)該指針調(diào)用給類中的成員函數(shù)了。但是該方法仍然需要將該消息處理函數(shù)定義成一個(gè)靜態(tài)成員函數(shù)或者全局函數(shù)。本文將介紹一種完全使用(非靜態(tài))類成員函數(shù)實(shí)現(xiàn)Win32的窗口過(guò)程函數(shù)和窗口過(guò)程子類化的新方法。雖然也是基于Thunk,但是實(shí)現(xiàn)方法完全不同于之前所說(shuō)的那種,我所采用的是方法是——通過(guò)對(duì)Thunk的調(diào)用,將類對(duì)象的this指針直接傳遞給在類中定義的窗口處理函數(shù)(通過(guò)ECX或棧進(jìn)行傳遞),這樣就能夠使Windows直接成功地調(diào)用我們窗口過(guò)程函數(shù)了。另外,本文介紹一種使用C++模板進(jìn)行消息處理函數(shù)的“重載”,這種方法直接避免了虛函數(shù)的使用,因此所有基類及其派生類中均無(wú)虛函數(shù)表指針以及相應(yīng)的虛函數(shù)表(在虛函數(shù)較多的情況下,該數(shù)組的大小可是相當(dāng)可觀的)。從而為每個(gè)類的實(shí)例“節(jié)省”了不少內(nèi)存空間(相對(duì)于使用傳統(tǒng)的函數(shù)重載機(jī)制)。

關(guān)鍵字: C++ 模板,調(diào)用約定,Thunk,機(jī)器指令(編碼),內(nèi)嵌匯編
環(huán)境:VC7,VC8,32位Windows

內(nèi)容

前言

也許你是一位使用MFC或ATL進(jìn)行編程的高手,并且能在很短的時(shí)間內(nèi)寫出功能齊全的程序。但是,你是否曾經(jīng)花時(shí)間去想過(guò)“MFC或ATL是通過(guò)什么樣的途徑來(lái)調(diào)用我們的消息處理函數(shù)的呢?他們是怎樣將Windows產(chǎn)生的消息事件傳遞給我們的呢?”在MFC中定義一個(gè)從CWnd繼承而來(lái)的類,相應(yīng)的消息事件就會(huì)發(fā)送到我們定義的類中來(lái),你不覺得這背后所隱藏的一切很奇怪嗎?如果你的感覺是這樣,那么本文將使用一種簡(jiǎn)單并且高效的方法來(lái)揭開這個(gè)神秘的面紗以看個(gè)究竟,同時(shí)我將非常詳細(xì)地介紹需要使用到的各種知識(shí),以便能讓更多初學(xué)者更容易掌握這些知識(shí)。

在Windows中,所有的消息均通過(guò)窗口過(guò)程函數(shù)進(jìn)行處理,窗口過(guò)程函數(shù)是我們和Windows操作系統(tǒng)建立聯(lián)系的唯一途徑,窗口過(guò)程函數(shù)的聲明均為:

LRESULT __stdcall WndProc
    (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

MSDN有對(duì)該函數(shù)中的每個(gè)參數(shù)的詳細(xì)描述,如果你現(xiàn)在仍然對(duì)該函數(shù)存在疑問(wèn),那么請(qǐng)先參照MSDN中的相關(guān)內(nèi)容后,再繼續(xù)閱讀本文。通常,Windows均要求我們將消息處理函數(shù)定義為一個(gè)全局函數(shù),或者是一個(gè)類中的靜態(tài)成員函數(shù)。并且該函數(shù)必須采用__stdcall的調(diào)用約定(Calling Convention),因?yàn)開_stdcall的函數(shù)在返回前會(huì)自己修正ESP的值(由于參數(shù)傳遞所導(dǎo)致的ESP的改變),從而使ESP的值恢復(fù)到函數(shù)調(diào)用之前的狀態(tài)。

全局函數(shù)或靜態(tài)成員函數(shù)的使用使得我們很難發(fā)揮C++的優(yōu)勢(shì)(除非你一直想使用C進(jìn)行Windows程序開發(fā),如果是這樣,那么你也就沒有必要再繼續(xù)閱讀本文了),因?yàn)樵谶@種結(jié)構(gòu)下(即面向過(guò)程),我們不能很方便地在消息處理函數(shù)中使用我們的C++對(duì)象,因?yàn)樵谶@樣的消息處理函數(shù)中,我們很難得到我們對(duì)象的指針,從而導(dǎo)致我們不能很方便的操作C++對(duì)象中的屬性。為了解決對(duì)Win32的封裝,Microsoft先后推出了MFC和ATL,可以說(shuō)兩者都是非常優(yōu)秀的解決方案,并且一直為多數(shù)用戶所使用,為他們?cè)赪indows下的程序開發(fā)提供了很大的便利。

但是,MFC在我們大家的眼里都是一種比較笨重的方法,使用MFC開發(fā)出來(lái)的程序都必須要在MFC相關(guān)動(dòng)態(tài)庫(kù)的支持下才能運(yùn)行,并且這些動(dòng)態(tài)庫(kù)的大小可不一般(VS2005中的mfc80.dll就有1.04M),更為甚者,CWnd中包含大量的虛函數(shù),所以每個(gè)從他繼承下來(lái)的子類都有一個(gè)數(shù)量相當(dāng)可觀的虛函數(shù)表(雖然通過(guò)在存在虛函數(shù)的類上使用sizeof得到的結(jié)果是該類對(duì)象的大小只增長(zhǎng)了4個(gè)字節(jié),即虛函數(shù)表指針,但是該指針?biāo)赶虻奶摵瘮?shù)數(shù)組同樣需要內(nèi)存空間來(lái)存儲(chǔ)。一個(gè)虛函數(shù)在虛函數(shù)表中需占用4個(gè)字節(jié),如果在我們的程序中用到較多的CWnd的話,定會(huì)消耗不少內(nèi)存(sizeof(CWnd) = 84,sizeof(CDialog) = 116)。ATL與MFC完全不同,ATL采用模板來(lái)對(duì)Win32中的所有內(nèi)容進(jìn)行封裝,使用ATL開發(fā)出來(lái)的程序不需要任何其他動(dòng)態(tài)庫(kù)的支持(當(dāng)然,除基本的Windows庫(kù)外),ATL使用Thunk將C++對(duì)象的指針通過(guò)消息處理函數(shù)的第一個(gè)參數(shù)(即hWnd)傳入,這樣,在消息處理函數(shù)中,我們就可以很方便地通過(guò)該指針來(lái)訪問(wèn)C++對(duì)象中的屬性和成員函數(shù)了,但這種方法也必須要借助幾個(gè)靜態(tài)成員函數(shù)來(lái)實(shí)現(xiàn)。

本文將采用這幾種技術(shù)實(shí)現(xiàn)一種完全由C++類成員函數(shù)(非靜態(tài))實(shí)現(xiàn)的Windows消息處理機(jī)制,并最終開發(fā)一個(gè)封裝Windows消息處理的工具包(KWIN)。首先讓我們來(lái)看看怎樣使用KWIN來(lái)開發(fā)的一個(gè)簡(jiǎn)單程序:

創(chuàng)建一個(gè)簡(jiǎn)單的窗口程序: 

#include "kwin.h"
class MyKWinApp : public KWindowImpl<MyKWinApp>
{
public:
    MyKWinApp () : KWindowImpl<MyKWinApp> ("MyKWinAppClassName") 
    {}

    /* Overriede the window procdure */
    LRESULT KCALLBACK KWndProc (
        HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {   /* Do anthing you want here */
        return __super::KWndProc (hWnd, msg, wParam, lParam);
    }

    BOOL OnDestroy () { PostQuitMessage (0); return TRUE; }
    /* Override other message handler */
};

INT __stdcall WinMain (
    HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    MyKWinApp kapp;
    kapp.CreateOverlappedWindow ("This is my first KWinApp");

    MSG msg;
    while (GetMessage (&msg, 0, 0, 0))
    {   TranslateMessage (&msg);
	DispatchMessage (&msg);
    }
    return msg.wParam;
}

創(chuàng)建一個(gè)簡(jiǎn)單的對(duì)話框程序:

#include "kwin.h"
class MyKDlgApp : public KDialogImpl<MyKDlgApp>
{
public:
    enum {IDD = IDD_DLG_MYFIRSTDIALOG };
    BOOL OnCommand(WORD wNotifyCode, WORD wId, HWND hWndCtrl) 
    {   if (wId == IDOK) EndDialog (m_hWnd, wId); return TRUE; }
};

INT __stdcall WinMain (
    HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    MyKDlgApp kapp;
    kapp.DoModal ();
    return 0;
}

怎么樣?使用KWIN開發(fā)包后,你的程序結(jié)構(gòu)是不是變得更加清晰了呢?你可以在你的類中(如MyKWinApp或MyKDlgApp)“重載”更多的消息處理函數(shù),你甚至可以“重載”窗口過(guò)程函數(shù)(如MyKWinApp)。這里的重載跟通常意義上的重載可不是一個(gè)意思,這里的“重載”是使用C++模板機(jī)制實(shí)現(xiàn)的,而傳統(tǒng)意義上的重載需要通過(guò)虛函數(shù)(更準(zhǔn)確地說(shuō)應(yīng)該是虛函數(shù)表)來(lái)實(shí)現(xiàn)。好,你現(xiàn)在是不是很想知道,這個(gè)KWIN的內(nèi)部到底是怎樣實(shí)現(xiàn)的了吧?好,讓我來(lái)一步一步帶你步入KWIN的內(nèi)部中去吧,現(xiàn)在你唯一需要的就是耐心,因?yàn)檫@篇文章寫得似乎太長(zhǎng)了些^_^ ...

傳統(tǒng)的C++重載

通常,如果我們要重載父類中的方法,那么我們必須在父類中將該方法聲明為虛函數(shù),這樣每個(gè)擁有虛函數(shù)的類對(duì)象將會(huì)擁有一個(gè)自己的虛函數(shù)表(指針),也就是說(shuō),該虛函數(shù)表(數(shù)組)就成了該類對(duì)象的“‘靜態(tài)’成員變量(之所以說(shuō)是‘靜態(tài)’的,是因?yàn)樵撎摵瘮?shù)數(shù)組及其指向該數(shù)組的指針在該類的所有實(shí)例中均為同一份數(shù)據(jù))”了,C++(更確切地說(shuō)應(yīng)該是編譯器)就是通過(guò)虛函數(shù)表來(lái)實(shí)現(xiàn)函數(shù)的重載機(jī)制的,因?yàn)橛辛颂摵瘮?shù)表后,對(duì)虛函數(shù)的調(diào)用就是通過(guò)該虛函數(shù)表來(lái)完成的,因?yàn)榫幾g器在生成代碼的時(shí)候會(huì)根據(jù)每個(gè)虛函數(shù)在類中的位置而對(duì)其進(jìn)行編號(hào),并且通過(guò)該序號(hào)來(lái)對(duì)虛函數(shù)進(jìn)行調(diào)用,所以你通常會(huì)在反匯編中看到如下代碼:

mov edx, this pointer        ; Load EAX with the value of 'this pointer'(or vptr)
mov edx, dword ptr [edx + 4] ; Get the address of the second virtual function in vtable
push ...                     ; Pass argument 1 
push ...                     ; Pass other arguments here ...
call edx                     ; Call virtual function

當(dāng)子類從有虛函數(shù)的父類中派生時(shí),他將擁有一份獨(dú)立的虛函數(shù)表指針以及虛函數(shù)數(shù)組,并且“開始時(shí)”該數(shù)組中的內(nèi)容和基類一模一樣。但是,當(dāng)編譯器在編譯時(shí)檢測(cè)到子類重載了父類中的虛函數(shù)時(shí),編譯器就會(huì)修改子類的虛函數(shù)數(shù)組表,將該表中被重載的虛函數(shù)的地址改為子類中的函數(shù)地址,對(duì)于那些沒有被重載的虛函數(shù),該表中的函數(shù)地址和父類的虛函數(shù)表中的地址一樣!當(dāng)一個(gè)子類從多個(gè)帶有虛函數(shù)的父類中繼承時(shí),該子類就擁有多個(gè)虛函數(shù)表指針了(也就是將擁有多個(gè)虛函數(shù)指針數(shù)組),當(dāng)我們?cè)谶@種情況下進(jìn)行指針的轉(zhuǎn)換的時(shí)(通常為將子類指針轉(zhuǎn)換成父類指針),所進(jìn)行最終操作的就是虛函數(shù)數(shù)組指針的轉(zhuǎn)換。如:

class A : 
    public VBase1, 
    public VBase2, 
    public VBase3 
    /* VBase1, VBase2, VBase3 均為存在虛函數(shù)的父類 */
{ ... };

A a;
VBase1* p1;
VBase2* p2;
VBase3* p3;
p1 = &a;
p2 = &a;
p3 = &a;
 // 假定這里a的地址為0x0012f564,那么(按字節(jié)為單位)
p1 = &a + 0 = 0x0012f564,
p2 = &a + 4 = 0x0012f568,
p3 = &a + 8 = 0x0012f56C

因?yàn)樵陬悓?duì)象的內(nèi)存布局中,編譯器總是將虛函數(shù)數(shù)組指針?lè)旁谄茷?的地方。

好了,似乎我們已經(jīng)跑題了,關(guān)于這方面的知識(shí)在網(wǎng)上也可以找到很多,如果你有興趣,可以參見我的另一篇文章:略談虛函數(shù)的調(diào)用機(jī)制,至此,我相信你已經(jīng)對(duì)C++中的重載機(jī)制有一定的認(rèn)識(shí)了吧,現(xiàn)在再讓我們來(lái)看看怎樣在C++中使用模板來(lái)實(shí)現(xiàn)函數(shù)的“重載”。

使用C++模板實(shí)現(xiàn)函數(shù)的“重載”

通過(guò)我們對(duì)周圍事物(或數(shù)據(jù))的抽象,我們得到類。如果我們?cè)賹?duì)類進(jìn)行抽象,得到就是一個(gè)模板。模板是一種完全基于代碼級(jí)的重用機(jī)制,同時(shí)也為我們編寫一些結(jié)構(gòu)良好,靈活的程序提供了手段,所有的這一切都得歸功于編譯器的幫助,因?yàn)榫幾g器最終會(huì)將我們所有的這些使用這些高級(jí)技術(shù)編寫的代碼轉(zhuǎn)換成匯編代碼(或者應(yīng)該說(shuō)是機(jī)器碼),好,廢話少說(shuō)為好!^_^。

通常我們會(huì)使用下面的方式來(lái)實(shí)現(xiàn)函數(shù)的“重載”,這里似乎沒有很復(fù)雜的“理論”要闡述,我就簡(jiǎn)單的把大致實(shí)現(xiàn)過(guò)程描述一下吧:

template <class T> class TBase
{
public:
    void foo1 () { printf ("This is foo1 in TBase\n"); }
    void foo2 () { printf ("This is foo2 in TBase\n"); }

    void callback ()
    {	/* 如果子類重載了TBase中的函數(shù),通過(guò)pThis將會(huì)直接調(diào)用子類中的函數(shù) */
        T* pThis = static_cast<T *> (this); 
        pThis->foo1 ();
        pThis->foo2 ();
    }
};

class TDerive : public TBase <TDerive>
{
public:
    void foo1 () { printf ("This is foo1 in TDerive\n"); } /* “重載”父類中的foo1 */
};

TDerive d;
d.callback ();
輸出結(jié)果為:
This is foo1 in TDerive
This is foo2 in TBase

雖然上面的代碼看起來(lái)很奇怪,因?yàn)樽宇悓⒆约鹤鳛閰?shù)傳遞給父類。并且子類中定義的“重載”函數(shù)只能通過(guò)“回調(diào)”的方式才能被調(diào)用,但這對(duì)Windows中的消息處理函數(shù)來(lái)說(shuō)無(wú)疑是一大福音,因?yàn)閃indows中的消息函數(shù)均是回調(diào)函數(shù),似乎這樣的“重載”就是為Windows中的消息處理函數(shù)而定制的。雖然有些奇怪,但是他真的很管用,尤其是在實(shí)現(xiàn)我們的消息處理函數(shù)的時(shí)候。不是嗎?

C++對(duì)象中的屬性和方法(成員函數(shù))

我們通常會(huì)將一些相關(guān)的屬性以及操作這些屬性的方法“封裝”到一個(gè)類中,從而實(shí)現(xiàn)所謂的信息(屬性)隱藏,各類對(duì)象(或類的實(shí)例)之間的交互都得通過(guò)成員函數(shù)來(lái)進(jìn)行,這些屬性的值在另一方面也表明了該類對(duì)象在特定時(shí)刻的狀態(tài)。由于每個(gè)類對(duì)象都擁有各自的屬性(或狀態(tài)),所以系統(tǒng)需要為每個(gè)類對(duì)象分配相應(yīng)的屬性(內(nèi)存)存儲(chǔ)空間。但是類中的成員函數(shù)卻不是這樣,他們不占用類對(duì)象的任何內(nèi)存空間。這就是為什么使用sizeof (your class)總是返回該類中的成員變量的字節(jié)大小(如果該類中存在虛函數(shù),則還會(huì)多出4個(gè)字節(jié)的虛函數(shù)數(shù)組指針來(lái))。因?yàn)槌蓡T函數(shù)所要操作的只是類對(duì)象中的屬性,他們所關(guān)心的不是這些屬性的值。

那么,這些類成員函數(shù)又是怎么知道他要去操作哪個(gè)類對(duì)象中的屬性呢?答案就是通過(guò)this指針。this指針說(shuō)白了就是一個(gè)指向該類對(duì)象在創(chuàng)建之后位于內(nèi)存中的內(nèi)存地址。當(dāng)我們調(diào)用類中的成員函數(shù)時(shí),編譯器會(huì)“悄悄地”將相應(yīng)類對(duì)象的內(nèi)存地址(也就是this指針)傳給我們的類成員函數(shù),有了這個(gè)指針,我們就可以在類成員函數(shù)中對(duì)該類對(duì)象中的屬性進(jìn)行訪問(wèn)了。this指針被“傳入”成員函數(shù)的方式主要取決于你的類成員函數(shù)在聲明時(shí)所使用的調(diào)用約定,如果使用__thiscall調(diào)用約定,那么this指針將通過(guò)寄存器ECX進(jìn)行傳遞,該方式通常是編譯器(VC)在缺省情況下使用的調(diào)用約定。如果是__stdcall或__cdecl調(diào)用約定,this指針將通過(guò)棧進(jìn)行傳遞,并且this指針將是最后一個(gè)被壓入棧的參數(shù),雖然我們?cè)诼暶鞒蓡T函數(shù)時(shí),并沒有聲明有這個(gè)參數(shù)。

關(guān)于調(diào)用約定與this指針的傳遞

簡(jiǎn)單說(shuō)來(lái),調(diào)用約定(Calling Convention)主要是用來(lái)指定相應(yīng)的函數(shù)在被調(diào)用時(shí)的參數(shù)傳遞順序,以及在調(diào)用完成后由誰(shuí)(調(diào)用者還是被調(diào)用者)來(lái)修正ESP寄存器的值(因?yàn)檎{(diào)用者向被調(diào)用者通過(guò)棧來(lái)傳遞參數(shù)時(shí),ESP的值會(huì)被修改,系統(tǒng)必須要能夠保證被調(diào)用函數(shù)返回后,ESP的值要能夠恢復(fù)到調(diào)用之前的值,這樣調(diào)用者才能正確的運(yùn)行下去,對(duì)于其他寄存器,編譯器通常會(huì)自動(dòng)為我們生成相應(yīng)的保存與恢復(fù)代碼,通常是在函數(shù)一開始將相關(guān)寄存器的值PUSH到棧中,函數(shù)返回之前再依次pop出來(lái))。

通常對(duì)ESP的修正有兩種方式,一種是直接使用ADD ESP, 4*n,一種是RET 4*n(其中n為調(diào)用者向被調(diào)用者所傳遞的參數(shù)個(gè)數(shù),乘4是因?yàn)樵跅V械拿總€(gè)參數(shù)需要占用4個(gè)字節(jié),因?yàn)榫幾g器為了提高尋址效率,會(huì)將所有參數(shù)轉(zhuǎn)換成32位,即即使你傳遞一個(gè)字節(jié),那么同樣會(huì)導(dǎo)致ESP的值減少4)。通常我們使用的調(diào)用約定主要有__stdcall,__cdecl,__thiscall,__fastcall。有關(guān)這些調(diào)用約定的詳細(xì)說(shuō)明,請(qǐng)參見MSDN(節(jié)Argument Passing and Naming Conventions )。這里只簡(jiǎn)略地描述他們的用途:幾乎所有的Windows API均使用__stdcall調(diào)用約定,ESP由被調(diào)用者函數(shù)自行修正,通常在被調(diào)用函數(shù)返回之前使用RET 4 * n的形式。__cdecl調(diào)用約定就不一樣,它是由調(diào)用者來(lái)對(duì)ESP進(jìn)行修正,即在被調(diào)用函數(shù)返回后,調(diào)用者采用ADD ESP, 4 *n的形式進(jìn)行棧清除,通常你會(huì)看到這樣的代碼:

push argument1
push argument2
call _cdecl_function
add esp, 8

另外一個(gè)__cdecl不得不說(shuō)的功能是,使用__cdecl調(diào)用約定的函數(shù)可以接受可變數(shù)量的參數(shù),我們見得最多的__cdecl函數(shù)恐怕要算printf了(int __cdecl printf  (char *format, ...)),瞧!是不是很cool啊。因?yàn)開_cdecl是由調(diào)用者來(lái)完成棧的清除操作,并且他自己知道自己向被調(diào)用函數(shù)傳遞了多少參數(shù),此次他自己也知道該怎樣去修正ESP。跟__stdcall比起來(lái),唯一的“缺點(diǎn)”恐怕就是他生成的可執(zhí)行代碼要比__stdcall稍長(zhǎng)些(即在每個(gè)CALL之后,都需要添加一條ADD ESP, X的指令)。但跟他提供給我們的靈活性來(lái)說(shuō),這點(diǎn)“缺點(diǎn)”又算什么呢?

__thiscall主要用于類成員函數(shù)的調(diào)用約定,在VC中它是成員函數(shù)的缺省調(diào)用約定。他跟__stdcall一樣,由被調(diào)用者清除調(diào)用棧。唯一不同的恐怕就是他們對(duì)于this指針的傳遞方式了吧!在__stdcall和__cdecl中,this指針通過(guò)棧傳到類成員函數(shù)中,并且被最后一個(gè)壓入棧。而在__thiscall中,this指針通過(guò)ECX進(jìn)行傳遞,直接通過(guò)寄存器進(jìn)行參數(shù)傳遞當(dāng)然會(huì)得到更好的運(yùn)行效率。另外一種,__fastcall,之所以叫fast,是因?yàn)槭褂眠@種調(diào)用約定的函數(shù),調(diào)用者會(huì)“盡可能”的將參數(shù)通過(guò)寄存器的方式進(jìn)行傳遞。另外,編譯器將為每種調(diào)用約定的函數(shù)產(chǎn)生不同的命名修飾(即Name-decoration convention),當(dāng)然這些只是編譯器所關(guān)心的東西,我們就不要再深入了吧!。

通過(guò)其他途徑調(diào)用類中的成員函數(shù)

對(duì)于給定的一個(gè)類:

class MemberCallDemo
{
public:
    void __stdcall foo (int a) { printf ("In MemberCallDemo::foo, a = %d\n", a); };
};

通常我們會(huì)通過(guò)下面的方式進(jìn)行調(diào)用該類中的成員方法foo:

MemberCallDemo mcd;
mcd.foo (9);
或者通過(guò)函數(shù)指針的方式:
void (__stdcall MemberCallDemo::*foo_ptr)(int) = &MemberCallDemo::foo;
(mcd.*foo_ptr) (9);

我總是認(rèn)為這中使用成員函數(shù)指針的調(diào)用方式(或是語(yǔ)法)感到很奇怪,不過(guò)它畢竟是標(biāo)準(zhǔn)的并且能夠?yàn)镃++編譯器認(rèn)可的調(diào)用方式。幾乎在所有編譯器都不允許將成員函數(shù)的地址直接賦給其他類型的變量(如DWORD等,即使使用reinterpret_cast也無(wú)濟(jì)于事)例如:而只能將其賦給給與該成員函數(shù)類型聲明(包括所使用的調(diào)用約定,返回值,函數(shù)參數(shù))完全相同的變量。因?yàn)槌蓡T函數(shù)的聲明中都有一個(gè)隱藏的this指針,該指針會(huì)通過(guò)ECX或棧的方式傳遞到成員函數(shù)中,為了成員函數(shù)被安全調(diào)用,編譯器禁止此類型的轉(zhuǎn)換也在情理當(dāng)中。但有時(shí)我們?yōu)榱藢?shí)現(xiàn)特殊的目的需要將成員函數(shù)的地址直接賦給一個(gè)DWORD變量(或其他任何能夠保存一個(gè)指針值的變量,通常只要是一個(gè)32位的變量即可),我們可以通過(guò)下面兩種方法來(lái)實(shí)現(xiàn):下面的這幾種試圖將一個(gè)成員函數(shù)的地址保存到一個(gè)DWORD中都將被編譯器認(rèn)為是語(yǔ)法錯(cuò)誤: 我總是認(rèn)為這中使用成員函數(shù)指針的調(diào)用方式(或是語(yǔ)法)感到很奇怪,不過(guò)它畢竟是標(biāo)準(zhǔn)的并且能夠?yàn)镃++編譯器認(rèn)可的調(diào)用方式。幾乎在所有編譯器都不允許將成員函數(shù)的地址直接賦給其他類型的變量(如DWORD等,即使使用reinterpret_cast也無(wú)濟(jì)于事)。下面的這幾種試圖將一個(gè)成員函數(shù)的地址保存到一個(gè)DWORD中都將被編譯器認(rèn)為是語(yǔ)法錯(cuò)誤:

DWORD dwFooAddrPtr= 0;
dwFooAddrPtr = (DWORD) &MemberCallDemo::foo; /* Error C2440 */
dwFooAddrPtr = reinterpret_cast<DWORD> (&MemberCallDemo::foo); /* Error C2440 */

因?yàn)槌蓡T函數(shù)的聲明中都有一個(gè)隱藏的this指針,該指針會(huì)通過(guò)ECX或棧的方式傳遞到成員函數(shù)中,為了成員函數(shù)被安全調(diào)用,編譯器禁止此類型的轉(zhuǎn)換也在情理當(dāng)中。但有時(shí)我們?yōu)榱藢?shí)現(xiàn)特殊的目的需要將成員函數(shù)的地址直接賦給一個(gè)DWORD變量(或其他任何能夠保存一個(gè)指針值的變量,通常只要是一個(gè)32位的變量即可)。

我們只能將成員函數(shù)的地址賦給給與該成員函數(shù)類型聲明(包括所使用的調(diào)用約定,返回值,函數(shù)參數(shù))完全相同的變量(如前面的void (__stdcall MemberCallDemo::*foo_ptr)(int) = &MemberCallDemo::foo)。因?yàn)槌蓡T函數(shù)的聲明中都有一個(gè)隱藏的this指針,該指針會(huì)通過(guò)ECX或棧的方式傳遞到成員函數(shù)中,為了成員函數(shù)被安全調(diào)用,編譯器禁止此類型的轉(zhuǎn)換也在情理當(dāng)中。但有時(shí)我們?yōu)榱藢?shí)現(xiàn)特殊的目的需要將成員函數(shù)的地址直接賦給一個(gè)DWORD變量(或其他任何能夠保存一個(gè)指針值的變量,通常只要是一個(gè)32位的變量即可),就像我們即將介紹的情況。

通過(guò)前面幾節(jié)的分析我們知道,成員函數(shù)的調(diào)用和一個(gè)普通的非成員函數(shù)(如全局函數(shù),或靜態(tài)成員函數(shù)等)唯一不同的是,編譯器會(huì)在背后“悄悄地”將類對(duì)象的內(nèi)存地址(即this指針)傳到類成員函數(shù)中,具體的傳遞方式以該類成員函數(shù)所采用的調(diào)用約定而定。所以,是不是只要我們能夠手動(dòng)地將這個(gè)this指針傳遞給一個(gè)成員函數(shù)(這是應(yīng)該是一個(gè)函數(shù)地址),是不是就可以使該成員函數(shù)被正確調(diào)用呢?答案是肯定的,但是我們迫在眉睫需要解決的是怎樣才能得到這個(gè)成員函數(shù)的地址呢?通常,我們有兩種方法可以達(dá)到此目的:

1。使用內(nèi)嵌匯編(在VC6及以前的版本中將不能編譯通過(guò))

DWORD dwFooAddrPtr = 0;
__asm
{
   /* 得到MemberCallDemo::foo偏移地址,事實(shí)上就是該成員函數(shù)的內(nèi)存地址(起始地址) */
   MOV EAX, OFFSET MemberCallDemo::foo 
   MOV DWORD PTR [dwFooAddrPtr], EAX
}

這種方法雖然看起來(lái)甚是奇怪,但是他卻能夠解決我們所面臨的問(wèn)題。雖然在目前的應(yīng)用程序開發(fā)中,很少甚至幾乎沒有人使用匯編語(yǔ)言去開發(fā),但是,往往有時(shí)一段小小的匯編代碼居然能夠解決我們使用其他方法不能解決的問(wèn)題,這在上面的例子和下面即將介紹的Thunk中大家就會(huì)看到他那強(qiáng)有力的問(wèn)題解決能力。所以說(shuō)我們不能將匯編仍在一邊,我們需要了解她,并且能夠在適當(dāng)?shù)臅r(shí)候使用她。畢竟她始終是一個(gè)最漂亮,最具征服力的編程語(yǔ)言。^_^

2。通過(guò)使用union來(lái)“欺騙”編譯器

或使用一種更為巧妙的方法,通過(guò)使用一個(gè)union數(shù)據(jù)結(jié)構(gòu)進(jìn)行轉(zhuǎn)換(Stroustrup在其《The C++ Programming Language》中講到類似方法),由于在union數(shù)據(jù)結(jié)構(gòu)中,所有的數(shù)據(jù)成員均享用同一內(nèi)存區(qū)域,只是我們?yōu)檫@一塊內(nèi)存區(qū)域賦予了不同的類型及名稱,并且我們修改該結(jié)構(gòu)中的任何“變量”都會(huì)導(dǎo)致其他所有“變量”的值均被修改。所以我們可以使用這種方法來(lái)“欺騙”編譯器,從而讓他認(rèn)為我們所進(jìn)行的“轉(zhuǎn)換”是合法的。

template <class ToType, class FromType>
ToType union_cast (FromType f)
{
    union 
    {    FromType _f;
         ToType   _t;
    } ut;
    ut._f = f;
    return ut._t;
}
DWORD dwAddrPtr = union_cast<DWORD> (&YourClass::MemberFunction);

怎么樣,這樣的類型轉(zhuǎn)換是不是很酷啊?就像使用reinterpret_cast和static_cast等之類的轉(zhuǎn)換操作符一樣。通過(guò)巧妙地使用union的特點(diǎn)輕松“逃”過(guò)編譯器的類型安全檢查這一關(guān),從而達(dá)到我們的數(shù)據(jù)轉(zhuǎn)換目的。當(dāng)然,我們通常不會(huì)這樣做,因?yàn)檫@樣畢竟是類型不安全的轉(zhuǎn)換,他只適用于特定的非常規(guī)的(函數(shù)調(diào)用)場(chǎng)合。

好,我們現(xiàn)在已經(jīng)得到了該成員函數(shù)的內(nèi)存地址了,下買面我們通過(guò)一個(gè)“更為奇怪的”方式來(lái)調(diào)用成員函數(shù)MemberCallDemo::foo(使用這種方式,該成員函數(shù)foo將不能使用缺省的__thiscall調(diào)用約定,而必須使用__stdcall或__cdecl):

void (__stdcall *fnFooPtr) (void*/* pThis*/, int/* a*/) = 
   (void (__stdcall *) (void*, int)) dwFooAddrPtr;
fnFooPtr (&mcd, 9);

執(zhí)行上面的調(diào)用后,屏幕上依然會(huì)輸出“In MemberCallDemo::foo, a = 9”。這說(shuō)明我們成功地調(diào)用了成員函數(shù)foo。當(dāng)然,使用這種方式使我們完全沖破了C++的封裝原則,打壞了正常的調(diào)用規(guī)則,即使你將foo聲明為private函數(shù),我們的調(diào)用同樣能夠成功,因?yàn)槲覀兪峭ㄟ^(guò)函數(shù)地址來(lái)進(jìn)行調(diào)用的。

你不禁會(huì)這樣問(wèn),在實(shí)際開發(fā)中誰(shuí)會(huì)這樣做呢?沒錯(cuò),我估計(jì)也沒有人會(huì)這樣來(lái)調(diào)用成員函數(shù),除非在一些特定的應(yīng)用下,比如我們接下來(lái)所要做的事情。

認(rèn)識(shí)Thunk

Thunk!Thunk?這是什么意思?翻開《Oxford Advanced Learner's Dictionary, Sixth-Edition》...查不到!再使用Kingsoft's PowerWord 2006,其曰“錚,鐺,鏘?”顯然是個(gè)象聲詞。頓暈,這跟我們所要描述的簡(jiǎn)直不挨邊啊!不知道人們?yōu)槭裁匆堰@種技術(shù)稱之為Thunk。不管了,暫時(shí)放置一邊吧!

通常,我們所編寫的程序最終將被編譯器的轉(zhuǎn)換成計(jì)算機(jī)能夠識(shí)別的指令碼(即機(jī)器碼),比如:

C/C++代碼               等價(jià)的匯編代碼                   編譯器產(chǎn)生的機(jī)器指令
==================== ==========================   =====================
int k1, k2, k;                    
k1 = 1;               mov  dword ptr [k1], 1      C7 45 E0 01 00 00 00
k2 = 2;               mov  dword ptr [k2], 2      C7 45 D4 02 00 00 00
k = k1 + k2;          mov  eax, dword ptr [k1]    8B 45 E0
                      add  eax, dword ptr [k2]    03 45 D4
                      mov  dword ptr [k], eax     89 45 C8

最終,CPU執(zhí)行完指令序列“C7 45 E0 01 00 00 00 C7 45 D4 02 00 00 00 8B 45 E0 03 45 D4 89 45 C8”后,就完成了上面的簡(jiǎn)單加法操作。從這里我們似乎能夠得到啟發(fā),既然CPU只能夠認(rèn)識(shí)機(jī)器碼,那么我們可以直接將機(jī)器碼送給CPU去執(zhí)行嗎?答案是:當(dāng)然可以,而且還非常高效!那么,怎么做到這一點(diǎn)呢?——定義一個(gè)機(jī)器碼數(shù)組,然后跳轉(zhuǎn)到該數(shù)組的起始地址處開始執(zhí)行:

unsigned char machine_code[] = {
    0xC7, 0x45, 0xE0, 0x01, 0x00, 0x00, 0x00,
    0xC7, 0x45, 0xD4, 0x02, 0x00, 0x00, 0x00, 
    0x8B, 0x45, 0xE0, 
    0x03, 0x45, 0xD4, 
    0x89, 0x45, 0xC8};
void* paddr = machine_code;

使用內(nèi)嵌匯編調(diào)用該機(jī)器碼:
__asm
{   MOV EAX, dword ptr [paddr] ; or  mov eax, dword ptr paddr ; or  mov eax, paddr
    CALL EAX
}
如果使用C調(diào)用該機(jī)器碼,則為:
void (*fn_ptr) (void) = (void (*) (void)) paddr;
fn_ptr ();

怎么樣?當(dāng)上面的CALL EAX執(zhí)行完后,變量k的值同樣等于3。但是,當(dāng)machine_code中的指令執(zhí)行完后,CPU將無(wú)法再回到CALL指令的下一條指令了!為什么啊?是的,因?yàn)閙achine_code中沒有返回指令的機(jī)器碼!要讓CPU能夠返回到正確的位置,我們必須將返回指令RET的機(jī)器碼(0xC3)追加到machine_code的末尾,即:unsigned char machine_code[] = {0xC7, 0x45, ..., 0x89, 0x45, 0xC8, 0xC3};。

這就是Thunk!一種能讓CPU直接執(zhí)行我們的機(jī)器碼的技術(shù),也有人稱其為自修改代碼(Self-Modifying Code)。但這有什么用呢?同樣,在通常的開發(fā)中,我們不可能通過(guò)這么復(fù)雜的代碼來(lái)完成上面的簡(jiǎn)單加法操作!誰(shuí)這樣做了,那他/她一定是個(gè)瘋子!^_^。目前所了解的最有用的也是用得最多的就是使用Thunk來(lái)更改棧中的參數(shù),甚至可以是棧中的返回地址,或者向棧中壓入額外的參數(shù)(就像我們的KWIN那樣),從而達(dá)到一些特殊目的。當(dāng)然,在你熟知Thunk的原理后,你可能會(huì)想出更多的用途來(lái),當(dāng)然,如果你想使用Thunk來(lái)隨意破壞當(dāng)前線程的棧數(shù)據(jù),從而直接導(dǎo)致程序或系統(tǒng)崩潰,那也不是不可能的,只要你喜歡,誰(shuí)又在乎呢?只要你不把這個(gè)程序拿給別人運(yùn)行就行!

通常我們會(huì)使用Thunk來(lái)“截獲”對(duì)指定函數(shù)的調(diào)用,并在真正調(diào)用該函數(shù)之前修改調(diào)用者傳遞給他的參數(shù)或其他任何你想要做的事情,當(dāng)我們做完我們想要做的時(shí)候,我們?cè)?#8220;跳轉(zhuǎn)到”真正需要被調(diào)用的函數(shù)中去。既然要跳轉(zhuǎn),就勢(shì)必要用到JMP指令。由于在Thunk中的代碼必須為機(jī)器指令,所以我們必須按照編譯器的工作方式將我們所需要Thunk完成的代碼轉(zhuǎn)換成機(jī)器指令,因此我們需要知道我們?cè)赥hunk所用到的指令的機(jī)器指令的編碼規(guī)則(通常,我們?cè)赥hunk中不可能做太多事情,了解所需的指令的編碼規(guī)則也不是件難事)。大家都知道,JMP為無(wú)條件轉(zhuǎn)移指令,并且有short、near、far轉(zhuǎn)移,通常編譯器會(huì)根據(jù)目標(biāo)地址距當(dāng)前JMP指令的下一條指令之間的距離來(lái)決定跳轉(zhuǎn)類型。在生成機(jī)器碼時(shí),并且編譯器會(huì)優(yōu)先考慮short轉(zhuǎn)移(如果目標(biāo)地址距當(dāng)前JMP指令的下一條指令的距離在-128和127之間),此時(shí),JMP對(duì)應(yīng)的機(jī)器碼為0xEB。如果超出這個(gè)范圍,JMP對(duì)應(yīng)的機(jī)器碼通常為0xE9。當(dāng)然,JMP還存在其他類型的跳轉(zhuǎn),如絕對(duì)地址跳轉(zhuǎn)等,相應(yīng)地也有其他形式的機(jī)器碼,如0xFF,0xEA。我們常用到的只有0xEB和0xE9兩種形式。另外,需要注意的是,在機(jī)器碼中,JMP指令后緊跟的是一個(gè)目標(biāo)地址到該條JMP指令的下一條指令之間的距離(當(dāng)然,以字節(jié)為單位),所以,如果我們?cè)赥hunk中需要用到JMP指令,我們就必須手動(dòng)計(jì)算該距離(這也是編譯器所需要做的一件事)。如果你已經(jīng)很了解JMP指令的細(xì)節(jié),那么你應(yīng)該知道了下面Thunk的將是什么樣的結(jié)果了吧:

unsigned char machine_code [] = {0xEB, 0xFE};

啊,沒錯(cuò),這將是一個(gè)死循環(huán)。這一定很有趣吧!事實(shí)上他跟JMP $是等價(jià)的。由于這里的機(jī)器碼是0xEB,它告訴CPU這是一個(gè)short跳轉(zhuǎn),并且,0xFE的最高位為1(即負(fù)數(shù)),所以CPU直到它是一個(gè)向后跳轉(zhuǎn)的JMP指令。由于向后跳轉(zhuǎn)的,所以此時(shí)該JMP所能跳轉(zhuǎn)到的范圍為-128至-1(即0x80至0xFF),但是由于這時(shí)的JMP指令為2個(gè)字節(jié),所以向后跳轉(zhuǎn)(從該條指令的下一條指令開始)2個(gè)字節(jié)后,就又回到了該條JMP指令的開始位置。

當(dāng)發(fā)生Short JMP指令時(shí),其所能跳轉(zhuǎn)的范圍如下:

偏移量 機(jī)器碼 =========== ====== (-128) 0x80 ?? (-127) 0x81 ?? : : (-3) 0xFD ?? (-2) 0xFE EB <- Short JMP指令 (-1) 0xFF XX <- XX為跳轉(zhuǎn)偏移量,其取值范圍可為[0x80 - 0x7F] (0) 0x00 ?? <- JMP下一條指令的開始位置 (+1) 0x01 ?? (+2) 0x02 ?? : : (+125) 0x7D ?? (+126) 0x7E ?? (+127) 0x7F ??

好,讓我們?cè)趤?lái)看一個(gè)例子,來(lái)說(shuō)明Thunk到底是怎樣修改棧中的參數(shù)的:

void foo(int a) 
{ printf ("In foo, a = %d\n", a); } 

unsigned char code[9]; 
* ((DWORD *) &code[0]) = 0x042444FF; /* inc dword ptr [esp+4] */
              code[4]  = 0xe9; /* JMP */
* ((DWORD *) &code[5]) = (DWORD) &foo - (DWORD) &code[0] - 9; /* 跳轉(zhuǎn)偏移量 */

void (*pf)(int/* a*/) = (void (*)(int)) &code[0];
pf (6);

當(dāng)執(zhí)行完pf (6)調(diào)用后,就會(huì)得到下面的輸出:
“In foo, a = 7”(明明傳入的是6,為什么到了foo中就變成了7了呢?)。怎么樣?我們?cè)赥hunk中通過(guò)強(qiáng)制CPU執(zhí)行機(jī)器碼0xFF,0x44,0x24,0x04來(lái)將棧中的傳入?yún)?shù)a(位于ESP + 4處)增加1,從而修改了調(diào)用者傳遞過(guò)來(lái)的參數(shù)。在執(zhí)行完INC DWORD PTR [ESP+4]后,再通過(guò)一個(gè)跳轉(zhuǎn)指令跳轉(zhuǎn)到真正的函數(shù)入口處。當(dāng)然,我們同樣不可能在實(shí)際的開發(fā)中使用這種方法進(jìn)行函數(shù)調(diào)用,之所以這樣做是為了能夠更加容易的弄清楚Thunk到底是怎么工作的!

讓W(xué)indows直接調(diào)用你在類中定義的(非靜態(tài))消息處理函數(shù)

好了,寫了這么久,似乎我們到這里才真正進(jìn)入我們的正題。上面幾節(jié)所描述的都是這一節(jié)所需的基本知識(shí),有了以上知識(shí),我們就能夠很容易的實(shí)現(xiàn)我們的最終目的——讓W(xué)indows來(lái)直接調(diào)用我們的類消息處理成員函數(shù),在這里無(wú)須使用任何靜態(tài)成員函數(shù)或全局函數(shù),所有的事情都將由我們定義的類成員函數(shù)來(lái)完成。由于Windows中所有的消息處理均為回調(diào)函數(shù),即它們是由操作系統(tǒng)在特定的消息發(fā)生時(shí)被系統(tǒng)調(diào)用的函數(shù),我們需要做的僅僅是定義該消息函數(shù),并將該消息函數(shù)的函數(shù)地址“告訴”Windows。既然我們能夠使用在通過(guò)其他途徑調(diào)用類中的成員函數(shù)中所描述的方法得到類成員函數(shù)(消息處理函數(shù))的地址,那么,我們能夠直接將該成員函數(shù)地址作為一個(gè)回調(diào)函數(shù)的地址傳給操作系統(tǒng)嗎?很顯然,這是不可能的。但是為什么呢?我想聰明的你已經(jīng)猜到,因?yàn)槲覀兊某蓡T函數(shù)需要類對(duì)象的this指針去訪問(wèn)類對(duì)象中的屬性,但是Windows是無(wú)法將相應(yīng)的類對(duì)象的this指針傳給我們的成員函數(shù)的!這就是我們所面臨的問(wèn)題的關(guān)鍵所在!如果我們能夠解決這個(gè)類對(duì)象的this指針傳遞問(wèn)題,即將類對(duì)象的this指針手動(dòng)傳遞到我們的類成員函數(shù)中,那么我們的問(wèn)題豈不是就解決了嗎?沒錯(cuò)!

Thunk可以為們解決這個(gè)難題!這里Thunk需要解決的是將消息處理函數(shù)所在的類的實(shí)例的this指針“傳遞”到消息處理函數(shù)中,從前面的描述我們已經(jīng)知道,this指針的傳遞有兩種方式,一種是通過(guò)ECX寄存器進(jìn)行傳遞,一種是使用棧進(jìn)行傳遞。

1。使用ECX傳遞this指針(__thiscall)

這是一種最簡(jiǎn)單的方式,它只需我們簡(jiǎn)單地在Thunk中執(zhí)行下面的指令即可:

LEA ECX, this pointer
JMP member function-based message handler

使用這種方式傳遞this指針時(shí),類中的消息處理函數(shù)必須使用__thiscall調(diào)用約定!在 關(guān)于調(diào)用約定與this指針的傳遞中我們對(duì)調(diào)用約定有較為詳細(xì)的討論。

2。使用棧傳遞this指針(__stdcall或__cdecl)

這是一種稍復(fù)雜的方式,使用棧傳遞this指針時(shí)必須確保類中的消息處理函數(shù)使用__stdcall調(diào)用約定,這跟通常的消息處理函數(shù)(靜態(tài)成員函數(shù)或全局函數(shù))使用的是同一種條用約定,唯一不同的是現(xiàn)在我們使用的是類成員函數(shù)(非靜態(tài))。之所以說(shuō)他稍復(fù)雜,是因?yàn)槲覀円赥hunk中要做稍多的工作。前面我們已經(jīng)說(shuō)過(guò),我們已經(jīng)將我們定義的Thunk的地址作為“消息處理回調(diào)函數(shù)”地址傳給了Windows,那么,當(dāng)有消息需要處理時(shí),Windows就會(huì)調(diào)用我們的消息處理函數(shù),不過(guò)這時(shí)它調(diào)用的是Thunk中的代碼,并不是真正的我們?cè)陬愔卸x的消息處理函數(shù)。這時(shí),要將this指針?biāo)腿氘?dāng)前棧中可不是件“容易”的事情。讓我們來(lái)看看Windows在調(diào)用我們的Thunk代碼時(shí)的棧的參數(shù)內(nèi)容:

this指針被壓入棧之前             this指針被壓入棧之后

:      ...      :            :       ...      :                                        
|---------------|            |----------------|                                        
|     LPARAM    |            |     LPARAM     |                                        
|---------------|            |----------------|                                        
|     WPARAM    |            |     WPARAM     |                                        
|---------------|            |----------------|                                        
|   UINT (msg)  |            |   UINT (msg)   |                                        
|---------------|            |----------------|                                        
|      HWND     |            |      HWND      |                                        
|---------------|            |----------------|                                        
| (Return Addr) | <- ESP     | <this pointer> | <- New item inserted by this thunk code
|---------------|            |----------------|                                        
:      ...      :            | (Return Addr)  | <- ESP                                 
                             |----------------|                                        
                             :       ...      :                                        
                                                                                      
       圖1                           圖2                         

從圖1可以看出,為了將this指針?biāo)腿霔V校覀兛刹荒芎?jiǎn)單地使用PUSH this pointer的方式將this指針“壓入”棧中!但是為什么呢?想想看,如果直接將this指針壓入棧中,那么原來(lái)的返回地址將不能再起效。也就是說(shuō)我們將不能在我們的消息處理函數(shù)執(zhí)行結(jié)束后“返回”到正確的地方,這勢(shì)必會(huì)導(dǎo)致系統(tǒng)的崩潰。另一方面,我們的成員函數(shù)要求this指針必須是最后一個(gè)被送入棧的參數(shù),所以,我們必須將this指針“插入”到HWND參數(shù)和返回地址(Return Addr)之間。如圖2所示。所以,在這種情況下,我們須在Thunk中完成以下工作:

PUSH  DWORD PTR [ESP]                          ; 保存(復(fù)制)返回地址到當(dāng)前棧中
MOV   DWORD PTR [ESP + 4], pThis               ; 將this指針?biāo)腿霔V校丛瓉?lái)的返回地址處
JMP   member function-based message handler    ; 跳轉(zhuǎn)至目標(biāo)消息處理函數(shù)(類成員函數(shù))

實(shí)現(xiàn)我們的KWIN包

好了,有了以上知識(shí)后,現(xiàn)在就只剩下我們的KWIN包的開發(fā)了,當(dāng)然,如果你掌握以上知識(shí)后,你可以運(yùn)用這些知識(shí),甚至找出新的方法來(lái)實(shí)現(xiàn)你自己的消息處理包。我想,那一定時(shí)間非常令人激動(dòng)的事情!如果你想到更好的方法,千萬(wàn)別忘了告訴我一聲哦。

首先來(lái)看看我們?cè)贙WIN中需要使用到的一個(gè)比較重要的宏:

#define __DO_DEFAULT (LRESULT) -2

#define _USE_THISCALL_CALLINGCONVENTION

#ifdef _USE_THISCALL_CALLINGCONVENTION
#define THUNK_CODE_LENGTH      10 /* For __thiscall calling convention ONLY */
#define KCALLBACK __thiscall
#else
#define THUNK_CODE_LENGTH      16 /* For __stdcall or __cdecl calling convention ONLY */
#define KCALLBACK __stdcall
#endif

在KWIN中同時(shí)實(shí)現(xiàn)了__thiscall和__stdcall兩種調(diào)用約定,如果定義了_USE_THISCALL_CALLINGCONVENTION宏,那么就使用__thiscall調(diào)用約定,否則將使用__stdcall調(diào)用約定。宏KCALLBACK在定義了_USE_THISCALL_CALLINGCONVENTION宏后將被替換成__thiscall,否則為__stdcall。THUNK_CODE_LENGTH定義了在不同的調(diào)用約定下所需的機(jī)器指令碼的長(zhǎng)度,如果使用__thiscall,我們只需要10個(gè)字節(jié)的機(jī)器指令碼,而在__stdcall下,我們需要使用16字節(jié)的機(jī)器指令碼。

我們將實(shí)現(xiàn)對(duì)話框和一般窗口程序的消息處理函數(shù)進(jìn)行封裝的包(KWIN),我們力求使用KWIN能為我們的程序帶來(lái)更好的靈活性和結(jié)構(gòu)“良好”性,就像我們?cè)诒疚拈_始時(shí)向大家展示的一小部分代碼那樣。首先我們將定義一個(gè)對(duì)話框和窗口程序都需要的數(shù)據(jù)結(jié)構(gòu)_K_THUNKED_DATA,該結(jié)構(gòu)封裝了所有所需的Thunk代碼(機(jī)器指令碼)。整個(gè)KWIN的結(jié)構(gòu)大致如下:


圖3

在_K_THUNKED_DATA中有一個(gè)非常重要的函數(shù)—Init,它的原型如下:

void Init (
    DWORD_PTR pThis,      /* 消息處理類對(duì)象的內(nèi)存地址(this指針) */
    DWORD_PTR dwProcPtr   /* 消息處理函數(shù)(類成員函數(shù))的地址 */
    )
{
    DWORD dwDistance = (DWORD) dwProcPtr - (DWORD) &pThunkCode[0] - THUNK_CODE_LENGTH;

#ifdef _USE_THISCALL_CALLINGCONVENTION
    /*
      Encoded machine instruction   Equivalent assembly languate notation
      ---------------------------   -------------------------------------
      B9 ?? ?? ?? ??                mov    ecx, pThis ; Load ecx with this pointer
      E9 ?? ?? ?? ??                jmp    dwProcPtr  ; Jump to target message handler
    */
    pThunkCode[0] = 0xB9;
    pThunkCode[5] = 0xE9;

    *((DWORD *) &pThunkCode[1]) = (DWORD) pThis;
    *((DWORD *) &pThunkCode[6]) = dwDistance;
#else
    /*           
      Encoded machine instruction   Equivalent assembly languate notation
      ---------------------------   -------------------------------------
      FF 34 24                      push  dword ptr [esp]          ; Save (or duplicate) the Return Addr into stack
      C7 44 24 04 ?? ?? ?? ??       mov   dword ptr [esp+4], pThis ; Overwite the old Return Addr with 'this pointer'
      E9 ?? ?? ?? ??                jmp   dwProcPtr                ; Jump to target message handler
    */

    pThunkCode[11] = 0xE9; 

    *((DWORD *) &pThunkCode[ 0]) = 0x002434FF;
    *((DWORD *) &pThunkCode[ 3]) = 0x042444C7;
    *((DWORD *) &pThunkCode[ 7]) = (DWORD) pThis;
    *((DWORD *) &pThunkCode[12]) = dwDistance;
#endif

}

看見了吧,該函數(shù)的實(shí)現(xiàn)異常簡(jiǎn)單,但是在KWIN中的作用非凡。我們的KWIN的成敗就依賴于該函數(shù)是否正確的“生成”了我們所需要的機(jī)器指令碼。Init通過(guò)將類對(duì)象的this指針和消息處理函數(shù)的地址“硬編碼”到數(shù)組_machine_code數(shù)組中,這樣該數(shù)組中就擁有了可以讓W(xué)indows正確調(diào)用我們?cè)陬愔卸x的成員函數(shù)所需的指令了。接下來(lái)所需要做的事情就全在我們創(chuàng)建的類(KWindowImpl和KDialogImpl或你自己創(chuàng)建的從這兩個(gè)類派生出來(lái)的類)中了。

_K_WINDOW_ROOT_IMPL中提供了一些對(duì)話框和窗口應(yīng)用程序都需要處理的公共消息的缺省處理(如WM_CREATE,WM_DESTROY,WM_COMMAND,WM_NOTIFY等等),和一些與窗口句柄(HWND)相關(guān)的函數(shù)(如GetWindowRect等等)。并且在_K_WINDOW_ROOT_IMPL的缺省構(gòu)造函數(shù)中完成了Thunk的初始化。這里主要說(shuō)一下該類中的ProcessBaseMessage方法:

template <class T>
class __declspec(novtable) _K_WINDOW_ROOT_IMPL
{
    _K_THUNKED_DATA _thunk;

public:
    _K_WINDOW_ROOT_IMPL () : m_hWnd (NULL)
    {
        T* pThis = static_cast<T *>(this);
        _thunk.Init ((DWORD_PTR) pThis, pThis->GetMessageProcPtr());
        /* The above GetMessageProcPtr was defined in derived class KDialogImpl and KWindowImpl */
    }

    ...

    LRESULT ProcessBaseMessage (UINT msg, WPARAM wParam, LPARAM lParam)
    {   
        T* pThis = static_cast<T *>(this); /* 'Override' support */
        LRESULT r = __DO_DEFAULT;

        switch (msg)
        {
        case WM_COMMAND:
            r = pThis->OnCommand (HIWORD(wParam), LOWORD(wParam), (HWND) lParam); break;

        case WM_NOTIFY:
            r = pThis->OnNotify ((int) wParam, (LPNMHDR) lParam); break;
            
        /* Other window message can be handled here*/
        }
        
        return r == __DO_DEFAULT ? pThis->DoDefault (msg, wParam, lParam) : r;
    }
    
    LRESULT OnCommand (WORD wNotifyCode, WORD wId, HWND hWndCtrl) { return __DO_DEFAULT; }
    LRESULT OnNotify (int idCtrl, LPNMHDR pnmh) { return __DO_DEFAULT; }
    LRESULT OnXXXXXX (...) { return __DO_DEFAULT; } /* 其他消息處理函數(shù)的定義 */
    ...
};

需要說(shuō)明的是, 在該類中定義的大量(缺省)消息處理函數(shù),是為了能夠在其子類中“重載”這些消息處理函數(shù)。這里為了實(shí)現(xiàn)的缺省消息處理的簡(jiǎn)單性使用了一種比較勉強(qiáng)的方法,即假定沒有任何消息處理函數(shù)會(huì)返回-2。比如,當(dāng)你在你的對(duì)話框類中“重載”OnCommand方法時(shí)(當(dāng)然,你的對(duì)話框類必須要繼承KDialogImpl),該方法就能夠在WM_COMMAND消息發(fā)生時(shí)被系統(tǒng)自動(dòng)調(diào)用,關(guān)于基于模板的重載,請(qǐng)參見前面的章節(jié):使用C++模板實(shí)現(xiàn)函數(shù)的“重載”

好,再讓我們來(lái)看看KDialogImpl的實(shí)現(xiàn):

template <class T>
class KDialogImpl
    : public _K_WINDOW_ROOT_IMPL <T>
{
public:
    KDialogImpl () : _K_WINDOW_ROOT_IMPL<T> () {}
    inline DWORD GetMessageProcPtr () 
    {
        DWORD dwProcAddr = 0;
        __asm
        {   /* Use the prefix 'T::' to enable the overload of KDialogProc in derived class */
            mov eax, offset T::KDialogProc 
            mov dword ptr [dwProcAddr], eax
        }
        return dwProcAddr;
    }

    INT_PTR OnInitDialog (HWND hWndFocus, LPARAM lParam) { return __DO_DEFAULT; }
    /* Other dialog-based message hanlder can be declared here */
    LRESULT DoDefault (UINT msg, WPARAM wParam, LPARAM lParam) { return 0; }

    INT_PTR DoModal (
        HWND hWndParent = ::GetActiveWindow( ),
        LPARAM lpInitParam = NULL)
    {
        return DialogBoxParam (
            m_hInstance, MAKEINTRESOURCE(T::IDD), 
            hWndParent, 
            (DLGPROC) GetThunkedProcPtr(), lpInitParam);
    }

    INT_PTR KCALLBACK KDialogProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {
        /* Cache the window handle when KDialogProc was called at first time */
        if (m_hWnd == NULL) 
            m_hWnd = hWnd;

        __ASSERT(m_hWnd == hWnd);

        T* pThis = static_cast<T *> (this); /* 'Override' support */
        INT_PTR fReturn = 0;

        switch (msg)
        {
        case WM_INITDIALOG:
            fReturn = pThis->OnInitDialog ((HWND) wParam, lParam);
            break;

        case WM_GETDLGCODE:
            fReturn = pThis->OnGetDlgCode ((LPMSG) lParam);
            break;
        /* Other dialog-based message can be handled here */

        default:
            fReturn = __super::ProcessBaseMessage (msg, wParam, lParam);
        }

        if (fReturn == __DO_DEFAULT) return DoDefault (msg, wParam, lParam);

        if (fReturn)
        {
            switch (msg)
            {
            case WM_COMPAREITEM         :
            case WM_VKEYTOITEM          :
            case WM_CHARTOITEM          :
            case WM_INITDIALOG          :
            case WM_QUERYDRAGICON       :
            case WM_CTLCOLORMSGBOX      :
            case WM_CTLCOLOREDIT        :
            case WM_CTLCOLORLISTBOX     :
            case WM_CTLCOLORBTN         :
            case WM_CTLCOLORDLG         :
            case WM_CTLCOLORSCROLLBAR   :
            case WM_CTLCOLORSTATIC      :
                break;

            default:
               ::SetWindowLongPtr(m_hWnd, DWLP_MSGRESULT, (LONG) fReturn);
               break;
            }
        }

        return fReturn;
    }

    ...

    inline BOOL SetDlgItemInt (int nIDDlgItem, UINT uValue, BOOL bSigned = TRUE) 
        { return ::SetDlgItemInt (m_hWnd, nIDDlgItem, uValue, bSigned); }
    inline UINT IsDlgButtonChecked  (int nIDButton) 
        { return ::IsDlgButtonChecked (m_hWnd, nIDButton); }
    /* 其他對(duì)話框常用函數(shù) */
};

最后我們來(lái)看看KWindowImpl的實(shí)現(xiàn),KWindowImpl的實(shí)現(xiàn)較KDialogImpl要復(fù)雜些,在KWindowImpl中,我們需要注冊(cè)特定的窗口類,另外,對(duì)于同一類型的窗口,在該類窗口的多實(shí)例下,我們需要保證每個(gè)實(shí)例的this指針能夠被正確傳遞到消息處理函數(shù)中,所以我們需在每次創(chuàng)建該類型的窗口時(shí)將該類窗口的窗口過(guò)程(Window Procedure)地址改為當(dāng)前實(shí)例的窗口過(guò)程。所以我們需要使用SetClassLongPtr來(lái)修改窗口類的消息處理函數(shù)地址,因此我們需要一個(gè)窗口句柄,這也就是為什么我要在KWindowImpl中定義類型為HWND的靜態(tài)變量_hWndLastCreated,我們需要通過(guò)它來(lái)調(diào)用函數(shù)SetClassLongPtr(該方法覺得很勉強(qiáng)。也許有更好的方法,但是目前沒有發(fā)現(xiàn))。

template <class T>
class KWindowImpl
    : public _K_WINDOW_ROOT_IMPL <T>
{
    LPTSTR m_lpszClassName;

    static HWND _hWndLastCreated; /* work as sentinel */
    static CRITICAL_SECTION _cs; /* Enable multi-thread safe support */
    static BOOL _fInitilized;

public:
    inline DWORD GetMessageProcPtr () 
    {
        DWORD dwProcAddr = 0;
        __asm
        {   /* Use the prefix 'T::' to enable the overload of KWndProc in derived class */
            mov eax, offset T::KWndProc 
            mov dword ptr [dwProcAddr], eax
        }
        return dwProcAddr;
    }

    /* Disable the default constructor, without specifying the name of window class */
    KWindowImpl () { __ASSERT (FALSE); } 
    KWindowImpl (LPCTSTR lpszClassName) : _K_WINDOW_ROOT_IMPL ()
    {   
        m_lpszClassName = new TCHAR [_tcslen(lpszClassName) + _tcslen(_T("KWindowImpl")) + 8];
        _stprintf (m_lpszClassName, _T("%s:%s"), _T("JKTL::KWindowImpl"), lpszClassName);
        
        if (!_fInitilized)
        {   ::InitializeCriticalSection (&_cs);
            _fInitilized = TRUE;
        }
    }

    ~KWindowImpl ()
    {   if (m_lpszClassName) delete[] m_lpszClassName; }

    LRESULT DoDefault (UINT msg, WPARAM wParam, LPARAM lParam) 
    {   return DefWindowProc (m_hWnd, msg, wParam, lParam); }

    LRESULT KCALLBACK KWndProc (HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {
        if (m_hWnd == NULL)
        {   m_hWnd = hWnd;
            _hWndLastCreated = m_hWnd;

            /* Leaving the critical section, when window was created successfully */
            ::LeaveCriticalSection (&_cs);
        }

        __ASSERT (m_hWnd == hWnd);

        /* Do some default message process */
        return __super::ProcessBaseMessage (msg, wParam, lParam);
    }

    BOOL KRegisterClass ()
    {
        WNDCLASSEX wcx = {sizeof WNDCLASSEX};

        /* Enable multi-thread safe, for SetClassLongPtr use ONLY, call here for convenience */
        ::EnterCriticalSection (&_cs);

        if (GetClassInfoEx (m_hInstance, m_lpszClassName, &wcx))
        {    /* Ensure that window subsquently created use it's thunked window procedure,
               SetClassLongPtr will not effect those windows had been created before */
        SetClassLongPtr (_hWndLastCreated, GCL_WNDPROC, (LONG) GetThunkedProcPtr ());
            return TRUE;
        }

        wcx.cbClsExtra       = 0;
        wcx.cbWndExtra       = 0;
        wcx.hbrBackground    = (HBRUSH) (COLOR_BTNFACE + 1);;
        wcx.hCursor          = LoadCursor (NULL, IDC_ARROW);
        wcx.hIcon            = LoadIcon (NULL, IDI_APPLICATION);
        wcx.hInstance        = m_hInstance;
        wcx.lpfnWndProc      = (WNDPROC) GetThunkedProcPtr ();
        wcx.lpszClassName    = m_lpszClassName;
        wcx.lpszMenuName     = NULL;
        wcx.style            = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;

        return RegisterClassEx (&wcx);
    }

    HWND Create (HWND hWndParent, DWORD dwExStyle, DWORD dwStyle, LPCTSTR lpszWndName, 
                 RECT& rc, int nCtrlId = 0, LPVOID lpParam = NULL)
    {
        __ASSERT (m_hWnd == NULL);

        if (!KRegisterClass ()) return NULL;
        m_hWnd = ::CreateWindowEx (
            dwExStyle, m_lpszClassName, 
            lpszWndName, dwStyle, 
            rc.left, rc.top, _RECT_W(rc), _RECT_H(rc),
            hWndParent, (HMENU) nCtrlId, m_hInstance, lpParam);

        return m_hWnd;
    }
};

對(duì)于_K_WINDOW_ROOT_IMPL中的m_hWnd成員的賦值,我們是在我們的窗口過(guò)程第一次被調(diào)用是設(shè)定的(見代碼)。雖然我并不是很喜歡這種寫法,因?yàn)槲也幌M诖翱谔幚磉^(guò)程出現(xiàn)于消息處理無(wú)關(guān)的代碼。事實(shí)上,我們?nèi)匀豢梢栽赥hunk代碼中完成對(duì)m_hWnd的賦值,因?yàn)楫?dāng)系統(tǒng)調(diào)用我們的“窗口處理過(guò)程”(即Thunk代碼)時(shí),系統(tǒng)已經(jīng)為我們的窗體分配好了窗口句柄,并位于棧中的ESP+4位置(見圖1),我們完全可以在Thunk中將位于ESP+4處的值保存到m_hWnd中,但是這將增加我們?cè)赥hunk中的機(jī)器指令的長(zhǎng)度,使用下面的代碼就要增加16字節(jié)的機(jī)器指令(__stdcall調(diào)用約定下,在__thiscall調(diào)用約定下,只需增加10個(gè)字節(jié)左右,因?yàn)榭梢灾苯邮褂肊CX來(lái)進(jìn)行數(shù)據(jù)交換),在執(zhí)行Thunk代碼之前,我們需要計(jì)算出類對(duì)象中m_hWnd成員在該類對(duì)象中的偏移位置,這通常可以在類的構(gòu)造函數(shù)中完成(下面的代碼假定m_hWnd在類對(duì)象中的偏移位置為8):

PUSH        DWORD PTR [ESP]
MOV         DWORD PTR[ESP + 4], pThis
    PUSH    EAX                         ; Save EAX
    PUSH    EBX                         ; Save EBX
    MOV     EAX, pThis
    MOV     EBX, DWORD PTR [ESP + 0Ch]  ; Load HWND from ESP + 0Ch
    MOV     DWORD PTR [EAX + 08h], EBX  ; Set m_hWnd with value of EBX
    POP     EBX                         ; Restore EAX
    POP     EAX                         ; Restore EBX
JMP         member function-based message handler

在__thiscall調(diào)用約定下為:

MOV         ECX, pThis
    PUSH    EAX
    MOV     EAX, DWORD PTR [ESP + 8]    ; Load HWND from ESP + 8
    MOV     DWORD PTR [EAX + 08h], EAX  ; Save HWND to m_hWnd
    POP     EAX
JMP         member function-based message handler
    

使用類成員函數(shù)子類化窗口過(guò)程

在_K_WINDOW_ROOT_IMPL中實(shí)現(xiàn)了SubclassWindow和SubclassDialog方法,你可以使用這兩個(gè)方法來(lái)輕松的實(shí)現(xiàn)窗體的子類化操作。下面的代碼演示了怎樣使用SubclassWindow來(lái)子類化輸入框(Edit Control):

class SubclassDemo
    : public KDialogImpl <SubclassDemo>
{
    _K_THUNKED_SUBCLASS_DATA* m_lpTsdEdit;
    
public:
    INT_PTR OnInitDialog (HWND hWndFocus, LPARAM lParam)
    {
         HWND hEdit = GetDlgItem (IDC_EDIT0);
         m_lpTsdEdit = SubclassWindow (hEdit, EditWndProcHook);

         return TRUE;
    }
    
    LRESULT KCALLBACK EditWndProcHook (HWND hEdit, UINT msg, WPARAM wParam, LPARAM lParam)
    {
        /* Do anything here you like */
        return CallWindowProc (m_lpTsdEdit->fnOldWndProc, hEdit, msg, wParam, lParam);
    }
}

結(jié)束語(yǔ)

非常高興你能夠讀到這篇文章的末尾,同時(shí)也希望這篇文章能對(duì)你理解Windows中的消息處理機(jī)制提供一些幫助。歡迎你將你的看法和建議反饋給我(JERKII@HOTMAIL.COM),以彌補(bǔ)由于我當(dāng)前的知識(shí)限制而導(dǎo)致的一些錯(cuò)誤。

KWIN的源碼可以從這里下載。

謝謝!^_^


posted on 2010-05-08 20:26 會(huì)飛的兔子 閱讀(2236) 評(píng)論(1)  編輯 收藏 引用 所屬分類: 系統(tǒng)API,底層技術(shù)

Feedback

# re: 一種實(shí)現(xiàn)Win32窗口過(guò)程函數(shù)(Window Procedure)的新方法 2010-06-03 22:19 Condor
不錯(cuò),轉(zhuǎn)換收藏了。  回復(fù)  更多評(píng)論
  

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            久久天天躁狠狠躁夜夜爽蜜月| 久久久99国产精品免费| 一区二区国产在线观看| 国产亚洲毛片| 国产精品福利在线观看网址| 久久蜜桃资源一区二区老牛| 亚洲午夜在线| 亚洲直播在线一区| 欧美亚洲三区| 久久精品在这里| 久久综合色婷婷| 欧美精品一区二区三区视频| 欧美电影电视剧在线观看| 欧美电影免费网站| 欧美偷拍一区二区| 国产一区二区三区久久| 亚洲欧洲三级电影| 午夜国产精品影院在线观看| 久久久91精品国产一区二区三区 | 欧美精品一区二区三区高清aⅴ| 久久久精品999| 国产精品www994| 亚洲第一偷拍| 欧美伊人久久久久久久久影院| 久热爱精品视频线路一| 最新成人在线| 亚洲一区二区三区免费在线观看| 午夜日韩激情| 国产精品99一区二区| 亚洲国产91精品在线观看| 久久国产精品久久久久久| 国产老肥熟一区二区三区| 亚洲精品日韩在线观看| 久久夜色精品国产欧美乱极品| 亚洲深夜福利视频| 欧美激情影院| 亚洲精品久久视频| 亚洲国产一区二区视频| 久久免费视频在线| 激情综合久久| 久久综合九色综合欧美就去吻| 一区二区免费在线观看| 欧美亚洲成人精品| 亚洲欧美综合国产精品一区| av成人黄色| 国产三级精品三级| 久久婷婷国产综合国色天香| 欧美一区二区三区免费视频| 国产精品国产三级国产a| 亚洲视频欧洲视频| 亚洲一区二区三| 国产亚洲成av人在线观看导航| 欧美一区二区三区免费视| 亚洲综合日韩| 国产一区二区三区最好精华液| 久久综合九色综合久99| 欧美精品一区二区三区四区| 亚洲一区二区在线免费观看视频| 一本色道久久综合精品竹菊 | 欧美日韩国产首页| 在线亚洲一区观看| 久久久成人网| 亚洲欧美99| 欧美久久久久| 国产一区二区高清不卡| 久久久午夜视频| 欧美日韩理论| 欧美国产日韩一区二区| 国产精品视频yy9299一区| 亚洲激情成人网| 在线观看日韩www视频免费| 一本在线高清不卡dvd| 亚洲精品一区在线| 久久久免费观看视频| 午夜一区二区三区不卡视频| 猫咪成人在线观看| 免费观看成人www动漫视频| 欧美日韩一区精品| 日韩一区二区精品在线观看| 亚洲高清视频在线| 欧美不卡视频| 亚洲精品一区二区三区99| 一本色道久久综合精品竹菊| 久久一区免费| 亚洲精品无人区| 亚洲一区三区在线观看| 国产精品第2页| 欧美一级二级三级蜜桃| 久久久一二三| 日韩视频在线观看免费| 亚洲深夜福利在线| 性欧美激情精品| 欧美日韩在线不卡一区| 亚洲图中文字幕| 免播放器亚洲| 亚洲美女av网站| 国产精品裸体一区二区三区| 亚洲欧美日韩天堂| 亚洲第一黄色网| 亚洲一区二区三区四区五区黄| 国产欧美婷婷中文| 欧美精品在线观看一区二区| 亚洲日本aⅴ片在线观看香蕉| 欧美吻胸吃奶大尺度电影| 久久精品成人| 亚洲天堂偷拍| 亚洲精品中文字幕在线| 欧美一级淫片aaaaaaa视频| 亚洲国产精品毛片| 国产一区二区毛片| 国产精品美女久久久久久久| 欧美成人免费观看| 蜜乳av另类精品一区二区| 午夜欧美视频| 久久久国际精品| 午夜精品久久久久久99热| 日韩视频一区二区在线观看 | 亚洲一区二区三区乱码aⅴ| 欧美激情精品久久久久久| 亚洲欧美韩国| 久久久免费观看视频| 欧美怡红院视频一区二区三区| 亚洲美女诱惑| 一区二区三区黄色| 亚洲欧美一区二区三区久久| 香港久久久电影| 欧美中文日韩| 欧美1级日本1级| 亚洲精品久久久久久久久久久久| 欧美成人国产va精品日本一级| 欧美成年视频| aa级大片欧美| 欧美一区二区在线看| 欧美成年人视频网站| 亚洲男女自偷自拍图片另类| 国产精品www994| 久久精品综合网| 蜜桃av久久久亚洲精品| 欧美电影电视剧在线观看| 国产精品一区二区男女羞羞无遮挡| 国产精品亚洲综合天堂夜夜| 亚洲第一中文字幕| 一区二区三区高清在线观看| 亚洲综合激情| 亚洲第一天堂无码专区| 亚洲与欧洲av电影| 亚洲毛片在线免费观看| 久久久久久久一区二区| 国产精品裸体一区二区三区| 亚洲人成亚洲人成在线观看 | 欧美国产日韩一区二区在线观看| 欧美成人一区二区三区在线观看 | 欧美高清自拍一区| 国产精品香蕉在线观看| 一区二区三区高清视频在线观看| 久久精品国产第一区二区三区最新章节| 亚洲片在线观看| 欧美高清一区| 9人人澡人人爽人人精品| 亚洲第一黄色网| 欧美日韩高清在线观看| 亚洲夜间福利| 亚洲深夜福利在线| 国内精品视频久久| 欧美高清你懂得| 久久综合久久综合这里只有精品| 亚洲国产日韩在线一区模特| 麻豆精品精华液| 一本色道久久综合狠狠躁的推荐| 亚洲最快最全在线视频| 国产一区二区三区网站| 欧美大片在线观看一区| 欧美另类极品videosbest最新版本| 亚洲黄一区二区三区| 欧美激情视频在线免费观看 欧美视频免费一| 久久av老司机精品网站导航| 国产精品一区二区久久| 久久久人成影片一区二区三区| 久久久久久久97| 亚洲欧美成人一区二区在线电影| 久久成人综合网| 亚洲欧美国产日韩天堂区| 久久男女视频| 销魂美女一区二区三区视频在线| 久久久精品久久久久| 欧美综合国产精品久久丁香| 欧美精品 日韩| 欧美国产综合视频| **网站欧美大片在线观看| 亚洲欧美在线看| 亚洲欧洲99久久| 国产精品天美传媒入口| 一区二区三区精品在线| 日韩一区二区精品视频| 蜜桃久久av一区| 欧美激情亚洲精品| 日韩视频精品在线| 欧美精品久久99| 亚洲美女精品一区| 99国产精品久久久久久久|