bytekinの動作
このドキュメントでは、bytekinの内部メカニズムとバイトコードをどのように変換するかを説明します。
変換プロセス
ステップ1: 初期化
フッククラスを定義
↓
BytekinTransformer.Builderを作成
↓
フッククラスをBuilderに渡す
例:
BytekinTransformer transformer = new BytekinTransformer.Builder(
CalculatorHooks.class,
StringHooks.class
).build();
ステップ2: 分析
build()
が呼び出されると、bytekinは:
- フッククラスのアノテーションをスキャン
- 変換ルールを抽出
- メソッドシグネチャを検証
- 変換戦略を準備
- BytekinClassTransformerインスタンスを作成
Builder.build()
↓
@ModifyClassアノテーションをスキャン
↓
@Inject、@Invokeなどを抽出
↓
トランスフォーマーマップを作成
↓
BytekinTransformerを返す
ステップ3: 変換
transform()
が呼び出されると:
byte[] transformed = transformer.transform("com.example.Calculator", bytecode);
bytekinは:
- ターゲットクラスを検索
- 一致するトランスフォーマーを見つける
- ASMを使用してバイトコードを解析
- 登録されたすべての変換を適用
- 変更されたバイトコードを返す
ターゲットバイトコード
↓
ASM ClassReader
↓
BytekinClassVisitor
↓
インジェクションを適用
↓
インボケーションを適用
↓
リダイレクトを適用
↓
定数の変更を適用
↓
変数の変更を適用
↓
ASM ClassWriter
↓
変更されたバイトコード
アーキテクチャ概要
コアコンポーネント
┌─────────────────────────────────────┐
│ BytekinTransformer (メインAPI) │
└──────────────┬──────────────────────┘
│
┌──────┴──────┐
↓ ↓
Builder transform()
│ │
└──────┬──────┘
↓
┌───────────────────────────┐
│ BytekinClassTransformer │
└───────────┬───────────────┘
↓
┌───────────────────────────┐
│ BytekinClassVisitor │
│ (ASM ClassVisitor) │
└───────────┬───────────────┘
↓
┌───────────────────────────┐
│ BytekinMethodVisitor │
│ (ASM MethodVisitor) │
└───────────────────────────┘
ビジターパターン
bytekinはビジターパターン(ASMから)を使用します:
┌─ ClassVisitor
│ └─ Method Visitor
│ └─ Code Visitor
│ └─ Instruction Handlers
ASMがバイトコードを解析する際、各要素(クラス、メソッド、フィールド、命令など)についてビジターのメソッドを呼び出して通知します。
変換タイプ
1. インジェクション(コード挿入)
目標: メソッドの特定箇所にコードを挿入
メソッドバイトコード
↓
インジェクションポイントを見つける
↓
フックメソッドへの呼び出しを挿入
↓
元のコードを続行
例の場所:
At.HEAD
: メソッド本体の前At.RETURN
: return文の前At.TAIL
: メソッドの終わり
2. インボケーション(メソッド呼び出しのインターセプト)
目標: メソッド内のメソッド呼び出しをインターセプト
メソッドバイトコード
↓
ターゲットメソッド呼び出しを見つける
↓
同じ引数でフックメソッドを呼び出す
↓
必要に応じて引数を変更
↓
メソッド呼び出しを行う(またはスキップ)
3. リダイレクト(呼び出し先の変更)
目標: どのメソッドが呼び出されるかを変更
A()へのメソッド呼び出し
↓
呼び出しをインターセプト
↓
B()にリダイレクト
4. 定数の変更
目標: 定数値を変更
定数Xをロード
↓
定数Yに置き換え
5. 変数の変更
目標: ローカル変数の値を変更
インデックスNのローカル変数
↓
スロットからロード
↓
変更を適用
↓
戻して格納
主要なデータ構造
Injection
コードのインジェクションを表します:
- ターゲットメソッド: どのメソッドにインジェクトするか
- フックメソッド: どのフックメソッドを呼び出すか
- 場所: どこにインジェクトするか(HEAD、RETURNなど)
- パラメータ: どのパラメータを渡すか
Invocation
メソッド呼び出しのインターセプトを表します:
- ターゲットメソッド: どのメソッドがターゲットを呼び出すか
- ターゲット呼び出し: どの呼び出しをインターセプトするか
- フックメソッド: どのフックを呼び出すか
- シフト: 呼び出しの前か後か
CallbackInfo
インジェクションの動作を制御します:
public class CallbackInfo {
public boolean cancelled; // 実行をキャンセル?
public Object returnValue; // カスタムリターン?
public Object[] modifyArgs; // 変更された引数?
}
マッピングシステム
bytekinは難読化されたコードのクラス/メソッド名マッピングをサポートしています:
元の名前 マップされた名前
↓ ↓
a.class ────→ com.example.Calculator
b(II)I ────→ add(II)I
マッピングは変換中に適用されます:
フッククラスが"com.example.Calculator"を参照
↓
マッピングを適用
↓
バイトコード内のマップされた名前を探す
↓
それに応じて変換
スレッドセーフティ
- BytekinTransformer:
build()
後はスレッドセーフ - Builder: 設定中はスレッドセーフではない
- transform(): 複数のスレッドから同時に呼び出し可能
パフォーマンスの考慮事項
効率
- 1回限りのコスト: トランスフォーマーのビルド
- 変換時間: バイトコードサイズに比例
- ランタイムオーバーヘッド: インジェクトされたコードのみが実行される
最適化のヒント
- トランスフォーマーを1回ビルドして再利用
- クラスロードの早い段階で変換を使用
- フックメソッドの複雑さを最小限に抑える
- ボトルネックを特定するためにプロファイル