前書き
この記事は、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,
}
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は、それぞれ0x01
、0x89
です。これはそのままそういう仕様です。
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#でインラインアセンブラのようなことができました。厳密にはアセンブラではなく機械語直書きですが、アセンブラとは対応するものですし、成功といってよいのではないでしょうか!
これができることで幸せになれるシチュエーションがあんまり思いつきませんが…(何かあればコメントください🙇)