
2026/01/03 10:18
**基本的なジャスト・イン・タイム コンパイラ(2015)**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
## 要約 この記事は、シンプルな再帰関係式を評価するための Hacker News /r/dailyprogrammer チャレンジに対して、小さな x86‑64 JIT コンパイラを構築する方法を示しています。解法では、各演算子(`+2`、`*3`、`-5`、`/7`)を実行時に機械語へコンパイルします。 ## 実装手順 1. **メモリ割り当て** * `mmap()`(Unix)または `VirtualAlloc()`(Windows)で書き込み可能なページを確保。 * 4096 バイトのバッファ(`PAGE_SIZE`)と次に空いているバイトを追跡するカウンタを持つ `asmbuf` 構造体を使用。 2. **命令発行** * ヘルパー関数 `asmbuf_ins()` と `asmbuf_immediate()` が生のオペコードバイトと即値を書き込み。 * 例として使われるオペコード: * 恒等変換 – `mov rax, rdi` (`0x48 89 f8`) + `ret` (`0xc3`). * 加算 – `add rax, rdi` (`0x48 01 f8`). * 減算 – `sub rax, rdi` (`0x48 29 f8`). * 乗算 – `imul rax, rdi` (`0x48 0f af c7`). * 除算 – RDX をゼロ拡張するために `xor rdx, rdx` (`0x48 31 d2`) と `idiv rdi` (`0x48 f7 f8`)。 3. **実行可能化** * ページ保護を実行可能に変更:Unix では `mprotect()`、Windows では `VirtualProtect()` を使用。 4. **JIT 生成関数の呼び出し** * バッファポインタを型 `long (*recurrence)(long)` の関数ポインタへキャストし、現在の項値で呼び出す。 ## 呼び出し規約 * System V AMD64:引数は `rdi` に渡され、結果は `rax` で返却。 * Windows:最初の引数は `rcx` に渡される。 JIT は直線コードのみを生成します;分岐やスタック使用がないため、線形再帰列にしか対応できません。 ## 今後の作業 著者はさらに演算子(剰余、XOR、ビットシフト)と浮動小数点演算を追加する予定です。軽量アプローチは、LLVM などの重いフレームワークよりも速度や単純さで優れる場合に有用です。例としては、組込みシステム、ゲームスクリプティング、科学計算が挙げられます。
本文
2015年3月19日 – nullprogram.com/blog/2015/03/19/
(著者は現在米国での雇用機会を探しています。)
この記事は Hacker News と Reddit で議論されました。
課題
/r/dailyprogrammer の週次課題では、再帰関係式を読み取り、それを解釈し、指定された項数だけ繰り返すプログラムを書きます。
- 初期値
と操作列u(0)
が与えられます。f - 各操作は前の項を更新します:
。u(n+1) = f(u(n)) - 操作は 加算、減算、乗算、除算 のみで、それぞれ一つのオペランドがあります。
例:
u(n + 1) = (u(n) + 2) * 3 - 5
は
+2 *3 -5 と入力します。u(0)=0 のとき、列は次のようになります:
| n | u(n) |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 4 |
| 3 | 13 |
| 4 | 40 |
| 5 | 121 |
各操作を一つずつステップ実行するインタプリタを書く代わりに、著者は 簡易 x86‑64 Just‑In‑Time (JIT) コンパイラ を作成しました。
この JIT は操作列をネイティブ機械語へ変換し、CPU が直接実行できるようにします。
その後の課題では逆ポーランド記法(RPN)を採用してより複雑な式が扱えるようにし、その提出には別の JIT が書かれました。
実行可能メモリの確保
現代OSは ページ単位での保護 を課します:読み取り、書き込み、実行の権限。
典型的なコードページは read+execute ですが書けません;データページは read+write で実行不可です。
これによりシェルコードなどの攻撃を防ぎ、不正アクセスはセグメンテーションフォールトで即座に検出されます。
命令バッファ
malloc() は書き込み可能だが実行不可のメモリを返します。したがって JIT では
mmap()(Windows なら VirtualAlloc())を使い、ページ境界に揃ったバッファを取得します。
#define PAGE_SIZE 4096 struct asmbuf { uint8_t code[PAGE_SIZE - sizeof(uint64_t)]; uint64_t count; }; struct asmbuf *asmbuf_create(void) { int prot = PROT_READ | PROT_WRITE; int flags = MAP_ANONYMOUS | MAP_PRIVATE; return mmap(NULL, PAGE_SIZE, prot, flags, -1, 0); }
Windows 版:
struct asmbuf *asmbuf_create(void) { DWORD type = MEM_RESERVE | MEM_COMMIT; return VirtualAlloc(NULL, PAGE_SIZE, type, PAGE_READWRITE); }
コード生成後に
mprotect()(または VirtualProtect())で実行権限を付与し、終了時には munmap()(VirtualFree())で解放します。
命令の挿入
生の機械語をバッファへ書き込むヘルパー関数が二つあります:
asmbuf_ins(struct asmbuf *, int size, uint64_t ins); asmbuf_immediate(struct asmbuf *, int size, const void *value);
例:
恒等関数(
mov rax,rdi と ret)を生成する場合:
asmbuf_ins(buf, 3, 0x4889f8); // mov rax,rdi asmbuf_ins(buf, 1, 0xc3); // ret
オペランドは
asmbuf_immediate() で埋め込みます。
呼び出し規約
生成される関数は System V AMD64 ABI を使用します:
- 最初の整数引数 →
。rdi - 戻り値 →
。rax
Windows では最初の引数が
rcx に渡りますが、それ以外は同じアセンブリです。
操作の生成
各演算子に対応する命令列を組み立てます。
例:
switch (operator) { case '+': asmbuf_ins(buf, 3, 0x4801f8); // add rax,rdi break; case '-': asmbuf_ins(buf, 3, 0x4829f8); // sub rax,rdi break; case '*': asmbuf_ins(buf, 4, 0x480fafc7); // imul rax,rdi break; case '/': asmbuf_ins(buf, 3, 0x4831d2); // xor rdx,rdx asmbuf_ins(buf, 3, 0x48f7ff); // idiv rdi break; }
Tip:
%(剰余)、^(XOR)、ビットシフト <<, >> を追加したり、浮動小数点演算に切り替えることも可能です。
生成コードの実行
バッファを確定させた後:
asmbuf_finalize(buf); long (*recurrence)(long) = (void *)buf->code;
これで通常の C 関数として呼び出せます:
x[n + 1] = recurrence(x[n]);
要約
- JIT は単純な算術再帰関係式をネイティブ x86‑64 コードへコンパイルします。
- メモリ保護は安全に扱い、呼び出し規約を尊守し、生成コードを最小オーバーヘッドで実行します。
- この手法は特定の問題にはシンプルかつ有効ですが、より複雑なケースでは LLVM などフルスケールのコンパイラフレームワークを検討すべきです。