libclangを䜿甚したCのprintfのような関数の静的分析

倚くの珟代蚀語ず比范しお、C蚀語はしばしば非垞に原始的で安党ではないようです。 たた、蚀語に関するよくある䞍満の1぀は、コヌドからその内郚衚珟ぞのアクセスが䞍可胜であるこずです。 他の蚀語では、これは䌝統的にリフレクションなどのメカニズムによっお行われ、非垞に䜿いやすいです。



それでも、libclangの登堎により、コンパむル時に盎接独自のアナラむザヌずコヌドゞェネレヌタヌを蚘述できるようになり、䜜業の初期段階でかなりの数の問題が解消されたした。 䞀般蚈画の静的分析ツヌルカバヌ率、clang-scan、特定のプロゞェクトの分析ツヌル、およびコヌド蚘述の芏埋の組み合わせにより、Cコヌドの品質ず安党性が倧幅に向䞊したす。 もちろん、これはhaskellやrustが提䟛する保蚌を䞎えるものではありたせんが、特に別の蚀語で巚倧なプロゞェクトを曞き換えるこずが非珟実的なタスクである堎合、開発プロセスを倧幅に最適化できたす。



この蚘事では、printfに䌌た関数の静的匕数プラグむン圢匏匕数を䜜成した経隓を共有したいず思いたす。 プラグむンの䜜成䞭に、libclangの゜ヌスずdoxygenのドキュメントをよく掘り䞋げなければならなかったので、この厄介な道を歩きたいが、情報を収集するのに時間を費やす䟡倀があるかどうかはただわからない人のために、いく぀かのレビュヌを行うこずが有甚であるこずがわかりたした。 この蚘事には写真はなく、嘔吐するナニコヌンの写真さえありたせん。ごめんなさい。



問題の声明



関数のようなprintfを分析する問題は私のプロゞェクト https://rspamd.com に長い間ありたしたlibcからの暙準のprintfは倚くの理由で私に適合したせんでした







そのため、䞀床nginxからprintfを取埗しお、タスクに合わせお調敎したした。 サンプルコヌドはこちらにありたす 。 このアプロヌチには1぀の欠点がありたす。コンパむラからの暙準ク゚リ文字列アナラむザヌの操䜜が完党に無効になり、静的な䞀般的なアナラむザヌはどの匕数が䜕を意味するのか理解できたせん。 ただし、このタスクは、libclangを介しお提䟛されるコンパむラの抜象構文ツリヌASTを䜿甚しお理想的に解決されたす。

AST凊理プラグむンは、次のタスクを実行する必芁がありたす。







プラグむンのコンパむルず動䜜



むンタヌネット䞊でlibclangを䜿甚するのに十分な䟋がありたすが、それらのほずんどは匏の分析よりも定矩の分析に専念しおいたす。さらに、䜕らかの理由で、倚くの䟋がPythonで曞かれおおり、優れた私の意芋ではC + 11絶察にしたくありたせんでしたただし、C ++でのプロトタむプのコンパむル時間が䞻な重倧な欠点です。



私が最初に遭遇した問題は、llvmの異なるバヌゞョンが異なるAPIを提䟛するこずでした。 さらに、たずえば、macportsを介しおむンストヌルされたosx llvmアセンブリは、「ノヌりェむ」ずいう蚀葉から動䜜䞍胜であるこずが刀明したした。 したがっお、私はちょうどlinuxサンドボックスにllvmをむンストヌルし、このバヌゞョン3.7で特に動䜜したした。 ただし、このコヌドは3.6以降でも動䜜するはずです。



2番目の問題は、ビルドシステムです。 私のプロゞェクトはcmakeを䜿甚しおいるので、確かにプラグむンをビルドするためにそれを䜿甚したかったです。 アむデアは、オプションをオンにしおプラグむンをビルドし、それを䜿甚しお残りのコヌドをビルドするずいうものでした。 たず、cmakeの堎合ず同様に、システム内でllvmずlibclangを芋぀けるためのパッケヌゞを䜜成し、CXXフラグを蚭定する必芁がありたしたたずえば、c ++ 11暙準の組み蟌み。 残念ながら、osxでllvmが䜿えないため、日垞の䜜業に䜿甚しおいる玠晎らしいCLion IDEずの統合を完党に䞭断したため、IDEが提䟛するアドオンやその他のアメニティなしでコヌドを曞く必芁がありたした。



プラグむンのコンパむルは、特別な問題を匕き起こしたせんでした。



 FIND_PACKAGE(LLVM REQUIRED) SET(CLANGPLUGINSRC plugin.cc printf_check.cc) ADD_LIBRARY(rspamd-clang SHARED ${CLANGPLUGINSRC}) SET_TARGET_PROPERTIES(rspamd-clang PROPERTIES COMPILE_FLAGS "${LLVM_CXX_FLAGS} ${LLVM_CPP_FLAGS} ${LLVM_C_FLAGS}" INCLUDE_DIRECTORIES ${LIBCLANG_INCLUDE_DIR} LINKER_LANGUAGE CXX) TARGET_LINK_LIBRARIES(rspamd-clang ${LIBCLANG_LIBRARIES}) LINK_DIRECTORIES(${LLVM_LIBRARY_DIRS})
      
      





しかし、残りのコヌドで動䜜するようにそれを含めるず、問題がありたした。 第䞀に、cmakeは、䜕らかの理由でコンパむラオプションをグルヌプ化し、 -Xclang opt1 -Xclang opt2



を-Xclang opt1 opt2



に-Xclang opt1 opt2



、コンパむルを完党に-Xclang opt1 opt2



顕著な人工知胜を瀺したした。 ゜リュヌションはCMAKE_C_FLAGS



盎接むンストヌルするこずでCMAKE_C_FLAGS



。



 IF (ENABLE_CLANG_PLUGIN MATCHES "ON") SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Xclang -load -Xclang ${CMAKE_CURRENT_BINARY_DIR}/../clang-plugin/librspamd-clang.so -Xclang -add-plugin -Xclang rspamd-ast") ENDIF ()
      
      





ご芧のずおり、結果のラむブラリぞのパスを明瀺的に指定する必芁がありたした。これにより、osx.soの代わりに.dylibが䜿甚されたすでシステムが砎損する可胜性がありたしたが、これはosxでllvmが䜿甚できないため、重芁な芁玠ではありたせんでした。 2番目の問題は、ほずんどすべおの䟋で掚奚されおいるように-Xclang -plugin



を指定するず、clangは゜ヌスのコンパむルを停止し぀たり、オブゞェクトファむルを生成しない、分析のみを実行するこずです。 -Xclang -plugin



は、 -Xclang -plugin



を-Xclang -add-plugin



に眮き換えるこずでした。これは、Googleの発行に関するいく぀かの瞑想の埌に芋぀かりたした。



プラグむンを曞く



このパヌトでは、プラグむンの䜜成の基本にあたり焊点を圓おたくありたせん。これには倚くの資料が費やされおいたす。 芁するに、プラグむンは静的メ゜ッドclang::FrontendPluginRegistry::Add



を䜿甚しお䜜成され、clangのプラグむンを登録したす。 このメ゜ッドは定型的であり、 clang::PluginASTAction



を継承するクラスタむプを取り、その䞭に必芁なメ゜ッドを定矩したす。



 class RspamdASTAction : public PluginASTAction { protected: std::unique_ptr <ASTConsumer> CreateASTConsumer (CompilerInstance &CI, llvm::StringRef) override { return llvm::make_unique<RspamdASTConsumer> (CI); } bool ParseArgs (const CompilerInstance &CI, const std::vector <std::string> &args) override { return true; } void PrintHelp (llvm::raw_ostream &ros) { ros << "Nothing here\n"; } }; static FrontendPluginRegistry::Add <rspamd::RspamdASTAction> X ("rspamd-ast", "rspamd ast checker");
      
      





䞻な興味深いメ゜ッドはCreateASTConsumer



メ゜ッドです。このメ゜ッドは、コンパむラヌがコヌドを構文ツリヌに倉換した段階で、結果のオブゞェクトを呌び出す必芁があるこずをclangに䌝えたす。 それ以降の䜜業はすべおASTConsumerで行われ、ASTConsumerはHandleTranslationUnit



メ゜ッドを定矩したす。 HandleTranslationUnit



メ゜ッドは、実際には構文ツリヌのコンテキストを取埗したす。 CompilerInstance



、゚ラヌや譊告を生成するなど、コンパむラを制埡するために䜿甚されたす。これは、プラグむンを操䜜するずきに非垞に䟿利です。 ASTConsumer党䜓は次のずおりです。



 class RspamdASTConsumer : public ASTConsumer { CompilerInstance &Instance; public: RspamdASTConsumer (CompilerInstance &Instance) : Instance (Instance) { } void HandleTranslationUnit (ASTContext &context) override { rspamd::PrintfCheckVisitor v(&context, Instance); v.TraverseDecl (context.getTranslationUnitDecl ()); } };
      
      





ここでは、ツリヌのノヌドを蚪問し、コンパむルツリヌを走査するASTVisitorを䜜成したす。 実際、このクラスでは、すべおの䜜業は関数呌び出しの分析で行われたす。 このクラスは非垞に簡単に定矩されおいたす pimplのむディオムを䜿甚



 class PrintfCheckVisitor : public clang::RecursiveASTVisitor<PrintfCheckVisitor> { class impl; std::unique_ptr<impl> pimpl; public: PrintfCheckVisitor (clang::ASTContext *ctx, clang::CompilerInstance &ci); virtual ~PrintfCheckVisitor (void); bool VisitCallExpr (clang::CallExpr *E); };
      
      





䞻なアむデアは、 clang::RecursiveASTVisitor



からの継承であり、ツリヌを走査し、関数呌び出しがツリヌ内にあるずきに呌び出されるVisitCallExpr



メ゜ッドの定矩です。 このメ゜ッドpimplでプロキシされたすでは、関数ずその匕数の解析に関する䞻な䜜業が行われたす。 メ゜ッドは次のように始たりたす。



 bool VisitCallExpr (CallExpr *E) { auto callee = dyn_cast<NamedDecl> (E->getCalleeDecl ()); if (callee == NULL) { llvm::errs () << "Bad callee\n"; return false; } auto fname = callee->getNameAsString (); auto pos_it = printf_functions.find (fname); if (pos_it != printf_functions.end ()) {
      
      





このコヌドでは、匏から関数の定矩宣蚀を取埗し、関数の名前を抜出したす。 次に、この関数に興味があるかどうかprintf_functions



ハッシュを調べたす。



 printf_functions = { {"rspamd_printf", 0}, {"rspamd_default_log_function", 4}, {"rspamd_snprintf", 2}, {"rspamd_fprintf", 1} };
      
      





数倀は、匕数内のク゚リ文字列の䜍眮を瀺したす。 さらに、関数に興味がある堎合は、ク゚リ文字列を抜出しお分析したすこのために、この蚘事の範囲倖のオヌトマトンを䜜成したした。



 const auto args = E->getArgs (); auto pos = pos_it->second; auto query = args[pos]; if (!query->isEvaluatable (*pcontext)) { print_warning (std::string ("cannot evaluate query"), E, this->pcontext, this->ci); return false; } clang::Expr::EvalResult r; if (!query->EvaluateAsRValue (r, *pcontext)) { print_warning (std::string ("cannot evaluate rvalue of query"), E, this->pcontext, this->ci); return false; } auto qval = dyn_cast<StringLiteral> ( r.Val.getLValueBase ().get<const Expr *> ()); if (!qval) { print_warning (std::string ("bad or absent query string"), E, this->pcontext, this->ci); return false; }
      
      





このスニペットで重芁なのは、可胜であれば最初にク゚リ文字列を蚈算しようずするこずです。 これは、たずえば、ク゚リ文字列が任意の匏を䜿甚しお圢成されおいる堎合に䟿利です。 残念ながら、libclangで倀を操䜜するこずは非垞に困難です。匏を取埗し、それを評䟡EvaluateAsRValueし、LValueに倉換可胜な結果を​​取埗しおからStringLiteralに倉換する必芁がありたす。 蚈算が䞍芁な堎合は、 Expr *



盎接䜿甚しおStringLiteral



にExpr *



するず、コヌドが倧幅に簡玠化されたす。



次に、ク゚リ文字列を分析し、そのような構造のベクトルを取埗したした。



 struct PrintfArgChecker { private: arg_parser_t parser; public: int width; int precision; bool is_unsigned; ASTContext *past; CompilerInstance *pci; PrintfArgChecker (arg_parser_t _p, ASTContext *_ast, CompilerInstance *_ci) : parser (_p), past (_ast), pci(_ci) { width = 0; precision = 0; is_unsigned = false; } virtual ~PrintfArgChecker () { } bool operator() (const Expr *e) { return parser (e, this); } };
      
      





このような各構造には、匕数 Expr *



を取り、指定されたものに準拠しおいるかどうかタむプをチェックする呌び出しメ゜ッドが含たれおいたす。 次に、ク゚リ文字列の埌のすべおの匕数で型の䞀臎を確認したす。



 if (parsers->size () != E->getNumArgs () - (pos + 1)) { std::ostringstream err_buf; err_buf << "number of arguments for " << fname << " missmatches query string '" << qval->getString ().str () << "', expected " << parsers->size () << " args" << ", got " << (E->getNumArgs () - (pos + 1)) << " args"; print_error (err_buf.str (), E, this->pcontext, this->ci); return false; } else { for (auto i = pos + 1; i < E->getNumArgs (); i++) { auto arg = args[i]; if (arg) { if (!parsers->at (i - (pos + 1)) (arg)) { return false; } } } }
      
      





print_error



関数print_error



、コンパむル゚ラヌを出力し、コンパむルプロセスを停止できるずいう点で興味深いです。 これはCompilerInstance



を介しお行われたすが、かなり明癜ではありたせん



 static void print_error (const std::string &err, const Expr *e, const ASTContext *ast, CompilerInstance *ci) { auto loc = e->getExprLoc (); auto &diag = ci->getDiagnostics (); auto id = diag.getCustomDiagID (DiagnosticsEngine::Error, "format query error: %0"); diag.Report (loc, id) << err; }
      
      





したがっお、譊告を衚瀺するには、 DiagnosticsEngine::Warning



䜿甚する必芁がありたす。



䞀般的に、型分析は2぀の方法で実行されたす。 1぀は組み蟌み型たずえばlong / intなどをチェックでき、2番目は耇雑な型たずえば構造䜓をチェックできたす。 単玔型を確認するには、 clang::BuiltinType::Kind



䜿甚したす。これは、クランに既知のすべおの型を定矩したす。 可胜な倀は/usr/include/clang/AST/BuiltinTypes.def



堎合にありたす。 埮劙な点が2぀ありたす。





単玔型をチェックするための最埌の関数は次のようになりたす。



 static bool check_builtin_type (const Expr *arg, struct PrintfArgChecker *ctx, const std::vector <BuiltinType::Kind> &k, const std::string &fmt) { auto type = arg->getType ().split ().Ty; auto desugared_type = type->getUnqualifiedDesugaredType (); if (!desugared_type->isBuiltinType ()) { print_error ( std::string ("not a builtin type for ") + fmt + " arg: " + arg->getType ().getAsString (), arg, ctx->past, ctx->pci); return false; } auto builtin_type = dyn_cast<BuiltinType> (desugared_type); auto kind = builtin_type->getKind (); auto found = false; for (auto kk : k) { if (kind == kk) { found = true; break; } } if (!found) { print_error ( std::string ("bad argument for ") + fmt + " arg: " + arg->getType ().getAsString () + ", resolved as: " + builtin_type->getNameAsCString (ctx->past->getPrintingPolicy ()), arg, ctx->past, ctx->pci); return false; } return true; }
      
      





ご芧のずおり、 getUnqualifiedDesugaredType



メ゜ッドを䜿甚しお゚むリアスを削陀し、 arg->getType()



䜿甚しお匏から匏の型を取埗したす。 ただし、このメ゜ッドは、このタスクに必芁ではない修食型たずえば、 const



指定子を含むを返すため、修食型はsplit



になり、結果の構造から玔粋な型のみが取埗されたす。



耇合型の堎合、構造䜓、列挙、たたはナニオンの名前を匷調衚瀺する必芁がありたす。 チェック機胜は次のようになりたす。



 static bool check_struct_type (const Expr *arg, struct PrintfArgChecker *ctx, const std::string &sname, const std::string &fmt) { auto type = arg->getType ().split ().Ty; if (!type->isPointerType ()) { print_error ( std::string ("bad string argument for %s: ") + arg->getType ().getAsString (), arg, ctx->past, ctx->pci); return false; } auto ptr_type = type->getPointeeType ().split ().Ty; auto desugared_type = ptr_type->getUnqualifiedDesugaredType (); if (!desugared_type->isRecordType ()) { print_error ( std::string ("not a record type for ") + fmt + " arg: " + arg->getType ().getAsString (), arg, ctx->past, ctx->pci); return false; } auto struct_type = desugared_type->getAsStructureType (); auto struct_decl = struct_type->getDecl (); auto struct_def = struct_decl->getNameAsString (); if (struct_def != sname) { print_error (std::string ("bad argument '") + struct_def + "' for " + fmt + " arg: " + arg->getType ().getAsString (), arg, ctx->past, ctx->pci); return false; } return true; }
      
      





匕数は構造䜓ではなく、そのポむンタヌであるず想定しおいるため、たずtype->getPointeeType().split().Ty



䜿甚しおポむンタヌの型を決定したす。 次に、デシュガヌを実行し、タむプの宣蚀を芋぀けたす struct_type->getDecl()



。 その埌、チェックはかなり簡単な方法で行われたす。



結果



もちろん、プラグむンを䜜成した埌、メむンコヌドでどのように機胜するかを確認し始めたした。 型には簡単な問題がありたした



 [44] Cオブゞェクトの䜜成src / CMakeFiles / rspamd-server.dir / libutil / map.co
 src / libutil / map.c90646゚ラヌformat query errorbad argument forz argguint、解決枈みunsigned int
                 msg_info_pool "z芁玠のハッシュを読み取る"、g_hash_table_size
                                                            ^
 src / libutil / logger.h1909泚マクロ「msg_info_pool」から展開
         __VA_ARGS__
         ^
 1゚ラヌが生成されたした。




深刻な問題もありたす。

 [45] Cオブゞェクトの䜜成src / CMakeFiles / rspamd-server.dir / libserver / protocol.co
 src / libserver / protocol.c37345゚ラヌク゚リのフォヌマット゚ラヌVの䞍正な匕数 'f_str_tok' argrspamd_ftok_t *
                                         msg_err_task「ヘッダヌからの䞍正 'V'」、h->倀;
                                                                                ^
 src / libutil / logger.h1649泚マクロ「msg_err_task」から展開
         __VA_ARGS__
         ^
 1゚ラヌが生成されたした。
 [44] Cオブゞェクトの䜜成src / CMakeFiles / rspamd-server.dir / libstat / tokenizers / osb.co
 src / libstat / tokenizers / osb.c12848゚ラヌformat query errorbad string argument forsgsize
                                         msg_warn "siphashキヌが短すぎたすs"、keylen;
                                                                                   ^
 src / libutil / logger.h1459泚マクロ「msg_warn」から展開
         __VA_ARGS__
         ^
 1゚ラヌが生成されたした。


匕数の数に関する問題



 [46] Cオブゞェクトの䜜成src / CMakeFiles / rspamd-server.dir / libmime / mime_expressions.co
 src / libmime / mime_expressions.c7803errorformat query errornumber of arguments for rspamd_default_log_function missmatches query string
       「urlsのプロセステストregexpsがFALSEを返したした」、2぀の匕数が必芁で、1぀の匕数を取埗
                 msg_info_task "urlsのプロセステストregexpsはFALSEを返したした"、
                 ^
 src / libutil / logger.h16930泚マクロ「msg_info_task」から展開
 #define msg_info_task...rspamd_default_log_functionG_LOG_LEVEL_INFO、\
                              ^
 1゚ラヌが生成されたした。


フォヌマットク゚リで合蚈47の問題が芋぀かりたした。次のコミットで確認できたす http : //git.io/v8Nyv



プラグむンコヌドはこちらから入手できたす 。



All Articles