clangで関数呼び出しを抜き出す
今までずっと「C言語のソースコードを解析する技術」をものにしようと色々勉強していたのが、clangでようやくなんとかなりそうな予感がしてきたのでやったことをまとめようと思う。
これまで挫折した方法
今まではチャレンジしては挫折を繰り返してきた。
試したこと | 挫折した点 | 結果 |
---|---|---|
全部自前 | ・工数的に無理… | 行数カウントとMcCabeのメトリクス集計機能まで作って終了 |
bison/flex++ | ・使い方が難しすぎて導入で挫折 ・文法データの入手が大変 ・結局それなりにコードを書く必要がありそうで心が折れた |
お試し版作成前に頓挫 |
CDTのAST | ・情報自体はそれなりにありそうだが使い方がよくわからず ・そもそもEclipseのplugin作成方法が不明 |
お試し前にclangを発見したので触らず終い |
という感じで10年くらい「ソースをプログラムで解析できると便利なのになぁ」とか思いつつくすぶっていた。
やったこと
今回は手始めに「Cのソースコードから関数呼び出しを抜き出してCSVファイル形式で出力する」ツールを作った。
このツールをどう使うかは今作ってるもう1つの追加ツールがある程度できてから発表したいと思う。
作ったソースコード
基本的に参考文献3のサンプルコードをベースとしている。
LibToolingというclangのライブラリを利用した単体ツールになるみたい。他にもclangのpluginとしてclang自身に組み入れる方法もあるっぽいけど、今回は調査していない。
で、ソースコードは説明のためにぶつ切りで掲載する。
面倒だけど、試す際にはぶつ切りコードを上から順にくっつけて1つに戻して欲しい。
#include "clang/AST/ASTContext.h" #include "clang/AST/ASTConsumer.h" #include "clang/AST/RecursiveASTVisitor.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/FrontendAction.h" #include "clang/Tooling/Tooling.h" #include "clang/Tooling/CommonOptionsParser.h" using namespace clang; class MyAstVisitor : public RecursiveASTVisitor<MyAstVisitor> { public: explicit MyAstVisitor(ASTContext *Context, llvm::StringRef InFile) : Context(Context), source_file(InFile) {}
RecursiveASTVisitor
今回作ったツールを動かすとRecursiveASTVisitorクラスの仮想メンバ関数が適宜呼ばれるのでそいつをオーバーライドしてお好みの解析を実行すれば良いみたい。
std::string toString(const Stmt *p) { std::string s; llvm::raw_string_ostream raw(s); p->printPretty(raw, NULL, Context->getPrintingPolicy()); return raw.str(); }
内部で利用するメンバ関数。
以下の解析処理でいろいろな「Expr」を受け取ることになるが、ExprはStmtのサブクラスなのでStmtを文字列化する処理があると出力が楽。「実クラスが何なのかよく知らないけど、とりあえずソース上の文字列に戻したい」的な時に使う。
実装は「printPretty」とかで適当にググって見つけたコードを参考にした。
std::string getMemberName(MemberExpr *mem) { Expr *base = mem->getBase(); ImplicitCastExpr *parent_imp = dyn_cast<ImplicitCastExpr>(base); MemberExpr *parent_mem = dyn_cast<MemberExpr>(base); DeclRefExpr *parent_def = dyn_cast<DeclRefExpr>(base); std::string dot_arrow; if(parent_mem){ Expr *grandpa = parent_mem->getBase(); ImplicitCastExpr *imp = dyn_cast<ImplicitCastExpr>(grandpa); if(imp){ // ルート1 return getFunctionName(grandpa) + "->" + mem->getMemberNameInfo().getAsString(); }else{ // ルート2 return getMemberName(parent_mem) + "." + mem->getMemberNameInfo().getAsString(); } } if(parent_imp){ // ルート3 return getFunctionName(base) + "->" + mem->getMemberNameInfo().getAsString(); } if(parent_def){ // ルート4 return std::string(parent_def->getFoundDecl()->getName()) + "." + mem->getMemberNameInfo().getAsString(); } return ""; }
内部で利用するメンバ関数。
「xxx->yyy」とか「zzz.www」的な関数名を調べるメンバ関数。
この関数は独自に作ったもので、どこかのドキュメントやサンプルに書いていないのでかなり怪しいと思ってほしい。少なくともC++ソースではまともに機能しないことはわかっている。手元のCソースだとそれっぽくは動いているが未知のソースだとまだダメなコードがあるかもしれない。
で、この関数を説明するためにはclangの動作についてある程度説明をしなければならない。(あくまで独自研究で判明した範囲なので間違いがあることを前提として読んでほしい)
MemberExpr
上記の「xxx->yyy」とか「zzz.www」的な表現がMemberExprに該当する模様。MemberExpr::getBase()で「->」とか「.」の左側の「xxx」とか「zzz」を取り出せるようだ。右側の「yyy」とか「www」はMemberExpr::getMemberNameInfo()で取り出して、文字列としてはgetAsString()を呼び出せばもらえるらしい。
ImplicitCastExpr
ポインタが出現する場面で出てくるっぽい。関数名が関数ポインタとして解釈される時や「->」でポインタ経由する場合などが該当するようだ。ポインタの先?はImplicitCastExpr::getSubExpr()で取得できるけど、getMemberName()では使わない。
DeclRefExpr
これが本当は何を表現しているのか、についてはよくわからない。ただ、左辺値を評価する場面でよく見る気がする。関数呼び出しの文脈においては、関数名や構造体名「xxx」とか「zzz」的な場面で見かける。
これらの名前を取得するにはDeclRefExpr::getFoundDecl()を呼んで、さらにgetName()で文字列化する。
dyn_cast()
参考文献2によると、clangはC++のRTTI機能を使わずに自前でやってるらしい。のできっとその機能なんだと思う。dynamic_cast
RecursiveASTVisitorクラスと今回作ったMyAstVisitorクラスを見たら、サブクラス自身を親クラスのテンプレート引数にするとかって感じでこれがそうだよね。Lokiライブラリだっけ?なんか懐かしい。
getFunctionName()
独自に作ったメンバ関数。この後解説するので省略。
std::string getFunctionName(Expr *callee_expr) { ImplicitCastExpr *imp = dyn_cast<ImplicitCastExpr>(callee_expr); if(!imp){ ParenExpr *paren = dyn_cast<ParenExpr>(callee_expr); if(paren){ // ルートA return toString(paren); } return "unknown"; } Expr *sub = imp->getSubExpr(); if(!sub){ return ""; } DeclRefExpr *def = dyn_cast<DeclRefExpr>(sub); if(def){ // ルートB return def->getFoundDecl()->getName(); } MemberExpr *mem = dyn_cast<MemberExpr>(sub); if(mem){ // ルートC return getMemberName(mem); } ParenExpr *par = dyn_cast<ParenExpr>(sub); if(par){ // ルートD return toString(par); } return ""; }
ASTの実例
で、これでもまだgetFunctionName()/getMemberName()の内容を説明しづらいのでASTの実例を掲載しながら説明する。
解析対象ソースコードはlinux kernel 2.6.39のsocket関連部分からうまく解析できなかった箇所を適当にピックアップした。
// ソースコード x86_init.mpparse.get_smp_config(0); これがこうなる。 ↓ AST出力 | `-CallExpr 0x3507760 <line:67:2, col:35> 'void' | |-ImplicitCastExpr 0x3507748 <col:2, col:19> 'void (*)(unsigned int)' <LValueToRValue> | | `-MemberExpr 0x35076f8 <col:2, col:19> 'void (*)(unsigned int)' lvalue .get_smp_config 0x34e4f00 | | `-MemberExpr 0x35076c8 <col:2, col:11> 'struct x86_init_mpparse':'struct x86_init_mpparse' lvalue .mpparse 0x34e8c80 | | `-DeclRefExpr 0x35076a0 <col:2> 'struct x86_init_ops':'struct x86_init_ops' lvalue Var 0x34ea370 'x86_init' 'struct x86_init_ops':'struct x86_init_ops'
まずCallExprの段階でgetFunctionName()が呼ばれる。該当箇所(VisitCallExpr()仮想メンバ関数)は後ほど出てくるのでそちらを見て欲しい。
ASTがCall、ImplicitCast、Member、Member、DeclRef、なので、getFunctionName()、imp、memでルートCからgetMemberName()、parent_mem、!impでルート2を通る。
(何を言っているのか意味不明だと思うが、上のgetFunctionName()/getMemberName()ソースコードと見比べて欲しい。ルートCとルート2を通るパスの条件文に書かれている条件に注目)
// ソースコード if (sk->sk_prot->get_port(sk, 0)) { } これがこうなる。 ↓ AST出力 | | | | |-CallExpr 0x4311e78 <line:179:7, col:34> 'int' | | | | | |-ImplicitCastExpr 0x4311e60 <col:7, col:20> 'int (*)(struct sock *, unsigned short)' <LValueToRValue> | | | | | | `-MemberExpr 0x4311de8 <col:7, col:20> 'int (*)(struct sock *, unsigned short)' lvalue ->get_port 0x3fe4d20 | | | | | | `-ImplicitCastExpr 0x4311dd0 <col:7, include/net/sock.h:257:31> 'struct proto *' <LValueToRValue> | | | | | | `-MemberExpr 0x4311da0 </dev/shm/linux-2.6.39/net/ipv4/af_inet.c:179:7, include/net/sock.h:257:31> 'struct proto *' lvalue .skc_prot 0x3fc6760 | | | | | | `-MemberExpr 0x4311d70 </dev/shm/linux-2.6.39/net/ipv4/af_inet.c:179:7, include/net/sock.h:257:19> 'struct sock_common':'struct sock_common' lvalue ->__sk_common 0x3fc6e00 | | | | | | `-ImplicitCastExpr 0x4311d58 </dev/shm/linux-2.6.39/net/ipv4/af_inet.c:179:7> 'struct sock *' <LValueToRValue> | | | | | | `-DeclRefExpr 0x4311d30 <col:7> 'struct sock *' lvalue ParmVar 0x4311900 'sk' 'struct sock *'
ASTがCall、ImplicitCast、Member、ImplicitCast、Member、Member、ImplicitCast、DeclRefで、getFunctionName()、imp、memのルートCからgetMemberName()、parent_impでルート3にいったん入り、再びgetFunctionName()、imp、memのルートCからgetMemberName()、parent_mem、impのルート1に入り、最後にgetFunctionName()、imp、defのルートBで終わり。
// ソースコード unsigned int ret = conf->ops->find(conf, state); これがこうなる。 ↓ AST出力 | | |-DeclStmt 0x40d6ec0 <line:106:2, col:49> | | | `-VarDecl 0x40d6ce0 <col:2, col:48> ret 'unsigned int' | | | `-CallExpr 0x40d6e58 <col:21, col:48> 'unsigned int' | | | |-ImplicitCastExpr 0x40d6e40 <col:21, col:32> 'unsigned int (*)(struct ts_config *, struct ts_state *)' <LValueToRValue> | | | | `-MemberExpr 0x40d6dc0 <col:21, col:32> 'unsigned int (*)(struct ts_config *, struct ts_state *)' lvalue ->find 0x40d5a20 | | | | `-ImplicitCastExpr 0x40d6da8 <col:21, col:27> 'struct ts_ops *' <LValueToRValue> | | | | `-MemberExpr 0x40d6d78 <col:21, col:27> 'struct ts_ops *' lvalue ->ops 0x40d62e0 | | | | `-ImplicitCastExpr 0x40d6d60 <col:21> 'struct ts_config *' <LValueToRValue> | | | | `-DeclRefExpr 0x40d6d38 <col:21> 'struct ts_config *' lvalue ParmVar 0x40d6b10 'conf' 'struct ts_config *' | | | |-ImplicitCastExpr 0x40d6e90 <col:37> 'struct ts_config *' <LValueToRValue> | | | | `-DeclRefExpr 0x40d6df0 <col:37> 'struct ts_config *' lvalue ParmVar 0x40d6b10 'conf' 'struct ts_config *' | | | `-ImplicitCastExpr 0x40d6ea8 <col:43> 'struct ts_state *' <LValueToRValue> | | | `-DeclRefExpr 0x40d6e18 <col:43> 'struct ts_state *' lvalue ParmVar 0x40d6b90 'state' 'struct ts_state *'
2つはスルーして3つ目のCallExprからたどればこれまでと同じようにたどれるので説明省略。
// ソースコード err = sock->ops->getname(sock, (struct sockaddr *)&address, &len, 1); これがこうなる。 ↓ AST出力 | | | | `-CallExpr 0x4995690 <line:1649:7, line:1650:13> 'int' | | | | |-ImplicitCastExpr 0x4995678 <line:1649:7, col:18> 'int (*)(struct socket *, struct sockaddr *, int *, int)' <LValueToRValue> | | | | | `-MemberExpr 0x4995528 <col:7, col:18> 'int (*const)(struct socket *, struct sockaddr *, int *, int)' lvalue ->getname 0x3f10570 | | | | | `-ImplicitCastExpr 0x4995510 <col:7, col:13> 'const struct proto_ops *' <LValueToRValue> | | | | | `-MemberExpr 0x49954e0 <col:7, col:13> 'const struct proto_ops *' lvalue ->ops 0x3f0ee00 | | | | | `-ImplicitCastExpr 0x49954c8 <col:7> 'struct socket *' <LValueToRValue> | | | | | `-DeclRefExpr 0x49954a0 <col:7> 'struct socket *' lvalue Var 0x4994c10 'sock' 'struct socket *'
これも説明省略。
// ソースコード err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys, total_len, flags); これがこうなる。 ↓ AST出力 | |-BinaryOperator 0x49a4c68 </dev/shm/linux-2.6.39/net/socket.c:2032:2, line:2033:26> 'int' '=' | | |-DeclRefExpr 0x49a49b8 <line:2032:2> 'int' lvalue Var 0x49a2980 'err' 'int' | | `-CallExpr 0x49a4b90 <col:8, line:2033:26> 'int' | | |-ParenExpr 0x49a4ad0 <line:2032:8, col:50> 'int (*)(struct socket *, struct msghdr *, size_t, int)' | | | `-ConditionalOperator 0x49a4aa0 <col:9, col:38> 'int (*)(struct socket *, struct msghdr *, size_t, int)' | | | |-ImplicitCastExpr 0x49a4a58 <col:9> 'int' <LValueToRValue> | | | | `-DeclRefExpr 0x49a49e0 <col:9> 'int' lvalue ParmVar 0x49a2410 'nosec' 'int' | | | |-ImplicitCastExpr 0x49a4a70 <col:17> 'int (*)(struct socket *, struct msghdr *, size_t, int)' <FunctionToPointerDecay> | | | | `-DeclRefExpr 0x49a4a08 <col:17> 'int (struct socket *, struct msghdr *, size_t, int)' Function 0x4962440 'sock_recvmsg_nosec' 'int (struct socket *, struct msghdr *, size_t, int)' | | | `-ImplicitCastExpr 0x49a4a88 <col:38> 'int (*)(struct socket *, struct msghdr *, size_t, int)' <FunctionToPointerDecay> | | | `-DeclRefExpr 0x49a4a30 <col:38> 'int (struct socket *, struct msghdr *, size_t, int)' Function 0x4961ad0 'sock_recvmsg' 'int (struct socket *, struct msghdr *, size_t, int)'
これと次のがParenExprの実例。こっちは関数呼び出しに3項演算子が使われているアクロバティックなコード。sock_recvmsg_nosec()とsock_recvmsg()がそれぞれ関数だが、関数名を個別に取り出すのが面倒なのでとりあえずParenExprを丸ごとtoString()に放り込んで「(nosec ? sock_recvmsg_nosec : sock_recvmsg)」が得られる。
ただ、ちゃんとした関数呼び出し関係を調べたいのならここもまじめにParenExpr以下をたどる必要があると思う。
// ソースコード (*sk)->sk_prot->unhash(*sk); これがこうなる。 ↓ AST出力 | | | |-CallExpr 0x439b8e8 </dev/shm/linux-2.6.39/net/ipv4/af_inet.c:1406:3, col:29> 'void' | | | | |-ImplicitCastExpr 0x439b8d0 <col:3, col:19> 'void (*)(struct sock *)' <LValueToRValue> | | | | | `-MemberExpr 0x439b840 <col:3, col:19> 'void (*)(struct sock *)' lvalue ->unhash 0x3fe4990 | | | | | `-ImplicitCastExpr 0x439b828 <col:3, include/net/sock.h:257:31> 'struct proto *' <LValueToRValue> | | | | | `-MemberExpr 0x439b7f8 </dev/shm/linux-2.6.39/net/ipv4/af_inet.c:1406:3, include/net/sock.h:257:31> 'struct proto *' lvalue .skc_prot 0x3fc6760 | | | | | `-MemberExpr 0x439b7c8 </dev/shm/linux-2.6.39/net/ipv4/af_inet.c:1406:3, include/net/sock.h:257:19> 'struct sock_common':'struct sock_common' lvalue ->__sk_common 0x3fc6e00 | | | | | `-ImplicitCastExpr 0x439b7b0 </dev/shm/linux-2.6.39/net/ipv4/af_inet.c:1406:3, col:7> 'struct sock *' <LValueToRValue> | | | | | `-ParenExpr 0x439b790 <col:3, col:7> 'struct sock *' lvalue | | | | | `-UnaryOperator 0x439b770 <col:4, col:5> 'struct sock *' lvalue prefix '*' | | | | | `-ImplicitCastExpr 0x439b758 <col:5> 'struct sock **' <LValueToRValue> | | | | | `-DeclRefExpr 0x439b730 <col:5> 'struct sock **' lvalue ParmVar 0x439ad90 'sk' 'struct sock **'
ParenExprの実例2つ目。(*sk)部分の間接演算子「*」部分がParenExprになっている。これも面倒なのでtoString()に放り込むと「(*sk)」になる。
ということで、AST出力との対比はここまで。ここから再びソースコードに戻る。
bool VisitCallExpr(CallExpr *cexpr) { FullSourceLoc FullLocation = Context->getFullLoc(cexpr->getLocStart()); llvm::outs() << "call," // 関数呼び出しの印で「call」 << last_func << "," // 呼び出し元関数名 << getFunctionName(cexpr->getCallee()) << "," // 対象関数名 << FullLocation.getManager().getFilename(FullLocation) << "," // ファイル名 << FullLocation.getSpellingLineNumber() << "," // 行番号 << FullLocation.getSpellingColumnNumber(); // カラム番号 for(clang::CallExpr::arg_iterator iter = cexpr->arg_begin(); iter != cexpr->arg_end(); ++iter){ llvm::outs() << ",\"" << toString(*iter) << "\""; // 引数(「"」囲み) } llvm::outs()<< "\n"; return true; }
これは適宜呼び出されるRecursiveASTVisitorクラスの仮想メンバ関数のオーバーライド版。CallExprが現れるたびにコールバックされる模様。
ということで、このメンバ関数を実装してあげると関数呼び出しの情報収集が可能になる。
ここは割と見たままなので詳細な説明は省略。出力はCSV形式としている点、引数を一応「"」で囲んでいる点あたりが注意かな。まじめにやるなら引数の中に「"」が入っていたらエスケープしなきゃいけない気もする。
bool VisitFunctionDecl(FunctionDecl *Decl) { last_func = Decl->getQualifiedNameAsString(); // 関数名 FullSourceLoc loc[2] = { Context->getFullLoc(Decl->getSourceRange().getBegin()), // 関数定義の先頭 Context->getFullLoc(Decl->getSourceRange().getEnd()) // 関数定義の最後 }; llvm::outs() << "function," // 関数定義の印「function」 << last_func // 関数名 << (Decl->getStorageClass()==SC_Static? ",static,": ",global,") // static or グローバル << loc[0].getManager().getFilename(loc[0]) << "," // ファイル名 << loc[0].getSpellingLineNumber() << "," // 開始行 << loc[1].getSpellingLineNumber(); // 終了行 if(Decl->getStorageClass()==SC_Static){ llvm::outs() << "," << source_file; // static関数ならコンパイル対象ファイル名を記録する(定義がヘッダに書かれていると「ファイル名」がヘッダになりどの.cファイルなのかわからなくなるので) } for(clang::FunctionDecl::param_const_iterator iter = Decl->param_begin(); iter != Decl->param_end(); ++iter){ llvm::outs() << ",\"" << (*iter)->getQualifiedNameAsString(); // 引数 if((*iter)->hasDefaultArg()){ // デフォルト引数(のつもり) llvm::outs() << " = " << toString((*iter)->getDefaultArg()); } llvm::outs() << "\""; } llvm::outs()<<"\n"; return true; }
こっちも適宜呼び出されるRecursiveASTVisitorクラスの仮想メンバ関数のオーバーライド版。FunctionDeclが現れるたびにコールバックされる模様。
ということで、このメンバ関数を実装してあげると関数定義の情報収集が可能になる。
ここも割と見たままだけど、呼び出し元関数名を覚えておくためにlast_funcメンバ変数に記録している。コールバックの順番としては先にFunctionDeclが呼ばれて、関数内部で配下の関数呼び出しに遭遇するとCallExprが呼ばれる。
CallExpr側から呼び出し元関数名を取り出せないかいろいろ探したけど、見つからなかったのでこの方法になった。どうようにコンパイル中のソースファイル名を取り出す方法が見つからなかったのでコンストラクタで受け取ってsource_fileメンバ変数に覚えておく方法にしている。
private: ASTContext *Context; // たまに使うので覚えておく std::string last_func; // 最新の解析対象関数名(CallExprで呼び出し元関数として使う) std::string source_file; // コンパイル対象ファイル名 };
これでMyAstVisitorクラスの定義は終了。
class MyAstConsumer : public clang::ASTConsumer { public: explicit MyAstConsumer(ASTContext *Context, llvm::StringRef InFile) : Visitor(Context, InFile) {} virtual void HandleTranslationUnit(clang::ASTContext &Context) { Visitor.TraverseDecl(Context.getTranslationUnitDecl()); } private: MyAstVisitor Visitor; }; class MyAnalysisAction : public clang::ASTFrontendAction { public: virtual clang::ASTConsumer *CreateASTConsumer(clang::CompilerInstance &Compiler, llvm::StringRef InFile) { return new MyAstConsumer(&Compiler.getASTContext(), InFile); } };
このあたりはよくわかっていない。もとにしたサンプルソースから変更したのはソースファイル名を受け取ってバケツリレーしている箇所だけ。
たぶんFrontendActionってのがツールから呼ばれるやつで、ASTConsumerがASTデータを処理するクラスで、1ファイルコンパイルが終わってASTデータが出来上がるとHandleTranslationUnit()が呼ばれてRecursiveASTVisitorで次々たどる、ってな感じではないかと思う。
int main(int argc, const char **argv) { clang::tooling::CommonOptionsParser OptionsParser(argc, argv); #if (__clang_major__ == 3) && (__clang_minor__ == 2) clang::tooling::ClangTool Tool(OptionsParser.GetCompilations(), OptionsParser.GetSourcePathList()); #else clang::tooling::ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList()); #endif Tool.setArgumentsAdjuster(new clang::tooling::ClangSyntaxOnlyAdjuster()); return Tool.run(clang::tooling::newFrontendActionFactory<MyAnalysisAction>()); }
で、これがmain()関数。ここもよくわかってないけど、参考文献1、2、3あたりを見ながらDoxygenでそれっぽいのをピックアップして呼んでみただけ。
あと、getXXX()の名前がclang3.2と3.3で違うみたい。大文字小文字の差だけど。
コンパイルと実行
公式サイトの解説だと、LibToolingを使ったツールのコンパイルにはcmakeとninjaってツールが必要らしい。また新しい環境を構築しようとしてはまるのも嫌なので無理やり、というか参考文献1を参考にしてmakefileを作ってみた。
makefile
最終的にはこんな感じになった。
CC = clang INC = LIBS = \ -lclangFrontend \ -lclangParse \ -lclangSema \ -lclangAnalysis \ -lclangAST \ -lclangLex \ -lclangBasic \ -lclangDriver \ -lclangSerialization \ -lclangTooling \ -lclangEdit \ -lclangRewriteCore \ CFLAGS = $(shell llvm-config --cxxflags) -fno-rtti ifeq ($(OS_TYPE),win) LFLAGS = $(shell llvm-config --ldflags --libs) -lstdc++ -lpthread -limagehlp -lpsapi -static EXT = .exe else LFLAGS = $(shell llvm-config --ldflags --libs) -lstdc++ -lpthread -lm LIBS += -lclangLex endif SRCS = \ main.cpp \ OBJS = $(SRCS:.cpp=.o) TARGET = call_picker$(EXT) .PHONY: all all: $(TARGET) $(TARGET): $(OBJS) $(CC) -o $@ $(OBJS) $(LIBS) $(LFLAGS) .SUFFIXES: .cpp .o .cpp.o: $(CC) -c $(INC) $(CFLAGS) $<
WindowsとFedora15だと必要なライブラリが微妙に違ったりした。
あと、環境の都合でWindowsはclang3.2、Fedoraはclang3.3で確認した。Fedora側は依存関係が変わったのかclangLexをもう一度リンクリストの最後に持ってきている。
Windowsは
mingw32-make OS_TYPE=win
linux
自分が使っているFedora15環境だとcmakeのRPMだけ追加したら後は以下のコマンドでclang3.3をビルドできた。というか、コンパイル済みバイナリを使おうとしたらlibcのバージョンが古くてダメだった。
[maminus@localhost ~]# uname -a
Linux localhost.localdomain 2.6.40.3-0.fc15.x86_64 #1 SMP Tue Aug 16 04:10:59 UTC 2011 x86_64 x86_64 x86_64 GNU/Linux
[maminus@localhost build]$ pwd
/opt/lib/llvm/build
[maminus@localhost build]$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/lib/llvm/root -DLLVM_TARGETS_TO_BUILD=X86 -DCMAKE_EXE_LINKER_FLAGS="-L /usr/lib64" -DCMAKE_SHARED_LINKER_FLAGS="-L /usr/lib64" -G "Unix Makefiles" ../llvm-3.3.src
[maminus@localhost build]$ make
[maminus@localhost build]$ make check
[maminus@localhost build]$ make clang-test
[maminus@localhost build]$ make install
コンパイル方法はここ*1を参考にした。
あと、make check、make clang-testは動作確認なので特に実施不要。
今回作ったツールの実行
clangのコンパイルと同様にツールでソースを解析する際にも
- PATH
- C_INCLUDE_PATH
- CPLUS_INCLUDE_PATH
の設定が必要だった。
特に%MINGW%\lib\gcc\mingw32\4.6.2\includeがC_INCLUDE_PATHに入っていないとstddef.hが見つからなくてエラーになってしまう。
それで、ツールの引数はちょっと変わっていて
call_picker target_source_filename.c -- clang compile_options
という感じで、コンパイル引数はそのまま使えるけど、対象ソースファイル名だけを先にもってこないといけない。
(「--」はハイフンが2つ)
あと、解析時にコンパイルエラーが発生するとまず間違いなく正しく解析できていない。具体的にはASTの一部情報が抜けていたりする。
ので、コンパイルエラーが出ない状況を整える必要がある。今回WindowsとLinux両方でツールをコンパイルできるようにしたのはLinux上じゃないとエラーがなくならないソースを解析したかったからだったりする。
はまった時に調べたこと
ということで、何とかツールは動くところまで行けたが、あまり情報が無くて苦労することになった。今回自分が活用したのは以下のやり方。
1つ目については、捕捉をしておきたい。通常のコンパイル引数に「-Xclang -ast-dump」を追加するとASTデータがどばどば出力される。上で引用したAST出力もこのオプションで出したもの。
どんなソースでどんなASTになるのかは素人にはわからないので実際に動かすのが一番早かった。うまく解析できない場合もAST出力を見れば実際に来たデータが何かわかるので対応がしやすい。
例えば、linux kernelソースの net/socket.c なら net ディレクトリをカレントにして以下のコマンドを発射すればよい。
make -C ../linux-2.6.39 M=`pwd` CC="clang -Xclang -ast-dump" socket.o
ちなみにこの時にもC_INCLUDE_PATHなどの設定は必要なので注意。
2つ目はそのままだけど、百聞は一見にしかず、というか、数撃ちゃ当たる的な。ライブラリの仕様とか素人がわかるはずもないしDoxygenに書かれていないことはわからないのでとにかく試してみるしかない的な。悩んでも時間の無駄なので動かした結果を見て「そういうものだ」と思うのが良いかと。
3つ目はあまりお勧めしない。読んでも何やっているかわからないので。自分は6か所ほど読んで3か所ほどが参考になっただけだったし。
対応できてないこと
で、今わかっている範囲で今回のツールのダメな点を列挙する。
- C++ソースはまともに解析できない
- defineマクロ類が展開されてしまう
- 引数の型情報が取れない
1つ目はまぁ、そのままの意味なんだけど、unknownルートや関数名が取れないルートに入ってしまう。ASTのパターンが違うんだと思われる。個人的にしばらくはC++の解析ニーズが無いので今のまま放置予定。
誰か気合で対応してくれたらぜひ公開してくださいm(_ _)m
2つ目もそのままなんだけど、
#define HOGEHOGE 1 void foo(void) { bar(HOGEHOGE); }
みたいなコードが「bar(1)」としてわたされてくる。
clang::Preprocessorってのがいるんだけど、それっぽいメンバ関数が無いんだよね。
もしかしてプリプロセス過程に割り込んで変換結果を覚えておかないとダメ?
なんか、それっぽいテーブルみたいなやつも名前を見るんだけど何が何だかわからなくて今は対応できる気がしない。
とりあえずは、展開後の情報がもらえるだけで個人的には大きな前進なのでしばらく今のままで放置予定。
3つ目もまぁそのまま。foo(int a)的な時に「a」は取れるけど「int」が取れない。やり方不明。これも今すぐは無くても大丈夫なので放置。
次回予告
次回は今回作ったツールを既存のmakefileをそのまま使って動かす方法について書きたいと思う。
参考文献
*1:http://d.hatena.ne.jp/osyo-manga/20110211/1297443058
*2:Arpan Sen,2012, http://www.ibm.com/developerworks/jp/opensource/library/os-createcompilerllvm2/
*3:柏木 餅子, 風薬「きつねさんでもわかるLLVM 〜コンパイラを自作するためのガイドブック〜」インプレスジャパン (2013/06/21) p.70、pp.157-167
*4:Clang 3.4 documentation,http://clang.llvm.org/docs/RAVFrontendAction.html
*5:Clang 3.4 documentation,http://clang.llvm.org/docs/index.html
*6:clang API Documentation,http://clang.llvm.org/doxygen/index.html