StackFrame クラス (System.Diagnostics) を使用すると、呼び出し元メソッドを取得することができる。しかし、Release ビルドされたアセンブリでは JIT 最適化により呼び出し元メソッドがインライン化されている可能性がある。
例えば、次のコードの実行結果を Debug ビルドと Release ビルド (非デバッグ実行) とで比較すると一目瞭然だ。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
Debug ビルドでは Hoge と出力されるが、Release ビルド (非デバッグ実行) では Main と出力されることが確認できる。つまり、Release ビルド (非デバッグ実行) では JIT 最適化により Hoge メソッドがインライン展開されているわけである。
JIT 最適化によるインライン展開を抑止する方法としては、
MethodImpl 属性 を付加して
MethodImplOptions.NoInlining を指定するという方法が提供されている。しかし、この方法でインライン展開が抑止されるのは属性が付加されたメソッドのみである。そのため、呼び出し元のメソッド (上記の例なら Hoge メソッド) にこの属性を付加しなければならない。
また、この属性は絶対的なものではなく、64 bit CLR では無視されてしまうらしい。
また、インライン展開の抑止は、C# ・ IL レベルのメソッド呼び出しとコールスタックの一致を保証するわけではない。64 bit - CLR では、インライン展開以外に末尾最適化によっても、メソッド呼び出しとコールスタックの不一致が生じる場合があるのだが、NoInlining では末尾最適化は抑止されない。
デバッグ技 : .ini ファイルによる JIT コンパイラ制御
# このリンク先では ini ファイルを使用して
インライン展開を抑止する方法 コールスタックの不一致 を防ぐ方法が紹介されているが、これはあくまでもデバッグ目的の手段である。
では、呼び出し元メソッドのコールスタックを維持する方法は無いだろうか。
実はある。
IL レベルで reqsecobj キーワードが付加されているメソッドでは、呼び出し元メソッドのコールスタックが維持されるのである。 (reqsecobj キーワードが付加されているメソッド自身のコールスタックも維持される。)
C# コンパイラには、このキーワードをメソッドに付加する方法が用意されていて、メソッドに DynamicSecurityMethodAttribute クラス (System.Security) を属性として付加すれば良い。
このクラスは mscorlib.dll の internal クラスであり僕らは本来使用することができないのだが、同名のクラスを自前で用意して使用すれば C# コンパイラは reqsecobj キーワードを付加してくれる。
先ほどのコードに、DynamicSecurity 属性を付けて、再び Release ビルド (非デバッグ実行) で実行してみると、見事にコールスタックが維持されることが確認できる。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Security;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[DynamicSecurityMethod]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
namespace System.Security
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
internal sealed class DynamicSecurityMethodAttribute : Attribute
{
}
}
見ての通り、呼び出し元メソッドである Hoge メソッドには属性の付加が一切不要である。