Generating Control Flow Graph using C Intermediate Language (CIL)
C プログラムから Control Flow Graph(CFG)を取り出す必要があり、色々と調べてみた。結果、以下の2つの方法が有望そう。1つはgcc の"-fdump-tree-fixupcfg" オプションを使う方法。もう一つは、Berkley が作っている C Intermediate Language (CIL) を使う方法。gcc はオプションで簡単に出せるのでこっちでいいかなぁ、と考えていたら、同僚からは、gcc はドキュメントがないから、まじめにやるのであれば CILを薦められた。
ただ、このCILが使い方が結構厄介。まず、最初は Windows上でインストールしようとしたのだが、うまくいかず。あれこれ時間かかってできなくて、結局あきらめてLinux上でやるとあっさりインストール成功。これだけで多くの時間を費やしてしまう。どうやら、CILというよりも、CILの実装言語であるOcamlがWindowsはあまりサポートされていない模様。
さて、インストールが成功したので、次は、CILを使って、CFGを出すという作業。
CILは、インストールを澄ますと、コマンドラインからアクセスする方法と、Ocamlのプログラムからアクセスする方法の二つの方法がある。コマンドラインで、CILからCFGを出すには"--domakeCFG" オプションで、以下の通り実行すればよいとのこと。
cilly.asm.exe --domakeCFG sample.c --out sample.o
しかし、出力ファイルを見ると、なんとなくCFGが生成されていないような気がする。私の読み方が悪いのかもしれないが。
というわけで、もう一つの方法である、OcamlコードからCFGの取り出しを試してみることに。ただこれがなかなか骨が折れる作業である。なぜなら、私はOcamlを知らないから。言語設計はともかく、環境も、ocamlfind、ocamloptいろいろありこれらをゼロから学ぶのは骨が折れる。
そんな折、参考になるのがサンプルコード。CILのOcamlコードからの扱いのサンプルコードを探すと以下のページにたどり着く。
このサンプルコードをちょっとずつ直して、自分のやりたいSample.cからCFGを生成することに。
まず上記のサイトより、cil-testing-1.1.tar.gz を取得。
実行方法は、make run-ciltest1 を実行すると以下の表示が得られる。
test1.c:11: test1_add has 2 arg(s)
test1.c:22: test1_inc has 1 arg(s)
test1.c:36: test1_print has 1 arg(s)
test1.c:48: test1_last_error has 0 arg(s)
test1.c:56: test1_print_last_error has 0 arg(s)
main.c:10: main has 0 arg(s)
すると、test1.c 内のすべての関数についてのCFGが、.dot 形式で出力されているので、それをGraphbiz等で開けばよい。
さて。
このOcamlプログラムはCFGの出力が、test1.cに固定であったが、任意のCプログラムに対してCFGを取得することを試みる。ここでは、以下のSample.cを例にで実演する。
#include <stdio.h> #include "sample.h" int someFunction (int a, int b) { int result = 0; if (a < b && a != result) { return 0; } else { int c = a + b; int i = 0; while (i < c) { result = (result + a) / b; i++; } } return result; }
この手持ちのSample.cのCFGをCil-testingをもとに作る際、変更・新規作成が必要なファイルは以下の4つ。
- Makefile(変更)
- sample.ml(新規作成)
- test1.mlをもとに作る。
- sample.h (新規作成)
- main.c
まず、変更対象のMakefileである、cil-testing に同梱されているMakefaileに以下の記述を追加。
sample: sample.cmx ocamlfind ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTINCS) $(OCAMLOPTPACKAGES) $(OCAMLOPTLIBS) $^ -o $@ run-sample: sample sample.i main.i ./sample
また各所test1.iが現れる個所に、sample.iを追加。つまり、全体で以下のようなMakefileとなる。
# $Id: Makefile,v 1.4 2007/07/07 13:41:23 rjones Exp $ PACKAGE := cil-testing VERSION := 1.1 CC := gcc CFLAGS := -Wall -Werror LIBVIRTDIR := /home/rjones/d/libvirt-cil OCAMLCFLAGS := OCAMLCPACKAGES := -package unix,str,cil OCAMLOPTFLAGS := $(OCAMLCFLAGS) OCAMLOPTINCS := $(OCAMLCINCS) OCAMLOPTPACKAGES := $(OCAMLCPACKAGES) OCAMLOPTLIBS := -linkpkg all: test cilprint ciltest1 cilimportvirt sample test: main.o test1.o sample.o $(CC) $(CFLAGS) $^ -o $@ cilprint: cilprint.cmx ocamlfind ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTINCS) $(OCAMLOPTPACKAGES) $(OCAMLOPTLIBS) $^ -o $@ run-cilprint: cilprint test1.i main.i @./cilprint ciltest1: ciltest1.cmx ocamlfind ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTINCS) $(OCAMLOPTPACKAGES) $(OCAMLOPTLIBS) $^ -o $@ run-ciltest1: ciltest1 test1.i main.i ./ciltest1 ################################################### ## 追加部分 ここから ### sample3: sample3.cmx ocamlfind ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTINCS) $(OCAMLOPTPACKAGES) $(OCAMLOPTLIBS) $^ -o $@ run-sample3: sample3 sample3.i main.i ./sample3 ## 追加部分 ここまで ### ################################################### cilimportvirt: cilimportvirt.cmx ocamlfind ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTINCS) $(OCAMLOPTPACKAGES) $(OCAMLOPTLIBS) $^ -o $@ run-cilimportvirt: build-libvirt libvirt-files libvirt-syms cilimportvirt ./cilimportvirt build-libvirt: if [ ! -f $(LIBVIRTDIR)/Makefile ]; then \ cd $(LIBVIRTDIR) && ./autogen.sh; \ fi if [ ! -f $(LIBVIRTDIR)/tests/virshtest.i ]; then \ make -C $(LIBVIRTDIR) CC="gcc -save-temps"; \ rm -f libvirt-files; \ fi libvirt-files: find $(LIBVIRTDIR) -name '*.i' | \ egrep '/(qemud|src)/' | \ egrep -v 'virsh.i' > $@ libvirt-syms: egrep -io '\bvir[a-z0-9]+' \ < $(LIBVIRTDIR)/src/libvirt_sym.version > $@ # Run the C files through the C preprocessor. .c.i: $(CPP) $< > $@ # Common rules for building OCaml objects. .mli.cmi: ocamlfind ocamlc $(OCAMLCFLAGS) $(OCAMLCINCS) $(OCAMLCPACKAGES) -c $< .ml.cmo: ocamlfind ocamlc $(OCAMLCFLAGS) $(OCAMLCINCS) $(OCAMLCPACKAGES) -c $< .ml.cmx: ocamlfind ocamlopt $(OCAMLOPTFLAGS) $(OCAMLOPTINCS) $(OCAMLOPTPACKAGES) -c $< # Clean. clean: rm -f *~ *.bak core *.cmi *.cmo *.cmx *.cma *.cmxa *.o *.so *.a \ test cilprint *.i ciltest1 *.dot cilimportvirt # Distribution. dist: $(MAKE) check-manifest rm -rf $(PACKAGE)-$(VERSION) mkdir $(PACKAGE)-$(VERSION) tar -cf - -T MANIFEST | tar -C $(PACKAGE)-$(VERSION) -xf - tar zcf $(PACKAGE)-$(VERSION).tar.gz $(PACKAGE)-$(VERSION) rm -rf $(PACKAGE)-$(VERSION) ls -l $(PACKAGE)-$(VERSION).tar.gz check-manifest: @for d in `find -type d -name CVS | grep -v '^\./debian/'`; \ do \ b=`dirname $$d`/; \ awk -F/ '$$1 != "D" {print $$2}' $$d/Entries | \ sed -e "s|^|$$b|" -e "s|^\./||"; \ done | sort > .check-manifest; \ sort MANIFEST > .orig-manifest; \ diff -u .orig-manifest .check-manifest; rv=$$?; \ rm -f .orig-manifest .check-manifest; \ exit $$rv .SUFFIXES: .c .i .cmo .cmi .cmx .ml .mli .PHONY: run-cilprint run-ciltest1 run-cilimportvirt build-libvirt
次に、test1.ml をベースに、以下のsample.ml を作成。(#変更点は、test1.ml 中の「test1.i」となっているところを、「sample.i」に変更するだけ。)
sample.ml
(* Test CIL loading and printing of files. * $Id: ciltest1.ml,v 1.1 2007/07/07 12:03:30 rjones Exp $ *) open Printf open Cil let files = [ "sample.i"; "main.i" ] let () = (* Load each input file. *) let files = List.map ( fun filename -> (* Why does parse return a continuation? *) let f = Frontc.parse filename in f () ) files in (* Merge them. *) let file = Mergecil.merge files "test" in (* Remove unused prototypes and variables. *) Rmtmps.removeUnusedTemps file; (* Do control-flow-graph analysis. *) Cfg.computeFileCFG file; (* Go over the internal CIL structure and print some facts. *) printf "CIL has loaded the files, merged them and removed unused code.\n\n"; printf "Merged file has %d globals ...\n\n" (List.length file.globals); List.iter ( function | GType _ -> () (* typedef *) | GCompTag _ -> () (* struct/union *) | GCompTagDecl _ -> () (* forward prototype of struct/union *) | GEnumTag _ -> () (* enum *) | GEnumTagDecl _ -> () (* forward prototype of enum *) | GVarDecl _ -> () (* variable/function prototype *) | GVar _ -> () (* variable definition *) | GFun (fundec, loc) -> (* function definition *) printf "%s:%d: %s has %d arg(s)\n" loc.file loc.line fundec.svar.vname (List.length fundec.sformals); (* If it's a library function, print its CFG to a file. *) if loc.file <> "main.c" then Cfg.printCfgFilename (fundec.svar.vname ^ ".dot") fundec | GAsm _ -> () (* global asm statement *) | GPragma _ -> () (* toplevel #pragma *) | GText _ -> () (* verbatim text, comments *) ) file.globals;
最後に、以下の二つのファイルの作成。
- sample.h (新規作成)
- main.c(新規作成)
これらは両方とも、cil-testing のコードを使うために、書式をそろえるためのもの。以下の通り作際。
sample.h
/* Fake C library for CIL testing. * $Id: sample.h,v 1.1 2007/07/07 10:52:16 rjones Exp $ */ #ifndef __SAMPLE_H__ #define __SAMPLE_H__ extern int someFunction (int a, int b); #endif /* __SAMPLE_H__ */
main.c
/* Main program for testing CIL. * $Id: main.c,v 1.1 2007/07/07 10:52:16 rjones Exp $ */ #include <stdio.h> #include <stdlib.h> #include "sample.h" int main () { int a, b, c; a = 1; b = 3; c = someFunction (a, b); printf("%d\n", c); exit (0); }
これでファイルの準備は完了。
以下のコマンドを実行すると、sample.c 中の関数「someFunction」のCFGが、「someFunction.dot」として得られる。
> make run-sample
以上。