• <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>

            loop_in_codes

            低調(diào)做技術(shù)__歡迎移步我的獨(dú)立博客 codemaro.com 微博 kevinlynx

            使用Clang實(shí)現(xiàn)C語(yǔ)言編程規(guī)范檢查

            概述

            Clang是LLVM編譯器工具集的前端部分,也就是涵蓋詞法分析、語(yǔ)法語(yǔ)義分析的部分。而LLVM是Apple在Mac OS上用于替代GCC工具集的編譯器軟件集合。Clang支持類C語(yǔ)言的語(yǔ)言,例如C、C++、Objective C。Clang的與眾不同在于其模塊化的設(shè)計(jì),使其不僅實(shí)現(xiàn)編譯器前端部分,并且包裝成庫(kù)的形式提供給上層應(yīng)用。使用Clang可以做諸如語(yǔ)法高亮、語(yǔ)法檢查、編程規(guī)范檢查方面的工作,當(dāng)然也可以作為你自己的編譯器前端。

            編程規(guī)范一般包含編碼格式和語(yǔ)義規(guī)范兩部分。編碼格式用于約定代碼的排版、符號(hào)命名等;而語(yǔ)義規(guī)范則用于約定諸如類型匹配、表達(dá)式復(fù)雜度等,例如不允許對(duì)常數(shù)做邏輯運(yùn)算、檢查變量使用前是否被賦值等。本文描述的主要是基于語(yǔ)義方面的檢查,其經(jīng)驗(yàn)來(lái)自于最近做的一個(gè)檢查工具,該工具實(shí)現(xiàn)了超過(guò)130條的規(guī)范。這份規(guī)范部分規(guī)則來(lái)自于MISRA C

            編程模式

            編譯器前端部分主要是輸出代碼對(duì)應(yīng)的抽象語(yǔ)法樹(AST)。Clang提供給上層的接口也主要是圍繞語(yǔ)法樹來(lái)做操作。通過(guò)google一些Clang的資料,你可能會(huì)如我當(dāng)初一樣對(duì)該如何正確地使用Clang心存疑惑。我最后使用的方式是基于RecursiveASTVisitor。這是一種類似回調(diào)的使用機(jī)制,通過(guò)提供特定語(yǔ)法樹節(jié)點(diǎn)的接口,Clang在遍歷語(yǔ)法樹的時(shí)候,在遇到該節(jié)點(diǎn)時(shí),就會(huì)調(diào)用到上層代碼。不能說(shuō)這是最好的方式,但起碼它可以工作。基于RecursiveASTVisitor使用Clang,程序主體框架大致為:

            // 編寫你感興趣的語(yǔ)法樹節(jié)點(diǎn)訪問(wèn)接口,例如該例子中提供了函數(shù)調(diào)用語(yǔ)句和goto語(yǔ)句的節(jié)點(diǎn)訪問(wèn)接口
            class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
            public:
                bool VisitCallExpr(CallExpr *expr);
            
                bool VisitGotoStmt(GotoStmt *stmt);
                ...
            };
            
            class MyASTConsumer : public ASTConsumer {
            public:
                virtual bool HandleTopLevelDecl(DeclGroupRef DR) {
                    for (DeclGroupRef::iterator b = DR.begin(), e = DR.end(); b != e; ++b) {
                        Visitor.TraverseDecl(*b);
                    }
                    return true;
                } 
                
            private:
                MyASTVisitor Visitor;
            };
            
            int main(int argc, char **argv) {
                CompilerInstance inst;
                Rewriter writer;
                inst.createFileManager();
                inst.createSourceManager(inst.getFileManager());
                inst.createPreprocessor();
                inst.createASTContext();
                writer.setSourceMgr(inst.getSourceManager(), inst.getLangOpts());
                ... // 其他初始化CompilerInstance的代碼
              
                const FileEntry *fileIn = fileMgr.getFile(argv[1]);
                sourceMgr.createMainFileID(fileIn);
                inst.getDiagnosticClient().BeginSourceFile(inst.getLangOpts(), &inst.getPreprocessor());
                MyASTConsumer consumer(writer);
                ParseAST(inst.getPreprocessor(), &consumer, inst.getASTContext());
                inst.getDiagnosticClient().EndSourceFile();
                return 0;
            }
            

            以上代碼中,ParseAST為Clang開始分析代碼的主入口,其中提供了一個(gè)ASTConsumer。每次分析到一個(gè)頂層定義時(shí)(Top level decl)就會(huì)回調(diào)MyASTConsumer::HandleTopLevelDecl,該函數(shù)的實(shí)現(xiàn)里調(diào)用MyASTVisitor開始遞歸訪問(wèn)該節(jié)點(diǎn)。這里的decl實(shí)際上包含定義。

            這里使用Clang的方式來(lái)源于Basic source-to-source transformation with Clang

            語(yǔ)法樹

            Clang中視所有代碼單元為語(yǔ)句(statement),Clang中使用類Stmt來(lái)代表statement。Clang構(gòu)造出來(lái)的語(yǔ)法樹,其節(jié)點(diǎn)類型就是Stmt。針對(duì)不同類型的語(yǔ)句,Clang有對(duì)應(yīng)的Stmt子類,例如GotoStmt。Clang中的表達(dá)式也被視為語(yǔ)句,Clang使用Expr類來(lái)表示表達(dá)式,而Expr本身就派生于Stmt

            每個(gè)語(yǔ)法樹節(jié)點(diǎn)都會(huì)有一個(gè)子節(jié)點(diǎn)列表,在Clang中一般可以使用如下語(yǔ)句遍歷一個(gè)節(jié)點(diǎn)的子節(jié)點(diǎn):

            for (Stmt::child_iterator it = stmt->child_begin(); it != stmt->child_end(); ++it) {
                Stmt *child = *it;
            }
            

            但遺憾的是,無(wú)法從一個(gè)語(yǔ)法樹節(jié)點(diǎn)獲取其父節(jié)點(diǎn),這將給我們的規(guī)范檢測(cè)工具的實(shí)現(xiàn)帶來(lái)一些麻煩。

            TraverseXXXStmt

            在自己實(shí)現(xiàn)的Visitor中(例如MyASTVisitor),除了可以提供VisitXXXStmt系列接口去訪問(wèn)某類型的語(yǔ)法樹節(jié)點(diǎn)外,還可以提供TraverseXXXStmt系列接口。Traverse系列的接口包裝對(duì)應(yīng)的Visit接口,即他們的關(guān)系大致為:

            bool TraverseGotoStmt(GotoStmt *s) {
                VisitGotoStmt(s);
                return true;
            }
            

            例如對(duì)于GotoStmt節(jié)點(diǎn)而言,Clang會(huì)先調(diào)用TraverseGotoStmt,在TraverseGotoStmt的實(shí)現(xiàn)中才會(huì)調(diào)用VisitGotoStmt。利用Traverse和Visit之間的調(diào)用關(guān)系,我們可以解決一些因?yàn)椴荒茉L問(wèn)某節(jié)點(diǎn)父節(jié)點(diǎn)而出現(xiàn)的問(wèn)題。例如,我們需要限制逗號(hào)表達(dá)式的使用,在任何地方一旦檢測(cè)到逗號(hào)表達(dá)式的出現(xiàn),都給予警告,除非這個(gè)逗號(hào)表達(dá)式出現(xiàn)在for語(yǔ)句中,例如:

            a = (a = 1, b = 2); /* 違反規(guī)范,非法 */
            for (a = 1, b = 2; a < 2; ++a) /* 合法 */
            

            逗號(hào)表達(dá)式對(duì)應(yīng)的訪問(wèn)接口為VisitBinComma,所以我們只需要提供該接口的實(shí)現(xiàn)即可:

            class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
            public:
                ...
                bool VisitBinComma(BinaryOperator *stmt) {
                    /* 報(bào)告錯(cuò)誤 */
                    return true;
                }
                ...
            };
            

            (注:BinaryOperator用于表示二目運(yùn)算表達(dá)式,例如a + b,逗號(hào)表達(dá)式也是二目表達(dá)式)

            但在循環(huán)中出現(xiàn)的逗號(hào)表達(dá)式也會(huì)調(diào)用到VisitBinComma。為了有效區(qū)分該逗號(hào)表達(dá)式是否出現(xiàn)在for語(yǔ)句中,我們可以期望獲取該逗號(hào)表達(dá)式的父節(jié)點(diǎn),并檢查該父節(jié)點(diǎn)是否為for語(yǔ)句。但Clang并沒(méi)有提供這樣的能力,我想很大一部分原因在于臆測(cè)語(yǔ)法樹(抽象語(yǔ)法樹)節(jié)點(diǎn)的組織結(jié)構(gòu)(父節(jié)點(diǎn)、兄弟節(jié)點(diǎn))本身就不是一個(gè)確定的事。

            這里的解決辦法是通過(guò)提供TraverseForStmt,以在進(jìn)入for語(yǔ)句前得到一個(gè)標(biāo)識(shí):

            class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
            public:
                ...
                // 這個(gè)函數(shù)的實(shí)現(xiàn)可以參考RecursiveASTVisitor的默認(rèn)實(shí)現(xiàn),我們唯一要做的就是在for語(yǔ)句的頭那設(shè)定一個(gè)標(biāo)志m_inForLine
                bool TraverseForStmt(ForStmt *s) {
                    if (!WalkUpFromForStmt(s))
                        return false;
                    m_inForLine = true;
                    for (Stmt::child_range range = s->children(); range; ++range) {
                        if (*range == s->getBody())
                            m_inForLine = false;
                        TraverseStmt(*range);
                    }
                    return true;
                }
            
                bool VisitBinComma(BinaryOperator *stmt) {
                    if (!m_inForLine) {
                        /* 報(bào)告錯(cuò)誤 */
                    }
                    return true;
                }
                ...
            };
            

            (注:嚴(yán)格來(lái)說(shuō),我們必須檢查逗號(hào)表達(dá)式是出現(xiàn)在for語(yǔ)句的頭中,而不包括for語(yǔ)句循環(huán)體)

            類型信息

            對(duì)于表達(dá)式(Expr)而言,都有一個(gè)類型信息。Clang直接用于表示類型的類是QualType,實(shí)際上這個(gè)類只是一個(gè)接口包裝。這些類型信息可以用于很多類型相關(guān)的編程規(guī)范檢查。例如不允許定義超過(guò)2級(jí)的指針(例如int ***p):

            bool MyASTVisitor::VisitVarDecl(VarDecl *decl) { // 當(dāng)發(fā)現(xiàn)變量定義時(shí)該接口被調(diào)用
                QualType t = decl->getType(); // 取得該變量的類型
                int pdepth = 0;
                // check pointer level
                for ( ; t->isPointerType(); t = t->getPointeeType()) { // 如果是指針類型就獲取其指向類型(PointeeType)
                    ++pdepth;
                }
                if (pdepth >= 3)
                    /* 報(bào)告錯(cuò)誤 */
            }
            

            可以直接調(diào)用Expr::getType接口,用于獲取指定表達(dá)式最終的類型,基于此我們可以檢查復(fù)雜表達(dá)式中的類型轉(zhuǎn)換,例如:

            float f = 2.0f;
            double d = 1.0;
            f = d * f; /* 檢查此表達(dá)式 */
            

            對(duì)以上表達(dá)式的檢查有很多方法,你可以實(shí)現(xiàn)MyASTVisitor::VisitBinaryOperator(只要是二目運(yùn)算符都會(huì)調(diào)用),或者M(jìn)yASTVisitor::VisitBinAssign(賦值運(yùn)算=調(diào)用)。無(wú)論哪種方式,我們都可以提供一個(gè)遞歸檢查兩個(gè)表達(dá)式類型是否相同的接口:

            bool HasDiffType(BinaryOperator *stmt) {
                Expr *lhs = stmt->getLHS()->IgnoreImpCasts(); // 忽略隱式轉(zhuǎn)換
                Expr *rhs = stmt->getRHS()->IgnoreImpCasts();
                if (lhs->getType() == rhs->getType())) {
                    if (isa<BinaryOperator>(lhs) && HasDiffType(cast<BinaryOperator>(lhs)))
                        return true;
                    if (isa<BinaryOperator>(rhs) && HasDiffType(cast<BinaryOperator>(rhs)))
                        return true;
                    return false;
                }
                return true;
            }
            

            (注:此函數(shù)只是簡(jiǎn)單實(shí)現(xiàn),未考慮類型修飾符之類的問(wèn)題)

            該函數(shù)獲得二目運(yùn)算表達(dá)式的兩個(gè)子表達(dá)式,然后遞歸檢測(cè)這兩個(gè)表達(dá)式的類型是否相同。

            Expr類提供了更多方便的類型相關(guān)的接口,例如判定該表達(dá)式是否為常數(shù),是否是布爾表達(dá)式,甚至在某些情況下可以直接計(jì)算得到值。例如我們可以檢查明顯的死循環(huán):

            while (1) { }
            

            可以使用:

            ASTContext &context = inst.GetASTContext();
            bool result;
            // 假設(shè)stmt為WhileStmt
            if (stmt->getCond()->EvaluateAsBooleanCondition(result, context)) {
                if (result) 
                    /* 死循環(huán) */
            

            符號(hào)表

            符號(hào)表這個(gè)概念比較廣義,這里我僅指的是用于保存類型和變量信息的表。Clang中沒(méi)有顯示的符號(hào)表數(shù)據(jù)結(jié)構(gòu),但每一個(gè)定義都有一個(gè)DeclContextDeclContext用于描述一個(gè)定義的上下文環(huán)境。有一個(gè)特殊的DeclContext被稱為translation unit decl,其實(shí)也就是全局環(huán)境。利用這個(gè)translation unit decl,我們可以獲取一些全局符號(hào),例如全局變量、全局類型:

            // 獲取全局作用域里指定名字的符號(hào)列表
            DeclContext::lookup_result GetGlobalDecl(const std::string &name) {
                ASTContext &context = CompilerInst::getSingleton().GetASTContext();
                DeclContext *tcxt = context.getTranslationUnitDecl();
                IdentifierInfo &id = context.Idents.get(name);
                return tcxt->lookup(DeclarationName(&id));
            }
            
            // 可以根據(jù)GetGlobalDecl的返回結(jié)果,檢查該列表里是否有特定的定義,例如函數(shù)定義、類型定義等
            bool HasSpecDecl(DeclContext::lookup_result ret, Decl::Kind kind) {
                for (size_t i = 0; i < ret.size(); ++i) {
                    NamedDecl *decl = ret[i];
                    if (decl->getKind() == kind) {
                        return true;
                    }
                }
                return false;
            }
            

            有了以上兩個(gè)函數(shù),我們要檢測(cè)全局作用域里是否有名為”var”的變量定義,就可以:

            HasSpecDecl(GetGlobalDecl("var"), Decl::Var);
            

            每一個(gè)Decl都有對(duì)應(yīng)的DeclContext,要檢查相同作用域是否包含相同名字的符號(hào),其處理方式和全局的方式有點(diǎn)不一樣:

            // 檢查在ctx中是否有與decl同名的符號(hào)定義
            bool HasSymbolInContext(const NamedDecl *decl, const DeclContext *ctx) {
                for (DeclContext::decl_iterator it = ctx->decls_begin(); it != ctx->decls_end(); ++it) {
                    Decl *d = *it;
                    if (d != decl && isa<NamedDecl>(d) && 
                        cast<NamedDecl>(d)->getNameAsString() == decl->getNameAsString())
                        return true;
                }
                return false;
            }
            
            bool HasSymbolInContext(const NamedDecl *decl) {
                return HasSymbolInContext(decl, decl->getDeclContext());
            }
            

            可以看出,這里檢查相同作用域的方式是遍歷上下文環(huán)境中的所有符號(hào),但對(duì)于全局作用域卻是直接查找。對(duì)于DeclContext的詳細(xì)信息我也不甚明了,只能算湊合使用。實(shí)際上,這里使用“作用域”一詞并不準(zhǔn)確,在C語(yǔ)言中的作用域概念,和這里的context概念在Clang中并非等同。

            如果要檢查嵌套作用域里不能定義相同名字的變量,例如:

            int var;
            {
                int var;
            }
            

            通過(guò)Clang現(xiàn)有的API是無(wú)法實(shí)現(xiàn)的。因?yàn)镃lang給上層的語(yǔ)法樹結(jié)構(gòu)中,并不包含作用域信息(在Clang的實(shí)現(xiàn)中,用于語(yǔ)義分析的類Sema實(shí)際上有作用域的處理)。當(dāng)然,為了實(shí)現(xiàn)這個(gè)檢測(cè),我們可以手動(dòng)構(gòu)建作用域信息(通過(guò)TraverseCompoundStmt)。

            宏的處理屬于預(yù)處理階段,并不涵蓋在語(yǔ)法分析階段,所以通過(guò)Clang的語(yǔ)法樹相關(guān)接口是無(wú)法處理的。跟宏相關(guān)的接口,都是通過(guò)Clang的Preprocessor相關(guān)接口。Clang為此提供了相應(yīng)的處理機(jī)制,上層需要往Preprocessor對(duì)象中添加回調(diào)對(duì)象,例如:

            class MyPPCallback : public PPCallbacks {
            public:
                // 處理#include
                virtual void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok,
                    StringRef FileName, bool IsAngled, CharSourceRange FilenameRange,
                    const FileEntry *File, StringRef SearchPath, StringRef RelativePath, const Module *Imported) {
                }
            
                // 處理#define
                virtual void MacroDefined(const Token &MacroNameTok, const MacroInfo *MI) {
                }
            
                virtual void MacroUndefined(const Token &MacroNameTok, const MacroInfo *MI) {
                } 
            }
            
            inst.getPreprocessor().addPPCallbacks(new MyPPCallback());
            

            即,通過(guò)實(shí)現(xiàn)PPCallbacks中對(duì)應(yīng)的接口,就可以獲得處理宏的通知。

            Clang使用MacroInfo去表示一個(gè)宏。MacroInfo將宏體以一堆token來(lái)保存,例如我們要檢測(cè)宏體中使用###的情況,則只能遍歷這些tokens:

            // 分別記錄#和##在宏體中使用的數(shù)量
            int hash = 0, hashhash = 0;
            for (MacroInfo::tokens_iterator it = MI->tokens_begin(); it != MI->tokens_end(); ++it) {
                const Token &token = *it;
                hash += (token.getKind() == tok::hash ? 1 : 0);
                hashhash += (token.getKind() == tok::hashhash ? 1 : 0);
            }
            

            其他

            在我們所支持的編程規(guī)范中,有些規(guī)范是難以支持的,因此我使用了一些蹩腳的方式來(lái)實(shí)現(xiàn)。

            手工解析

            在針對(duì)函數(shù)的參數(shù)定義方面,我們支持的規(guī)范要求不能定義參數(shù)為空的函數(shù),如果該函數(shù)沒(méi)有參數(shù),則必須以void顯示標(biāo)識(shí),例如:

            int func(); /* 非法 */
            int func(void); /* 合法 */
            

            對(duì)于Clang而言,函數(shù)定義(或聲明)使用的是FunctionDecl,而Clang記錄的信息僅包括該函數(shù)是否有參數(shù),參數(shù)個(gè)數(shù)是多少,并不記錄當(dāng)其參數(shù)個(gè)數(shù)為0時(shí)是否使用void來(lái)聲明(記錄下來(lái)沒(méi)多大意義)。解決這個(gè)問(wèn)題的辦法,可以通過(guò)SourceLocation獲取到對(duì)應(yīng)源代碼中的文本內(nèi)容,然后對(duì)此文本內(nèi)容做手工分析即可。

            (注:SourceLocation是Clang中用于表示源代碼位置的類,包括行號(hào)和列號(hào),所有Stmt都會(huì)包含此信息)

            通過(guò)SourceLocation獲取對(duì)應(yīng)源碼的內(nèi)容:

            std::pair<FileID, unsigned> locInfo = SM.getDecomposedLoc(loc);
            bool invalidTemp = false;
            llvm::StringRef file = SM.getBufferData(locInfo.first, &invalidTemp);
            if (invalidTemp)
                return false;
            // tokenBegin即為loc對(duì)應(yīng)源碼內(nèi)容的起始點(diǎn)
            const char *tokenBegin = file.data() + locInfo.second;
            

            要手工分析這些內(nèi)容實(shí)際上還是有點(diǎn)繁雜,為此我們可以直接使用Clang中詞法分析相關(guān)的組件來(lái)完成這件事:

            Lexer *lexer = new Lexer(SM.getLocForStartOfFile(locInfo.first), opts, file.begin(), tokenBegin, file.end());
            Token tok;
            lexer->Lex(tok); // 取得第一個(gè)tok,反復(fù)調(diào)用可以獲取一段token流
            

            Diagnostic

            Clang中用Diagnostic來(lái)進(jìn)行編譯錯(cuò)誤的提示。每一個(gè)編譯錯(cuò)誤(警告、建議等)都會(huì)有一段文字描述,這些文字描述為了支持多國(guó)語(yǔ)言,使用了一種ID的表示方法。總之,對(duì)于一個(gè)特定的編譯錯(cuò)誤提示而言,其diagnostic ID是固定的。

            在我們的規(guī)范中,有些規(guī)范檢測(cè)的代碼在Clang中會(huì)直接編譯出錯(cuò),例如函數(shù)調(diào)用傳遞的參數(shù)個(gè)數(shù)不等于函數(shù)定義時(shí)的形參個(gè)數(shù)。當(dāng)Clang編譯出錯(cuò)時(shí),其語(yǔ)法樹實(shí)際上是不完善的。解決此問(wèn)題的最簡(jiǎn)單辦法,就是通過(guò)diagnostic實(shí)現(xiàn)。也就是說(shuō),我是通過(guò)將我們的特定規(guī)范映射到特定的diagnostic,當(dāng)發(fā)生這個(gè)特定的編譯錯(cuò)誤時(shí),就可以認(rèn)定該規(guī)范實(shí)際上被檢測(cè)到。對(duì)于簡(jiǎn)單的情況而言,這樣的手段還算奏效。

            // `TextDiagnosticPrinter`可以將錯(cuò)誤信息打印在控制臺(tái)上,為了調(diào)試方便我從它派生而來(lái)
            class MyDiagnosticConsumer : public TextDiagnosticPrinter {
            public:
                // 當(dāng)一個(gè)錯(cuò)誤發(fā)生時(shí),會(huì)調(diào)用此函數(shù),我會(huì)在這個(gè)函數(shù)里通過(guò)Info.getID()取得Diagnostic ID,然后對(duì)應(yīng)地取出規(guī)范ID
                virtual void HandleDiagnostic(DiagnosticsEngine::Level DiagLevel,
                    const Diagnostic &Info) {
                    TextDiagnosticPrinter::HandleDiagnostic(DiagLevel, Info);
                    // 例如檢查三字母詞(trigraph)的使用
                    if (Info.getID() == 816)
                        /* 報(bào)告使用了三字母詞 */
                }
            };
            
            // 初始化時(shí)需傳入自己定義的diagnostic
            inst.createDiagnostics(0, NULL, new MyDiagnosticConsumer(&inst.getDiagnosticOpts()));
            

            該例子代碼演示了對(duì)三字母詞(wiki trigraph)使用限制的規(guī)范檢測(cè)。

            全文完。

            posted on 2013-02-12 21:53 Kevin Lynx 閱讀(10215) 評(píng)論(0)  編輯 收藏 引用 所屬分類: c/c++

            久久综合日本熟妇| 国产精品久久久久影院嫩草| 国产精品内射久久久久欢欢| 久久se精品一区精品二区国产| 少妇被又大又粗又爽毛片久久黑人| 亚洲精品无码久久毛片| 欧美黑人又粗又大久久久| 热re99久久精品国产99热| 日韩中文久久| 麻豆久久| 久久综合久久综合亚洲| 久久精品这里热有精品| 99久久免费国产精精品| 久久成人影院精品777| 亚洲另类欧美综合久久图片区| 久久成人永久免费播放| 久久婷婷五月综合色99啪ak| 亚洲国产精品综合久久一线| 亚洲欧洲久久久精品| 日韩欧美亚洲综合久久| 国产99久久久久久免费看 | 久久亚洲天堂| 日本高清无卡码一区二区久久| 久久亚洲2019中文字幕| 久久人人爽人人人人爽AV| 99久久国产主播综合精品| 性欧美大战久久久久久久久| 深夜久久AAAAA级毛片免费看| 99久久做夜夜爱天天做精品| 人妻无码αv中文字幕久久琪琪布 人妻无码久久一区二区三区免费 人妻无码中文久久久久专区 | 国产69精品久久久久APP下载| 国产亚洲精久久久久久无码77777 国产亚洲精品久久久久秋霞 | 国产午夜精品久久久久九九| 亚洲国产成人精品久久久国产成人一区二区三区综| 久久婷婷五月综合色99啪ak| 狼狼综合久久久久综合网| 丁香五月综合久久激情| 久久久久久久久久久精品尤物 | 亚洲精品国产第一综合99久久| 久久综合给久久狠狠97色| 精品免费久久久久国产一区|