圖: http://llvm.org/Logo.html

身為速食工作者,我本身不太喜歡浪費太多時間在瑣碎的細節,在加上本業非 Compiler 背景,只是從 Google 那邊翻到一些編譯、執行 Clang Plugin 的方式,順便筆記下來,畢竟網路上的資料真的不多,也該為繁體中文留下點足跡吧。

操作環境:

OS: Ubuntu Server 64Bit
llvm src location: ~/llvm
clang src location: ~/llvm/tools/clang
gcc: gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3

首先依照 LLVM 官網介紹的方式,下載 LLVM、clang 等相關程式碼,接著編譯,之後換到 llvm/tools/clang/example 目錄中,裡頭有一個 clang plugin 範例程式 PrintFunctionNames,僅需切換進入此目錄打 make 後,即可編出來 (細節都寫在llvm/tools/clang/examples/PrintFunctionNames/README.txt)

其中,有兩種編譯方式,第一種是直接在 llvm 目錄下,執行 configure 後再打 make -j4 即可編譯出 llvm 和 clang,接著切換到 llvm/tools/clang/examples/PrintFunctionNames 裡頭在執行 make 後,東西就產生了;另一個編法是用 cmake 把 source 跟產出分開,例如 mkdir ~/build && cd ~/build && cmake ~/llvm && make -j4 後,接著切換到 ~/build/tools/clang/examples/PrintFunctionNames,執行 make 後,產出物在 ~/build/lib/PrintFunctionNames.so。由於使用官方推薦的 configure 方式有碰到問題,所以我就改用 cmake 方式,也意外發現 cmake 編得比較快(不知有沒遺漏什麽)。

編譯完,就可以用 clang -ccl -load /path/libPrintFunctionNames.so -plugin print-fns some-input-file.c 執行。(若用 cmake 編出的是 /path/PrintFunctionNames.so)

大部分的 clang plugin 範例就是從 PrintFunctionNames 改來的,介紹一下 PrintFunctionNames 的使用方式:

$ vim t1.c
int main() { 
	return 0;
}

$ clang -ccl -load /path/PrintFunctionNames.so -plugin print-fns t1.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "main"

$ vim t2.c
#include 
int main(int argc,char *argv[] ) {
	printf("Hello World!\n");
        return 0;
}

$ clang -ccl -load /path/PrintFunctionNames.so -plugin print-fns t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
/path/t2.c:1:10: fatal error: 'stdio.h' file not found
#include 
         ^
top-level-decl: "__builtin_va_list"
top-level-decl: "main"
1 error generated.

這是因為 clang 不知哪邊找 stdio.h 檔,所以就透過 -I 指令去指定吧(過程中還需 stddef.h)

$ clang -cc1 -I/usr/include -I/usr/lib/gcc/x86_64-linux-gnu/4.4.3/include -load /path/PrintFunctionNames.so -plugin print-fns ~/t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "size_t"
...
top-level-decl: "ctermid"
top-level-decl: "flockfile"
top-level-decl: "ftrylockfile"
top-level-decl: "funlockfile"
top-level-decl: "main"

其中 -I/usr/include 是找 stdio.h,而 I/usr/lib/gcc/x86_64-linux-gnu/4.4.3/include 是找 stddef.h (依個人安裝位置不同)

然而,每次執行都用 clang -load *.so -plugin ... 指令進行有點麻煩,所以另一種使用方式就是寫隻 main 程式,在裡頭呼叫 clang 相關函數物件來操作,這樣的好處是可以編出一隻 tool 來用,也不用每次執行時下很長的指令了:

$ mkdir ~/print-fns-tools
$ cp ~/llvm/tools/clang/examples/PrintFunctionNames/PrintFunctionNames.cpp ~/print-fns-tools/main.cpp
$ vim ~/print-fns-tools/main.cpp

// 在檔案最後面新增程式碼
#include "llvm/Support/Host.h"
#include "clang/Parse/ParseAST.h"
#include "clang/Basic/FileManager.h"

#include "clang/Frontend/HeaderSearchOptions.h"
#include "clang/Frontend/Utils.h"

#include <iostream>
int main(int argc, char *argv[]) {
        clang::CompilerInstance *ci = new clang::CompilerInstance();
        ci->createDiagnostics(0,NULL);
        clang::TargetOptions to;
        to.Triple = llvm::sys::getDefaultTargetTriple();
        clang::TargetInfo *pti = TargetInfo::CreateTargetInfo(ci->getDiagnostics(), to);
        ci->setTarget(pti);
        ci->createFileManager();
        ci->createSourceManager( ci->getFileManager() );
        ci->createPreprocessor();
        ci->createASTContext();

        if( argc < 2 ) {
                std::cout << "Usage: " << argv[0] << " [-I /usr/include] file.c " << std::endl;
                return 0;
        } else if ( argc >= 4 ) {
                clang::HeaderSearchOptions headerSearchOptions;
                for( int i=1 ; i<argc-1 ; ++i )
                        if( argv[i][0] == '-' && argv[i][1] == 'I' && i+1 < argc ) {
                                std::cout << "Search header: " << argv[i+1] << std::endl;
                                headerSearchOptions.AddPath( argv[++i] , clang::frontend::Angled, false, false, false );
                        }

                clang::PreprocessorOptions preprocessorOptions;
                clang::FrontendOptions frontendOptions;
                clang::InitializePreprocessor( ci->getPreprocessor(), preprocessorOptions, headerSearchOptions, frontendOptions );
        }

        std::cout << "Target: " << argv[argc-1] << std::endl;
        ci->getSourceManager().createMainFileID( ci->getFileManager().getFile(argv[argc-1]) );

        PrintFunctionsConsumer *mConsumer = new PrintFunctionsConsumer();
        ParseAST(ci->getPreprocessor(), mConsumer, ci->getASTContext());

        delete ci;
        delete mConsumer;

        return 0;
}

直接編譯 main.cpp (假設環境變數中可以取得 clang++、llvm-config 指令,否則取代成 /path/clang++、/path/llvm-config 的用法):

$ cd print-fns-tools/
$ clang++ `llvm-config --cxxflags` -fno-rtti main.cpp -lclangFrontend -lclangDriver -lclangSerialization -lclangParse -lclangSema -lclangAnalysis -lclangRewrite -lclangEdit -lclangAST -lclangLex -lclangBasic -lLLVMMC -lLLVMSupport `llvm-config --ldflags --libs cppbackend`

操作使用:

$ ./a.out
Usage: ./a.out [-I /usr/include] file.c

$ ./a.out t1.c 
Target: t1.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "main"

$ ./a.out t2.c 
Target: t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
a.out: /path/llvm/tools/clang/lib/Frontend/TextDiagnosticPrinter.cpp:158: virtual void clang::TextDiagnosticPrinter::HandleDiagnostic(clang::DiagnosticsEngine::Level, const clang::Diagnostic&): Assertion `TextDiag && "Unexpected diagnostic outside source file processing"' failed.
Stack dump:
0.      t2.c:1:2: current parser token 'include'
Aborted

$ ./a.out -I /usr/include -I /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include t2.c
Search header: /usr/include
Search header: /usr/lib/gcc/x86_64-linux-gnu/4.4.3/include
Target: t2.c
top-level-decl: "__va_list_tag"
top-level-decl: "__va_list_tag"
top-level-decl: "__builtin_va_list"
top-level-decl: "size_t"
...
top-level-decl: "ftrylockfile"
top-level-decl: "funlockfile"
top-level-decl: "main"

總結一下,原先使用 clang -plugin 的方式啟動,本身有 clang 這個環境可以操作,而單純寫成一隻 toolbase 方式,則需要初始化 clang 的操作環境,這就是在 main 裡頭做的事,包含建立一個 Compiler Instance 及其初始化、設定 header search 位置、要處理的 file.c 等等,如此一來就完成了。

其他筆記(用 clang++ 編譯時出現訊息): 

  • undefined reference to `typeinfo for clang::ASTConsumer'
    • 編譯時加上 "-fno-rtti" 參數即可解決
  • error: no member named 'getDefaultTargetTriple' in namespace 'llvm::sys'
    to.Triple = llvm::sys::getDefaultTargetTriple();
    • 加上 #include "llvm/Support/Host.h" 即可解決
  • error: member access into incomplete type 'clang::FileManager'
    note: forward declaration of 'clang::FileManager'
    • 加上 #include "clang/Basic/FileManager.h" 即可解決 

最後一提,網路上資源不見得少,但因為語系與背景不同,常常關鍵字下很久都找不到,無意間還找到 Google 員工在去年論壇討論串裡,正抱怨著 clang plugin 很難寫 XDD 當下我才發現,大家都是普通人 XDDD 此外,有興趣可以多多參考這裡:https://github.com/loarabia/Clang-tutorial,裡頭有簡單又豐富的程式碼、makefile 喔。


, , , ,

changyy 發表在 痞客邦 PIXNET 留言(0) 人氣()