Mono.Cecilでは ".s" 命令にご用心
これはC# Advent Calendar 2023の20日目の記事です。
この記事が言っていること
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.s
はif
の条件が偽の場合にelse
節にジャンプするための命令ですが、ジャンプ先のオフセットがIL_ffffffd8
になっています。もちろんそんなアドレスはありません。
原因
Mono.Cecilでは、ジャンプ系の命令におけるジャンプ先のオフセットを命令同士の参照関係として表現します。命令の追加によってオフセットがズレた場合でも、保存時には正しいオフセットに直してくれます。
blt.s
はblt
という命令の短縮型であり、blt
がint32
でジャンプ先のオフセットを指定するのに対し、blt.s
はint8
で指定します。オフセットは相対アドレスとなっており、今回は最大値を超えてオーバーフローしてしまったようです。本来は、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