compilerbook (→ x86-64 入門程度まで)
レジスタ再び
DEBUG HACKS という本を買いました。この本では、『デバッグ前に知っておくべきこと』と称して x86-64 が紹介されます。やっぱりアセンブリって教養なんですか……?:
たまには compilerbook にも取り組んでみます。以前は Step 7 の比較演算子で挫折しました:
リポジトリ:
背景
事前学習
3 ~ 4 年前に Crafting Interpreters の第二章をやりました (loxrs) 。
- BNF や再帰下降型パーサはわかります。
- スタックマシンも電卓レベルなら理解しています。
compilerbook の趣旨
ではなぜ 2 年前に compilerbook を挫折したかというと、レジスタが分からなかったためです。
compilerbook では、 C のソースを x86-64 のアセンブリに変換します。アセンブリでは初めからスタックマシンを利用できますから、その点はインタープリタを作るよりも楽です。しかし計算実行にレジスタを使わなければならないため、『足す』とか『引く』みたいな単純な計算においてもレジスタが主役になります。
レジスタを使いこなすことが compilerbook の趣旨だと思います。
期待すること
- スタックマシンに詳しくなりたい (→ toylispの開発に繋げたい)
-
for
文を単純なgoto
に展開する - 末尾再帰の最適化など
-
- 低レイヤに詳しくなりたい (→ DEBUG HACKS を読みたい、活かしたい)
- レジスタとアセンブリに詳しくなりたい
- アドレスや仮想メモリに詳しくなりたい (載っているかな?)
ストイックに 29 ステップやることはないと思いますが、ぼちぼちやってみます。
レジスタは序の口だったという所まで進められない気がします
現状の入出力 (〜 step 7 比較演算子まで)
レジスタの名前が良くない。 付録1:x86-64命令セット チートシート に助けられました。
以下のアセンブリは、
cc
で実行ファイルに変換して動作確認できます:$ cc asm.s > executable
42
- rax は 64 bit のレジスタです
.intel_syntax noprefix
.global main
main:
; stack rax
push 42 ; [42]
pop rax ; [] 42
ret ; rax が返り値として使用される
5 * (9 - 6)
- rax, rdi は 64 bit のレジスタです
- add, sub, imul は 2 つの引数 (dst, src) を取り dst を上書きします
.intel_syntax noprefix
.global main
main:
; stack rax rdi
push 5 ; [5] - -
push 9 ; [5, 9] - -
push 6 ; [5, 9, 6] - -
pop rdi ; [5, 9] - 6
pop rax ; [5] 9 6
sub rax, rdi ; [5] 3 6
push rax ; [5, 3] 3 5
pop rdi ; [5] 3 3
pop rax ; [] 5 3
imul rax, rdi ; [] 15 3
push rax ; [15] 15 1
pop rax ; [] 15 1
ret ; rax が返値として使用される
最後の 3 行で
push rax
,pop rax
としているのは無駄です。
これは、再帰計算の終着点で単項を評価して単項を返す動作が反映されています。
2 <= 1
- cmp の計算結果はフラグレジスタに入ります
- set* でフラグレジスタから値 (0: false, 1: true) を取り出します (setle で取り出せば <= (less than) 演算子の計算結果に)
.intel_syntax noprefix
.global main
main:
; stack rax rdi flag
push 2 ; [2] - - -
push 1 ; [2, 1] - - -
pop rdi ; [2] - 1 -
pop rax ; [] 2 1 -
cmp rax, rdi ; [] 2 1 (値) ; フラグレジスタに比較演算の計算結果
setle al ; [] 1 1 (値) ; rax の下位 8 bit (al) にフラグレジスタの値を代入
; ; setle (<=) のため 2 <= 1 の計算結果が al に代入される
movzb rax, al ; [] 1 1 (値) ; rax (64 bit) を al (8 bit) で上書き
; ; 上位 54 bit (al 以外) は 0 で埋まる
push rax ; [1] 1 1 (値)
pop rax ; [] 1 1 (値)
ret ; rax が返値として用いられる
環境構築
そういえばやっていました。デバッガについては必要になったら準備してみます。
compilerbook に解説があるもの:
- Docker
- Makefile
解説が無かったもの:
- .clang-format
- clangd (言語サーバ)
あっ……
ハイライトの都合上 ;
をコメントとして使っていますが、本当のコメントは #
みたいです。
ステップ 9: 1 文字のローカル変数
main 関数に 26 個のローカル変数を持たせます。目標設定が上手い!
2 種類の stack ポインタ
- RSP (register's stack pointer)
『現在の』スタックポインタです。push
やpop
の度に移動します。明示的な操作も可能です。 - RBP (register's base pointer)
次に戻るべきメモリアドレスを記憶するポインタです。
0;
のコンパイル結果
.intel_syntax noprefix
.global main
main:
+ ; 関数プロローグ
+ push rbp ; 最後の RBP を保存する
+ mov rbp, rsp ; 現在の RSP を RBP に記憶する .. (1)
+ sub rsp, 208 ; ローカル変数を確保 (スタックポインタは負の方向へ移動)
; 0 を rax に返すだけ
push 0
pop rax
+ ; 関数エピローグ
+ mov rsp, rbp ; RBP へ移動、ローカル変数を破棄
+ pop rbp ; [RSP] から以前の RBP を取り出して RBP を書き換え .. (1)'
+ ret
-
[ .. ]
とマークされたレジスタは、アドレスのデリファレンスを表します
a = 10; a + 2;
.intel_syntax noprefix
.global main
main:
;; プロローグ
push rbp
mov rbp, rsp
sub rsp, 208
;; ----------------------------------------
;; a = 10;
;; ----------------------------------------
; 変数 a のアドレスを push
mov rax, rbp
sub rax, 8
push rax
push 10
; 変数 a に 10 を代入
pop rdi
pop rax
mov [rax], rdi
push rdi
pop rax
;; ----------------------------------------
;; a + 12;
;; ----------------------------------------
;; a
; 変数 a のアドレスを push
mov rax, rbp
sub rax, 8
push rax
; 変数 a のアドレスをデリファレンスして
; 変数 a の値を手に入れる
pop rax
mov rax, [rax]
push rax
;; 2
push 2
;; +
pop rdi
pop rax
add rax, rdi
; 単項式の push/pop で再帰評価が停止
push rax
pop rax
;; エピローグ
mov rsp, rbp
pop rbp
ret
ベースポインタの連結リスト
関数プロローグで RBP の連結リストを伸ばしていました。これはいい!
- ローカル変数はベースポインタからのオフセットで表現します
- スタックの pop (
ret
) は、ベースポインタの連結リストの pop に相当します
for 文やらネストしたスコープも実装できる気がして来ましたよ!
ステップ 10: 複数文字のローカル変数
何という名前のローカル変数が使用されるか追跡します。
a = 10; a + 2;
スタックのサイズを、ローカル変数 (の集まり) の大きさだけ拡張するようにできました:
.intel_syntax noprefix
.global main
main:
# prologue
push rbp
mov rbp, rsp
- sub rsp, 256
+ sub rsp, 16
# lval
mov rax, rbp
sub rax, 8
push rax
push 10
# * assign
pop rdi
pop rax
mov [rax], rdi
push rdi
pop rax
# lval
mov rax, rbp
sub rax, 8
push rax
# lvar (dereference the last lval)
pop rax
mov rax, [rax]
push rax
push 2
pop rdi
pop rax
add rax, rdi
push rax
pop rax
# epilogue
mov rsp, rbp
pop rbp
ret
ステップ11:return文
ベースポインタを pop するだけ。つまり、関数エピローグを関数の途中に挿入するだけです。あるいは関数の末尾に暗黙の return 文があるとも捉えられます。
return
文を関数エピローグへのjmp
として実装することもできます。
次回は制御構文です。ベースポインタが分かったので、ソースコードから CFG (control flow graph) への展開も当たり前に見えてくる気がします。ここから解説が薄くなるそうですが、実装できるでしょうか……?
TODO: 式文の後始末
return
文の追加に伴って、 2;
とか 3;
のような式文の結果が stack から pop
されなくなりました。 if などで消費されない式文の出力は破棄しなければなりません。
ステップ 12: 制御構文 (WIP)
フラグレジスタと condition codes
前述のフラグレジスタとは rflags
のこと [1] です。 rflags
の中には condition codes と呼ばれる bit が保存されます。これらの bit は実は計算の際に自動で更新されますし、 cmp
命令を使って明示的に更新することもできます。
short | full | 補足 |
---|---|---|
ZF | zero flag | 計算結果が 0 なら 1 |
SF | sign flag | 計算結果が負なら 1 |
OF | overflow flag | オーバー or アンダーフローが起きたか (符号あり計算のみ) |
CF | carry flag | 最上位ビットが桁上がりした (符号なし計算のみ) |
フラグレジスタは基本的に直接読み取ることはできません。 Condition code を指定してその値を読み取ります。たとえば以前使ったsetle
のうち、 le
の部分が condition code です。
conditon code | full | 式 |
---|---|---|
l | less than | SF != OF |
le | less than or equal to | SF != OF or ZF = =1 |
g | greater than | SF == OF |
ge | greater than or equal to | SF == OF or ZF = =1 |
cmp a b
は、 a - b
を実行し、計算結果を破棄することに相当します。したがって ZF
(a - b == 0) と SF
(a - b の符号) を使って大小関係の判定ができます。オーバーフローが起きた場合も、大小関係を正しく返します (オーバーフローが起きると OF は 1 になり、 a - b の符号 SF も逆転します) 。
Conditional jump
ジャンプ命令の名前は、 j
+ condition code です。
参考
-
64 ビットマシンの場合 ↩︎
このスクラップ……
割と出来が良いのでは……?
ステップ 12: 制御構文 (ブロック {}
なし)
分岐は conditional jump で実装します。
if (1) return 10; else return 11;
else
へのジャンプが 1 つ、 end
へのジャンプが 2 つあります。
.intel_syntax noprefix
.global main
main:
; prologue
push rbp
mov rbp, rsp
sub rsp, 8
; if else
push 1 ; cond 判定
pop rax
cmp rax, 0
je .Lelse0 ; 偽 (0) なら else 句へ飛ぶ
push 10 ; then 句 (`return 10;`)
pop rax
mov rsp, rbp
pop rbp
ret
jmp .Lend0 ; end へ飛ぶ
.Lelse0: ; else 区 (`return 11;`)
push 11
pop rax
mov rsp, rbp
pop rbp
ret
jmp .Lend0 ; 一応 end へ飛ぶ
.Lend0:
; epilogue
mov rsp, rbp
pop rbp
ret
a = 10; while (a > 0) a = a - 1; return a;
実装はすぐ終わりましたが、出力が長くて面食らいました。インデントを変えると少し易しくなりました。
while
.intel_syntax noprefix
.global main
main:
; prologue
push rbp
mov rbp, rsp
sub rsp, 16
; push address
mov rax, rbp
sub rax, 8
push rax
push 10
; assign
pop rdi
pop rax
mov [rax], rdi
push rdi
.Lloop_while0:
; local variable (push address + dereference rax)
; push address
mov rax, rbp
sub rax, 8
push rax
; dereference rax
pop rax
mov rax, [rax]
push rax
push 0
pop rdi
pop rax
; >
cmp rdi, rax
setl al
movzb rax, al
push rax
pop rax
cmp rax, 0
je .Lend_while0
; push address
mov rax, rbp
sub rax, 8
push rax
; local variable (push address + dereference rax)
; push address
mov rax, rbp
sub rax, 8
push rax
; dereference rax
pop rax
mov rax, [rax]
push rax
push 1
pop rdi
pop rax
sub rax, rdi
push rax
; assign
pop rdi
pop rax
mov [rax], rdi
push rdi
jmp .Lloop_while0
.Lend_while0:
; local variable (push address + dereference rax)
; push address
mov rax, rbp
sub rax, 8
push rax
; dereference rax
pop rax
mov rax, [rax]
push rax
pop rax
; return (embedded epilogue)
mov rsp, rbp
pop rbp
ret
; epilogue
mov rsp, rbp
pop rbp
ret
for
文
文法は
"for" "(" expr? ";" expr? ";" expr ")" stmt
for
.intel_syntax noprefix
.global main
main:
; prologue
push rbp
mov rbp, rsp
sub rsp, 24
; push address
mov rax, rbp
sub rax, 8
push rax
push 0
; assign
pop rdi
pop rax
mov [rax], rdi
push rdi
; push address
mov rax, rbp
sub rax, 16
push rax
push 0
; assign
pop rdi
pop rax
mov [rax], rdi
push rdi
.Lloop_for0:
; local variable (push address + dereference rax)
; push address
mov rax, rbp
sub rax, 16
push rax
; dereference rax
pop rax
mov rax, [rax]
push rax
push 10
pop rdi
pop rax
; <
cmp rax, rdi
setl al
movzb rax, al
push rax
cmp rax, 0
je .Lend_for0
; push address
mov rax, rbp
sub rax, 16
push rax
; local variable (push address + dereference rax)
; push address
mov rax, rbp
sub rax, 16
push rax
; dereference rax
pop rax
mov rax, [rax]
push rax
push 1
pop rdi
pop rax
add rax, rdi
push rax
; assign
pop rdi
pop rax
mov [rax], rdi
push rdi
; push address
mov rax, rbp
sub rax, 8
push rax
; local variable (push address + dereference rax)
; push address
mov rax, rbp
sub rax, 8
push rax
; dereference rax
pop rax
mov rax, [rax]
push rax
push 1
pop rdi
pop rax
add rax, rdi
push rax
; assign
pop rdi
pop rax
mov [rax], rdi
push rdi
jmp .Lloop_for0
.Lend_for0:
; local variable (push address + dereference rax)
; push address
mov rax, rbp
sub rax, 8
push rax
; dereference rax
pop rax
mov rax, [rax]
push rax
pop rax
; return (embedded epilogue)
mov rsp, rbp
pop rbp
ret
; epilogue
mov rsp, rbp
pop rbp
ret
どこまでやるか
大雑把に 3 言語を比較してみます。
比較項目 | Lox (Crafting Interpreters) | C (compierbook) | toylisp (作りたい言語) |
---|---|---|---|
ランタイム | 自作 VM | x86-64 | 自作 VM |
マシン種別 | スタックマシン | スタックマシン | スタックマシン |
型付け | 動的型付け | 静的型付け | 静的型付け |
型推論 | — | なし | 型推論したい |
ユーザー定義型 | class | struct | class 風 |
文法 | 簡単 | 艱難辛苦 | マクロがある |
エラー検査 | synchronize | 即 exit | 全検査 + LSP 出力 |
メモリ管理 | GC | malloc / free | Vec へのバインド? |
最適化 | 若干 | ? | 若干 |
C から学びたいのは静的型付け (型推論なし) の部分です。関数呼び出しや構造体のメンバ解決などのコンパイル部分を見れたら引き上げようかと思います。
あと C 構造体の ABI を理解できたら、ホスト言語とのデータのやり取りがスムーズになる気がします。
2 年前の挫折の理由 (2)
気づいたこと
course2020 を見たところ、ソースが本の内容と一致していません。何ならアセンブリの記法すら違います (Intel 記法にも色々あるようです) 。その辺の説明を聞き逃していことが、最大の挫折原因だったかもしれません。
憶測では、 course2020
は先回りして上手に書いてあるソースです。履歴としては綺麗な反面、知識面でインクリメンタルなのは rui314/chibicc の方かもしれません。引き続き chibicc の方を見ます。
アーカイブ 確認
course2020 (ジンク) の方は本の内容と不一致だと明言されていました。
- パーサの違い
ステートフルかステートレスか (ストリームか (読み取った値、残り) の返却か) 。後者はピークに強かったりプリプロセッサの扱いに向いている。 - コード生成の違い
chibicc では常にpush
する。course2020
ではレジスタを深さ 6 のスタックと見做して利用する。ただし 7 つ目のスタックが積めない時はabort
する。
初回でこれを聴いても付いていけなかったのでしょうね。当時 chibicc
の方を読んでいたら、案外スラスラ進めたかもしれません。
push / pop の数を揃えてみた
Discard フラグが ON の場合は、スタックに値を残さないように調整してみました。
3 + 4;
式文も値を残さなくなりました。
push 3
push 4
pop rdi
pop rax
add rax, rdi
push rax
# discard
pop rax
関数呼び出し
もう続きを作るつもりは無い のですが、スタックマシンについて知りたいので x86-64 を調べていきます。
スタックの使い方
こちらの記事が素晴らしかったです。以降では、記事中のコードを拝借して x86-64 の使い方を確認します。
数個の引数を取る関数
レジスタで処理が終わってしまい、スタックの使い方が見れません。
10 個の引数を取る関数 (int)
記事中のコードを godbolt.org でコンパイルしてみます:
int add(int a1, int a2, int a3, int a4, int a5, int a6,
int a7, int a8, int a9, int a10)
{
int c;
c = a1+a2+a3+a4+a5+a6+a7+a8+a9+a10;
return c;
}
int main(int argc, char *argv[])
{
int ret = add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
return ret;
}
x86-64 (`-O0 -masm=intel`):
main:
push rbp
mov rbp, rsp
sub rsp, 32
# <1>
mov DWORD PTR [rbp-20], edi
mov QWORD PTR [rbp-32], rsi
# <2>
push 10
push 9
push 8
push 7
mov r9d, 6
mov r8d, 5
mov ecx, 4
mov edx, 3
mov esi, 2
mov edi, 1
call add(int, int, int, int, int, int, int, int, int, int)
add rsp, 32
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
leave
ret
add(int, int, int, int, int, int, int, int, int, int):
push rbp
mov rbp, rsp
# <3>
mov DWORD PTR [rbp-20], edi
mov DWORD PTR [rbp-24], esi
mov DWORD PTR [rbp-28], edx
mov DWORD PTR [rbp-32], ecx
mov DWORD PTR [rbp-36], r8d
mov DWORD PTR [rbp-40], r9d
# <4>
mov edx, DWORD PTR [rbp-20]
mov eax, DWORD PTR [rbp-24]
add edx, eax
mov eax, DWORD PTR [rbp-28]
add edx, eax
mov eax, DWORD PTR [rbp-32]
add edx, eax
mov eax, DWORD PTR [rbp-36]
add edx, eax
mov eax, DWORD PTR [rbp-40]
add edx, eax
mov eax, DWORD PTR [rbp+16]
add edx, eax
mov eax, DWORD PTR [rbp+24]
add edx, eax
mov eax, DWORD PTR [rbp+32]
add edx, eax
mov eax, DWORD PTR [rbp+40]
add eax, edx
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
pop rbp
ret
- eax, edi, .. は rax, rdi, .. の下位 8 ビットを指します
- <1>: main 関数の引数をレジスタで受け取りスタックに積んでいます
- <2>: 6 つの引数をレジスタで、 4 つの引数をスタックで渡しています
- <3>: レジスタで受け取った 6 つの引数を改めてスタックに積み直しています。無駄な感じがあります
- <4>: eax, edi を使って和を計算します。返値は edx に入れます
ちなみに -O1
で以下のように最適化されます:
main:
# <2>
mov eax, 55
ret
add(int, int, int, int, int, int, int, int, int, int):
add edi, esi
add edi, edx
add edi, ecx
add edi, r8d
# <1>
lea eax, [rdi+r9]
add eax, DWORD PTR [rsp+8]
add eax, DWORD PTR [rsp+16]
add eax, DWORD PTR [rsp+24]
add eax, DWORD PTR [rsp+32]
ret
- <1>: もはや
add
を呼んでいません。add
の呼び出し結果に置換されています。定数伝播? - <2>:
lea
はrdi+r9
をeax
に代入します。mov
命令を使うよりも命令の数が少ないのと、右辺 (?) の計算結果がデリファレンスというか『メモリアクセス』されない点が異なります。この違いは『アドレッシングモード』と呼ばれるらしく、 compilerbook のチートシートでは明確に区別が付くようにかき分けられています。
main 関数のみ最適化されないように pragma を追加 することもできます。その場合、 add
の呼び方は変わっていませんでした。
10 個の引数を取る関数 (char)
char
はスタックの中にぎっしりと詰まっているのか確認します。
int addCh(char a1, char a2, char a3, char a4, char a5, char a6,
char a7, char a8, char a9, char a10)
{
int c = a1+a2+a3+a4+a5+a6+a7+a8+a9+a10;
return c;
}
int main(int argc, char *argv[])
{
int ret = addCh(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
return ret;
}
なんだかややこしそうなアセンブリが出てきました:
x86-64 (`-O0 -masm=intel`)
addCh:
push rbp
mov rbp, rsp
push rbx
mov eax, ecx
mov ebx, r8d
mov r11d, r9d
# <2>
mov r10d, DWORD PTR [rbp+16]
mov r9d, DWORD PTR [rbp+24]
mov r8d, DWORD PTR [rbp+32]
mov ecx, DWORD PTR [rbp+40]
mov BYTE PTR [rbp-28], dil
mov BYTE PTR [rbp-32], sil
mov BYTE PTR [rbp-36], dl
mov BYTE PTR [rbp-40], al
mov eax, ebx
mov BYTE PTR [rbp-44], al
mov eax, r11d
mov BYTE PTR [rbp-48], al
mov eax, r10d
mov BYTE PTR [rbp-52], al
mov eax, r9d
mov BYTE PTR [rbp-56], al
mov eax, r8d
mov BYTE PTR [rbp-60], al
mov eax, ecx
mov BYTE PTR [rbp-64], al
movsx edx, BYTE PTR [rbp-28]
movsx eax, BYTE PTR [rbp-32]
add edx, eax
movsx eax, BYTE PTR [rbp-36]
add edx, eax
movsx eax, BYTE PTR [rbp-40]
add edx, eax
movsx eax, BYTE PTR [rbp-44]
add edx, eax
movsx eax, BYTE PTR [rbp-48]
add edx, eax
movsx eax, BYTE PTR [rbp-52]
add edx, eax
movsx eax, BYTE PTR [rbp-56]
add edx, eax
movsx eax, BYTE PTR [rbp-60]
add edx, eax
movsx eax, BYTE PTR [rbp-64]
add eax, edx
mov DWORD PTR [rbp-12], eax
mov eax, DWORD PTR [rbp-12]
mov rbx, QWORD PTR [rbp-8]
leave
ret
main:
push rbp
mov rbp, rsp
sub rsp, 32
mov DWORD PTR [rbp-20], edi
mov QWORD PTR [rbp-32], rsi
# <1>
push 10
push 9
push 8
push 7
mov r9d, 6
mov r8d, 5
mov ecx, 4
mov edx, 3
mov esi, 2
mov edi, 1
call addCh
add rsp, 32
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
leave
ret
- <1>:
addCh
の呼び方はadd
と変わりありません。レジスタ・スタック共に 1 つの char が 4 バイトを使っています。 - <2>: それぞれの
char
を 4 単位でスタックに積んでいます。読み出す際は 8 ビットだけ使うように注意しているように見えます。
ローカル変数はぎっしり詰まっていた のですが、スタックの primitive は (最適化 off の場合) 4 バイト使うんですね。 8 バイトじゃなく……? ほわぁあああああああ
おおお (セキュキャン 2022 ログ)
面白かったです。 PDF にして 150 ページ! 皆さま非常に逞しく、講師の方は異常に強く (各種言語にも強く) 、魔法学校の件ではお世話になっております (?) 。また知識の深い人ほど意外とアウトプットしてくれない (本を読むだけ・ソースを読むだけとか言い出す) もので、 1 対 1 のコミュニケーションは貴重だなという気もしました。いいなぁ。
私用メモ
- 教材のポインタ本 がめちゃめちゃ強いみたい
- プリプロセッサとか大変そうだなと
- 意味解析を追加したりパスを分けたり
- そういえば関数呼び出しに関するアラインメントがあるので、それが構造体のアラインメントと噛み合って word 単位で読めるようになっている?
- SSA (静的単一化代入) が非常に優秀な最適化らしいです。 MinCaml とは immutable なので話が違いそう
- C (の GCC 拡張?) には文式というのがあるようで、
({ .. })
の中の最後の文を式に変えます。えぇ……? - 『push と pop の対を消すのは peehole 最適化ででき』るそうです。
- 『スカラ』というのは primitive + 組み込み演算子という感じみたいです。
- 構造体の返し方が謎…… 関数呼び出しの ABI
- スコープを作るときは、コールフレームのサイズは一番大きなスコープの分確保する。そうとは思っていましたが、 裏付けが取れると心強いものですね。
- ピンポイントで標準言語仕様からポイントを見つけていて訳がわからないよ!
- 出たよ mod と準同型
- 小数型に関する ABI は地獄?
- フランケンコンパイルとは
- 壊れた SL の華
なんか走るルーターの話はどこかで聞いたことありますね……。疲れたのでまた今度