関数コールグラフもどきを出してみる

前回*1まででmakefileから関数呼び出しをCSVへ抜き出す用意ができたので、関数コールグラフもどきに変換するツールを作ってみた。

利用イメージ

[maminus@localhost shm]$ call_grapher.exe net-socket.csv net-core-sock.csv > call-graph.txt

のような感じで、関数呼び出しを抜き出すツールcall_pickerで出力したCSVファイルを複数受け取って、関数コールグラフもどきを組み立てて標準出力へ出す。で、お好みで標準出力をリダイレクトしてもらうイメージ。

ソースコード

無駄に長いがこれもそのままコピペで使えるように全部掲載する。

まずあまり面白くない?ソースから。

// LineString.hpp
//-----------------------------------------------------------------------------
#ifndef LineString_hpp_
#define LineString_hpp_
//-----------------------------------------------------------------------------

#include <iostream>	// istream
#include <string>
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
namespace Utility
{

// istream_iterator 向け1行処理屋さん
// >> 演算子で1行読み込んで string キャスト演算子で読み込んだ文字列を返す
class LineString
{
    private:
        std::string     str_;
    public:
        operator const std::string & () const { return str_; }

        friend std::istream &operator >> (std::istream &ist, LineString &me)
        {
            std::getline(ist, me.str_);
            return ist;
        }
};

}   // namespace Utility
//-----------------------------------------------------------------------------
#endif  // LineString_hpp_
//-----------------------------------------------------------------------------

このソースはLineString.hppのファイル名で保存すればOK。
ソース内コメントの通り、istream_iteratorで1行ずつ取り出すために使うヘルパークラス。実際の使い方は以下の実例を見て欲しい。
(ちなみにこのクラスは海外の掲示板(場所失念)で掲載されていたもの。名前などは変えているがアイデアはそのまま)

次にCSVファイルのローダクラス。

// CsvLoader.hpp
//-----------------------------------------------------------------------------
#ifndef CsvLoader_hpp_
#define CsvLoader_hpp_
//-----------------------------------------------------------------------------
#include <string>
#include <vector>

// for impl
#include <istream>
#include <fstream>          // ifstream
#include <iterator>         // istream_iterator
#include <algorithm>        // for_each, find_if, find
#include <boost/ref.hpp>
#include <boost/bind.hpp>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
#include <boost/tokenizer.hpp>          // tokenizer, escaped_list_separator
#pragma clang diagnostic pop
#include <boost/algorithm/string.hpp>   // trim_left_copy, trim_copy
#include "LineString.hpp"
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
namespace Utility
{


class CsvLoader                 // CSVファイルのデータローダ
{
    public:
        typedef std::vector<std::string>    row_type;   // CSV1行分の型
        typedef std::vector<row_type>       list_type;  // CSV表(Excelを思い浮かべて欲しい)の型
        typedef list_type::size_type        size_type;

    private:
        list_type   lists_;     // データ本体
        bool isCommentLine(const std::string &input);   // コメント行ならtrueを返す
        void extractValue(row_type &output, const std::string &str, bool isTrim);   // 1セル分取り出し
        void scanSingleLine(const std::string &input, bool isTrim);                 // 1行分取り出し

    public:
        void load(const std::string &csv_filename, bool isTrim=true);               // ファイル読み込み
        size_type size(void) const { return lists_.size(); }                        // 行数を返す
        const row_type &operator[] (size_type row) const { return lists_[row]; }    // csv[row] としたときに呼ばれる
};

//-----------------------------------------------------------------------------
// for impl
namespace impl
{
typedef boost::escaped_list_separator<char> token_sep_type;
typedef boost::tokenizer<token_sep_type>    token_base_type;
}
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
bool CsvLoader::isCommentLine(const std::string &input)
{
    return boost::trim_left_copy(input).find("#") != std::string::npos; // 空白除去後に「#」始まりならコメント行扱い
}
//-----------------------------------------------------------------------------
void CsvLoader::extractValue(row_type &output, const std::string &str, bool isTrim)
{
    output.push_back((isTrim)? boost::trim_copy(str): str);
}
//-----------------------------------------------------------------------------
void CsvLoader::scanSingleLine(const std::string &input, bool isTrim)
{
    if(isCommentLine(input)){
        return;
    }

    // \\ によるエスケープなし、「,」区切り、「"」囲みあり
    // ただし、このコードだと「"」自体のエスケープが無理っぽい感触
    impl::token_base_type   csv_tokenizer(input, token_sep_type(std::string(), ",", "\""));

    row_type                tmp;

    std::for_each(  // 1行分の文字列を分割して1セルずつ取り出してtmpへ入れる
            csv_tokenizer.begin(),
            csv_tokenizer.end(),
            boost::bind(&CsvLoader::extractValue, this, boost::ref(tmp), _1, isTrim)
        );

    lists_.push_back(tmp);
}
//-----------------------------------------------------------------------------
void CsvLoader::load(const std::string &csv_filename, bool isTrim)
{
    lists_.clear();
    std::ifstream   ifs(csv_filename.c_str());
    std::for_each(
            std::istream_iterator<LineString>(ifs),
            std::istream_iterator<LineString>(),
            boost::bind(&CsvLoader::scanSingleLine, this, _1, isTrim)
        );
}
//-----------------------------------------------------------------------------

}   // namespace Utility
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
#endif  // CsvLoader_hpp_
//-----------------------------------------------------------------------------

という感じで。一応ソース内に最低限のコメントを書いたのでstd::for_eachとboost::bindがわかる人なら読めるつもり。trimがどうこうってのは単なる空白文字の除去。まじめに実装していないのでExcelとかで作ったファイルは読めないものもあると思われる。ちゃんとやるならregexとかが必要かと。
このソースはファイル名をCsvLoader.hppにしておいてもらえればOK。

で、ここからはmain.cpp(名前ひねりなし)

// main.cpp
#include <CsvLoader.hpp>
#include <map>
#include <vector>
#include <string>
#include <set>
#include <algorithm>
#include <iostream>
#include <boost/bind.hpp>

struct CallingInfo;
typedef struct FunctionInfo                         FunctionInfoType;       // 関数情報の型
typedef std::map<std::string, FunctionInfoType>     FunctionTableType;      // 関数テーブル型
typedef FunctionTableType::iterator                 FunctionIteratorType;   // 関数テーブルのiterator型(名前紛らわしいかも...)
typedef std::vector<CallingInfo>                    CallingListType;        // 関数呼び出しリスト型

struct CallingInfo  // 関数呼び出し情報
{
    typedef std::vector<std::string>        arg_list_type;  // 引数リストの型
    typedef arg_list_type::const_iterator   arg_iter_type;  // 上記リストのiterator型
    FunctionIteratorType    function_iter;  // 呼び出し対象(へのiterator)
    arg_list_type           args;           // 実引数のリスト
    std::string             filename;       // 呼び出し場所ファイル名
    std::string             linenumber;     // 行番号
    std::string             columnnumber;   // カラム番号(タブは4で数えられる模様...)

    CallingInfo(FunctionIteratorType &i, arg_iter_type b, arg_iter_type e, const std::string &f, const std::string &l, const std::string &c):function_iter(i), args(b, e), filename(f), linenumber(l), columnnumber(c) {}
};
struct FunctionInfo // 関数自体の情報
{
    typedef std::vector<std::string>        arg_list_type;  // 上と同じ型を定義。なんとなくグローバルにするのが嫌だったので
    typedef arg_list_type::const_iterator   arg_iter_type;
//  std::string     function_name;      // 関数名...は特に不要だった(テーブルのキーになっているためこの構造体には不要)
    arg_list_type   args;               // 仮引数のリスト
    std::string     filename;           // 関数定義場所ファイル名
    std::string     linenumber_begin;   // 開始行番号
    CallingListType parents_list;       // 呼び出し元リスト。メモリ使用量を節約するならこちらはCallingIteratorTypeとかにすべき
    CallingListType children_list;      // 対象関数から呼び出している関数のリスト
};

struct CallDataLoadHelper   // データ読み込み処理のヘルパ
{
    typedef Utility::CsvLoader::size_type   index_type;     // CSVの列番号型
    static const index_type     kind_index;         // 1列目の関数呼び出し(call) or 関数定義(function)
    static const index_type     name_index;         // 関数名の列番号
    static const index_type     storageclass_index; // static関数 or global関数
    static const index_type     filename_index;     // ファイル名
    static const index_type     linenumber_index;   // 行番号
    static const index_type     columnnumber_index; // カラム番号
    static const index_type     parent_index;       // 呼び出し元
    static const index_type     child_index;        // 呼び出し先
    static const index_type     arg_top_index;      // 引数リストの先頭
    static const index_type     minimal_data_count; // 列数の最小値
    static const index_type     static_arg_offset;  // static関数時の引数リストまでのオフセット列数
    static const std::string    kind_string_call;       // "call"の文字列
    static const std::string    kind_string_function;   // "function"の文字列
    static void load(FunctionTableType &target, const std::string &csv_filename);
private:
    static void processOneLine(FunctionTableType &target, const Utility::CsvLoader::row_type &one);
    static void addCallData(FunctionTableType &target, const Utility::CsvLoader::row_type &one);
    static void addFunctionData(FunctionTableType &target, const Utility::CsvLoader::row_type &one);
    static FunctionIteratorType getFunctionInfo(FunctionTableType &target, const std::string &func_name);
};

// FunctionIteratorTypeの operator < を実現する関数オブジェクトのクラス
struct iter_less : public std::binary_function<FunctionIteratorType, FunctionIteratorType, bool>
{   // 関数名を基準とする
    bool operator() (FunctionIteratorType lhs, FunctionIteratorType rhs){ return lhs->first < rhs->first; }
};
class TextCallGraphDumper   // コールグラフもどきを出力するクラス
{
    std::ostream                                    &ost_;      // 出力先
    std::set<FunctionIteratorType, iter_less>       list_;      // 出力済みリスト
    void dumpACall(int level, const CallingInfo &info);
    std::string buildArg(const CallingInfo &info);
    bool isAlreadyDump(const FunctionIteratorType &func_iter);
    void dumpAFunction(int level, const CallingInfo &info);
public:
    TextCallGraphDumper(std::ostream &o):ost_(o) {}
    void dumpAsRoot(FunctionIteratorType &func_iter);
};

class CallGrapher   // 最上位のクラス。ファイルを読んで統合してコールグラフを出力
{
    FunctionTableType   table_;
public:
    void addCallData(const std::string &csv_filename);
    void dumpGraph(std::ostream &ost);
};
//------------------------------------------------------------------------------
// CSV列番号
const CallDataLoadHelper::index_type    CallDataLoadHelper::kind_index          = 0;
const CallDataLoadHelper::index_type    CallDataLoadHelper::name_index          = 1;
const CallDataLoadHelper::index_type    CallDataLoadHelper::storageclass_index  = 2;
const CallDataLoadHelper::index_type    CallDataLoadHelper::filename_index      = 3;
const CallDataLoadHelper::index_type    CallDataLoadHelper::linenumber_index    = 4;
const CallDataLoadHelper::index_type    CallDataLoadHelper::columnnumber_index  = 5;
const CallDataLoadHelper::index_type    CallDataLoadHelper::parent_index        = 1;
const CallDataLoadHelper::index_type    CallDataLoadHelper::child_index         = 2;
const CallDataLoadHelper::index_type    CallDataLoadHelper::arg_top_index       = 6;
const CallDataLoadHelper::index_type    CallDataLoadHelper::minimal_data_count  = 6;
const CallDataLoadHelper::index_type    CallDataLoadHelper::static_arg_offset   = 1;
// 1列目の種別欄文字列
const std::string                       CallDataLoadHelper::kind_string_call        = "call";
const std::string                       CallDataLoadHelper::kind_string_function    = "function";

という感じで宣言類はこれで終わり。

ここからは各クラスの実装。CallDataLoadHelperクラスから

//------------------------------------------------------------------------------
void CallDataLoadHelper::load(FunctionTableType &target, const std::string &csv_filename)
{
    Utility::CsvLoader  csv;

    csv.load(csv_filename);                     // CSVをロードして
    for(index_type i=0;i<csv.size();i++){       // 各行を
        processOneLine(target, csv[i]);         // 関数テーブルtargetへ取り込む
    }
}
//------------------------------------------------------------------------------
void CallDataLoadHelper::processOneLine(FunctionTableType &target, const Utility::CsvLoader::row_type &one)
{
    if(one.size() < minimal_data_count){
        return;     // 列数が足りていないデータは壊れていると見なして捨てる
    }
    if(one[kind_index] == kind_string_call){
        addCallData(target, one);       // "call"なら関数呼び出しデータの処理
    }
    if(one[kind_index] == kind_string_function){
        addFunctionData(target, one);   // "function"なら関数定義データの処理
    }
}
//------------------------------------------------------------------------------
void CallDataLoadHelper::addCallData(FunctionTableType &target, const Utility::CsvLoader::row_type &one)
{
    FunctionIteratorType    parent = getFunctionInfo(target, one[parent_index]);    // 呼び出し元をテーブルからとってくる
    FunctionIteratorType    child  = getFunctionInfo(target, one[child_index]);     // 呼び出し先も同様

    // 呼び出し情報を構造体に入れて
    CallingInfo             call(child, one.begin() + arg_top_index, one.end(), one[filename_index], one[linenumber_index], one[columnnumber_index]);

    // 呼び出し元、先リストへ入れる。同じ情報を入れているのでメモリは無駄。嫌なら片方をもう片方へのiteratorにすべし
    parent->second.children_list.push_back(call);
    child->second.parents_list.push_back(call);
}
//------------------------------------------------------------------------------
void CallDataLoadHelper::addFunctionData(FunctionTableType &target, const Utility::CsvLoader::row_type &one)
{
    FunctionIteratorType    iter = getFunctionInfo(target, one[name_index]);    // 関数名でテーブルから引っ張ってくる
    index_type              offset = (one[storageclass_index] == "static")? static_arg_offset : 0;  // static関数は列数がずれてる

    if(one.size() < (minimal_data_count + offset)){
        return;     // 上位関数で一度調べているがstatic関数の場合を考慮しなおして再度入力データ数チェック
    }
    if(iter->second.filename.size() > one[filename_index].size()){
        return;     // ファイル名、引数が空欄の場合は上書きしない
    }

    // 関数情報を入れる
    iter->second.filename = one[filename_index];
    iter->second.linenumber_begin = one[linenumber_index];
    iter->second.args.assign(one.begin() + arg_top_index + offset, one.end());
}
//------------------------------------------------------------------------------
FunctionIteratorType CallDataLoadHelper::getFunctionInfo(FunctionTableType &target, const std::string &func_name)
{
    target[func_name];      // この1行で未登録なら登録される。

    return target.find(func_name);      // 関数名で検索して返す(上の行で未登録でも安心)
}
//------------------------------------------------------------------------------

これでCallDataLoadHelperは終わり。addFunctionData()だけ補足する。保存済みのファイル名長とCSVから取り出したファイル名長を比較しているが、これはCSVファイルにファイル名、引数が空欄の謎のfunction記録が残っているのを発見したから。
有効な情報を無効な情報で上書きしないようにガードを追加している。なぜ空白のデータが出力されるのかは調査していない。call_pickerに問題があるのかも。

次のクラス。

//------------------------------------------------------------------------------
void TextCallGraphDumper::dumpACall(int level, const CallingInfo &info)
{
    // 呼び出しの深さに応じてインデントを入れる「└」とかそういうのがお好みならここを変更すればOK
    for(int i=0;i<level;i++){ ost_ << "\t"; }

    ost_ << info.function_iter->first << buildArg(info);    // 関数名と引数を出力する
    if(info.filename != ""){
        // ファイル名と行番号、カラム番号をSakuraEditor形式で出力する
        ost_ << "\t// " << info.filename << "(" << info.linenumber << "," << info.columnnumber << ")";
    }
    ost_ << std::endl;

    list_.insert(info.function_iter);   // 出力済みリストへ追加する
}
//------------------------------------------------------------------------------
std::string TextCallGraphDumper::buildArg(const CallingInfo &info)  // 関数呼び出しの引数を文字列化する
{
    std::string     ret;

    ret += "(";     // 呼び出しのかっこ
    for(            // 引数分だけループ
        CallingInfo::arg_iter_type vir = info.function_iter->second.args.begin(), act = info.args.begin();
        vir != info.function_iter->second.args.end();
        vir++
    ){  // 「仮引数 = 実引数, 」の形式で出力する
        ret += *vir;
        if(act != info.args.end()){ // 実引数の数が足りていれば出力する
            ret += " = " + *act;
            act++;
        }
        ret += ", ";
    }
    ret = ret.substr(0, ret.size()-2);  // 最後の「,」がいらないので除去
    ret += ")";     // 閉じかっこ

    return ret;
}
//------------------------------------------------------------------------------
bool TextCallGraphDumper::isAlreadyDump(const FunctionIteratorType &func_iter)
{
    return list_.find(func_iter) != list_.end();    // 出力済みリストに入っていればtrue
}
//------------------------------------------------------------------------------
void TextCallGraphDumper::dumpAFunction(int level, const CallingInfo &info)     // ある関数とその子供を出力する
{
    bool    is_already = isAlreadyDump(info.function_iter);     // 先に調べる(次の処理でリスト入りするため)

    dumpACall(level, info);     // 自分の呼び出しを表示する(そして出力済みリストに入る)

    if(is_already){
        return;     // 出力済み関数はたどらない
    }

    // 自分が呼び出している関数たちを再帰的に処理する
    std::for_each(info.function_iter->second.children_list.begin(), info.function_iter->second.children_list.end(), boost::bind(&TextCallGraphDumper::dumpAFunction, this, level+1, _1));
}
//------------------------------------------------------------------------------
void TextCallGraphDumper::dumpAsRoot(FunctionIteratorType &func_iter)           // 最上位関数とすべての子供を出力する
{
    if(func_iter->second.parents_list.size() > 0){
        return;     // 呼び出し元関数がある場合は最上位じゃないので戻る
    }

    // 自分自身とその子供を出力する(後は再帰的に呼び出す)
    // 最上位関数なので実引数は不明。よって仮引数を入れておく(出力が仮引数 = 実引数と見えるがまぁいいんじゃない?)
    dumpAFunction(0, CallingInfo(func_iter, func_iter->second.args.begin(), func_iter->second.args.end(), func_iter->second.filename, func_iter->second.linenumber_begin, "1"));
}
//------------------------------------------------------------------------------

このクラスはコメントだけだとわからない部分について補足しておきたい。

まず、dumpACall()について。
インデントは自分の好みでタブ文字としている。が、ツリー表示が好みの方も多いと思う。適当にいじってほしい。
それからファイル名はSakuraEditorのタグジャンプ用書式にあわせてある。書式を合わせておくとSakuraEditorでF12キーのタグジャンプを行うと実際のソースコードにジャンプできる。コールグラフもどきを見ながら実際の呼び出し箇所を見たくなった時に1キーで飛べるので便利だと思って入れてみた。本当はGNU Globalで出したHTMLへのURLとかも掲載できると良かったんだけど、いまいちGNU Globalの出力ファイル名規則がわからないから載せられない…
あと、dumpAFunction()で一度表示した関数は表示しないようにしている。理由は2つあって、何度も表示すると長くて見づらいのと、再帰呼び出しになっている場合に無限ループしないようにする対策を兼ねるというのを狙っている。

最後はCallGrapherクラスとmain()。

//------------------------------------------------------------------------------
void CallGrapher::addCallData(const std::string &csv_filename)
{
    // table_にcsv_filenameをロードする
    CallDataLoadHelper::load(table_, csv_filename);
}
//------------------------------------------------------------------------------
void CallGrapher::dumpGraph(std::ostream &ost)
{
    TextCallGraphDumper dumper(ost);    // 出力先をostとして登録

    // table_全部を最上位関数として表示する(最上位でなければすぐ返ってくる)
    for(FunctionIteratorType iter = table_.begin(); iter != table_.end(); iter++){
        dumper.dumpAsRoot(iter);
    }
}
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
int main(int argc, char *argv[])
{
    CallGrapher     grapher;

    for(int i=1;i<argc;i++){
        grapher.addCallData(argv[i]);   // コマンドライン引数は全部ファイルとして放り込む
    }
    grapher.dumpGraph(std::cout);       // 標準出力へ出す

    return 0;
}
//------------------------------------------------------------------------------

ここは割とそのままだから説明なし。

makefile

makefileはこんな感じ。

mingw32-make

コンパイルOK。

CC      = clang
INC     =                   \
    -I C:\boost_1_53_0      \
    -I .                    \

LIBS    =                   \
    -lstdc++                \

CFLAGS  = -Wall -W
LFLAGS  = -static
SRCS    =                   \
    main.cpp                \

OBJS    = $(SRCS:.cpp=.o)
TARGET  = call_grapher.exe


.PHONY: all clean delete_objs delete_target

all: CFLAGS += -O2
all: $(TARGET)

debug: CFLAGS += -g
debug: LFLAGS += -g
debug: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) -o $@ $(OBJS) $(LFLAGS) $(LIBS)

clean: delete_objs delete_target

delete_objs:
	del /f /q $(OBJS)

delete_target:
	del /f /q $(TARGET)


.SUFFIXES: .cpp .o

.cpp.o:
	$(CC) -c $(INC) $(CFLAGS) $<

お試し

実際に試してみた。
まずはLinux側。(不必要な出力は省略)

[maminus@localhost linux-2.6.39]$ make defconfig
[maminus@localhost linux-2.6.39]$ cd ..
[maminus@localhost tmp]$ ls -lh
-rwxrwxr-x. 1 maminus maminus 12M 9月 27 18:30 call_picker
drwxr-xr-x. 23 maminus maminus 680 9月 27 18:34 linux-2.6.39
-rwxrwxr-x. 1 maminus maminus 8.6K 9月 27 18:31 translater
[maminus@localhost tmp]$ export LLVM_ROOT=/opt/lib/llvm/root
[maminus@localhost tmp]$ export PATH=$PATH:$LLVM_ROOT/bin:`pwd`
[maminus@localhost tmp]$ export C_INCLUDE_PATH=$LLVM_ROOT/include:$LLVM_ROOT/lib/clang/3.3/include
[maminus@localhost tmp]$ export CPLUS_INCLUDE_PATH=$C_INCLUDE_PATH
[maminus@localhost tmp]$ cd linux-2.6.39/init
[maminus@localhost init]$ make -C .. CC=translater M=`pwd` main.o > ../../init-main.csv

という感じで、kernel configだけ入れてから解析を実施している。
いったんgccでmakeしてmake cleanしておくのが一番確実だとは思う。
前に別のファイルで試したときにはアーキテクチャ固有のincludeパスにシンボリックリンクを張らないとダメだった。

で、configを適用したらclang用にパス類を設定する。
これでgcc側でのビルドはできなくなるので注意。(C_INCLUDE_PATHが変化するため)

そしてtranslaterを指定してmakeすればOK。
先日「M=`pwd`」が必要かどうかって話題を書いたけど、書かないとダメだった。

ちなみにkernelじゃない普通?のソースコードならmakeコマンドの引数に「--ignore-errors」とか「--always-make」を適宜付けてあげると良い感じに解析を実行できると思う。



で、今回はinit/main.cを解析してみた。

CSVが出力できたら、call_grapherにかける。
今回はWindows側でかけてみた。(もちろんLinux上でやってもOK)

J:\project\clang>call_grapher.exe init-main.csv > init-main.txt

これだけ。

そして出てきた.txtからkernel_init()配下を抜粋したのがこれ。

kernel_init(unused = unused)    // /dev/shm/tmp/linux-2.6.39/init/main.c(776,1)
    wait_for_completion( = &kthreadd_done)  // /dev/shm/tmp/linux-2.6.39/init/main.c(781,2)
    set_mems_allowed(nodemask = node_states[N_HIGH_MEMORY]) // /dev/shm/tmp/linux-2.6.39/init/main.c(785,2)
    smp_prepare_cpus(maxcpus = setup_max_cpus)  // /dev/shm/tmp/linux-2.6.39/init/main.c(793,2)
    do_pre_smp_initcalls()  // /dev/shm/tmp/linux-2.6.39/init/main.c(795,2)
        do_one_initcall(fn = *fn)   // /dev/shm/tmp/linux-2.6.39/init/main.c(726,3)
            do_one_initcall_debug(fn = fn)  // /dev/shm/tmp/linux-2.6.39/init/main.c(668,9)
                ktime_get() // /dev/shm/tmp/linux-2.6.39/init/main.c(651,13)
                fn()    // /dev/shm/tmp/linux-2.6.39/init/main.c(652,8)
                ktime_get() // /dev/shm/tmp/linux-2.6.39/init/main.c(653,12)
                ktime_sub(lhs = rettime, rhs = calltime)    // /dev/shm/tmp/linux-2.6.39/init/main.c(654,10)
                printk(s = <7>initcall %pF returned %d after %lld usecs\n)  // /dev/shm/tmp/linux-2.6.39/init/main.c(656,2)
            fn()    // /dev/shm/tmp/linux-2.6.39/init/main.c(670,9)
            arch_irqs_disabled()
            strlcat( = msgbuf,  = disabled interrupts, __kernel_size_t = sizeof (msgbuf))   // /dev/shm/tmp/linux-2.6.39/init/main.c(682,3)
            arch_local_irq_enable()
            printk(s = initcall %pF returned with %s\n) // /dev/shm/tmp/linux-2.6.39/init/main.c(686,3)
    lockup_detector_init()  // /dev/shm/tmp/linux-2.6.39/init/main.c(796,2)
    sched_init_smp()    // /dev/shm/tmp/linux-2.6.39/init/main.c(799,2)
    do_basic_setup()    // /dev/shm/tmp/linux-2.6.39/init/main.c(801,2)
        cpuset_init_smp()   // /dev/shm/tmp/linux-2.6.39/init/main.c(712,2)
        usermodehelper_init()   // /dev/shm/tmp/linux-2.6.39/init/main.c(713,2)
        init_tmpfs()    // /dev/shm/tmp/linux-2.6.39/init/main.c(714,2)
        driver_init()   // /dev/shm/tmp/linux-2.6.39/init/main.c(715,2)
        init_irq_proc() // /dev/shm/tmp/linux-2.6.39/init/main.c(716,2)
        do_ctors()  // /dev/shm/tmp/linux-2.6.39/init/main.c(717,2)
        do_initcalls()  // /dev/shm/tmp/linux-2.6.39/init/main.c(718,2)
            do_one_initcall(fn = *fn)   // /dev/shm/tmp/linux-2.6.39/init/main.c(700,3)
    sys_dup(fildes = 0) // /dev/shm/tmp/linux-2.6.39/init/main.c(807,9)
    sys_dup(fildes = 0) // /dev/shm/tmp/linux-2.6.39/init/main.c(808,9)
    sys_access(filename = (const char *)ramdisk_execute_command, mode = 0)  // /dev/shm/tmp/linux-2.6.39/init/main.c(817,6)
    prepare_namespace() // /dev/shm/tmp/linux-2.6.39/init/main.c(819,3)
    init_post() // /dev/shm/tmp/linux-2.6.39/init/main.c(828,2)
        async_synchronize_full()    // /dev/shm/tmp/linux-2.6.39/init/main.c(741,2)
        free_initmem()  // /dev/shm/tmp/linux-2.6.39/init/main.c(742,2)
        mark_rodata_ro()    // /dev/shm/tmp/linux-2.6.39/init/main.c(743,2)
        numa_default_policy()   // /dev/shm/tmp/linux-2.6.39/init/main.c(745,2)
        run_init_process(init_filename = ramdisk_execute_command)   // /dev/shm/tmp/linux-2.6.39/init/main.c(751,3)
        printk(s = <4>Failed to execute %s\n)   // /dev/shm/tmp/linux-2.6.39/init/main.c(752,3)
        run_init_process(init_filename = execute_command)   // /dev/shm/tmp/linux-2.6.39/init/main.c(763,3)
        printk(s = <4>Failed to execute %s.  Attempting defaults...\n)  // /dev/shm/tmp/linux-2.6.39/init/main.c(764,3)
        run_init_process(init_filename = /sbin/init)    // /dev/shm/tmp/linux-2.6.39/init/main.c(767,2)
        run_init_process(init_filename = /etc/init) // /dev/shm/tmp/linux-2.6.39/init/main.c(768,2)
        run_init_process(init_filename = /bin/init) // /dev/shm/tmp/linux-2.6.39/init/main.c(769,2)
        run_init_process(init_filename = /bin/sh)   // /dev/shm/tmp/linux-2.6.39/init/main.c(770,2)
        panic(fmt = No init found.  Try passing init= option to kernel. See Linux Documentation/init.txt for guidance.) // /dev/shm/tmp/linux-2.6.39/init/main.c(772,2)

ざっと見た感じだと、仮引数名が取れてなさそうな個所がある。
ファイル名が見えてない関数は別ファイルでの定義と思われる。ただし、ほかの解析結果を見るとヘッダ定義の関数が一部ファイル名取得できてなかった。理由は謎。

ちなみにSakuraEditorで出力ファイルを開くときには当然Linuxと別フォルダ階層にソースコードを置くと思う。ファイル名部分が絶対パスで出力されているのでトップディレクトリを置き換えコマンドで変更してしまえば良いと思う。
ジャンプしてみると、ソースコードのタブ文字を4文字としてカウントしてしまう点が残念な感じだった。
今回試したソースは問題なかったが、カラム数が実際のソースコードより大きくなるとジャンプに時間がかかってしまう。SakuraEditorに負担をかけてしまうようだ。
場合によってはカラム番号は常に1指定とするか、いっそのことカラム番号無しとしても良いと思う。(確か無くてもジャンプできるはず)

ちなみに古い環境だと…

Fedora7環境(gcc バージョン 4.1.2 20070925 (Red Hat 4.1.2-27))にもclangを入れてみたけど、この時はcmakeが入ってなくて環境をいじりたくなかったのでconfigureでやってみた。

[maminus@localhost build]$ ../3.3-src/configure --prefix=/usr/local/etc --enable-optimized --enable-jit
[maminus@localhost build]$ make
[maminus@localhost build]$ make check
[root@localhost build]# make install

でいけた。インストール先はとりあえず空っぽだった/usr/local/etcにした。深い意味は無い。

make clang-testは

make: *** ターゲット `clang-test' を make するルールがありません. 中止.

って言われて実行できなかった。謎。

あと、call_grapherとcall_pickerのコンパイルが通らなかった。
どうやらclangだとgccの古いバージョンについてるC++関連ヘッダファイルがコンパイルできないらしい。

仕方がないのでclangへのパスだけ通してgccでmakeしたらコンパイルできた。

make -C call_grapher CC=gcc
export PATH=$PATH:/usr/local/etc/bin
make -C call_picker CC=gcc

こんな感じで。

制限事項

今わかっている制限事項は以下の通り。

  1. 同名のstatic関数を同一の関数として扱ってしまう
  2. 関数ポインタは追えない
  3. 引数が文字列だと出力結果がおかしくなる

今は関数名だけで関数の区別をしているのでstatic関数で同名の別関数があっても同じものとして扱ってしまう。実際には同一関数でもコンパイル単位ごとに別扱いのはずが同一として扱うのでコールグラフもどきとしては問題なくても統計処理とかをやるときには困りそうな気がする。この問題は関数名にファイル名を付与するなどの工夫をしたら解消できそうな気はする。

2つ目は現状でも関数ポインタでの関数呼び出しまでは捕捉できている。ただ、関数ポインタにどんな関数が設定されうるのかまでは追えていない。これを解消するにはcall_pickerツール側での対応も必要になると思う。とりあえずの回避策としては中間ファイルとしてCSVファイルに関数呼び出しの記録が出てくるので手動でCSVファイルに関数ポインタから実際に呼ばれる関数の呼び出しを追加すればコールグラフもどきとしては呼び出しがつながる。

3つ目はCSVファイルの処理を手抜きにしたせいで「"」のエスケープが機能していないせい。コールグラフもどきとしては特に困らないのでとりあえず放置。

今後

まだアイデアを練り切れていないけど、変数のread/writeや処理の分岐を追いかけられるようにしたいなぁと思う。いつになるかわからないけど、何かできたらまた公開したい。