はかせのラボ

私の頭の中を書いていく雑記ブログです

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がうまく動かなくて色々試したからですね。


(軽く絶望臭がしますねw)

ちなみにこのコードで吐いたexeは動きません。
多分System.MissingMethodException吐かれます。
このエラーは簡単言えば「そんなメソッドねーよ」ってエラーです。

うまくILが組めなかったんかな・・・?
そう思い神ツールDnSpyで見てみました。
f:id:hakase0274:20190912223739p:plain
普通に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に直してみます。

実行・・・
f:id:hakase0274:20190912224402p:plain
成功した!!

でもCallvirtをCallに直しただけなのになんで・・・?
とりあえず調べてみますか。
なになに・・・
f:id:hakase0274:20190912224636p:plain
f:id:hakase0274:20190912224645p:plain
遅延バインディングメソッドって
確か仮想メソッドみたいなもんだったっけ・・・?
(間違ってたら教えてください)

うーん・・・?
仮想メソッドの呼び出しじゃないとこで
Callvirt使ってたからエラーになってたってことなんですかね・・・?

実際本腰入れて組むってなったら
この辺意識せずに済むようなヘルパー作らなきゃあかんかもしれん・・・
(そいやneuecc神もそんなの作ってたような)

あとがき

今回はIL手書きでした。
dllは楽でしたが、exeは変にハマりましたね。
やはり低級言語の仲間だからか
わかりにくいところがありますね。

それはそれとして今回自前でIL吐いて
DnSpy使ったわけですけど、
確かにイテレーションスピード遅いですね。

一回一回閉じて開けてをしなきゃいけないのは
地味にだるい・・・・

それでは今回はこの辺でノシ