Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Xbyak Internals and Hacks

herumi
October 03, 2024
120

Xbyak Internals and Hacks

Binary Hacks Rebooted ~ Forkwell Library#68
https://forkwell.connpass.com/event/331098/

herumi

October 03, 2024
Tweet

Transcript

  1. 自己紹介 @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
  2. 特長 既存(静的)アセンブラとの違い プログラム実行時に確定する変数に応じた最適化が可能 変数から定まる各種定数を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
  3. サンプル 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
  4. 実行例 AddCodeのインスタンスを生成し関数ポインタを得る Hack #77も参照 AddCode c(2); // n = 2の例

    const auto add = c.getCode<void (*)(uint64_t, const uint64_t*, const uint64_t*)>(); 生成されたコード 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
  5. 実行時に機械語を生成する(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
  6. 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
  7. /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
  8. 実装詳細 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
  9. 答えは0xffffffff Integral conversions [conv.integral] int を uint32_t にキャストすると が適用される 問題2.

    次の結果は? #include <stdio.h> #include <stdint.h> 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
  10. 答えは未定義 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
  11. 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
  12. プロファイラ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<Fp2T<FpT 0.57% mt_test.exe [JIT] tid 110290 [.] 0x00007541a87023c2 0.56% mt_test.exe [JIT] tid 110290 [.] 0x00007541a87026f6 0.55% mt_test.exe [JIT] tid 110290 [.] 0x00007541a87027ce 0.55% mt_test.exe [JIT] tid 110290 [.] 0x00007541a8702662 0.52% mt_test.exe mt_test.exe [.] void ec::addJacobi 0.51% mt_test.exe [JIT] tid 110290 [.] 0x00007541a870376d 18 / 22
  13. perfにJITされた関数の情報を渡す /tmp/perf-<PID>.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
  14. 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
  15. ブレークポイント 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
  16. 環境変数との併用 コードを変更しなくてよいのでデバッグに便利 デバッグしたいコードの先頭に次の行を追加する 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