17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2023-12-08

前書き

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

TL;DR;

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だと使えないけど)

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

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ということに限定します。

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,
}

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

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

アセンブリ

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

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を指定します。

REX

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

OPECODE

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

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です。

VirtualProtect

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

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

Unamanged関数ポインタ

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

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

まとめ

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

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

17
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?