前言:
在上一篇中,我們介紹了 ILGenerator 輔助方法。
本篇,將詳細介紹指令方法,並詳細介紹指令的相關用法。
在接下來的教程,關於IL指令部分,會將指令分為以下幾個分類進行講解:
1、引數載入指令:ld 開頭的指令,單詞為:load argument 2、引數儲存指令:st 開頭的指令,單詞為:store 3、建立例項指令: new 開頭的指令。 4、方法呼叫指令:call 開頭的指令。 5、分支條件指令:br 開頭的指令,單詞為 break 6、型別轉換指令:cast 或 conv 開頭的指令,單詞為:convert 7、運算操作指令:add/sub/mul/div/rem ,加減乘除取餘。 8、其它指令
下面開始介紹第一部分,引數載入指令:
引數載入指令:
引數載入指令用於在方法中載入引數到運算元棧中,為後續的操作做準備。
當涉及 CIL(Common Intermediate Language)指令時,以 "ld" 開頭的指令通常用於載入資料到運算元棧中。
以下是一些常見的以 "ld" 開頭的引數載入指令及其簡要說明:
-
ldarg: 將指定索引位置的引數載入到運算元棧中。用於將方法的引數載入到運算元棧中,以便在方法中進行操作或傳遞給其他方法。
-
ldarga: 將指定索引位置的引數的地址載入到運算元棧中。通常用於獲取引數的地址,以便在方法中對引數進行引用傳遞。
-
ldc_X: 將常量載入到運算元棧中。其中 X 可以是 I(整數)、I4(32 位整數)、I8(64 位整數)、R4(單精度浮點數)、R8(雙精度浮點數)、I4_M1(-1 的特殊表示)、I4_0、I4_1、I4_2、I4_3、I4_4、I4_5(特殊整數常量)等。
-
ldloc: 將指定索引位置的本地變數載入到運算元棧中。用於將方法內部的區域性變數載入到運算元棧中,以進行後續的操作或傳遞給其他方法。
-
ldloca: 將指定索引位置的本地變數的地址載入到運算元棧中。通常用於獲取區域性變數的地址,以便在方法中對區域性變數進行引用傳遞。
-
ldfld: 將物件的欄位值載入到運算元棧中。用於載入物件的欄位值,以便在方法中進行操作或傳遞給其他方法。
- 其它:。
這些指令提供了豐富的功能,使得在方法內部能夠方便地處理引數、常量、本地變數和物件欄位值。透過合理使用這些指令,可以實現對資料的靈活操作和處理。
引數載入指令:短格式和長格式
在 IL(Intermediate Language)中,有一些指令支援短格式和長格式的表示(以 “_S” 結尾代表 short 短指令,預設對應無 _S結尾的即代表長格式指令)。
短格式指令通常用於跳轉到相對較近的位置,而長格式指令則可以用於跳轉到較遠的位置。
在實際應用中,由於 IL 的靈活性和可擴充套件性,編譯器會根據需要自動選擇合適的指令格式。
這樣可以根據具體的跳轉距離來選擇最有效的指令格式,從而使生成的程式碼更加高效。
總的來說,短格式和長格式的區別在於其編碼的範圍,短格式指令編碼的範圍較小,適用於相對較近的跳轉位置,而長格式指令則可以覆蓋更大的跳轉範圍。
這種設計可以使得 IL 程式碼在執行時更加高效。
1、從方法傳參中載入:ldarg 載入引數值
ldarg: 將指定索引位置的引數載入到運算元棧中,該引數為 Load Argument(載入引數)的簡寫。
該引數的作用:是從指定的方法傳參中獲得引數值,並將該值載入到運算元棧中。
OpCodes一共提供了5個相關的指令:
Ldarg_0:0索引引數。
Ldarg_1:1索引引數
Ldarg_2:2索引引數
Ldarg_3:3索引引數
Ldarg:使用自定義索引
示例程式碼:
ILGenerator il = methodBuilder.GetILGenerator(); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg,2);//這裡使用自定義索引 il.Emit(OpCodes.Add); il.Emit(OpCodes.Ret);
對應生成程式碼:
在本段示例程式碼中:
Ldarg_0:代表 this 當前物件。 Ldarg_1:程式碼引數a Ldarg_2:程式碼引數b Ldarg_3:無。
需要注意的是:
示例中定義的是例項方法,因此 Ldarg_0 代表 this 物件,而透過 DynamicMethod 建立的方法,預設則是靜態方法,Ldarg_0 則會代表引數a。
2、從方法傳參中載入:ldarga 載入引數引用地址
ldarga: 將指定索引位置的引數的地址載入到運算元棧中,該引數為 Load Argument Address(載入引數地址)的簡寫。
該引數的作用:是從指定的方法傳參中獲得引數的地址,並將該地址載入到運算元棧中。
OpCodes一共提供了2個相關的指令:
Ldarga:需要指定索引值。
Ldarga_S:需要指定索引值
需要注意的是,在 C# 中除了操作 ref 或 out 會涉及到引用地址,其它操作操作引用地址(操作指標)都需要在unsafe方法下。
否則強行操作,會出現以下異常,例如:
也可能是以下異常:
3、數字型別值載入:ldc_X 載入引數值
在 Common Intermediate Language(CIL)中,以 "ldc" 開頭的操作碼指令主要用於將常量載入到運算元棧中。
這些指令在.NET平臺的程式集中起著重要的作用,用於處理常量資料的載入和操作。
以下是幾種以 "ldc" 開頭的常見操作碼指令及其分類和用途:
1. ldc_i4 / ldc_i4_s
- 分類: 整數常量載入指令
- 用途: 將 32 位整數常量載入到運算元棧中。
ldc_i4
指令用於載入常量值介於 -2^31 到 2^31-1 之間的整數,而ldc_i4_s
則用於載入介於 -128 到 127 之間的整數常量。
2. ldc_i8
- 分類: 長整數常量載入指令
- 用途: 將 64 位長整數常量載入到運算元棧中。適用於載入大於 Int32 範圍的整數常量。
3. ldc_r4 / ldc_r8
- 分類: 浮點數常量載入指令
- 用途: 將單精度浮點數(
float
)或雙精度浮點數(double
)常量載入到運算元棧中。ldc.r4
用於載入單精度浮點數常量,而ldc.r8
用於載入雙精度浮點數常量。
4. ldc_i4_m1 / ldc_i4_0 / ldc_i4_1 / ldc_i4_2 / ....../idc_i4_8
- 分類: 特殊整數常量載入指令
- 用途: 分別用於載入特定的整數常量值,例如 -1、0、1、2、......、6、7、8 等。
透過合理使用這些以 "ldc" 開頭的操作碼指令,開發人員可以方便地載入各種型別的常量資料到運算元棧中,為程式的執行和計算提供必要的資料支援。
下面來一個簡單的示例:
ILGenerator il = dynamicMethod.GetILGenerator(); il.Emit(OpCodes.Ldc_I4, 9999); il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) })); il.Emit(OpCodes.Ret);
執行後輸出:
4、區域性變數載入:ldloc 參加變數值
ldloc: 將指定索引位置的本地變數載入到運算元棧中。用於將方法內部的區域性變數載入到運算元棧中,以進行後續的操作或傳遞給其他方法。
該引數為:load local 載入本地變數的簡寫。
該方法需要配合輔助變數使用,這個在上一篇輔助方法中有介紹到,這裡重溫一下上一篇的輔助方法,定義變數的內容:
5、區域性變數載入:ldloca 引數變數值引用地址
ldloca: 將指定索引位置的本地變數的地址載入到運算元棧中。通常用於獲取區域性變數的地址,以便在方法中對區域性變數進行引用傳遞。
該引數為:load local address 載入本地(變數、引用)地址的簡寫。
在C#中,操作引用地址,只能是 ref 或 out 兩種方式,而操作指標(也是引用地址)則需要在unsafe 方法下才可以。
下面演示一個透過 ldloca 引數返回 out 引數的示例:
Dictionary<string, string> dic = new Dictionary<string, string>(); dic.Add("a", "aaa"); var dicType = typeof(Dictionary<string, string>); MethodInfo getValue = dicType.GetMethod("TryGetValue"); var method = new DynamicMethod("GetValue", typeof(object), new[] { typeof(Dictionary<string, string>), typeof(string) }, typeof(Dictionary<string, string>));// var il = method.GetILGenerator(); var outText = il.DeclareLocal(typeof(string)); il.Emit(OpCodes.Ldarg_0); // Load the dic object onto the stack il.Emit(OpCodes.Ldarg_1);//設定欄位名。 il.Emit(OpCodes.Ldloca_S, outText);// 使用地址變數來接收: out 值 il.Emit(OpCodes.Callvirt, getValue);//bool a=dic.tryGetValue(...,out value) il.Emit(OpCodes.Pop);//不需要執行的bool返回值 il.Emit(OpCodes.Ldloc_0);//載入 out 變數的值。 il.Emit(OpCodes.Ret); // Return the value var func = (Func<Dictionary<string, string>, string, object>)method.CreateDelegate(typeof(Func<Dictionary<string, string>, string, object>)); object result = func(dic, "a"); Console.WriteLine(result);
執行結果:
說明:
對於 out 引數,載入的是地址引數,用 loca 地址指令,返回的是值,用 ldloc 值指令。
6、對像成員變數值載入:ldfld 參載入物件成員變數值
ldfld: 將物件的欄位值載入到運算元棧中。用於載入物件的欄位值,以便在方法中進行操作或傳遞給其他方法,該引數為:load filed 載入欄位的簡寫。
ldsfld:該指令用於操作靜態成員變數,該引數為:load static filed 載入靜態欄位的簡寫。
下面給出一個示例,讀取例項類員變數的值:
private static void DMethod2() { MyEntity myEntity = new MyEntity() { ID = 111, Name = "hello" }; FieldInfo idInfo = typeof(MyEntity).GetField("ID"); var method = new DynamicMethod("GetterFunc", typeof(object), new[] { typeof(object) }, typeof(MyEntity)); var il = method.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); // Load the input object onto the stack il.Emit(OpCodes.Ldfld, idInfo); // 載入 Id 成員變數的值到堆疊 if (idInfo.FieldType.IsValueType) { il.Emit(OpCodes.Box, idInfo.FieldType); // Box the value type } il.Emit(OpCodes.Ret); // Return the value var func = (Func<object, object>)method.CreateDelegate(typeof(Func<object, object>)); object result = func(myEntity); Console.WriteLine(result); } class MyEntity { public int ID; public string Name; }
執行結果:
注意事項:
1、在定義 DynamicMethod 方法時,最好手動指定 Owner 歸屬,即該動態方法歸屬哪個型別,否則在執行時可能會報以下錯誤:
2、在操作例項成員變數(取值或賦值)時,通常需要載入兩個引數,第一個是物件值,第二個是物件的成員變數值。
7、其它常用型值載入:ldstr、ldnull、ldtoken
ldstr:將常量字串值載入到運算元棧中,示例如下:
ldnull:將null值載入到運算元棧中,示例如下:
ldtoken:通常適用於將執行時狀態型別載入到運算元棧中,如將 Type 型別值壓到棧中。
生成的示例程式碼:
總結:
本篇教程深入探討了 ILGenerator 中的引數載入指令,透過詳細解釋Ldarg、Ldarga、Ldloc和Ldloca 等各種指令的使用,
讀者能夠清晰地認識到Ld指令用於載入引數或本地變數到堆疊,而St指令用於將值從堆疊儲存到引數或本地變數中。
這些指令為動態方法的生成提供了基礎,幫助開發者更好地掌握IL程式碼的生成和除錯。
下一篇,將繼續介紹儲存指令部分。