• <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>
            隨筆 - 47, 文章 - 10, 評論 - 8, 引用 - 0
            數(shù)據(jù)加載中……

            簡明x86匯編語言教程(六)

            原創(chuàng):司徒彥南

            4.0 利用子程序與中斷

            已經(jīng)掌握了匯編語言?沒錯,你現(xiàn)在已經(jīng)可以去破譯別人代碼中的秘密。然而,我們還有一件重要的東西沒有提到,那就是自程序和中斷。這兩件東西是如此的重要,以至于你的程序幾乎不可能離開它們。

            4.1 子程序

            在高級語言中我們經(jīng)常要用到子程序。高級語言中,子程序是如此的神奇,我們能夠定義和主程序,或其他子程序一樣的變量名,而訪問不同的變量,并且,還不和程序的其他部分相沖突。

            然而遺憾的是,這種“優(yōu)勢”在匯編語言中是不存在的。

            匯編語言并不注重如何減輕程序員的負(fù)擔(dān);相反,匯編語言依賴程序員的良好設(shè)計,以期發(fā)揮CPU的最佳性能。匯編語言不是結(jié)構(gòu)化的語言,因此,它不提供直接的“局部變量”。如果需要“局部變量”,只能通過堆或棧自行實現(xiàn)。

            從這個意義上講,匯編語言的子程序更像GWBASIC中的GOSUB調(diào)用的那些“子程序”。所有的“變量”(本質(zhì)上,屬于進(jìn)程的內(nèi)存和寄存器)為整個程序所共享,高級語言編譯器所做的,將局部變量放到堆或棧中的操作,只能自行實現(xiàn)。

            參數(shù)的傳遞是靠寄存器和堆棧來完成的。高級語言中,子程序(函數(shù)、過程,或類似概念的東西)依賴于堆和棧來傳遞。

            讓我們來簡單地分析一下一般高級語言的子程序的執(zhí)行過程。無論C、C++、BASIC、Pascal,這一部分基本都是一致的。


            • 調(diào)用者將子程序執(zhí)行完成時應(yīng)返回的地址、參數(shù)壓入堆棧
            • 子程序使用BP指針+偏移量對棧中的參數(shù)尋址,并取出、完成操作
            • 子程序使用RET或RETF指令返回。此時,CPU將IP置為堆棧中保存的地址,并繼續(xù)予以執(zhí)行

            毋庸置疑,堆棧在整個過程中發(fā)揮著非常重要的作用。不過,本質(zhì)上對子程序最重要的還是返回地址。如果子程序不知道這個地址,那么系統(tǒng)將會崩潰。

            調(diào)用子程序的指令是CALL,對應(yīng)的返回指令是RET。此外,還有一組指令,即ENTER和LEAVE,它們可以幫助進(jìn)行堆棧的維護(hù)。

            CALL指令的參數(shù)是被調(diào)用子程序的地址。使用宏匯編的時候,這通常是一個標(biāo)號。CALL和RET,以及ENTER和LEAVE配對,可以實現(xiàn)對于堆棧的自動操作,而不需要程序員進(jìn)行PUSH/POP,以及跳轉(zhuǎn)的操作,從而提高了效率。

            作為一個編譯器的實現(xiàn)實例,我用Visual C++編譯了一段C++程序代碼,這段匯編代碼是使用特定的編譯選項得到的結(jié)果,正常的RELEASE代碼會比它精簡得多。包含源代碼的部分反匯編結(jié)果如下(取自Visual C++調(diào)試器的運(yùn)行結(jié)果,我刪除了10條int 3指令,并加上了一些注釋,除此之外,沒有做任何修改):

            1: int myTransform(int nInput){
            00401000 push ebp?????????????????? ; 保護(hù)現(xiàn)場原先的EBP指針
            00401001 mov ebp,esp
            2: return (nInput*2 + 3) % 7;
            00401003 mov eax,dword ptr [nInput] ; 取參數(shù)
            00401006 lea eax,[eax+eax+3]??????? ; LEA比ADD加法更快
            0040100A cdq??????????????????????? ; DWORD->QWORD(擴(kuò)展字長)
            0040100B mov ecx,7????????????????? ; 除數(shù)
            00401010 idiv eax,ecx?????????????? ; 除
            00401012 mov eax,edx??????????????? ; 商->eax(eax中保存返回值)
            3: }
            00401014 pop ebp??????????????????? ; 恢復(fù)現(xiàn)場的ebp指針
            00401015 ret??????????????????????? ; 返回
            ; 此處刪除10條int 3指令,它們是方便調(diào)試用的,并不影響程序行為。

            4:
            5:
            int main(int argc, char* argv[])
            6: {
            00401020 push ebp?????????????????? ; 保護(hù)現(xiàn)場原先的EBP指針
            00401021 mov ebp,esp
            00401023 sub esp,10h??????????????? ; 為取argc, argv修正堆棧指針。
            7:int a[3];
            8:for(register int i=0; i<3; i++){
            00401026 mov dword ptr [i],0??????? ; 0->i
            0040102D jmp main+18h (00401038)??? ; 判斷循環(huán)條件
            0040102F mov eax,dword ptr [i]????? ; i->eax
            00401032 add eax,1????????????????? ; eax ++
            00401035 mov dword ptr [i],eax????? ; eax->i
            00401038 cmp dword ptr [i],3??????? ; 循環(huán)條件: i與3比較
            0040103C jge main+33h (00401053)??? ; 如果不符合條件,則應(yīng)結(jié)束循環(huán)
            9: a[i] = myTransform(i);
            0040103E mov ecx,dword ptr [i]????? ; i->ecx
            00401041 push ecx?????????????????? ; ecx (i) -> 堆棧
            00401042 call myTransform (00401000); 調(diào)用myTransform
            00401047 add esp,4????????????????? ; esp+=4: 在堆中的新單元
            ??????????????????????????????????? ; 準(zhǔn)備存放返回結(jié)果
            0040104A mov edx,dword ptr [i]????? ; i->edx
            0040104D mov dword ptr a[edx*4],eax ; 將eax(myTransform返回值)
            ??????????????????????????????????? ; 放回a[i]
            10: }
            00401051 jmp main+0Fh (0040102f)??? ; 計算i++,并繼續(xù)循環(huán)
            11:return 0;
            00401053 xor eax,eax??????????????? ; 返回值應(yīng)該是0
            12: }
            00401055 mov esp,ebp??????????????? ; 恢復(fù)堆棧指針
            00401057 pop ebp??????????????????? ; 恢復(fù)BP
            00401058 ret??????????????????????? ; 返回調(diào)用者(C++運(yùn)行環(huán)境)

            上述代碼確實做了一些無用功,當(dāng)然,這是因為編譯器沒有對這段代碼進(jìn)行優(yōu)化。讓我們來關(guān)注一下這段代碼中,是如何調(diào)用子程序的。不考慮myTransform這個函數(shù)實際進(jìn)行的數(shù)值運(yùn)算,最讓我感興趣的是這一行代碼:

            00401003 mov eax,dword ptr [nInput] ; 取參數(shù)

            這里nInput是一個簡簡單單的變量符號嗎?Visual C++的調(diào)試器顯然不能告訴我們答案——它的設(shè)計目標(biāo)是為了方便程序調(diào)試,而不是向你揭示編譯器生成的代碼的實際構(gòu)造。我用另外一個反匯編器得到的結(jié)果是:

            00401003 mov eax,dword ptr [ebp+8] ? ; 取參數(shù)

            這和我們在main()中看到的壓棧順序是完全吻合的(注意,程序運(yùn)行到這個地方的時候,EBP=ESP)。main()最終將i的通過堆棧傳遞給了myTransform()。

            剖析上面的程序只是說明了我前面所提到的子程序的一部分用法。對于匯編語言來說,完全沒有必要拘泥于結(jié)構(gòu)化程序設(shè)計的框架(在今天,使用匯編的主要目的在于提高執(zhí)行效率,而不是方便程序的維護(hù)和調(diào)試,因為匯編不可能在這一點(diǎn)上做得比C++更好)??紤]下面的程序:

            void myTransform1(int nCount, char* sBytes){
            ? for(register int i=1; i<nCount; i++)
            ??? sBytes[i] += sBytes[i-1];
            ? for(i=0; i<nCount; i++)
            ??? sBytes[i] <<= 1;
            }

            void myTransform2(int nCount, char* sBytes){
            ? for(register int i=0; i<nCount; i++)
            ??? sBytes[i] <<= 1;
            }

            很容易看出,這兩個函數(shù)包含了公共部分,即

            ? for(i=0; i<nCount; i++)
            ??? sBytes[i] <<= 1;

            目前,還沒有編譯器能夠做到將這兩部分合并。依然沿用剛才的編譯選項,得到的反匯編結(jié)果是(同樣地刪除了int 3):

            1:void myTransform1(int nCount, char* sBytes){
            00401000 push ebp
            00401001 mov ebp,esp
            00401003 push ecx
            2:for(register int i=1; i<nCount; i++)
            00401004 mov dword ptr [i],1
            0040100B jmp myTransform1+16h (00401016)
            0040100D mov eax,dword ptr [i]
            00401010 add eax,1
            00401013 mov dword ptr [i],eax
            00401016 mov ecx,dword ptr [i]
            00401019 cmp ecx,dword ptr [nCount]
            0040101C jge myTransform1+3Dh (0040103d)
            3: sBytes[i] += sBytes[i-1];
            0040101E mov edx,dword ptr [sBytes]
            00401021 add edx,dword ptr [i]
            00401024 movsx eax,byte ptr [edx-1]
            00401028 mov ecx,dword ptr [sBytes]
            0040102B add ecx,dword ptr [i]
            0040102E movsx edx,byte ptr [ecx]
            00401031 add edx,eax
            00401033 mov eax,dword ptr [sBytes]
            00401036 add eax,dword ptr [i]
            00401039 mov byte ptr [eax],dl
            0040103B jmp myTransform1+0Dh (0040100d)
            4:for(i=0; i<nCount; i++)
            0040103D mov dword ptr [i],0
            00401044 jmp myTransform1+4Fh (0040104f)
            00401046 mov ecx,dword ptr [i]
            00401049 add ecx,1
            0040104C mov dword ptr [i],ecx
            0040104F mov edx,dword ptr [i]
            00401052 cmp edx,dword ptr [nCount]
            00401055 jge myTransform1+6Bh (0040106b)
            5: sBytes[i] <<= 1;
            00401057 mov eax,dword ptr [sBytes]
            0040105A add eax,dword ptr [i]
            0040105D mov cl,byte ptr [eax]
            0040105F shl cl,1
            00401061 mov edx,dword ptr [sBytes]
            00401064 add edx,dword ptr [i]
            00401067 mov byte ptr [edx],cl
            00401069 jmp myTransform1+46h (00401046)
            6: }
            0040106B mov esp,ebp
            0040106D pop ebp
            0040106E ret
            7:
            8:void myTransform2(int nCount, char* sBytes){
            00401070 push ebp
            00401071 mov ebp,esp
            00401073 push ecx
            9:for(register int i=0; i<nCount; i++)
            00401074 mov dword ptr [i],0
            0040107B jmp myTransform2+16h (00401086)
            0040107D mov eax,dword ptr [i]
            00401080 add eax,1
            00401083 mov dword ptr [i],eax
            00401086 mov ecx,dword ptr [i]
            00401089 cmp ecx,dword ptr [nCount]
            0040108C jge myTransform2+32h (004010a2)
            10: sBytes[i] <<= 1;
            0040108E mov edx,dword ptr [sBytes]
            00401091 add edx,dword ptr [i]
            00401094 mov al,byte ptr [edx]
            00401096 shl al,1
            00401098 mov ecx,dword ptr [sBytes]
            0040109B add ecx,dword ptr [i]
            0040109E mov byte ptr [ecx],al
            004010A0 jmp myTransform2+0Dh (0040107d)
            11: }
            004010A2 mov esp,ebp
            004010A4 pop ebp
            004010A5 ret
            12:
            13:
            int main(int argc, char* argv[])
            14: {
            004010B0 push ebp
            004010B1 mov ebp,esp
            004010B3 sub esp,0CCh
            15:char a[200];
            16:for(register int i=0; i<200; i++)a[i]=i;
            004010B9 mov dword ptr [i],0
            004010C3 jmp main+24h (004010d4)
            004010C5 mov eax,dword ptr [i]
            004010CB add eax,1
            004010CE mov dword ptr [i],eax
            004010D4 cmp dword ptr [i],0C8h
            004010DE jge main+45h (004010f5)
            004010E0 mov ecx,dword ptr [i]
            004010E6 mov dl,byte ptr [i]
            004010EC mov byte ptr a[ecx],dl
            004010F3 jmp main+15h (004010c5)
            17: myTransform1(200, a);
            004010F5 lea eax,[a]
            004010FB push eax
            004010FC push 0C8h
            00401101 call myTransform1 (00401000)
            00401106 add esp,8
            18: myTransform2(200, a);
            00401109 lea ecx,[a]
            0040110F push ecx
            00401110 push 0C8h
            00401115 call myTransform2 (00401070)
            0040111A add esp,8
            19:return 0;
            0040111D xor eax,eax
            20: }
            0040111F mov esp,ebp
            00401121 pop ebp
            00401122 ret

            非常明顯地,0040103d-0040106e和00401074-004010a5這兩段代碼存在少量的差別,但很顯然只是對寄存器的偏好不同(編譯器在優(yōu)化時,這可能會減少堆棧操作,從而提高性能,但在這里只是使用了不同的寄存器而已)

            對代碼進(jìn)行合并的好處是非常明顯的。新的操作系統(tǒng)往往使用頁式內(nèi)存管理。當(dāng)內(nèi)存不足時,程序往往會頻繁引發(fā)頁面失效(Page faults),從而引發(fā)操作系統(tǒng)從磁盤中讀取一些東西。磁盤的速度趕不上內(nèi)存的速度,因此,這一行為將導(dǎo)致性能的下降。通過合并一部分代碼,可以減少程序的大小,這意味著減少頁面失效的可能性,從而軟件的性能會有所提高?/p>

            當(dāng)然,這樣做的代價也不算低——你的程序?qū)⒆兊秒y懂,并且難于維護(hù)。因此,再進(jìn)行這樣的優(yōu)化之前,一定要注意:


            • 優(yōu)化前的程序必須是正確的。如果你不能確保這一點(diǎn),那么這種優(yōu)化必將給你的調(diào)試帶來極大的麻煩。
            • 優(yōu)化前的程序?qū)崿F(xiàn)最好是最優(yōu)的。仔細(xì)檢查你的設(shè)計,看看是否已經(jīng)使用了最合適(即,對于此程序而言最優(yōu))的算法,并且已經(jīng)在高級語言許可的范圍內(nèi)進(jìn)行了最好的實現(xiàn)。
            • 優(yōu)化最好能夠非常有效地減少程序大小(例如,如果只是減少十幾個字節(jié),恐怕就沒什么必要了),或非常有效地提高程序的運(yùn)行速度(如果代碼只是運(yùn)行一次,并且只是節(jié)省幾個時鐘周期,那么在多數(shù)場合都沒有意義)。否則,這種優(yōu)化將得不償失。

            4.2 中斷

            中斷應(yīng)該說是一個陳舊的話題。在新的系統(tǒng)中,它的作用正在逐漸被削弱,而變成操作系統(tǒng)專用的東西。并不是所有的計算機(jī)系統(tǒng)都提供中斷,然而在x86系統(tǒng)中,它的作用是不可替代的。

            中斷實際上是一類特殊的子程序。它通常由系統(tǒng)調(diào)用,以響應(yīng)突發(fā)事件。

            例如,進(jìn)行磁盤操作時,為了提高性能,可能會使用DMA方式進(jìn)行操作。CPU向DMA控制器發(fā)出指令,要求外設(shè)和內(nèi)存直接交換數(shù)據(jù),而不通過CPU。然后,CPU轉(zhuǎn)去進(jìn)行起他的操作;當(dāng)數(shù)據(jù)交換結(jié)束時,CPU可能需要進(jìn)行一些后續(xù)操作,但此時它如何才能知道DMA已經(jīng)完成了操作呢?

            很顯然不是依靠CPU去查詢狀態(tài)——這樣DMA的優(yōu)勢就不明顯了。為了盡可能地利用DMA的優(yōu)勢,在完成DMA操作的時候,DMA會告訴CPU“這事兒我辦完了”,然后CPU會根據(jù)需要進(jìn)行處理。

            這種處理可能很復(fù)雜,需要若干條指令來完成。子程序是一個不錯的主意,不過,CALL指令需要指定地址,讓外設(shè)強(qiáng)迫CPU執(zhí)行一條CALL指令也違背了CPU作為核心控制單元的設(shè)計初衷??紤]到這些,在x86系統(tǒng)中引入了中斷向量的概念。

            中斷向量表是保存在系統(tǒng)數(shù)據(jù)區(qū)(實模式下,是0:0開始的一段區(qū)域)的一組指針。這組指針指向每一個中斷服務(wù)程序的地址。整個中斷向量表的結(jié)構(gòu)是一個線性表。

            每一個中斷服務(wù)有自己的唯一的編號,我們通常稱之為中斷號。每一個中斷號對應(yīng)中斷向量表中的一項,也就是一個中斷向量。外設(shè)向CPU發(fā)出中斷請求,而CPU自己將根據(jù)當(dāng)前的程序狀態(tài)決定是否中斷當(dāng)前程序并調(diào)用相應(yīng)的中斷服務(wù)。

            不難根據(jù)造成中斷的原因?qū)⒅袛喾譃閮深悾河布袛嗪蛙浖袛?。硬件中斷有很多分類方法,如根?jù)是否可以屏蔽分類、根據(jù)優(yōu)先級高低分類,等等。考慮到這些分類并不一定科學(xué),并且對于我們介紹中斷的使用沒有太大的幫助,因此我并不打算太詳細(xì)地介紹它(在本教程的高級篇中,關(guān)于加密解密的部分會提到某些硬件中斷的利用,但那是后話)。

            在設(shè)計操作系統(tǒng)時,中斷向量的概念曾經(jīng)帶來過很大的便利。操作系統(tǒng)隨時可能升級,這樣,通過CALL來調(diào)用操作系統(tǒng)的服務(wù)(如果說每個程序都包含對于文件系統(tǒng)、進(jìn)程表這些應(yīng)該由操作系統(tǒng)管理的數(shù)據(jù)的直接操作的話,不僅會造成程序的臃腫,而且不利于系統(tǒng)的安全)就顯得不太合適了——沒人能知道,以后的操作系統(tǒng)的服務(wù)程序入口點(diǎn)會不會是那兒。軟件中斷的存在為解決這個問題提供了方便。

            對于一臺包含了BIOS的計算機(jī)來說,啟動的時候系統(tǒng)已經(jīng)提供了一部分服務(wù),例如顯示服務(wù)。無論你的BIOS、顯示卡有多么的“個性”,只要他們和IBM PC兼容,那么此時你肯定可以通過調(diào)用16(10h)號中斷來使用顯示服務(wù)。調(diào)用中斷的指令是


            int 中斷號
            ??

            這將引發(fā)CPU去調(diào)用一個中斷。CPU將保存當(dāng)前的程序狀態(tài)字,清除Trap和Interrupt兩個標(biāo)志,將即將執(zhí)行的指令地址壓入堆棧,并調(diào)用中斷服務(wù)(根據(jù)中斷向量表)。

            編寫中斷服務(wù)程序不是一件容易的事情。很多時候,中斷服務(wù)程序必須寫成可重入代碼(或純代碼,pure code)。所謂可重入代碼是指,程序的運(yùn)行過程中可以被打斷,并由開始處再次執(zhí)行,并且在合理的范圍內(nèi)(多次重入,而不造成堆棧溢出等其他問題),程序可以在被打斷處繼續(xù)執(zhí)行,并且執(zhí)行結(jié)果不受影響。

            由于在多線程環(huán)境中等其他一些地方進(jìn)行程序設(shè)計時也需要考慮這個因素,因此這里著重講一下可重入代碼的編寫。

            可重入代碼最主要的要求就是,程序不應(yīng)使用某個指定的內(nèi)存地址的內(nèi)存(對于高級語言來說,這通常是全局變量,或?qū)ο蟮某蓡T)。如果可能的話,應(yīng)使用寄存器,或其他方式來解決。如果不能做到這一點(diǎn),則必須在開始、結(jié)束的時候分別禁止和啟用中斷,并且,運(yùn)行時間不能太長。

            下面用C語言分別舉一個可重入函數(shù),和兩個非可重入函數(shù)的例子(注. 這些例子應(yīng)該是在某本多線程或操作系統(tǒng)的書上看到的,遺憾的是我想不起來是哪本書了,在這里先感謝那位作者提供的范例):

            可重入函數(shù):

            void strcpy(char* lpszDest, char* lpszSrc){
            ? while(*dest++=*src++);
            ? *dest=0;
            }

            非可重入函數(shù)

            char cTemp;???????????????? ????????????????? // 全局變量

            void SwapChar(char* lpcX, char* lpcY){
            ? cTemp = *lpcX; *lpcX = *lpcY; lpcY = cTemp; // 引用了全局變量,在分享內(nèi)存的多個線程中可能造成問題
            }

            非可重入函數(shù)

            void SwapChar2(char* lpcX, char* lpcY){
            ? static char cTemp;???????????????? ???????? // 靜態(tài)變量
            ? cTemp = *lpcX; *lpcX = *lpcY; lpcY = cTemp; // 引用了靜態(tài)變量,在分享內(nèi)存的多個線程中可能造成問題
            }

            中斷利用的是系統(tǒng)的棧。棧操作是可重入的(因為棧可以保證“先進(jìn)后出”),因此,我們并不需要考慮棧操作的重入問題。使用宏匯編器寫出可重入的匯編代碼需要注意一些問題。簡單地說,干脆不要用標(biāo)號作為變量是一個不錯的主意。

            使用高級語言編寫可重入程序相對來講輕松一些。把持住不訪問那些全局(或當(dāng)前對象的)變量,不使用靜態(tài)局部變量,堅持只適用局部變量,寫出的程序就將是可重入的。

            書歸正傳,調(diào)用軟件中斷時,通常都是通過寄存器傳進(jìn)、傳出參數(shù)。這意味著你的int指令周圍也許會存在一些“幫手”,比如下面的代碼:

            mov ax, 4c00h
            int 21h

            就是通過調(diào)用DOS中斷服務(wù)返回父進(jìn)程,并帶回錯誤反饋碼0。其中,ax中的數(shù)據(jù)4c00h就是傳遞給DOS中斷服務(wù)的參數(shù)。

            到這里,x86匯編語言的基礎(chǔ)部分就基本上講完了,《簡明x86匯編語言教程》的初級篇——匯編語言基礎(chǔ)也就到此告一段落。當(dāng)然,目前為止,我只是蜻蜓點(diǎn)水一般提到了一些學(xué)習(xí)x86匯編語言中我認(rèn)為需要注意的重要概念。許多東西,包括全部匯編語句的時序特性(指令執(zhí)行周期數(shù),以及指令周期中各個階段的節(jié)拍數(shù)等)、功能、參數(shù)等等,限于個人水平和篇幅我都沒有作詳細(xì)介紹。如果您對這些內(nèi)容感興趣,請參考Intel和AMD兩大CPU供應(yīng)商網(wǎng)站上提供的開發(fā)人員參考。

            在以后的簡明x86匯編語言教程中級篇和高級篇中,我將著重介紹匯編語言的調(diào)試技術(shù)、優(yōu)化,以及一些具體的應(yīng)用技巧,包括反跟蹤、反反跟蹤、加密解密、病毒與反病毒等等。

            posted on 2006-11-06 10:36 編程之道 閱讀(401) 評論(0)  編輯 收藏 引用 所屬分類: 開發(fā)相關(guān) 、ASM

            国内精品久久久久久中文字幕| 国产精品一久久香蕉国产线看 | 一本一道久久精品综合| 久久99中文字幕久久| 青青青国产精品国产精品久久久久 | 国产亚洲精久久久久久无码AV| 久久人人爽人人爽AV片| 久久精品中文騷妇女内射| 一本大道加勒比久久综合| 伊人热热久久原色播放www| 久久99精品国产麻豆宅宅| 久久精品免费网站网| 久久综合给合综合久久| 久久精品国产亚洲AV香蕉| 久久综合一区二区无码| 久久777国产线看观看精品| 国产成人综合久久精品红| 久久99国产一区二区三区| 九九精品99久久久香蕉| 久久午夜伦鲁片免费无码| 少妇无套内谢久久久久| 国产精品久久久99| 久久精品国产一区二区三区| 精品熟女少妇a∨免费久久| 热久久视久久精品18| 欧美成人免费观看久久| 99久久无码一区人妻| 久久精品国产91久久麻豆自制| 亚洲国产美女精品久久久久∴| 伊人久久久AV老熟妇色| 色狠狠久久AV五月综合| 女人高潮久久久叫人喷水| 欧洲性大片xxxxx久久久| 亚洲国产精品无码久久青草| 99久久免费只有精品国产| 久久精品成人国产午夜| 久久线看观看精品香蕉国产| 99久久免费国产精精品| 久久无码av三级| 免费一级欧美大片久久网 | 日韩精品久久久肉伦网站|