Clangのexample pluginを試してみる

Clangのソースツリーにpluginのサンプルが格納されているので試してみる。

 

1. ビルド方法

Clang/LLVMをビルドした状態でexample pluginのビルドを実行。

$ svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
$ cd llvm/tools
$ svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
$ cd ../..
$ cd llvm/tools/clang/tools
$ svn co http://llvm.org/svn/llvm-project/clang-tools-extra/trunk extra
$ cd ../../../..
$ cd llvm/projects
$ svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
$ cd ../..
$ mkdir llvm.build
$ cd llvm.build

1.1. configureを用いた場合

llvm/tools/clang/examples配下にpluginのサンプルコードがある。ビルドす るにはBUILD_EXAMPLES=1を設定してmake。

$ ../llvm/configure --prefix=$HOME --enable-optimized
$ make BUILD_EXAMPLES=1

以下のファイルが作成される。

./Release+Asserts/lib/libPrintFunctionNames.a
./Release+Asserts/lib/libPrintFunctionNames.dylib
./Release+Asserts/lib/SampleAnalyzerPlugin.dylib
./Release+Asserts/bin/clang-interpreter

1.2. cmakeを用いた場合

-DCLANG_BUILD_EXAMPLES:BOOL=ONをcmakeに渡してMakefileを生成。

$ cmake -G "Unix Makefiles" ../llvm/ \
-DCMAKE_INSTALL_PREFIX="$HOME" \
-DCMAKE_BUILD_TYPE="Release" \
-DCLANG_BUILD_EXAMPLES:BOOL=ON
$ make

以下のファイルが作成される。PrintFunctionNamesのプレフィックスにlibが つかないようですね。

./lib/SampleAnalyzerPlugin.so
./lib/PrintFunctionNames.so
./bin/clang-interpreter

2. PrintFunctionNames.cpp

translation unit単位(ヘッダーファイルのインクルードが済んだc/c++ソー スコード単位)に呼ばれるASTConsumerを利用して、NamedDecl(変数宣言、関 数宣言、構造体宣言等のスーパークラスを出力するサンプル。 

ASTConsumerを継承したクラスを用いてvirtualなメソッドを実装することで、 ClangによるAST作成後の各種エントリポイントからメソッドが呼ばれる。 

PrintFunctionNames.cppではvirtual bool HandleTopLevelDecl(DeclGroupRef DG)を使用している。

class PrintFunctionsConsumer : public ASTConsumer {
public:
  virtual bool HandleTopLevelDecl(DeclGroupRef DG) {
    for (DeclGroupRef::iterator i = DG.begin(), e = DG.end(); i != e; ++i) {
      const Decl *D = *i;
      if (const NamedDecl *ND = dyn_cast<NamedDecl>(D))
        llvm::errs() << "top-level-decl: \"" << ND->getNameAsString() << "\"\n";
    }
 
    return true;
  }
};

以下のおまじないでpluginとして登録。

class PrintFunctionNamesAction : public PluginASTAction {
<snip>
};
<snip>
static FrontendPluginRegistry::Add<PrintFunctionNamesAction>
X("print-fns", "print function names");

2.1. 実行

以下のhello.cを入力ファイルとして指定。

#include <stdio.h>
 
int main(void)
{
    int i;
    for (i = 0; i < 10; i++)
        printf("hello, world\n");
    return 0;
}

Clang/LLVMをビルドしたディレクトリにて、以下のコマンドでpluginを実行 (pluginのパスを指定すればどこでも良い)。ヘッダファイルの定義が大量に 出力される。

$ clang -cc1 -load ./Release+Asserts/lib/libPrintFunctionNames.dylib \
-plugin print-fns hello.c
top-level-decl: "__uint8_t"
top-level-decl: "__int16_t"
<snip>
top-level-decl: "__vsnprintf_chk"
top-level-decl: "main"

2.2. AST (Abstract Syntax Tree)

Clang内部で定義されているASTをダンプすることができる。先ほどのhello.c のASTをダンプしてみる。

$ clang -Xclang -ast-dump -fsyntax-only hello.c
TranslationUnitDecl 0x7fd7bb0216d0 <<invalid sloc>>
|-TypedefDecl 0x7fd7bb021bd0 <<invalid sloc>> __int128_t '__int128'
<snip>
`-FunctionDecl 0x7fd7bb898720 <hello.c:3:1, line:9:1> main 'int (void)'
  `-CompoundStmt 0x7fd7bb898b80 <line:4:1, line:9:1>
    |-DeclStmt 0x7fd7bb898868 <line:5:5, col:10>
    | `-VarDecl 0x7fd7bb898810 <col:5, col:9> i 'int'
    |-ForStmt 0x7fd7bb898b00 <line:6:5, line:7:32>
    | |-BinaryOperator 0x7fd7bb8988c8 <line:6:10, col:14> 'int' '='
    | | |-DeclRefExpr 0x7fd7bb898880 <col:10> 'int' lvalue Var 0x7fd7bb898810 'i' 'int'
    | | `-IntegerLiteral 0x7fd7bb8988a8 <col:14> 'int' 0
    | |-<<<NULL>>>
    | |-BinaryOperator 0x7fd7bb898950 <col:17, col:21> 'int' '<'
    | | |-ImplicitCastExpr 0x7fd7bb898938 <col:17> 'int' <LValueToRValue>
    | | | `-DeclRefExpr 0x7fd7bb8988f0 <col:17> 'int' lvalue Var 0x7fd7bb898810 'i' 'int'
    | | `-IntegerLiteral 0x7fd7bb898918 <col:21> 'int' 10
    | |-UnaryOperator 0x7fd7bb8989a0 <col:25, col:26> 'int' postfix '++'
    | | `-DeclRefExpr 0x7fd7bb898978 <col:25> 'int' lvalue Var 0x7fd7bb898810 'i' 'int'
    | `-CallExpr 0x7fd7bb898aa0 <line:7:9, col:32> 'int'
    |   |-ImplicitCastExpr 0x7fd7bb898a88 <col:9> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
    |   | `-DeclRefExpr 0x7fd7bb8989c0 <col:9> 'int (const char *, ...)' Function 0x7fd7bb883260 'printf' 'int (const char *, ...)'
    |   `-ImplicitCastExpr 0x7fd7bb898ae8 <col:16> 'const char *' <BitCast>
    |     `-ImplicitCastExpr 0x7fd7bb898ad0 <col:16> 'char *' <ArrayToPointerDecay>
    |       `-StringLiteral 0x7fd7bb898a28 <col:16> 'char [14]' lvalue "hello, world\n"
    `-ReturnStmt 0x7fd7bb898b60 <line:8:5, col:12>
      `-IntegerLiteral 0x7fd7bb898b40 <col:12> 'int' 0

TranslationUnitDeclが大元にいて、そこから各関数と変数定義(extern、本 体を持つもの)に繋がっている。特に本体を持つmain関数は本体の内容も出力されて いる。

Stmt(ステートメント)とDecl(関数宣言、変数宣言)はクラス階層になって おり、dyn_cast<Class>()でどのクラスかを判別することができる(これとは 別にenumでも判別可能)。

FunctionDeclは引数を辿るgetParamDecl、関数本体を辿るgetBodyを持つ (externな関数宣言は本体を持たない)。 

Clang ASTについての資料。

* The Clang AST Tutorial by Manuel Klimek
* Clang 3.5 documentation Introduction to the Clang AST

3. MainCallChecker.cpp

このCheckerはmain関数を呼び出している箇所をwarningとして出力 する。

下記でPreStmt<CallExpr>(関数呼び出し前のエクスプレッション)を使用し ている。筆者はエクスプレッションはステートメントを構成する部品と理解 してます。

class MainCallChecker : public Checker < check::PreStmt<CallExpr> >

CallExprの関数定義を取得し、その名前が"main"か確認する。

void MainCallChecker::checkPreStmt(const CallExpr *CE, CheckerContext &C) const {
<snip>
  const Expr *Callee = CE->getCallee();
  const FunctionDecl *FD = state->getSVal(Callee, LC).getAsFunctionDecl();
<snip>
  IdentifierInfo *II = FD->getIdentifier();
  if (!II)   // if no identifier, not a simple C function
    return;
<snip>
  if (II->isStr("main")) {

以下のおまじないを実行してpluginとして登録。

// Register plugin!
extern "C"
void clang_registerCheckers (CheckerRegistry &registry) {
  registry.addChecker<MainCallChecker>("example.MainCallChecker", "Disallows calls to functions called main");
}
 
extern "C"
const char clang_analyzerAPIVersionString[] = CLANG_ANALYZER_API_VERSION_STRING;

3.1. 実行

以下のhello.cを入力ファイルとして指定。

#include <stdio.h>
 
int main(void);
 
void call_main(void)
{
  printf("call_main\n");
  main();
}
 
int main(void)
{
  static int once = 0;
  printf("main\n");
  if (!once)
    once = 1;
  else
    return 0;
  call_main();
  return 0;
}

Clang/LLVMをビルドしたディレクトリにて、以下のコマンドでpluginを実行 (pluginのパスを指定すればどこでも良い)。

$ clang -cc1 -load ./Release+Asserts/lib/SampleAnalyzerPlugin.dylib \
-analyzer-checker=example.MainCallChecker -analyze hello.c
<path-to>/hello.c:8:3: warning: call to main
  main();
  ^~~~~~
1 warning generated.

Checker作成方法については以下の資料が分かりやすい。

* How to Write a Checker in 24 Hours Clang Static Analyzer

4. clang-interpreter

pluginというよりは、clangとは独立したツール。C/C++のソースコードをイン タープリタとして実行。Clang/LLVMをビルドしたディレクトリにて、 MainCallCheckerのhello.cを実行した場合、以下の出力が得られる。

$ ./Release+Asserts/bin/clang-interpreter hello.c 
main
call_main
main

以下の変更を加えて、clang-interpreterを再ビルド。

$ svn diff llvm/tools/clang/examples/clang-interpreter/main.cpp
Index: llvm/tools/clang/examples/clang-interpreter/main.cpp
===================================================================
--- llvm/tools/clang/examples/clang-interpreter/main.cpp
(revision 201037)
+++ llvm/tools/clang/examples/clang-interpreter/main.cpp
(working copy)
@@ -53,7 +53,7 @@
     return 255;
   }
 
-  llvm::Function *EntryFn = Mod->getFunction("main");
+  llvm::Function *EntryFn = Mod->getFunction("call_main");
   if (!EntryFn) {
     llvm::errs() << "'main' function not found in module.\n";
     return 255;

再ビルドしたclang-interpreterでhello.cを再実行。call_mainから実行され ている。

./Release+Asserts/bin/clang-interpreter ~/Sources/c/hello.c 
call_main
main
call_main
main

llvm::ExecutionEngineを用いてJITでエントリ関数を実行するらしいのですが、 おまじないが多くてよく分かりません。ただし、Clang/LLVMのAPIを使用して いるおかげでコードはとてもコンパクトです。

static int Execute(llvm::Module *Mod, char * const *envp) {
<snip>
  OwningPtr<llvm::ExecutionEngine> EE(
    llvm::ExecutionEngine::createJIT(Mod, &Error));
<snip>
  return EE->runFunctionAsMain(EntryFn, Args, envp);
}

5. まとめ

ASTConsumerを用いればコールグラフのビューアが作れそうです。Checkerを用 いれば独自の静的解析コードが追加できます。加えてclang-interpreterのように面白い独自ツールも作成できそうです。