C ソースをパースしてシンボルを抽出する Go ライブラリ symc を作った

C ソースをパースしてシンボルを抽出する Go ライブラリ symc を作った

注意 このライブラリは個人的な問題を解決するために作成したツールのため、かなり適当です. ご使用の場合はご注意ください.

C ソースをパースして変数, 関数の定義や使用箇所を抽出する Go ライブラリを作成した.

https://github.com/kita127/symc1

作った背景

普段 C ソースを相手に仕事をしてるが, ソースを静的に解析する機会など多い. ソース内にある識別子がどこで定義されどこで使用されるかなどの情報がそういった解析時に欲しくなる時がある. ソースを解析して識別子情報だけを手軽に抽出するライブラリを作成すれば, 応用して色々なツールが作れるのではないかと考えた. ツール類は Go で作成する機会が多いため, Go ライブラリとして作成.

機能

主な機能は以下.

  • プリプロ展開後の C ソースを入力とする
    • プリプロ解析からやるのは環境による差異など色々困難なため妥協
    • プリプロ展開していないソースを入力した場合, エラーで死にます
  • 解析したモジュールに対して以下の情報を抽出する
    • 定義している変数
    • 定義している関数
    • extern 宣言している変数
    • プロトタイプ宣言している関数
    • 関数内で参照している変数
    • 関数内でコールしている関数の情報

使い方

使い方は以下のとおり.

package main

import (
    "fmt"

    "github.com/kita127/symc"
)

func main() {

    cSrc := `
int variable;

int extFunc( int a );

int func( void ){

    variable++;

    return extFunc( variable );
}

`

    module := symc.ParseModule(string(cSrc))
    fmt.Println(module)
}
$go build && ./main
Module : Symbols={ VariableDef : Name=variable, PrototypeDecl : Name=extFunc, FunctionDef : Name=func, Params=[], Symbols=[Assigne : Name=variable CallFunc : Name=extFunc, Args=[RefVar : Name=variable]] }

github.com/kita127/symc をインポートして, symc.ParseModule() に C ソースの文字列を渡せば, そのソースの識別子情報を AST の構造体として返してくれる.

上記では C ソース解析後, そのソースの情報を持つ AST である symc.Module 構造体のポインタを取得している.

取得した symc.Module 構造体のポインタは 変数の定義VariableDef や関数の定義FunctionDef 情報などを保持してる.

関数の定義情報の中にはその関数内で参照している変数などの情報も取得できる.

その他の機能

PrettyString()

解析した情報を少し見やすくする Pretty string 機能.

package main

import (
    "fmt"
    "io/ioutil"
    "os"

    "github.com/kita127/symc"
)

    cSrc := `
int variable;

int extFunc( int a );

int func( void ){

    variable++;

    return extFunc( variable );
}

`


func main() {
    module := symc.ParseModule(string(cSrc))
    fmt.Println(module.PrettyString())
}
>go build && ./main
DEFINITION variable
PROTOTYPE extFunc
FUNC func() {
    ASSIGNE variable
    extFunc(variable)
}

Inspect()

引数に解析済みの AST と処理関数を受け取り, AST を深さ優先で走査しながら引数の関数を適用する.
go 標準の ast パッケージにも同様の機能がありそちらを参考にした.

以下は AST を走査し, 変数定義の場合は変数名を大文字に変更する例.

引数で渡す関数が false を返すとそのタイミングで走査をやめることもできる. 以下の例は常に true を返すため, AST を走査し終わるまで走査をやめない.

symc.Symbol は変数定義や関数定義などシンボル情報が実装しているインターフェースであり, AST を構成している型は全て実装している. そのため Inspect() に渡す値は *Module 以外の *VariableDef*FunctionDef 等でも良い.

package main

import (
    "fmt"
    "strings"

    "github.com/kita127/symc"
)

func main() {

    cSrc := `
int g_var;

void func(int a)
{
    int hoge;
    char fuga;

    fuga = cal( hoge );
}
`
    module := symc.ParseModule(cSrc)

    // To uppercase only variable definitions.
    symc.Inspect(module, func(s symc.Symbol) bool {
        if v, ok := s.(*symc.VariableDef); ok {
            v.Name = strings.ToUpper(v.Name)
        }
        return true
    })

    fmt.Println(module)

}
$ go build && ./main
Module : Symbols={ VariableDef : Name=G_VAR, FunctionDef : Name=func, Params=[VariableDef : Name=A], Symbols=[VariableDef : Name=HOGE VariableDef : Name=FUGA 
Assigne : Name=fuga CallFunc : Name=cal, Args=[RefVar : Name=hoge]] }

ReduceLocalVar()

ローカル変数の情報は不要だったり, 冗長だったりする場合もあるため, 削除するメソッドを実装.

以下の例では, module の情報を生成後, ReduceLocalVar() メソッドでローカル変数の定義, 参照, 代入の情報のみ削除している.

package main

import (
    "fmt"
    "github.com/kita127/symc"
)

func main() {

    cSrc := `
int g_var;
static int s_var;
extern short ext_var;

void func(int arg)
{
    int i;
    int l_var;

    for(i = 0; i < 10; i++){
        g_var++;
    }

    l_var = ext_var;
    l_var += s_var;
    cal( l_var );

    g_var = l_var;
}
`

    module := symc.ParseModule(cSrc)

    fmt.Println("before reducing")
    fmt.Println(module.PrettyString())

    // Reducing local variable infomation.
    fmt.Println("after reducing")
    module.ReduceLocalVar()
    fmt.Println(module.PrettyString())
}
$ go build && ./main
before reducing
DEFINITION g_var
DEFINITION s_var
DECLARE ext_var
FUNC func(DEFINITION arg) {
    DEFINITION i
    DEFINITION l_var
    ASSIGNE i
    i
    ASSIGNE i
    ASSIGNE g_var
    ASSIGNE l_var
    ext_var
    ASSIGNE l_var
    s_var
    cal(l_var)
    ASSIGNE g_var
    l_var
}


after reducing
DEFINITION g_var
DEFINITION s_var
DECLARE ext_var
FUNC func(DEFINITION arg) {
    ASSIGNE g_var
    ext_var
    s_var
    cal()
    ASSIGNE g_var
}

注意点

Macgcc(clang?) でしか確認していないため他のコンパイラプリプロ展開したソースは試していない. モダンな C 言語の構文には対応していない. 多分, C90 くらいまでの構文はいけると思う(適当).

また冒頭の注意でも記載している通り, 個人的な問題を解決するために作成したライブラリのため, さほど厳格には作っていない. ご使用の場合はその辺りを留意した上でお願いします.

反省点とその他

ライブラリ作成途中に goyacc という Go 用のパーサジェネレータの存在を知った. 本来であればこういった構文解析系のライブラリは既存のパーサジェネレータ等を使うべきだが, かなり作ってから知ったのでそのまま作り切った. (ちなみに自前で字句解析・構文解析している)

一応, ユニットテストをこまめに作りながら作成を進めたので大体いけているとは思う.