このメモは,Windows 環境で Soot を使って Java プログラムを解析する方法について 経験に基づいて記述しています. このメモは書きかけです.内容は随時変更される可能性があります. もし,誤りや疑問などありましたら,ご指摘いただけると幸いです.
このメモでは,Soot の機能全体を網羅するつもりはありませんが, プログラム解析処理を実装するにあたっての Soot から提供されるデータとか, 使っていて得られた経験を随時蓄積していくつもりです.
Soot: a Java Optimization Framework は,McGill 大学のプロジェクトとして開発されている Java バイトコードの解析系です.Aspect Bench Compiler "abc" なんかで利用されていたりします.
Soot は,Javaのバイトコードを解析して Jimple と呼ばれる 中間表現に変換します.他にも,もっと生バイトコードよりの Baf, SSA 形式である Shimple など,別の表現も用意されています. Java Optimization Framework という名の通り,一度バイトコードを中間表現に落として 開発者が定義した最適化処理などを適用した後, 再びバイトコードに戻すというのが本来の用途だと思われます. しかし,中間表現に落とされたものには制御フロー,データフローなどの 最適化以外のプログラム解析にとっても有用な情報が載っていますので, それをうまく利用することができれば,プログラム解析ツールの 開発コストを下げることができます.
Soot の特徴は,処理が複数のフェイズ(内部では「パック (pack)」と呼ばれています)に 分割されており,任意のフェイズに新たな処理を追加することができることにあります. また,フェイズごとに固有のオプションを取ることができ, 様々な設定を制御することができるようになっています. 何も用意しなくてもバイトコードからその中間表現を取り出すことはできますが, 途中にコード変換や最適化処理を挿むことで,様々な出力を獲得することができます.
Soot は,いくつかのライブラリに依存しています.
安定版を使う:
安定版の Soot を実行するには,以下のパッケージを使います.現在の安定版は 2.2.4 です.
Soot は Java で実装されているので,実行用の Java VM が必要です.
解析対象の Java プログラムが依存する JDK クラスも Soot は解析を行います. そのため,jre/lib ディレクトリに入っている rt.jar などが必要になります. そのほか,解析対象プログラムが参照する JAR ファイルを一式用意してください.
この中に jasmin,polyglot,soot の3つがまとめて入っています.
soot-2708/lib/sootclasses-2.2.4.jar
,polyglot-1.3.4/lib/polyglot.jar
,
jasmin-2708/lib/jasminclasses-2.2.4
をクラスパスに追加します.
http://www.sable.mcgill.ca/paddle/ にある Points-to Set Analysis の実装です. 標準で附属する Spark という実装を使う場合は,必要ありません.
Paddle フレームワークを使う場合に必要です.
http://www.sable.mcgill.ca/jedd/
にある 0.3 のバイナリパッケージの jedd-0.3/runtime/lib
ディレクトリに入っています.
DLL のほうは,-Djava.library.path=lib
という形式で
DLL のあるディレクトリを JVM に教える必要があります.
これも Paddle フレームワークを使う場合に必要になります. http://javabdd.sourceforge.net/から ダウンロードできる 0.6 のバイナリパッケージに入っています.
Paddle の Nightly-Buildを使う: Soot 用の points-to analysis の実装である paddle フレームワークの nightly-build の最新版を実行するには, Nightly Build ページにある にある以下のものが必要です.ページでは,微妙にリンクが分散していますし, 良く似た名前のものもいくつかあるので注意してください (コンパイル用と実行用でパッケージが違ったりしているようです……何が違うのか,いまいち分かっていませんが).
JavaBDD だけ,http://javabdd.sourceforge.net/から ダウンロードしてきます. それ以外は,paddle nightly build のディレクトリにあるファイルを使います.
Paddle のページに配置されている soot などのクラス群は, soot 自身の nightly-build とはバージョンが違います. Paddle のオプションの定義は paddle パッケージに入っていますが, そこから自動生成されるコードが soot パッケージに入っているので, paddle と soot のバージョンが違うと,オプション処理を誤るので注意が必要です.
ただし,Eclipse などからソースコードを少しは参照したいときは, Soot の nightly-build 版(soot-2327 など)のソースを割り付けておくとそれなりにうまく読めます (Soot の安定版からは,いくらか構成が変わっているようです).
実行時は,必要なファイル一通りにクラスパスを通す必要があります. 数が多いので,あらかじめバッチファイルなり, シェルスクリプトなり,Eclipse 実行環境を整える必要があります.
以下は,stable 版の soot を Cygwin から実行するときのコマンドを整形したものです.
target ディレクトリに解析対象であるtest.HelloWorld
クラスが入っているものとします.
java -Xss30m -Xms400M -Xmx900M -Djava.library.path=. -classpath "sootclasses-2.2.4.jar;polyglot.jar;jasminclasses-2.2.4.jar" soot.Main -cp "target; C:\Program Files\Java\j2re1.4.2_12\lib\charsets.jar; C:\Program Files\Java\j2re1.4.2_12\lib\jsse.jar; C:\Program Files\Java\j2re1.4.2_12\lib\rt.jar; C:\Program Files\Java\j2re1.4.2_12\lib\jce.jar" -whole-program -f jimple -p cg.cha enabled:false -p cg.spark verbose:true,enabled:true,propagator:iter --app test.HelloWorld
Main の後ろにある "-cp" オプションは,Soot が 解析する対象のクラス群のパス(JDK 含む)を指定しています. 筆者が利用している環境では,1.4 のクラス群を明示的に指定しています.
-whole-program
オプションは,コールグラフ生成などの
「全体を解析する」タイプの解析系を有効にしています.
今回は特に意味がありませんが,points-to set analysis などを有効にしたいときには
必ず指定する必要があるオプションです.
-f jimple
は,出力が Jimple であることを指定しています.
-p
がフェイズオプションと呼ばれるもので,
ここでは paddle フレームワークによる context-insensitive points-to set analysis を
有効にしています.
詳細については
Soot Phase Options
を参照してください.
最後の HelloWorld が,解析対象クラスです.-app
オプションは,
HelloWorld から直接・間接的に参照しているクラス群も
解析対象であることを指定しています.どれがライブラリクラス(解析対象外)であるかなどは,
別途指定可能です.これについては,
Soot command-line optionsを参照してください.
これを実行すると,test.HelloWorld クラスを読み込み,sootOutput
ディレクトリに
test.HelloWorld.jimple
という名前のファイルが生成されます.
この中身が,soot によってバイトコードから変換された jimple コードとなります.
このほかに使うオプションとしては,ソースコードでの行番号情報を保存する-keep-line-number
,
動的に生成されるために普通の利用関係では到達できないクラス
(jEdit 4.2 なら org.gjt.sp.jedit.options. のクラス群など)を
解析対象に加える-dynamic-package
オプションなどがあります.
Soot の特徴は,処理が複数のフェイズに 分割されており,任意のフェイズに新たな処理を追加することができることにあります. Soot は,各フェイズで実行すべき処理を Pack というオブジェクトに管理させています. たとえば Jimple コードの各種最適化のためのコード変換を 「Jimple Optimization Pack」に格納しています.
開発者は,自分が追加したい解析処理を Transformer クラスのサブクラスとして実装し, 実行したいタイミングに対応するパックに登録します. フェイズは全体解析用とメソッド単位解析の二種類に区分されており, 全体解析用フェイズには SceneTransformer を, 単体解析用フェイズには BodyTransformer を追加します. 使用する種類を間違うと,実行時に ClassCastException が発生します (少なくとも Soot の現時点のバージョンは,登録時にチェックは行われません).
以下に,Soot が バイトコードから Jimple 表現を取り出した後に 適用するパックの順序を示します.
コールグラフ生成.-whole-program
オプション有効時のみ実行される.
生成されたコールグラフは,Scene.v().getCallGraph()
で参照可能.
全体解析に基づく変換フェイズ.-whole-program
オプション有効時のみ実行される.
全体解析に基づく最適化フェイズ.-whole-program
オプション有効時のみ実行される.
全体解析に基づく情報付加フェイズ.-whole-program
オプション有効時のみ実行される.
メソッド単位の変換フェイズ.コードの変換を行いたい場合は,ここに処理を追加する.
メソッド単位の最適化フェイズ.各種処理が始めから用意されている.
メソッド単位の情報付加フェイズ.各命令ごとに追加情報である Tag を付与する.
この情報を利用した解析を行いたかったり,新たな情報付加を行う場合はここに処理を追加する.
なお,ソースコードの行番号情報だけは特殊で,
コマンドラインオプションとして -keep-line-number
を渡すと
行番号情報が生成される.
Jimple 命令を取り出した「後」しか書いていませんが, これは Jimple を抽出する最初のパック,jb および jj の振る舞いが固定(ハードコーディング) されており,これらのパックに自作の BodyTransformer を追加できないためです.
各フェイズの詳細は,
Soot Phase Optionsに記述されています.
また,実際に Soot で利用可能になっているフェイズの種類は,-phase-list
オプションで知ることができます.
生成するデータの形式によって,有効になるフェイズが違います.
Grimp 形式なら gop が有効とか,baf 形式なら bop が有効とか,
ある程度は名前から識別できます.
しかし,現在の Soot では,たとえば tag (Tag Aggregator) フェイズは
baf を生成するときにのみ有効といった,
あまり直観的ではない振る舞いもあるようです.
ドキュメントを十分に確認するのも大切ですが,
soot.PackManager
クラスのrunPacks()
メソッドや
runBodyPacks(SootClass)
メソッドのコードを
直接確認したほうが早いこともあります.
以下に,メソッドごとの解析を実現するための
BodyTransformer のサブクラスの雛形を示します.
BodyTransformer は,
internalTransform(Body b, String s, Map m)
というメソッドを持っているので,
それをサブクラス側でオーバーライドし,任意の処理を記述します.
Body はそのメソッドの命令列などの情報を格納しています.文字列は現在のフェイズ名を,
Map はオプション群を格納しています.
public class TestBodyTransformer extends BodyTransformer { public void internalTransform(Body b, String s, Map m) { // ここで変換処理や,必要な解析を実行 } // シングルトンパターンとして実装する static private TestBodyTransformer theInstance = null; private TestBodyTransformer() { } public static TestBodyTransformer v() { if (theInstance == null) { theInstance = new TestBodyTransformer(); } return theInstance; } }
この作成した Transformer クラスを,Soot に登録して実行することになります. 以下のように Transformer を追加してから Soot 本体の処理を実行するような main メソッドを定義することができます.
// "jtp" Java Transformation Pack に // TestBodyTransformer を追加して Soot 本体を起動するプログラム // "jtp" はコールグラフ構築などの処理より後, // 最適化を実行するより前に適用される. public static void main(String[] args) { if (args.length == 0) { System.out.println("Syntax: java "+ "soot.examples.instrumentclass.Main --app mainClass "+ "[soot options]"); System.exit(0); } // TestBodyTransformer クラスのインスタンスを追加 // 注: TestBodyTransformer.v() はシングルトンを返す PackManager.v().getPack("jtp").add( new Transform("jtp.test", TestBodyTransformer.v())); soot.Main.main(args); }
パックに処理を追加するとき,Transform クラスの第一引数の 文字列の先頭が Pack の名前と一致しないと, 実行時にエラーが起きるようです (現在の Soot では,追加時のエラーチェックはないようです).
メソッドの解析は,基本的に Body クラスの中身を調べることになります. あまり詳しく調べたわけではありませんが, 少なくとも以下のような方法で,情報にアクセスできるようです.
Body.getUnits()
の戻り値が,Jimple 命令列を返します.
Unit というのが,命令1つに相当する抽象クラスです. その他の解析の付加情報が,Tag として Unit に追加されます.
当然,サブクラスにキャストして使うことになります.
実装クラスは soot.jimple.internal
パッケージに,
それに対応するインタフェースが soot.jimple
パッケージに定義されています.
jap
フェイズでなら,Unit.getTags()
メソッドを使って
jap
パックに入っている他の "Tagger" クラス群による
付加情報を参照することができます.
Tag としては,その時点で有効な値定義を示す
Reaching Definition とか,Dominator 情報などが取得できるようです.
また, -keep-line-number
コマンドラインオプションを使っていれば,
ソースコードの行番号情報にもアクセスすることができます.
-xml-attribute
コマンドラインオプションを与えて Soot を実行すると,
各 jimple ファイルに対応した xml ファイルが生成されます.
この xml ファイルには,Tag 情報も格納されるため,
Jimple の各行がソースコードの何行目に対応するかなどを調べるために利用できます.
Jimple の行番号情報は,jimple ファイルを生成する段階まで決まらないため, 途中のフェイズからのアクセスはできないようです.
Scene.v().getCallGraph()
メソッドを使うことでアクセスできます.
body で与えられたメソッドを呼び出しているメソッドが知りたい場合には
CallGraph.edgesInto(body.getMethod())
を,
呼び出し先を知りたい場合にはCallGraph.edgesOutOf(body.getMethod())
を使います.
特定の呼び出し命令からのメソッド呼び出し先が知りたい場合は,
CallGraph.edgesOutOf(Unit)
を使います.
new ExceptionalUnitGraph(Body)
を使うことで
獲得できます(Dominator Analysisで利用されています).
ただし,これによって得られるグラフは,入り口・出口である頂点が1つとは限りません. Post-Dominator Analysis などを行う場合には出入り口を各1つずつとしたいところですが, その場合は,自分でそういうグラフを作れ,と Soot のメーリングリストでは 書かれていました.
ちなみに,これらの UnitGraph 系は,内部で unmodifiable コレクションを作っています. なので,後から辺を追加したりはできません.
出入り口1つに見せかける継承クラスは作ったので,そのうちここに置くと思います (忘れなければ).
Scene.v()
)がその代表ですし,
自分で別フェイズとして定義してシングルトンオブジェクトにアクセスすることもできます.
自分が実現したい処理をどういうフェイズに分割するか, 得られた情報をシングルトンオブジェクトに保持しておくのか,それとも Unit.addTag を使って Tag として情報を付加するのか,といったあたりが, 設計時の悩みどころかもしれません.
Scene.v().getPointsToAnalysis()
に格納されています.
標準では points-to set analysis の結果としては,ダミーオブジェクトが格納されているようです.
Points-to set は,context-insensitive でよければ
-whole-program -p cg.spark enabled:true,verbose:true
といった構成で実行します.
このとき,JavaVM の引数として-Xss30m -Xmx800M
のように,大きな値をセットしておく必要があります.
その他の構成については,長いので,points-to set 解析のオプションとして別にしておきました.
これ以降はまだ十分に理解できていないので,簡単に手がかりだけ書いておきます:
Soot では,変数や式はすべて Value クラスとして抽象化されており,
たとえば代入文なら左辺と右辺がそれぞれ Value オブジェクトとなります.
Value オブジェクトは,実体は式であったりローカル変数を表現する Local クラスだったりします.
で,PointsToAnalysis.reachingObjects(Local)
などを呼び出すと,
そこに到達する可能性のあるオブジェクトの集合が AllocNode
の集合として与えられるようです.
(points-to analysis のアルゴリズムとして,sparkとpaddleのどちらを使うかによって,
オブジェクト集合を表現するAllocNodeのクラスが変わります.)
Context-sensitive analysis を有効にしているときは,reachingObjects()
メソッドの
引数として「どのコンテキストでの結果が知りたいか」を与える必要があります.
context は,メソッドなどが該当するのですが,
object-sensitive analysis のときはどうなるのかなど,まだ把握できていません.
必須オプション: points-to set 解析のような「全体解析」を有効にします. これが指定されない場合,soot は個別のメソッド単位だけの解析を適用します.
-whole-program
JDKなど,ライブラリを含むすべてのクラスを解析対象とする場合は,以下のオプションも指定します.
-include-all
コールグラフの作り方は,points-to set analysis の結果に影響を与えます.
通常のコールグラフ解析では,forName メソッドや newInstance メソッドを処理しないため, コールグラフが不完全となります. その結果,エントリメソッド(main)から到達できないと判定されたメソッドにおいて points-to set の値が手に入らなかったり, CallGraph オブジェクトに「実行されうるメソッド」を要求しても何も得られなかったりします.
GUI ベースのプログラムやスレッド処理を使っているプログラムを解析する場合は, forName や newInstance を処理するよう,call graph フェイズに,以下のようにオプションを指定します.
-p cg verbose:true,jdkver:4,safe-forname:true,safe-newinstance:true
また,JDK メソッドを解析対象に加えるために,-include-all
オプションをさらに追加する
現時点では,spark フレームワークのほうが高速かつ安定している(クラッシュしない)ようです. 細かい設定はいくらかありますが,とりあえず有効にするだけで十分なようです. spark は context-insensitive analysis です.context-sensitive な結果が必須の場合は,paddle を使います.
-p cg.spark verbose:true,enabled:true
paddle フレームワークでは,かなり細かく解析方法を指定することができます.
context-insensitive analysis でよければ,以下のような構成が考えられます.
-p cg.paddle enabled:true,verbose:true,context:insens,bdd:false
このとき,JavaVM の引数として,-Xss30m -Xmx800M
というように,大きな値をセットしておきます.
bdd
オプションは,true に設定すると,
内部表現を BDD に切り替えることでメモリを節約します.
insensitive analysis の場合,false で十分実行可能で,
BDDを使うと逆に実行時間が遅くなるようです.
context-sensitive analysis については, とりあえず,文献[1]で良いトレードオフであると指摘されている object-sensitive analysis についてだけ調査しています.
object-sensitive analysis では,メモリが足りないので,
BDDの指定はほぼ必須なようです.
で,backend
オプションでどのBDDを指定するのかが問題となりますが,
Windows 上では,javabdd を指定するのが無難なようです.
buddy,cudd は,同 DLL を JNI を用いたラッパーで 包んで使えるようにしているようです. 標準では,Windows 版と Linux 版のバイナリが提供されています. FreeBSD 上では DLL ロード時のシンボル名の解決とかに失敗してエラーとなるので, そのままのバイナリを持ってきても動きません.
私の手元の Windows 上では,buddy を指定しても動きませんでした. 色々(jeddを手元でbuild試みたりとか)してみましたが,結局放り出しました.
Linux 版は,buddy を使っての object-sensitive analysis が動きました (テスト動作は VMWare 上の Vine Linux でやりました). 逆に,この環境では,javabdd 版のほうがメモリ不足で動かず, javabdd vs. buddy でのパフォーマンス比較はできていません.
object-sensitive analysis は,context-insensitive analysis に比べて,かなり時間がかかります.
Pentium 4 3.0GHz + DDR 2GB の PC 上で,
解析対象を10ファイル841行の Java プログラムにしたとき,
paddle nightly build / bdd 使用せずでの
object-insensitive analysis (context:insens,bdd:false
)が2分20秒程度で完了したのに対して,
JavaBDD を使っての object-sensitive analysis
(context:objsens,bdd:true,backend:javabdd
)は実行に約90分かかりました.
文献[1]では,
object-sensitive analysis や 1H-object-sensitive anlaysis の
結果が優れているという言及がありますが,
1H のほうは,context-heap:true
というオプションをさらに
追加したもののようです(説明を読んだ限りでは).
ただ,私の手元の環境ではメモリの不足のためか,処理が完了しませんでしたので,
実行時間その他の評価も行えていません.
現段階での私の結論は,「context-insensitive なら BDD なし」 「object-sensitive なら Windows 上では javabdd を使う」というところに落ち着いています.
[1] Ondrej Lhotak and Laurie Hendren: "Context-Sensitive Points-to Analysis: Is It Worth It?" Proceedings of the 15th International Conference on Compiler Construction, pp.47-64, Vienna, Austria, March 2006.
Soot は,Java ソースコードから Jimple 命令列を生成するためのコードを持っていますが, いくつかの Java のコードを食わせてもクラッシュしてしまったので, 筆者は使用をあきらめました.
ソースコード解析をやりたい人には, WALA のほうが有望かもしれません(試していませんが……).
Soot は,すべての例外を RuntimeException あるいは そのサブクラスとしてスローします. 例外に関するドキュメントはありません.
jb パックのようにカスタマイズできないパックの処理がどこまで終わってるかを調べるには,
Soot の "-v" オプションを使って,
Applying phase フェイズ名 to クラス名:メソッド名.
という形式の
メッセージを出力させます.
例: Applying phase jb.a to <sun.nio.ch.NativeObject: void putInt(int,int)>.
このメッセージは G.v().out に設定されている PrintStream に出力されますが, 放っておくと大量に出てくるので, 次のようにして,ログメッセージをフィルタする作戦が選べます.
// Soot.main の呼び出し前に,出力先ストリームを自作クラスに差し替える G.v().out = new ProgressObserver(); Soot.main(args); // 自作クラスでは,println の中で進行状況を獲得する class ProgressObserver extends PrintStream { public ProgressObserver() { super(System.out); // 文字列は標準出力に出すことにする } public void println(String s) { if (s.startsWith("Applying phase ")) { // 処理中のクラス名,メソッド名を得て覚えておくとか // ここでフェイズ適用を認識 } // データ自体を System.out へ素通しするなら, // 次の呼び出しを行う.この呼び出しをしなければ, // 出力がなかったことになる. super.println(s); } }
Soot が使用する中間表現である Jimple などは, Java バイトコードの仕様に依存している部分があります. ある程度 Java バイトコードと仮想マシンの振る舞いについて 知らないと,どうにもならないことがありますので, いくつか,情報源を列挙しておきます.
Java 仮想マシンの構造が解説されています.
バイトコード編は,個々のバイトコードの振る舞いについて, スタック操作の絵付きでかなり丁寧に説明されています.
バイトコードの仕様です.Jimple を使う場合,それほど参照する必要はないとは思いますが,いちおう挙げておきます.
あまり重要ではないですが,ついでに挙げておきます.