C#でインラインアセンブラをする

# 前書き

この記事は、2023のC#アドカレの12/9の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい！

https://qiita.com/advent-calendar/2023/present-calendar#qiita_award

# TL;DR;
```csharp
static unsafe long Add(long a, long b)
{
    var code = stackalloc byte[7]
    {
        0x48, 0x01, 0xD1, // add rcx, rdx
        0x48, 0x89, 0xC8, // mov rax, rcx
        0xC3, // ret
    };
    VirtualProtect(code, 7, PAGE.EXECUTE_READWRITE, out _);
    var add_func = (delegate* unmanaged<long, long, long>)code;
    return add_func(a, b);
}
```

# はじめに

C言語などにはインラインアセンブラという機能があります。
（確かMSVCでは64bitだと使えないけど）

インラインアセンブラは、高級言語のソースコード内にアセンブリを直接埋め込むことができます。

https://learn.microsoft.com/ja-jp/cpp/assembler/inline/asm?view=msvc-170#example

```c
int Add(int64_t a, int64_t b)
{
    __asm {
        add rcx, rdx
        mov rax, rcx
        ret
    }
}
```

これは、rcx（第1引数レジスタ）にrdx（第2引数レジスタ）を加算し、rax（戻り値レジスタ）にコピーするというアセンブリプログラムです。

C#には、直接インラインアセンブラの機能はありませんが、それっぽいことができたら素敵じゃないですか！？


# C#で無理やりインラインアセンブラ

アセンブリ…というか機械語をメモリに直接書き込んで、その領域を関数ポインタにキャストすることで、それっぽいことができます。

環境はx86_64上で動く、64bit Windowsということに限定します。

```csharp
Console.WriteLine($"10+59\t= {Add(10, 59)}");
Console.WriteLine($"12+(-3)\t= {Add(12, -3)}");

static unsafe long Add(long a, long b)
{
    var code = stackalloc byte[7]
    {
        0x48, 0x01, 0xD1, // add rcx, rdx
        0x48, 0x89, 0xC8, // mov rax, rcx
        0xC3, // ret
    };
    VirtualProtect(code, 7, PAGE.EXECUTE_READWRITE, out _);
    var add_func = (delegate* unmanaged<long, long, long>)code;
    return add_func(a, b);
}

[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern unsafe IntPtr VirtualProtect(byte* lpAddress, nuint dwSize, PAGE flNewProtect, out PAGE lpflOldProtect);

enum PAGE
{
    NOACCESS = 0x01,
    READONLY = 0x02, READWRITE = 0x04, WRITECOPY = 0x08, EXECUTE = 0x10,
    EXECUTE_READ = 0x20, EXECUTE_READWRITE = 0x40, EXECUTE_WRITECOPY = 0x80,
    GUARD = 0x100, NOCACHE = 0x200, WRITECOMBINE = 0x400,
}
```

https://sharplab.io/#v2:C4LgTgrgdgNAJiA1AHwAICYCMBYAUKgBgAJVMA6AJWmAEsBbAUzIEkpgGwB7ABwGUOAbjQDGDAM4BuPHlIBOABQASAESYCiAKyyAOsAC8RAN4BBOHHlqYRLQEoAvsptT8mBSszpE8gLQBmG7oGJmYW6FZ+9o7OMpgAbETQYgCGAGYMRAA2nFAA5kSm5lm5RElWRXkARjZ4hnhE9UQCSWBEwpxw6QZiwEnCANZJGVnCRBUAnuwA2gDsALp1DbW4DStEBAAeACwAHFYbBJh76wAih0QA9OclZkRgwutWYHDrC6trW7vv27JHAMKflyIdE4AluSQet3ur1WG1+visgLADGA0LszhWADUaGBgBBBgAFLjsYTAeRtDpWaZWfHGADiAFEyPSABr034AVQAKvSAPoUenGY4AdQozG5Vk4EGARB5TmhTRaSTMPJS0BGBnkHQyDBySXYACoElA6EkoEkcgw4AAecplbI5O25AB8NnJDHRDVQ02ucBVavkpVGctwdmkuEmxyGzDo3E4OPkyj6HCgDAyvnQZDgQ2UVn4wAAMkluvSwFwWgZgJAGFZ6etesBeNxUxkaMUK1WbPMXPEGOt2GAoEbkmkiKxgPjK0QsTi8RlCZxiaTxgbMtwCkixGIrFAIK3pXAAO68GgAL2rRBpDKIKQyADkGAf54uJVKL3T6aubwB5DJwJ8MElgzwBgdzoN8GRqaFby/Yxfl+eleF4IgDH2Q5oX5QUv1vfMAE1kPeAgwiIDDhVFbl8P2TYrBFMU2S/fE8JQ9YCE+Fk2S5D8mMsaE2I5bk+QFY4KPWdACBrVk+N5EiaPIpjNjEoheI4nkZLohjhO2MToVpdljAoISuIIBToN+WCAAlOPeUSFNU34vwAWQAIWYW9LI2eStJDPAgA===

```:output
10+59	= 69
12+(-3)	= 9
```

はい、ちゃんと計算されていますね！

# アセンブリ

C言語のインラインアセンブラの例と同じく、第1引数と第2引数を加算して返す関数です。

```asm
add rcx, rdx
mov rax, rcx
ret
```

rcx（第1引数レジスタ）にrdx（第2引数レジスタ）を加算し、rax（戻り値レジスタ）にコピーし、returnします。

# 機械語

```
0x48, 0x01, 0xD1, // add rcx, rdx
0x48, 0x89, 0xC8, // mov rax, rcx
0xC3, // ret
```

x86_64のadd,mov命令は、`[REX, OPECODE, ModR/M]`という形式になっています。ret命令はオペランドやREXを持たないので、そのままペコード`0xC3`を指定します。

https://www.felixcloutier.com/x86/ret

### REX

REXは、`0b0100[64bit][shiftReg][shiftSibIndex][shiftRM]`という形式です。64bitのところだけ立てておいて、`0b0100_1_0_0_0=0x48`です。

https://github.com/wallstudio/RecottePlugin/blob/v0.2.1/RecottePluginManager/PluginHelper.h#L152-L162

### OPECODE

OPECODEは、それぞれ`0x01`、`0x89`です。これはそのままそういう仕様です。

https://www.felixcloutier.com/x86/add

https://www.felixcloutier.com/x86/mov

### ModR/M

ModR/Mは、`0x[mod][reg][regMem]`で、それぞれ2/3/3bitです。regで第1オペランドを指定し、modで第2オペランドにregisterを使う指定（`0b11`）、regMemで第2オペランドを指定します。

`0x11[reg1][reg2]`ということで、addの方はrcx,rdxなので`0x11_010_001=0xD1`、movの方はrax,rcxなので`0x11_001_000=0xC8`です。

https://github.com/wallstudio/RecottePlugin/blob/v0.2.1/RecottePluginManager/PluginHelper.h#L164-L172

# VirtualProtect

`VirtualProtect`はメモリの属性を変更するWin32APIです。この関数で、読み書き実行である、`0x40`を指定します。

（実は、メモリ属性を変更せずとも動いてしまったのですが…）

https://learn.microsoft.com/ja-jp/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect

https://www.tokovalue.jp/function/VirtualProtect.htm

# Unamanged関数ポインタ

C#のめったに使わないシリーズの機能です。このような記法で、ネイティブの関数をC#に解釈させられます。関数ポインタは、デリゲートのように、`()`で呼び出しできます。

```csharp
var add_func = (delegate* unmanaged<long, long, long>)code;
return add_func(a, b);
```

# まとめ

C#でインラインアセンブラのようなことができました。厳密にはアセンブラではなく機械語直書きですが、アセンブラとは対応するものですし、成功といってよいのではないでしょうか！

これができることで幸せになれるシチュエーションがあんまり思いつきませんが…（何かあればコメントください🙇）