Slide 1

Slide 1 text

Xbyak Internals and Hacks 光成滋生 2024/10/03 Binary Hacks Rebooted ~ Forkwell Library#68 1 / 22

Slide 2

Slide 2 text

自己紹介 @herumi サイボウズ・ラボで暗号と高速化のR&D 『暗号と認証のしくみと理論がしっかりわかる』技術評論社 2021 『Binary Hacks Rebooted』にも寄稿 OSS開発 ペアリング暗号・BLS署名ライブラリ mcl/bls mcl-wasmのGitHub network dependentsは20万repo, NPM 2000万DL Ethereum Foundation Grants 獲得x2 (2024/8) Microsoft MVP C++, Developer Seucirty受賞 (2024/7) 2 / 22

Slide 3

Slide 3 text

JITアセンブラXbyak 実行時に機械語を生成できるC++ライブラリ コーデックや暗号ライブラリの開発を容易にするために開発開始(2006年後半から) mclでももちろん利用 Linux Foundation UXLのoneDNNの主にIntel CPU向け最適化エンジンで活用されている PyTorchやTensorFlowのIntel拡張など スーパーコンピュータ富岳やM1 Mac用のXbyak_aarch64の開発にも関わる RISC-V版もある Google Open Source Peer Bonus受賞 (2024/6) 3 / 22

Slide 4

Slide 4 text

特長 既存(静的)アセンブラとの違い プログラム実行時に確定する変数に応じた最適化が可能 変数から定まる各種定数をJITコードに埋め込む CPUキャッシュサイズに応じたループ展開 CPUに応じた最適化が可能 特定コンパイラの機能IFUNC(Hack #8)もあるがより柔軟な最適化が可能 // UXL(Intel) oneDNNのコードの一部 void jit_avx512_core_gemm_s8u8s32_kern::dot_product(const Xmm &dst, const Xmm &src1, const Xmm &src2) { if (cpu.has(tVNNI)) { // VNNI命令が使える場合 vpdpbusd(dst, src1, src2); } else { vpmaddubsw(dp_scratch, src1, src2); // [a0 b0 + a1 b1:a2 b2 + a3 b3:...] vpmaddwd(dp_scratch, ones, dp_scratch); // [a0 b0 + a1 b1 + a2 b2 + a3 b3:...] vpaddd(dst, dst, dp_scratch); } } 4 / 22

Slide 5

Slide 5 text

分かりやすい文法 従来のアセンブラの問題点 貧弱なマクロ(gas)かNASM, MASM固有の文法を覚える必要がある 移植性が無い C++との親和性が無い(C++の構造体や定数の情報をアセンブラに伝えるのが難しい) Xbyakの場合 アセンブラはIntel記法をほぼそのまま記述できるような演算子オーバーロードを提供 制御やマクロはC++の文法のままで記述できる int n = 4; lea(rax, ptr [rcx + rcx * 8]); // rax = 9 * rcx and_(rax, (1< struct Buffer { uint64_t v[N]; }; // Buffer<3> のサイズなどの情報をXbyakに伝えるのは容易 5 / 22

Slide 6

Slide 6 text

サンプル 64*Nビット加算コードの生成(ABIはHack #83参照) void add(uint64_t *z, const uint64_t *x, const uint64_t *y); struct AddCode : Xbyak::CodeGenerator { // CodeGeneratorを敬称 AddCode(int n) { // n は実行時に決まる Xbyak::Util::StackFrame sf(this, 3); // Linux/Windowsの呼び出し規約を吸収するクラス const auto& z = sf.p[0]; // 関数の引数に分かりやすい名前をつける const auto& x = sf.p[1]; const auto& y = sf.p[2]; for (int i = 0; i < n; i++) { mov(rax, ptr[x + i * 8]); // rax = x[i]; if (i == 0) { add(rax, ptr[y + i * 8]); // rax += y[i]; 最初はaddで次からcarryつきadd } else { adc(rax, ptr[y + i * 8]); } mov(ptr[z + i * 8], rax); // 結果をz[i]に格納 } } }; 6 / 22

Slide 7

Slide 7 text

実行例 AddCodeのインスタンスを生成し関数ポインタを得る Hack #77も参照 AddCode c(2); // n = 2の例 const auto add = c.getCode(); 生成されたコード add3_winはn=3を渡した場合の例 add2_linux: | add3_win: mov rax, [rdi] | mov rax, [rcx] add rax, [rsi] | add rax, [rdx] mov [rdx], rax | mov [r8], rax mov rax, [rdi+0x8] | mov rax, [rcx+0x8] adc rax, [rsi+0x8] | adc rax, [rdx+0x8] mov [rdx+0x8], rax | mov [r8+0x8], rax ret | mov rax, [rcx+0x10] | adc rax, [rdx+0x10] | mov [r8+0x10], rax | ret 7 / 22

Slide 8

Slide 8 text

Xbyak Internals Xbyakの実装紹介

Slide 9

Slide 9 text

実行時に機械語を生成する(Hack #85 参照) 大まかな手順 1. メモリを確保する 2. メモリに実行属性を付与する(Linuxならmprotect, WindowsならVirtualProtect) 読み(Read)書き(Write)実行(eXecute)の属性RWXを付与する 3. 機械語をメモリに書き込む(CPUに応じたコードを生成) 4. メモリを実行する(先頭ポインタにジャンプ) セキュリティ向上のために 攻撃を受けにくいよりよい方法(W^X : WとXは排他的にしか設定しない) 1. RWなメモリを確保する 2. 機械語をメモリに書き込む 3. RXに切り返る 9 / 22

Slide 10

Slide 10 text

mmapによるメモリ確保 posix_memalign + mprotectでは メモリマップを2個消費する /proc/sys/vm/max_map_count で制限されている(デフォルト65536) mmapで直接確保すると制約を受けない RWで直接確保する void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, mode, -1, 0); if (p == MAP_FAILED) return false; 機械語を書き込んだらRXに変更する return mprotect(p, size, PROT_READ | PROT_EXEC) == 0; 10 / 22

Slide 11

Slide 11 text

/proc/self/maps にJIT領域を表示する mapsについてはHack #25 procfs参照 /proc/self/maps には自分のプロセスのメモリマップが表示される JIT 領域は(当然)表示されない 表示例 599242fb3000-599242fb5000 r--p 00000000 103:03 70648018 /home/shigeo/Program/xbyak/sample/a.out 599244d11000-599244d32000 rw-p 00000000 00:00 0 [heap] 793433e1a000-793433e1c000 rw-p 00219000 103:03 115607834 /usr/lib/x86_64-linux-gnu/libc.so.6 793434226000-793434229000 rw-p 00225000 103:03 115605650 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30 79343440b000-793434416000 r--p 0002c000 103:03 115607828 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffd6f760000-7ffd6f781000 rw-p 00000000 00:00 0 [stack] 7ffd6f785000-7ffd6f789000 r--p 00000000 00:00 0 [vvar] 7ffd6f789000-7ffd6f78b000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall] XBYAK_USE_MEMFD を定義するとmapsに表示できる 表示例 7934343dc000-7934343dd000 rwxs 00000000 00:01 14342 /memfd:Xbyak::xyz (deleted) 793434416000-793434417000 rwxs 00000000 00:01 14341 /memfd:Xbyak::abc (deleted) 11 / 22

Slide 12

Slide 12 text

実装詳細 memfd_createを使う memfd_create で無名ファイルを作成する その fd をmmapに渡して関連づける xbyak.h の一部を抜粋 int fd = -1; #ifdef XBYAK_USE_MEMFD fd = memfd_create(name_.c_str(), MFD_CLOEXEC); // Xbyak::MmapAllocatorで渡された文字列 if (fd < 0) return false; mode = MAP_SHARED; if (ftruncate(fd, size) != 0) { close(fd); return false; } #endif void *p = mmap(NULL, size, PROT_READ | PROT_WRITE, mode, fd, 0); 12 / 22

Slide 13

Slide 13 text

環境の違いで気がついたバグ Xbyak_aarch64での開発中 PRIMEHPC FX700(富岳と同じCPU(A64FX)搭載)で開発は遅いのでXeonやM1 Macなどを併用 命令に対するコード生成部はCPUや処理系に寄らないので他のCPUで開発しても問題ない テストでA64FXとXeonで異なる動作が発生 問題1. 次の結果は? #include #include int main(int argc, char *argv[]) { printf("(uint32_t)(%d) = 0x%x\n", -argc, (uint32_t)(-argc)); } .gcc t.c && /a.out 13 / 22

Slide 14

Slide 14 text

答えは0xffffffff Integral conversions [conv.integral] int を uint32_t にキャストすると が適用される 問題2. 次の結果は? #include #include int main(int argc, char *argv[]) { printf("(uint32_t)(float)(%d) = %x\n", -argc, (uint32_t)(float)(-argc)); } .gcc t.c && /a.out 14 / 22

Slide 15

Slide 15 text

答えは未定義 Floating-integral conversions [conv.fpint] float を整数にキャストするときは小数部分が切り捨てられる 切り捨てられた結果が出力の型で表現できない場合は未定義 -1 は uint32_t で表現できない // x64 (uint32_t)(float)(-1) = ffffffff // A64FX (uint32_t)(float)(-1) = 0 x64では cvttss2si , A64FXでは fcvtzu w2, s0 が使われていた 未定義コードなのでコンパイラによっては異なる可能性あり これらの命令の詳細はHack #75参照(#72では丸め方の解説あり) あ, fcvtzuは解説されてなかった(ARMのマニュアル参照) (uint32_t)(int)(float)(-1) なら適格で 0xfffffff になる x64では cvttss2si のままでAArch64では fcvtzs が使われる 15 / 22

Slide 16

Slide 16 text

Xbyak Hacks デバッグやプロファイリングなどのtips

Slide 17

Slide 17 text

CPUに応じた動作確認 新しい専用命令は古いCPUでは動作しない(当たり前) Xbyak::util::Cpu でCPUが利用可能な命令セットを取得できる もちろん, その特性が利用可能なときのみその命令を生成するようにコードを書く が, 往々にして間違うことがある(とGitHub issueに上がる) 新しいCPUで開発してると気がつかない Intel SDEの利用(Hack #57も参照) IntelのさまざまなCPUの動作をエミュレートできる // 抜粋 -snb Set chip-check and CPUID for Intel(R) Sandy Bridge CPU -adl Set chip-check and CPUID for Intel(R) Alder Lake CPU -spr Set chip-check and CPUID for Intel(R) Sapphire Rapids CPU -future Set chip-check and CPUID for Intel(R) Future chip CPU sde -snb -- ./a.out でSandy Bridge CPUで動作確認 17 / 22

Slide 18

Slide 18 text

プロファイラperfでJIT領域を表示する Hack #85 参照 JIT領域の関数はperfコマンドで表示されない 何の処理が重たいか分からない >cat sudo perf report Samples: 4K of event 'cycles:P', Event count (approx.): 5620677239 Overhead Command Shared Object Symbol 4.24% mt_test.exe mt_test.exe [.] mcl_c5_vmulA 1.18% mt_test.exe mt_test.exe [.] void inv::exec<6> 0.99% mt_test.exe mt_test.exe [.] void ec::addJacobi 0.93% mt_test.exe mt_test.exe [.] void ec::dblJacobi 0.59% mt_test.exe [JIT] tid 110290 [.] 0x00007541a870344b 0.59% mt_test.exe mt_test.exe [.] EcT

Slide 19

Slide 19 text

perfにJITされた関数の情報を渡す /tmp/perf-.map 1行ずつ 関数の先頭ポインタ 関数のサイズ 関数名 を記述するとperfが参照する Xbyak::util::Profiler クラスを提供している void set(const char *funcName, const void *startAddr, size_t funcSize); で登録 実行例 Overhead Command Shared Object Symbol 25.82% mt_test.exe [JIT] tid 110558 [.] mclx_Fp_mul 20.81% mt_test.exe [JIT] tid 110558 [.] mclx_FpDbl_mod 17.81% mt_test.exe [JIT] tid 110558 [.] mclx_FpDbl_mulPre 5.76% mt_test.exe [JIT] tid 110558 [.] mclx_Fp2Dbl_mulPre 4.24% mt_test.exe mt_test.exe [.] mcl_c5_vmulA 3.93% mt_test.exe [JIT] tid 110558 [.] mclx_FpDbl_sqrPre 2.66% mt_test.exe [JIT] tid 110558 [.] mclx_Fp_sub 1.90% mt_test.exe [JIT] tid 110558 [.] mclx_Fp2_sub 19 / 22

Slide 20

Slide 20 text

JIT領域の逆アセンブル rawオブジェクトの逆アセンブル Hack#44 参照 JIT領域をdumpしてファイルに保存するとヘッダがついてない // x64 objdump -b binary -D -m i386:x86-64 ファイル名 // aarch64 objdump -b binary -D -m aarch64 ファイル名 Intel SDE(Software Development Emulator)を使う場合は xed -64 -ir ファイル名 20 / 22

Slide 21

Slide 21 text

ブレークポイント JIT領域はランダムに変わるのでブレークポイントを置きづらい int3(); を使うとgdbが止まる(aarch64では brk(0); で止まる) int3(); // ここで実行が止まる mov(eax, 5); mov(eax, 3); ret(); そのあと si などでステップ実行(aarch64では set $pc+=4 で1命令進めてから) (gdb) r ./a.out Program received signal SIGTRAP, Trace/breakpoint trap. 0x00007ffff7ffb001 in ?? () (gdb) x/10i $pc Dump of assembler code from 0x7ffff7ffb001 to 0x7ffff7ffb081: => 0x00007ffff7ffb001: mov eax,0x5 0x00007ffff7ffb006: mov eax,0x3 0x00007ffff7ffb00b: ret 21 / 22

Slide 22

Slide 22 text

環境変数との併用 コードを変更しなくてよいのでデバッグに便利 デバッグしたいコードの先頭に次の行を追加する XBYAK_BP=1 なら int3 を埋め込む if (const char *p = getenv("XBYAK_BP"); p && p == "1"sv) int3(); gdbでのバッチ処理(Hack #44も参照) > cat disas.txt r x/10i $pc q 環境変数を定義して実行 env XBYAK_BP=1 gdb --nx -q -batch -x disas.txt 実行ファイル XBYAK_BP=1 を定義したときだけ int3() から10命令文のJITコードを得られる 22 / 22