ここではインラインアセンブラや外部アセンブラと異なるXbyak特有の気をつけるべきことを紹介します。
コード生成している関数の中からグローバル変数や定数を参照したいことはよくあります。
x64のメモリアドレスのオフセットは±2GiBです。 32bit時代はメモリ空間が32bitなので問題なかったのですが、メモリ空間が64bitになるといま自分がいる領域とアクセスしたい変数のアドレスの差が32bitに収まるとは限りません。 そのためいったんポインタを経由してアクセスします。
static int g_i = 123;
static float g_f = 4.56;
struct Code : Xbyak::CodeGenerator {
Code()
{
mov(rax, (size_t)&g_i);
mov(eax, ptr[rax]);
mov(rax, (size_t)&g_f);
movss(xmm0, ptr[rax]);
}
};
Xbyakがアクセスしたい変数はそれほど多くないでしょう(無節操にアクセスするとそれはそれで困りものです)。 まとめて構造体に入れておくとアクセスしやすいです。
struct Param {
int i;
float f;
} g_p;
struct Code : Xbyak::CodeGenerator {
Code()
{
mov(rax, (size_t)&g_p);
mov(eax, ptr[rax]);
movss(xmm0, ptr[rax + offsetof(Param, f)]);
}
};
offsetofは指定したメンバ変数のオフセットを得るCのマクロです。 このようにすると先頭だけ設定したら、あとはオフセットでアクセスできます。
その関数でのみ利用するちょっとした定数にアクセスしたいときはコードの後ろに配置する方法があります。
struct Code : Xbyak::CodeGenerator {
Code()
{
Xbyak::Label dataL;
mov(rax, dataL);
mov(eax, ptr[rax]);
ret();
align(32);
L(dataL);
dd(123);
}
};
mov(rax, dataL);
はraxにdataLが設定されたアドレスを設定する命令です。
次のmov(eax, ptr[eax]);
でそのアドレスから4byteの整数を読み込んでいます。
L(dataL);
でdataLを設定し、そこにdd
でuint32_t(123)
を4byteとして書き込んでいます。
結果的にeax
に123が設定されて返る関数です。
align(32)
はなくても動作するのですが、CPUがret
の後ろのデータもデコードしようとしないよう開けています(気持ちの問題)。
本当は4096byteずらしてコードとデータを明確に分けるとよいです。 cf. Intel最適化マニュアル 3.6.8 Mixing Code and Data Coding Rule 51.
データとコードの配置も参照してください。
浮動小数点数の値を書き込む場合はビットパターンを整数に変換する関数を用意しておくとよいでしょう。
uint32_t ftoi(float f)
{
uint32_t v;
memcpy(&v, &f, sizeof(v));
return v;
}
dd(ftoi(1.23f));
データへのオフセットが±2GiB以内にあることが分かっている場合にはripによる相対アクセスができます。
struct Code : Xbyak::CodeGenerator {
Code()
{
Xbyak::Label dataL;
mov(eax, ptr[rip + dataL]);
ret();
L(dataL);
dd(123);
}
};
こちらのほうが1命令減らせますね。
putL
を使うとラベルをメモリに配置できます。
Code()
{
Xbyak::Label lpL, case0L, case1L, case2L, case3L;
#ifdef XBYAK64_WIN
const auto& x = rcx;
#elif defined(XBYAK64_GCC)
const auto& x = rdi;
#endif
mov(rax, lpL);
jmp(ptr[rax + x * 8]);
align(32);
L(lpL);
putL(case0L);
putL(case1L);
putL(case2L);
putL(case3L);
L(case0L);
mov(eax, 1);
ret();
L(case1L);
mov(eax, 10);
ret();
L(case2L);
mov(eax, 100);
ret();
L(case3L);
mov(eax, 1000);
ret();
}
生成する関数の第一引数はLinuxならrdi、Windowsならrcxです。 呼び出し規約についてはx64 アセンブリ言語プログラミングも参照してください。
mov(rax, lpL);
jmp(ptr[rax + x * 8]);
ラベルlpL
から8byteずつ並んでいるポインタの配列のx
番目を取り出します。
まともに使うときは値が配列のサイズに入っているかちゃんと確認する必要があります。
L(lpL);
putL(case0L);
putL(case1L);
putL(case2L);
putL(case3L);
C++では
uint64_t *lpL[] = {
case0L, case1L, case2L, case3L
};
に相当します(lookup.cpp)。
比較的大きめのプログラムになるとデータとコードをきちんと管理したくなります。 その場合はユーザがメモリレイアウトを決めてそれをXbyakに伝えることにします。
たとえば連続するdata領域4KiB、コード領域4KiBを用意してそれをXbyakで利用してみましょう(separate-code-data.cpp)。
+------+
| data |
| |
+------+
| code |
| |
+------+
まず
struct DataCode {
uint8_t data[4096];
uint8_t code[4096];
};
alignas(4096) DataCode g_dataCode;
でdataとcodeの領域を用意します。
Xbyak::CodeGenerator
にユーザが指定した領域を渡します。
struct Code : Xbyak::CodeGenerator {
Xbyak::Label dataL;
Code()
: Xbyak::CodeGenerator(sizeof(g_dataCode), &g_dataCode)
{
L(dataL);
dd(123);
setSize(sizeof(g_dataCode.data));
}
先頭4096byteがdata領域なのでその先頭アドレスをL(dataL)
でアクセスできるようにします。
dd(123)
やその他の方法ですきなようにDataCode::data
をいじります。
データの構築が終わったら
setSize(sizeof(g_dataCode.data));
でXbyakが書き込む場所をdataの最後(=codeの先頭)に設定します。
Code c;
auto f = c.getCurr<int (*)()>();
このとき、関数ポインタfのアドレスはg_dataCode.code
の先頭アドレスに等しくなります。
mov(rax, dataL);
mov(eax, ptr[rax]);
ret();
前節と同じくdataL
を介して値を読み込みます。
最後にコード領域のみread/exec属性に変更します。
protect(g_dataCode.code, sizeof(g_dataCode.code), Xbyak::CodeArray::PROTECT_RE);
Xbyak::CodeGenerator
はデフォルトではメモリを確保して、その領域にread/write/exec属性を付与します。
しかし、最近はそのようなメモリ領域は攻撃対象となりやすく嫌われる傾向にあります。
OSの設定によっては禁止されていることもあります。
そのため少しでも攻撃リスクを減らすために、実行属性(exec)と書き込み属性(write)を同時につけないようにするとよいです。
そのためにはCodeGenerator
のコンストラクタにDontSetProtectRWE
を指定します。
struct Code : Xbyak::CodeGenerator {
Code()
: Xbyak::CodeGenerator(4096, Xbyak::DontSetProtectRWE)
{
mov(eax, 123);
ret();
}
};
この場合Xbyakはmalloc(またはmmap)した領域にバイトコードを書き込むだけでメモリの属性は変更しません。
コード生成が完了したら、getCode
する前にsetProtectModeRE
を呼び出して実行権限を与えます。
Code c;
c.setProtectModeRE();
auto f = c.getCode<int (*)()>();
printf("f=%d\n", f());
c.setProtectModeRE();
を呼ばないと(当たり前ですが)Segmentation faultを起こすので注意してください。
再度コードを書き換える場合はc.setProtectModeRW();
でメモリを読み書きできるようにします。