在用 System.Reflection.Emit 動態生成構造函數時,遇到一個經典的 AccessViolationException:new Random(DateTime.Now.Millisecond) 看似正常,運行卻崩潰。經對比 Reflector 反編譯的標準 IL,發現缺少兩條關鍵指令——這正是值類型與引用類型在評估棧上的根本差異。
// 目標 C# 代碼
private Random _rand = new Random(DateTime.Now.Millisecond);
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed
{
.maxstack 3
.locals init ([0] valuetype [mscorlib]System.DateTime CS$0$0000)
L_0000: ldarg.0
L_0001: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
L_0006: stloc.0 // 存入局部變量
L_0007: ldloca.s CS$0$0000 // 關鍵:取局部變量**地址**
L_0009: call instance int32 [mscorlib]System.DateTime::get_Millisecond()
L_000e: newobj instance void [mscorlib]System.Random::.ctor(int32)
L_0013: stfld class [mscorlib]System.Random EmitExamples.Animal::rand
L_0018: ldarg.0
L_0019: call instance void [mscorlib]System.Object::.ctor()
L_001e: nop
L_001f: ret
}
L_0001: call valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
L_0006: call instance int32 [mscorlib]System.DateTime::get_Millisecond() // 直接在棧上調用,錯誤!
L_000e: newobj instance void [mscorlib]System.Random::.ctor(int32)
| 特性 | 值類型 | 引用類型 |
|---|---|---|
| 棧上佔用 | 直接包含數據 | 存儲引用指針 |
| 調用實例方法 | 需 ldloca 取地址傳 this | ldloc 直接傳引用 |
| 典型指令對 | stloc / ldloca | stloc / ldloc |
DateTime 是結構體。call get_Now() 返回值類型實例壓棧,若直接 call get_Millisecond(),CLR 會嘗試從評估棧頂讀取 this 指針,實際卻是結構體內容,導致內存訪問越界。
var nowLocal = il.DeclareLocal(typeof(DateTime));
il.Emit(OpCodes.Call, typeof(DateTime).GetProperty("Now").GetGetMethod());
il.Emit(OpCodes.Stloc, nowLocal); // 存局部變量
il.Emit(OpCodes.Ldloca, nowLocal); // 取地址
il.Emit(OpCodes.Call, typeof(DateTime).GetProperty("Millisecond").GetGetMethod());
il.Emit(OpCodes.Newobj, typeof(Random).GetConstructor(new[] { typeof(int) }));
struct (含 DateTime, Guid, Decimal, 自定義結構) 調用實例成員前必須 ldloca。il.DeclareLocal 顯式聲明局部變量,避免匿名佔位符混淆。PEVerify.exe 驗證,再運行測試。這個細節折騰了整個下午,記錄下來與大家共勉。若有更深層次的 CLR 實現見解,歡迎留言補充。