IL ILを手書きで出力してみる
あいさつ
どうも、はかせです。
IL、ひいては機械語眺めてると
どういう風にデータが動いていくのかが
見えるので見てるだけで楽しいですね。
永遠と自分が過去に作ったexeとかのILを眺めてるのもいいんですが、
そろそろ自分でもILを書いてみたいと思います。
ILを書く方法
IL手書きと言っても所詮はC#です。
ちゃんとそれようのメソッドとかが用意されています。
(バイトコード直打ちとかいう頭おかしいことしなくていい)
色々やり方はあるみたいですが、
今回はAssemblyBuilderというものを使ってみます。
AssemblyBuilderを使う
まず必要なusingを上げておきます。
using System.Reflection.Emit; using System.Reflection;
では実際に使ってみます。
流れとしては各種必要なものを定義し、
ILツッコんで、
Create○○を呼び出して実体化するって感じです。
static void CreateDLL() { const string ModuleName = "Hogehoge"; //動的アセンブラを定義する var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(ModuleName), AssemblyBuilderAccess.RunAndSave); //動的モジュールを定義する //DefineDynamicAssembly→DefineDynamicModuleの流れはお決まりの流れらしい var moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName, ModuleName + ".dll"); //Hoge型を定義 var typeBuilder = moduleBuilder.DefineType("Hoge", TypeAttributes.Public); //Hoge型からSumインスタンスメソッドを定義 //第一引数にメソッド名を、第二引数にはアクセス修飾子、第三引数は戻り値、第四引数はメソッドの引数 var sum = typeBuilder.DefineMethod("Sum", MethodAttributes.Public, typeof(int), new Type[] { typeof(int), typeof(int) }); //メソッドの中身をEmit var il = sum.GetILGenerator(); //インスタンスメソッドの場合arg0がthisだから、引数はarg1以降に格納されている il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Add); //締めはretで il.Emit(OpCodes.Ret); //CreateTypeで型を作る var hogeType = typeBuilder.CreateType(); //クラスインスタンスを生成する var instance = Activator.CreateInstance(hogeType); //さっき取得したインスタンスのSumメソッドを呼び出し、結果を格納 var result = hogeType.GetMethod("Sum").Invoke(instance, new object[] { 10, 20 }); Console.WriteLine(result); #if DEBUG try { assemblyBuilder.Save(ModuleName + ".dll"); Console.WriteLine("DllSaveSuccuss"); } catch (Exception e) { Console.WriteLine("SaveFaild" + e); } #endif }
実行後exeと同階層にHogehoge.dllというファイルが出来たら成功です。
では中身を追っていきましょう。
//動的アセンブラを定義する var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(ModuleName), AssemblyBuilderAccess.RunAndSave); //動的モジュールを定義する //DefineDynamicAssembly→DefineDynamicModuleの流れはお決まりの流れらしい var moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName, ModuleName + ".dll");
最初にアセンブラを定義して次にその中のモジュールを定義してます。
ここはおまじないレベルでお決まりの流れみたいです。
//Hoge型を定義 var typeBuilder = moduleBuilder.DefineType("Hoge", TypeAttributes.Public); //Hoge型からSumインスタンスメソッドを定義 //第一引数にメソッド名を、第二引数にはアクセス修飾子、第三引数は戻り値、第四引数はメソッドの引数 var sum = typeBuilder.DefineMethod("Sum", MethodAttributes.Public, typeof(int), new Type[] { typeof(int), typeof(int) }); //メソッドの中身をEmit var il = sum.GetILGenerator(); //インスタンスメソッドの場合arg0がthisだから、引数はarg1以降に格納されている il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Ldarg_2); il.Emit(OpCodes.Add); //締めはretで il.Emit(OpCodes.Ret);
モジュールの中にクラスを定義し、
クラスの中にメソッドを定義してます。
メソッドの中身はGetILGeneratorで取得したILGeneratorに
ILを手で打ち込んでいきます。
私はLINQPadに処理をC#で書いてそれをILに変換、
出てきたILをそのままコピペしました。
習熟度が上がればそんなことせず
ちゃかちゃか組むのかもしれませんが、
初心者の私にはそんな芸当は到底無理です。
//CreateTypeで型を作る var hogeType= typeBuilder.CreateType(); //クラスインスタンスを生成する var instance = Activator.CreateInstance(hogeType); //さっき取得したインスタンスのSumメソッドを呼び出し、結果を格納 var result = hogeType.GetMethod("Sum").Invoke(instance, new object[] { 10, 20 }); Console.WriteLine(result); #if DEBUG try { assemblyBuilder.Save(ModuleName + ".dll"); Console.WriteLine("DllSaveSuccuss"); } catch (Exception e) { Console.WriteLine("SaveFaild" + e); } #endif
一通り定義が終わったらCreateメソッドで
それぞれ実体化していきます。
実体化が終わった後でSaveメソッドを呼ぶことで
実際にdllが出てきます。
ファイルIOなんで一応try-catchしています。
exeを吐かせてみる
さて上記のコードでdllを吐かせました。
AssemblyBuilderはdllだけでなくexeも吐かせることができるそう。
さっそくやってみます。
static void CreateExe() { const string ModuleName = "Hogehoge"; var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(ModuleName), AssemblyBuilderAccess.RunAndSave); var moduleBuilder = assemblyBuilder.DefineDynamicModule(ModuleName, ModuleName + ".exe"); //第二引数はクラスの定義 //TypeAttributes.Public = アクセス修飾子はPublic //TypeAttributes.Class = クラスである var hogeBuilder = moduleBuilder.DefineType("Hoge",TypeAttributes.Class | TypeAttributes.Public); //Hoge型からMainインスタンスメソッドを定義 //第一引数にメソッド名を、第二引数にはアクセス修飾子、第三引数は呼び出し規則、第四引数は戻り値、第五引数はメソッドの引数 //MethodAttributes.HideBySig = 名前でのみ探せるようにする //MethodAttributes.Public = Publicメソッド //MethodAttributes.Static = Staticメソッド //CallingConventions.Standard = Staticメソッドにはつけるらしい var fuga = hogeBuilder.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(void), new Type[] { typeof(string[]) }); //IL手書き var il = fuga.GetILGenerator(); il.Emit(OpCodes.Ldstr, "Hello World"); il.Emit(OpCodes.Callvirt, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Callvirt, typeof(Console).GetMethod("ReadKey", new Type[] { typeof(bool) })); il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ret); var hogeType = hogeBuilder.CreateType(); //プログラムのエントリーポイントを指定 //PEFileKinds.ConsoleApplication = コンソールアプリ assemblyBuilder.SetEntryPoint(fuga, PEFileKinds.ConsoleApplication); #if DEBUG System.IO.File.Delete(ModuleName + ".exe"); try { //PortableExecutableKinds.Required32Bit = 32bitプロセスである //ImageFileMachine.I386 = 32bitインテルプロセッサが対象 assemblyBuilder.Save(ModuleName + ".exe", PortableExecutableKinds.Required32Bit, ImageFileMachine.I386); Console.WriteLine("ExeSaveSuccuss"); } catch(Exception e) { Console.WriteLine("SaveFaild" + e); } #endif Console.ReadLine(); }
基本はdllと同じで
定義→IL手書き→実体化→保存です。
さっきのdllと違うのはエントリーポイントを指定してるとこぐらいですかね。
//プログラムのエントリーポイントを指定 //PEFileKinds.ConsoleApplication = コンソールアプリ assemblyBuilder.SetEntryPoint(fuga, PEFileKinds.ConsoleApplication);
exeなのでプログラムがどこから始まるか指定しなくてはいけません。
あとエントリーポイントはstaticを強要されます。
(エントリーポイントのメソッドがいくつもあったら困りますもんね)
あとさっきのdll吐くときと比べて、
こっちではメソッドやクラスの詳細を結構細かく指定しています。
理由は吐いたexeがうまく動かなくて色々試したからですね。
吐いたexeの実行ができない・・・・
— hakase@ちょっと変わり種のプログラマー (@hakase70945250) 2019年9月12日
(軽く絶望臭がしますねw)
ちなみにこのコードで吐いたexeは動きません。
多分System.MissingMethodException吐かれます。
このエラーは簡単言えば「そんなメソッドねーよ」ってエラーです。
うまくILが組めなかったんかな・・・?
そう思い神ツールDnSpyで見てみました。
普通にC#できとるやんけ・・・
ただステップ実行できなかったんで
何かミスってるんだなってのはわかりました。
じゃあ何がミスってたのよって話ですね。
exeは吐けたのでAssemblyBuilderとかの
定義がミスってる説は薄いと思います。
となるとやはり手打ちしたILが何かミスってると。
とりあえずLINQPadでまたコード吐かせて
比較してみました。
そしたら違う部分がありました。
il.Emit(OpCodes.Callvirt, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })); il.Emit(OpCodes.Callvirt, typeof(Console).GetMethod("ReadKey", new Type[] { typeof(bool) }));
ここではCallvirtを使っているんですが、
LINQPadが吐いたコードはCallでした。
とりあえずここをCallに直してみます。
実行・・・
成功した!!
でもCallvirtをCallに直しただけなのになんで・・・?
とりあえず調べてみますか。
なになに・・・
遅延バインディングメソッドって
確か仮想メソッドみたいなもんだったっけ・・・?
(間違ってたら教えてください)
うーん・・・?
仮想メソッドの呼び出しじゃないとこで
Callvirt使ってたからエラーになってたってことなんですかね・・・?
実際本腰入れて組むってなったら
この辺意識せずに済むようなヘルパー作らなきゃあかんかもしれん・・・
(そいやneuecc神もそんなの作ってたような)
あとがき
今回はIL手書きでした。
dllは楽でしたが、exeは変にハマりましたね。
やはり低級言語の仲間だからか
わかりにくいところがありますね。
それはそれとして今回自前でIL吐いて
DnSpy使ったわけですけど、
確かにイテレーションスピード遅いですね。
一回一回閉じて開けてをしなきゃいけないのは
地味にだるい・・・・
それでは今回はこの辺でノシ