一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

DLL編寫教程

 軟件團隊頭目 2008-08-31

半年不能上網(wǎng),最近網(wǎng)絡(luò)終于通了,終于可以更新博客了,寫點什么呢?決定最近寫一個編程技術(shù)系列,其內(nèi)容是一些通用的編程技術(shù)。例如DLL,COM,Socket,多線程等等。這些技術(shù)的特點就是使用廣泛,但是誤解很多;網(wǎng)上教程很多,但是幾乎沒有什么優(yōu)質(zhì)良品。我以近幾個月來的編程經(jīng)驗發(fā)現(xiàn),很有必要好好的總結(jié)一下這些編程技術(shù)了。一來對自己是總結(jié)提高,二來可以方便光顧我博客的朋友。

好了,廢話少說,言歸正傳。第一篇就是《DLL編寫教程》,為什么起這么土的名字呢?為什么不叫《輕輕松松寫DLL》或者《DLL一日通》呢?或者更nb的《深入簡出DLL》呢?呵呵,常常上網(wǎng)搜索資料的弟兄自然知道。


DLL的優(yōu)點

簡單的說,dll有以下幾個優(yōu)點:

1)      節(jié)省內(nèi)存。同一個軟件模塊,若是以源代碼的形式重用,則會被編譯到不同的可執(zhí)行程序中,同時運行這些exe時這些模塊的二進制碼會被重復(fù)加載到內(nèi)存中。如果使用dll,則只在內(nèi)存中加載一次,所有使用該dll的進程會共享此塊內(nèi)存(當(dāng)然,像dll中的全局變量這種東西是會被每個進程復(fù)制一份的)。

2)      不需編譯的軟件系統(tǒng)升級,若一個軟件系統(tǒng)使用了dll,則該dll被改變(函數(shù)名不變)時,系統(tǒng)升級只需要更換此dll即可,不需要重新編譯整個系統(tǒng)。事實上,很多軟件都是以這種方式升級的。例如我們經(jīng)常玩的星際、魔獸等游戲也是這樣進行版本升級的。

3)      Dll庫可以供多種編程語言使用,例如用c編寫的dll可以在vb中調(diào)用。這一點上DLL還做得很不夠,因此在dll的基礎(chǔ)上發(fā)明了COM技術(shù),更好的解決了一系列問題。

最簡單的dll

開始寫dll之前,你需要一個c/c++編譯器和鏈接器,并關(guān)閉你的IDE。是的,把你的VCC++ BUILDER之類的東東都關(guān)掉,并打開你以往只用來記電話的記事本程序。不這樣做的話,你可能一輩子也不明白dll的真諦。我使用了VC自帶的cl編譯器和link鏈接器,它們一般都在vcbin目錄下。(若你沒有在安裝vc的時候選擇注冊環(huán)境變量,那么就立刻將它們的路徑加入path吧)如果你還是因為離開了IDE而害怕到哭泣的話,你可以關(guān)閉這個頁面并繼續(xù)去看《VC++技術(shù)內(nèi)幕》之類無聊的書了。

最簡單的dll并不比chelloworld難,只要一個DllMain函數(shù)即可,包含objbase.h頭文件(支持COM技術(shù)的一個頭文件)。若你覺得這個頭文件名字難記,那么用windows.H也可以。源代碼如下:dll_nolib.cpp

#include <objbase.h>

#include <iostream.h>

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       cout<<"Dll is attached!"<<endl;

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

       cout<<"Dll is detached!"<<endl;

       g_hModule=NULL;

       break;

    }

    return true;

}

其中DllMain是每個dll的入口函數(shù),如同cmain函數(shù)一樣。DllMain帶有三個參數(shù),hModule表示本dll的實例句柄(聽不懂就不理它,寫過windows程序的自然懂),dwReason表示dll當(dāng)前所處的狀態(tài),例如DLL_PROCESS_ATTACH表示dll剛剛被加載到一個進程中,DLL_PROCESS_DETACH表示dll剛剛從一個進程中卸載。當(dāng)然還有表示加載到線程中和從線程中卸載的狀態(tài),這里省略。最后一個參數(shù)是一個保留參數(shù)(目前和dll的一些狀態(tài)相關(guān),但是很少使用)。

從上面的程序可以看出,當(dāng)dll被加載到一個進程中時,dll打印"Dll is attached!"語句;當(dāng)dll從進程中卸載時,打印"Dll is detached!"語句。

編譯dll需要以下兩條命令:

cl /c dll_nolib.cpp

這條命令會將cpp編譯為obj文件,若不使用/c參數(shù)則cl還會試圖繼續(xù)將obj鏈接為exe,但是這里是一個dll,沒有main函數(shù),因此會報錯。不要緊,繼續(xù)使用鏈接命令。

Link /dll dll_nolib.obj

這條命令會生成dll_nolib.dll。

注意,因為編譯命令比較簡單,所以本文不討論nmake,有興趣的可以使用nmake,或者寫個bat批處理來編譯鏈接dll。

加載DLL(顯式調(diào)用)

使用dll大體上有兩種方式,顯式調(diào)用和隱式調(diào)用。這里首先介紹顯式調(diào)用。編寫一個客戶端程序:dll_nolib_client.cpp

#include <windows.h>

#include <iostream.h>

int main(void)

{

    //加載我們的dll

    HINSTANCE hinst=::LoadLibrary("dll_nolib.dll"); 

    if (NULL != hinst)

    {

       cout<<"dll loaded!"<<endl;

    }

    return 0;

}

注意,調(diào)用dll使用LoadLibrary函數(shù),它的參數(shù)就是dll的路徑和名稱,返回值是dll的句柄。 使用如下命令編譯鏈接客戶端:

Cl dll_nolib_client.cpp

并執(zhí)行dll_nolib_client.exe,得到如下結(jié)果:

Dll is attached!

dll loaded!

Dll is detached!

以上結(jié)果表明dll已經(jīng)被客戶端加載過。但是這樣僅僅能夠?qū)?/span>dll加載到內(nèi)存,不能找到dll中的函數(shù)。

使用dumpbin命令查看DLL中的函數(shù)

Dumpbin命令可以查看一個dll中的輸出函數(shù)符號名,鍵入如下命令:

Dumpbin –exports dll_nolib.dll

通過查看,發(fā)現(xiàn)dll_nolib.dll并沒有輸出任何函數(shù)。

如何在dll中定義輸出函數(shù)

總體來說有兩種方法,一種是添加一個def定義文件,在此文件中定義dll中要輸出的函數(shù);第二種是在源代碼中待輸出的函數(shù)前加上__declspec(dllexport)關(guān)鍵字。

Def文件

首先寫一個帶有輸出函數(shù)的dll,源代碼如下:dll_def.cpp

#include <objbase.h>

#include <iostream.h>

void FuncInDll (void)

{

    cout<<"FuncInDll is called!"<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

        g_hModule=NULL;

        break;

    }

    return TRUE;

}

這個dlldef文件如下:dll_def.def

;

; dll_def module-definition file

;

LIBRARY         dll_def.dll

DESCRIPTION     '(c)2007-2009 Wang Xuebin'

EXPORTS

                FuncInDll @1 PRIVATE

你會發(fā)現(xiàn)def的語法很簡單,首先是LIBRARY關(guān)鍵字,指定dll的名字;然后一個可選的關(guān)鍵字DESCRIPTION,后面寫上版權(quán)等信息(不寫也可以);最后是EXPORTS關(guān)鍵字,后面寫上dll中所有要輸出的函數(shù)名或變量名,然后接上@以及依次編號的數(shù)字(從1N),最后接上修飾符。

用如下命令編譯鏈接帶有def文件的dll

Cl /c dll_def.cpp

Link /dll dll_def.obj /def:dll_def.def

再調(diào)用dumpbin查看生成的dll_def.dll

Dumpbin –exports dll_def.dll

得到如下結(jié)果:

Dump of file dll_def.dll

File Type: DLL

 Section contains the following exports for dll_def.dll

           0 characteristics

    46E4EE98 time date stamp Mon Sep 10 15:13:28 2007

        0.00 version

           1 ordinal base

           1 number of functions

           1 number of names

    ordinal hint RVA      name

          1    0 00001000 FuncInDll

 Summary

        2000 .data

        1000 .rdata

        1000 .reloc

        6000 .text

觀察這一行

          1    0 00001000 FuncInDll

會發(fā)現(xiàn)該dll輸出了函數(shù)FuncInDll

顯式調(diào)用DLL中的函數(shù)

寫一個dll_def.dll的客戶端程序:dll_def_client.cpp

#include <windows.h>

#include <iostream.h>

int main(void)

{

    //定義一個函數(shù)指針

    typedef void (* DLLWITHLIB )(void); 

    //定義一個函數(shù)指針變量

    DLLWITHLIB pfFuncInDll = NULL; 

    //加載我們的dll

    HINSTANCE hinst=::LoadLibrary("dll_def.dll"); 

    if (NULL != hinst)

    {

       cout<<"dll loaded!"<<endl;

    }

    //找到dllFuncInDll函數(shù)

    pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll"); 

    //調(diào)用dll里的函數(shù)

    if (NULL != pfFuncInDll)

    {

       (*pfFuncInDll)();  

    }

    return 0;

}

有兩個地方值得注意,第一是函數(shù)指針的定義和使用,不懂的隨便找本c++書看看;第二是GetProcAddress的使用,這個API是用來查找dll中的函數(shù)地址的,第一個參數(shù)是DLL的句柄,即LoadLibrary返回的句柄,第二個參數(shù)是dll中的函數(shù)名稱,即dumpbin中輸出的函數(shù)名(注意,這里的函數(shù)名稱指的是編譯后的函數(shù)名,不一定等于dll源代碼中的函數(shù)名)。

編譯鏈接這個客戶端程序,并執(zhí)行會得到:

dll loaded!

FuncInDll is called!

這表明客戶端成功調(diào)用了dll中的函數(shù)FuncInDll

__declspec(dllexport)

為每個dlldef顯得很繁雜,目前def使用已經(jīng)比較少了,更多的是使用__declspec(dllexport)在源代碼中定義dll的輸出函數(shù)。

Dll寫法同上,去掉def文件,并在每個要輸出的函數(shù)前面加上聲明__declspec(dllexport),例如:

__declspec(dllexport) void FuncInDll (void)

這里提供一個dll源程序dll_withlib.cpp,然后編譯鏈接。鏈接時不需要指定/DEF:參數(shù),直接加/DLL參數(shù)即可,

Cl /c dll_withlib.cpp

Link /dll dll_withlib.obj

然后使用dumpbin命令查看,得到:

1    0 00001000 ?FuncInDll@@YAXXZ

可知編譯后的函數(shù)名為?FuncInDll@@YAXXZ,而并不是FuncInDll,這是因為c++編譯器基于函數(shù)重載的考慮,會更改函數(shù)名,這樣使用顯式調(diào)用的時候,也必須使用這個更改后的函數(shù)名,這顯然給客戶帶來麻煩。為了避免這種現(xiàn)象,可以使用extern “C”指令來命令c++編譯器以c編譯器的方式來命名該函數(shù)。修改后的函數(shù)聲明為:

extern "C" __declspec(dllexport) void FuncInDll (void)

dumpbin命令結(jié)果:

1    0 00001000 FuncInDll

這樣,顯式調(diào)用時只需查找函數(shù)名為FuncInDll的函數(shù)即可成功。

extern “C”

使用extern “C”關(guān)鍵字實際上相當(dāng)于一個編譯器的開關(guān),它可以將c++語言的函數(shù)編譯為c語言的函數(shù)名稱。即保持編譯后的函數(shù)符號名等于源代碼中的函數(shù)名稱。

隱式調(diào)用DLL

顯式調(diào)用顯得非常復(fù)雜,每次都要LoadLibrary,并且每個函數(shù)都必須使用GetProcAddress來得到函數(shù)指針,這對于大量使用dll函數(shù)的客戶是一種困擾。而隱式調(diào)用能夠像使用c函數(shù)庫一樣使用dll中的函數(shù),非常方便快捷。

下面是一個隱式調(diào)用的例子:dll包含兩個文件dll_withlibAndH.cppdll_withlibAndH.h。

代碼如下:dll_withlibAndH.h

extern "C" __declspec(dllexport) void FuncInDll (void);

dll_withlibAndH.cpp

#include <objbase.h>

#include <iostream.h>

#include "dll_withLibAndH.h"http://看到?jīng)]有,這就是我們增加的頭文件

extern "C" __declspec(dllexport) void FuncInDll (void)

{

    cout<<"FuncInDll is called!"<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

        g_hModule=NULL;

        break;

    }

    return TRUE;

}

編譯鏈接命令:

Cl /c dll_withlibAndH.cpp

Link /dll dll_withlibAndH.obj

在進行隱式調(diào)用的時候需要在客戶端引入頭文件,并在鏈接時指明dll對應(yīng)的lib文件(dll只要有函數(shù)輸出,則鏈接的時候會產(chǎn)生一個與dll同名的lib文件)位置和名稱。然后如同調(diào)用api函數(shù)庫中的函數(shù)一樣調(diào)用dll中的函數(shù),不需要顯式的LoadLibraryGetProcAddress。使用最為方便??蛻舳舜a如下:dll_withlibAndH_client.cpp

#include "dll_withLibAndH.h"

//注意路徑,加載 dll的另一種方法是 Project | setting | link 設(shè)置里

#pragma comment(lib,"dll_withLibAndH.lib")

int main(void)

{

    FuncInDll();//只要這樣我們就可以調(diào)用dll里的函數(shù)了

    return 0;

}

__declspec(dllexport)__declspec(dllimport)配對使用

上面一種隱式調(diào)用的方法很不錯,但是在調(diào)用DLL中的對象和重載函數(shù)時會出現(xiàn)問題。因為使用extern “C”修飾了輸出函數(shù),因此重載函數(shù)肯定是會出問題的,因為它們都將被編譯為同一個輸出符號串(c語言是不支持重載的)。

事實上不使用extern “C”是可行的,這時函數(shù)會被編譯為c++符號串,例如(?FuncInDll@@YAXH@Z、 ?FuncInDll@@YAXXZ),當(dāng)客戶端也是c++時,也能正確的隱式調(diào)用。

這時要考慮一個情況:若DLL1.CPP是源,DLL2.CPP使用了DLL1中的函數(shù),但同時DLL2也是一個DLL,也要輸出一些函數(shù)供Client.CPP使用。那么在DLL2中如何聲明所有的函數(shù),其中包含了從DLL1中引入的函數(shù),還包括自己要輸出的函數(shù)。這個時候就需要同時使用__declspec(dllexport)__declspec(dllimport)了。前者用來修飾本dll中的輸出函數(shù),后者用來修飾從其它dll中引入的函數(shù)。

所有的源代碼包括DLL1.HDLL1.CPP,DLL2.HDLL2.CPP,Client.cpp。源代碼可以在下載的包中找到。你可以編譯鏈接并運行試試。

值得關(guān)注的是DLL1DLL2中都使用的一個編碼方法,見DLL2.H

#ifdef DLL_DLL2_EXPORTS

#define DLL_DLL2_API __declspec(dllexport)

#else

#define DLL_DLL2_API __declspec(dllimport)

#endif

DLL_DLL2_API void FuncInDll2(void);

DLL_DLL2_API void FuncInDll2(int);

在頭文件中以這種方式定義宏DLL_DLL2_EXPORTSDLL_DLL2_API,可以確保DLL端的函數(shù)用__declspec(dllexport)修飾,而客戶端的函數(shù)用__declspec(dllimport)修飾。當(dāng)然,記得在編譯dll時加上參數(shù)/D “DLL_DLL2_EXPORTS”,或者干脆就在dllcpp文件第一行加上#define DLL_DLL2_EXPORTS。

VC生成的代碼也是這樣的!事實證明,我是抄襲它的,hoho!

DLL中的全局變量和對象

解決了重載函數(shù)的問題,那么dll中的全局變量和對象都不是問題了,只是有一點語法需要注意。如源代碼所示:dll_object.h

#ifdef DLL_OBJECT_EXPORTS

#define DLL_OBJECT_API __declspec(dllexport)

#else

#define DLL_OBJECT_API __declspec(dllimport)

#endif

DLL_OBJECT_API void FuncInDll(void);

extern DLL_OBJECT_API int g_nDll;

class DLL_OBJECT_API CDll_Object {

public:

    CDll_Object(void);

    show(void);

    // TODO: add your methods here.

};

Cpp文件dll_object.cpp如下:

#define DLL_OBJECT_EXPORTS

#include <objbase.h>

#include <iostream.h>

#include "dll_object.h"

DLL_OBJECT_API void FuncInDll(void)

{

    cout<<"FuncInDll is called!"<<endl;

}

DLL_OBJECT_API int g_nDll = 9;

CDll_Object::CDll_Object()

{

    cout<<"ctor of CDll_Object"<<endl;

}

CDll_Object::show()

{

    cout<<"function show in class CDll_Object"<<endl;

}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)

{

    HANDLE g_hModule;

    switch(dwReason)

    {

    case DLL_PROCESS_ATTACH:

       g_hModule = (HINSTANCE)hModule;

       break;

    case DLL_PROCESS_DETACH:

        g_hModule=NULL;

        break;

    }

    return TRUE;

}

編譯鏈接完后Dumpbin一下,可以看到輸出了5個符號:

1    0 00001040 ??0CDll_Object@@QAE@XZ

 2    1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z

 3    2 00001020 ?FuncInDll@@YAXXZ

 4    3 00008040 ?g_nDll@@3HA

 5    4 00001069 ?show@CDll_Object@@QAEHXZ

它們分別代表類CDll_Object,類的構(gòu)造函數(shù),FuncInDll函數(shù),全局變量g_nDll和類的成員函數(shù)show。下面是客戶端代碼:dll_object_client.cpp

#include "dll_object.h"

#include <iostream.h>

//注意路徑,加載 dll的另一種方法是 Project | setting | link 設(shè)置里

#pragma comment(lib,"dll_object.lib")

int main(void)

{

    cout<<"call dll"<<endl;

    cout<<"call function in dll"<<endl;

    FuncInDll();//只要這樣我們就可以調(diào)用dll里的函數(shù)了

    cout<<"global var in dll g_nDll ="<<g_nDll<<endl;

    cout<<"call member function of class CDll_Object in dll"<<endl;

    CDll_Object obj;

    obj.show();

    return 0;

}

運行這個客戶端可以看到:

call dll

call function in dll

FuncInDll is called!

global var in dll g_nDll =9

call member function of class CDll_Object in dll

ctor of CDll_Object

function show in class CDll_Object

可知,在客戶端成功的訪問了dll中的全局變量,并創(chuàng)建了dll中定義的C++對象,還調(diào)用了該對象的成員函數(shù)。

中間的小結(jié)

牢記一點,說到底,DLL是對應(yīng)C語言的動態(tài)鏈接技術(shù),在輸出C函數(shù)和變量時顯得方便快捷;而在輸出C++類、函數(shù)時需要通過各種手段,而且也并沒有完美的解決方案,除非客戶端也是c++。

記住,只有COM是對應(yīng)C++語言的技術(shù)。

下面開始對各各問題一一小結(jié)。

顯式調(diào)用和隱式調(diào)用

何時使用顯式調(diào)用?何時使用隱式調(diào)用?我認(rèn)為,只有一個時候使用顯式調(diào)用是合理的,就是當(dāng)客戶端不是C/C++的時候。這時是無法隱式調(diào)用的。例如用VB調(diào)用C++寫的dll。(VB我不會,所以沒有例子)

Def__declspec(dllexport)

其實def的功能相當(dāng)于extern “C” __declspec(dllexport),所以它也僅能處理C函數(shù),而不能處理重載函數(shù)。而__declspec(dllexport)__declspec(dllimport)配合使用能夠適應(yīng)任何情況,因此__declspec(dllexport)是更為先進的方法。所以,目前普遍的看法是不使用def文件,我也同意這個看法。

從其它語言調(diào)用DLL

從其它編程語言中調(diào)用DLL,有兩個最大的問題,第一個就是函數(shù)符號的問題,前面已經(jīng)多次提過了。這里有個兩難選擇,若使用extern “C”,則函數(shù)名稱保持不變,調(diào)用較方便,但是不支持函數(shù)重載等一系列c++功能;若不使用extern “C”,則調(diào)用前要查看編譯后的符號,非常不方便。

第二個問題就是函數(shù)調(diào)用壓棧順序的問題,即__cdecl__stdcall的問題。__cdecl是常規(guī)的C/C++調(diào)用約定,這種調(diào)用約定下,函數(shù)調(diào)用后棧的清理工作是由調(diào)用者完成的。__stdcall是標(biāo)準(zhǔn)的調(diào)用約定,即這些函數(shù)將在返回到調(diào)用者之前將參數(shù)從棧中刪除。

這兩個問題DLL都不能很好的解決,只能說湊合著用。但是在COM中,都得到了完美的解決。所以,要在Windows平臺實現(xiàn)語言無關(guān)性,還是只有使用COM中間件。

總而言之,除非客戶端也使用C++,否則dll是不便于支持函數(shù)重載、類等c++特性的。DLLc函數(shù)的支持很好,我想這也是為什么windows的函數(shù)庫使用Cdll實現(xiàn)的理由之一。

VC中編寫DLL

VC中創(chuàng)建、編譯、鏈接dll是非常方便的,點擊fileàNewàProjectàWin32 Dynamic-Link Library,輸入dll名稱dll_InVC然后點擊確定。然后選擇A DLL that export some symbols,點擊Finish。即可得到一個完整的DLL。

仔細(xì)觀察其源代碼,是不是有很多地方似曾相識啊,哈哈!

最后

貼上女兒近照一張,小家伙長得太快了!


    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多

    日本加勒比在线观看不卡| 黑丝袜美女老师的小逼逼| 日韩精品一区二区三区射精| 偷拍洗澡一区二区三区| 亚洲专区中文字幕在线| 五月天婷亚洲天婷综合网| 黄色美女日本的美女日人| 亚洲男女性生活免费视频| 伊人天堂午夜精品草草网| 国产成人一区二区三区久久| 一区二区日本一区二区欧美| 日本午夜免费观看视频| 国产一区二区久久综合| 好吊视频有精品永久免费| 精品欧美日韩一二三区| 国产丝袜极品黑色高跟鞋| 国产中文字幕一区二区| 亚洲综合香蕉在线视频| 欧美日韩综合综合久久久| 久久福利视频这里有精品| 激情图日韩精品中文字幕| 欧美黑人在线精品极品| 欧美做爰猛烈叫床大尺度| 欧美午夜视频免费观看| 亚洲深夜精品福利一区| 成人午夜爽爽爽免费视频| 亚洲一级二级三级精品| 国产麻豆视频一二三区| 午夜日韩在线观看视频| 亚洲综合日韩精品欧美综合区| 午夜精品福利视频观看| 国产精品久久熟女吞精| 日韩综合国产欧美一区| 国产高清一区二区不卡| 丝袜诱惑一区二区三区| 日韩亚洲精品国产第二页| 欧美一区二区三区99| 免费大片黄在线观看日本| 国产av一区二区三区久久不卡| 亚洲国产黄色精品在线观看| 亚洲国产精品久久综合网|