🚀

Mono.Cecilでは ".s" 命令にご用心

2023/12/20に公開

これはC# Advent Calendar 2023の20日目の記事です。

https://qiita.com/advent-calendar/2023/csharplang


この記事が言っていること

Mono.Cecilで命令を挿入するときはSimplifyMacros()OptimizeMacros()を呼ぼう!

本編

Mono.Cecilでメソッドに命令を挿入することを考えてみます。

private void M(int a)
{
    if(a >= 0)
    {
        Console.WriteLine("positive");
        // ここに命令を挿入したい
    }
    else
    {
        Console.WriteLine("negative");
    }
    Console.WriteLine("end");
}

このメソッドのifブロックの中に適当な命令を挿入したいとして、例えばこういうふうに書いてみます。

private void ProcessMethod(MethodDefinition method)
{
    var instructions = method.Body.Instructions;
    var processor = method.Body.GetILProcessor();

    var firstCall = instructions.First(instr => instr.OpCode == OpCodes.Call);
    processor.InsertAfter(firstCall, Instruction.Create(OpCodes.Nop));
}

これは正しく動作します。

……なんですが、この書き換えが正しく行われない場合があります。挿入する命令の数を200個にしてみましょう。

private void ProcessMethod(MethodDefinition method)
{
    var instructions = method.Body.Instructions;
    var processor = method.Body.GetILProcessor();

    var firstCall = instructions.First(instr => instr.OpCode == OpCodes.Call);
    for (int i = 0; i < 200; i++)
    {
        processor.InsertAfter(firstCall, Instruction.Create(OpCodes.Nop));
    }
}

これでC.M()を実行してみると……

System.InvalidProgramException: Common Language Runtime detected an invalid program.

どうやら不正なILが出力されたようです。

ILを見てみる

.method public hidebysig static 
	void M (
		int32 a
	) cil managed 
{
	.maxstack 2

	IL_0000: ldarg.0
	IL_0001: ldc.i4.0
	IL_0002: blt.s IL_ffffffd8
	IL_0004: ldstr "positive"
	IL_0009: call void [System.Console]System.Console::WriteLine(string)
	IL_000e: nop
	IL_000f: nop
	IL_0010: nop
	IL_0011: nop
	IL_0012: nop
	IL_0013: nop
	(略)

IL_0002の行に注目します。

blt.sifの条件が偽の場合にelse節にジャンプするための命令ですが、ジャンプ先のオフセットがIL_ffffffd8になっています。もちろんそんなアドレスはありません。

原因

Mono.Cecilでは、ジャンプ系の命令におけるジャンプ先のオフセットを命令同士の参照関係として表現します。命令の追加によってオフセットがズレた場合でも、保存時には正しいオフセットに直してくれます。

blt.sbltという命令の短縮型であり、bltint32でジャンプ先のオフセットを指定するのに対し、blt.sint8で指定します。オフセットは相対アドレスとなっており、今回は最大値を超えてオーバーフローしてしまったようです。本来は、blt.sではなくbltを使用する必要がありますが、Mono.Cecilはそこまではカバーしてくれないのです。

対処法

Mono.Cecil.Rocks名前空間のSimplifyMacros()OptimizeMacros()を使用します。

SimplifyMacros()はメソッド内の全ての短縮型命令を普通の形式に置き換え、対してOptimizeMacros()はメソッド内の命令を可能なものに限り短縮型に置き換えます。

つまり、命令を編集する前にSimplifyMacros()を、命令の編集後にOptimizeMacros()を呼べばいいわけです。

    // 追加
    method.Body.SimplifyMacros();
    
    var instructions = method.Body.Instructions;
    var processor = method.Body.GetILProcessor();

    var firstCall = instructions.First(instr => instr.OpCode == OpCodes.Call);
    for (int i = 0; i < 200; i++)
    {
        processor.InsertAfter(firstCall, Instruction.Create(OpCodes.Nop));
    }
    
    // 追加
    method.Body.OptimizeMacros();

これで正しいILが出力されるようになりました。

この知見、Mono.Cecilを使う上でほぼ必須に近い気がするのですが、あまり言及されていない気がするので記事にしてみました。

Discussion