あやみつさんの覚書き

メモや覚書きなど

asmであるクラスのインスタンスを生成する際の処理を置き換える

忘れないうちに書く感じで書くので少し粗いかもしれないですがご了承ください。

今回はあるメソッドの内部のあるクラスのインスタンスを生成する処理を、別のクラスのインスタンスを生成する処理を書きます。

関係したクラス

  • ClassNode
  • ClassReader
  • ClassWriter
  • MethodNode
  • AbstractInsnNode
  • MethodInsnNode
  • TypeInsnNode
  • Opecodes(なくてもいいけどあったほうがいい)

まずは前の記事のようにbyte配列を与えられた際にClassNodeを

public byte[] transform(byte[] arrayOfByte) {
	ClassNode cNode = new ClassNode();
	ClassReader cReader = new ClassReader(arrayOfByte);
	cReader.accept(cNode, 0);// 例によってなんで0かはおいといて
	
	// ここから目当てのMethodNodeを取得します
	// 場合によって目当てのMethodが違うので省きます
	MethodNode targetMNode = null;
	
	// MethodNodeの入ったListをforでまわして、目当てのMethodNodeがみつかればbreak
	for (MethodNode mNode : (List<MethodNode>)cNode.methods) {
		
		// 適当な条件
		if (isTargetMethodNode(mNode)) {
			targetMNode = mNode;
			break;
		}
	}

これでまず目当てのメソッドのMethodNodeを検出します。
次に、そのMethodNodeの内部の処理から目当てのものを検出します。
例えば

Foo foo = new Foo();

のような処理を、Fooを継承したBarというクラスのインスタンスを生成する処理にする場合の

Foo foo = new Bar();

のようにするとします。

検出する方法は、もちろんそれぞれの場合によりますのでご注意を。
今回は「Fooのクラスのインスタンス生成の処理」「Barのクラスのインスタンス生成の処理」に置き換えます。

まず検出する処理を書きましょう。
if文の部分は改行してみやすくしてるつもりです

// nullの確認はしときましょう
if (targetMNode != null) {
	// new を含むTypeInsnNodeと
	// コンストラクタの呼び出しを含むMethodInsnNodeを検出します
	
	// new にあたる部分とコンストラクタの呼び出しに改変を加えます
	// new の部分でもどのコンストラクタを呼ぶかの指定も一部もってるようです
	// それでだいぶつまづきました(java.lang.VerifyError)
	for (AbstractInsnNode aiNode : targetMNode.instructions.toArray()) {
		
		// new する部分の置き換え
		if (aiNode instanceof TypeInsnNode) {
			TypeInsnNode tiNode = (TypeInsnNode)aiNode;
			
			// new するクラスの名前(フルパス)と
			// new をあらわすopcodeで判定します
			// NEW はOpcodesというInterfaceを実装すると参照できます
			if (	   tiNode.desc.equals("Foo")
					&& tiNode.getOpcode() == NEW) {
				tiNode.desc = "Bar";// 置き換え
			}
		}
		
		// コンストラクタ呼び出し部分に改変を加えます
		// コンストラクタはメソッドの1つとして認識されます
		if (aiNode instanceof MethodInsnNode) {
			MethodInsnNode miNode = (MethodInsnNode)aiNode;
			
			// コンストラクタのメソッド名にあたる部分は<init>になります
			// ownerは呼び出しするクラス名(フルパス)でしょうね、たぶん
			// descは引数がなければ()、戻り値にあたる部分はVです。ちなみにVは他のメソッドではvoid型をあらわします
			// getOpcodeでopcodeを取得、INCOKESPECIALはコンストラクタの呼び出しっぽいです
			if (	   miNode.name.equals("<init>")
					&& miNode.owner.equals("Foo")
					&& miNode.desc.equals("()V")
					&& miNode.getOpcode() == INVOKESPECIAL) {
				miNode.owner = "Bar";// 置き換え
			}
		}
	}
}

これで改変を施した状態になりました。
あとはClassWriterでbyte配列にもどしてかえしてあげましょう。

ClassWriter cWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
	cNode.accept(cWriter);
	arrayOfByte = cWriter.toByteArray();
	return arrayOfByte;
}

お疲れ様です、これで終了です。
私はこれのTypeInsnNodeの部分を見落としてjava.lang.VerifyErrorを何度もだしてしにそうでした。

tips:
MethodInsnNodeなどのdescやsignatureを出力してみるとでてくる文字列について

例: (Ljava/lang/String;)V
訳: void型のStringを引数にもつメソッド

こんな感じのを少しでも判別できたらいいかもですね。
Vはvoid、byteはB、intはI、booleanはZなど、他にもありますが調べたらでてきます。
今度またまとめて書いときますか・・・