Closed19

compilerbook (→ x86-64 入門程度まで)

toyboot4etoyboot4e

レジスタ再び

DEBUG HACKS という本を買いました。この本では、『デバッグ前に知っておくべきこと』と称して x86-64 が紹介されます。やっぱりアセンブリって教養なんですか……?:

https://www.oreilly.co.jp/books/9784873114040/

たまには compilerbook にも取り組んでみます。以前は Step 7 の比較演算子で挫折しました:

https://www.sigbus.info/compilerbook

リポジトリ:

https://github.com/toyboot4e/cinc

toyboot4etoyboot4e

背景

事前学習

3 ~ 4 年前に Crafting Interpreters の第二章をやりました (loxrs) 。

  • BNF や再帰下降型パーサはわかります。
  • スタックマシンも電卓レベルなら理解しています。

https://craftinginterpreters.com/

compilerbook の趣旨

ではなぜ 2 年前に compilerbook を挫折したかというと、レジスタが分からなかったためです。

compilerbook では、 C のソースを x86-64 のアセンブリに変換します。アセンブリでは初めからスタックマシンを利用できますから、その点はインタープリタを作るよりも楽です。しかし計算実行にレジスタを使わなければならないため、『足す』とか『引く』みたいな単純な計算においてもレジスタが主役になります。

レジスタを使いこなすことが compilerbook の趣旨だと思います。

期待すること

  • スタックマシンに詳しくなりたい (→ toylispの開発に繋げたい)
    • for 文を単純な goto に展開する
    • 末尾再帰の最適化など
  • 低レイヤに詳しくなりたい (→ DEBUG HACKS を読みたい、活かしたい)
    • レジスタとアセンブリに詳しくなりたい
    • アドレスや仮想メモリに詳しくなりたい (載っているかな?)

ストイックに 29 ステップやることはないと思いますが、ぼちぼちやってみます。

toyboot4etoyboot4e

レジスタは序の口だったという所まで進められない気がします

toyboot4etoyboot4e

現状の入出力 (〜 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 が返値として用いられる
toyboot4etoyboot4e

環境構築

そういえばやっていました。デバッガについては必要になったら準備してみます。

compilerbook に解説があるもの:

解説が無かったもの:

toyboot4etoyboot4e

あっ……

ハイライトの都合上 ; をコメントとして使っていますが、本当のコメントは # みたいです。

toyboot4etoyboot4e

ステップ 9: 1 文字のローカル変数

main 関数に 26 個のローカル変数を持たせます。目標設定が上手い!

2 種類の stack ポインタ

  • RSP (register's stack pointer)
    『現在の』スタックポインタです。pushpop の度に移動します。明示的な操作も可能です。
  • 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
toyboot4etoyboot4e

ベースポインタの連結リスト

関数プロローグで RBP の連結リストを伸ばしていました。これはいい!

  • ローカル変数はベースポインタからのオフセットで表現します
  • スタックの pop (ret) は、ベースポインタの連結リストの pop に相当します

for 文やらネストしたスコープも実装できる気がして来ましたよ!

toyboot4etoyboot4e

ステップ 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
toyboot4etoyboot4e

ステップ11:return文

ベースポインタを pop するだけ。つまり、関数エピローグを関数の途中に挿入するだけです。あるいは関数の末尾に暗黙の return 文があるとも捉えられます。

return 文を関数エピローグへの jmp として実装することもできます。

次回は制御構文です。ベースポインタが分かったので、ソースコードから CFG (control flow graph) への展開も当たり前に見えてくる気がします。ここから解説が薄くなるそうですが、実装できるでしょうか……?

toyboot4etoyboot4e

TODO: 式文の後始末

return 文の追加に伴って、 2; とか 3; のような式文の結果が stack から pop されなくなりました。 if などで消費されない式文の出力は破棄しなければなりません。

toyboot4etoyboot4e

ステップ 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 です。

参考

https://web.stanford.edu/class/cs107/guide/x86-64.html

http://unixwiz.net/techtips/x86-jumps.html

https://lipoyang.hatenablog.com/entry/20131031/p1

https://riptutorial.com/x86/example/6976/flags-register

https://stackoverflow.com/questions/65517267/how-are-the-sign-and-overflow-flags-used-in-x86-64-assembly-language-to-determin

脚注
  1. 64 ビットマシンの場合 ↩︎

toyboot4etoyboot4e

ステップ 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
toyboot4etoyboot4e

どこまでやるか

大雑把に 3 言語を比較してみます。

比較項目 Lox (Crafting Interpreters) C (compierbook) toylisp (作りたい言語)
ランタイム 自作 VM x86-64 自作 VM
マシン種別 スタックマシン スタックマシン スタックマシン
型付け 動的型付け 静的型付け 静的型付け
型推論 なし 型推論したい
ユーザー定義型 class struct class 風
文法 簡単 艱難辛苦 マクロがある
エラー検査 synchronize 即 exit 全検査 + LSP 出力
メモリ管理 GC malloc / free Vec へのバインド?
最適化 若干 若干

C から学びたいのは静的型付け (型推論なし) の部分です。関数呼び出しや構造体のメンバ解決などのコンパイル部分を見れたら引き上げようかと思います。

あと C 構造体の ABI を理解できたら、ホスト言語とのデータのやり取りがスムーズになる気がします。

toyboot4etoyboot4e

2 年前の挫折の理由 (2)

気づいたこと

course2020 を見たところ、ソースが本の内容と一致していません。何ならアセンブリの記法すら違います (Intel 記法にも色々あるようです) 。その辺の説明を聞き逃していことが、最大の挫折原因だったかもしれません。

憶測では、 course2020 は先回りして上手に書いてあるソースです。履歴としては綺麗な反面、知識面でインクリメンタルなのは rui314/chibicc の方かもしれません。引き続き chibicc の方を見ます。

アーカイブ 確認

course2020 (ジンク) の方は本の内容と不一致だと明言されていました。

  • パーサの違い
    ステートフルかステートレスか (ストリームか (読み取った値、残り) の返却か) 。後者はピークに強かったりプリプロセッサの扱いに向いている。
  • コード生成の違い
    chibicc では常に push する。 course2020 ではレジスタを深さ 6 のスタックと見做して利用する。ただし 7 つ目のスタックが積めない時は abort する。

初回でこれを聴いても付いていけなかったのでしょうね。当時 chibicc の方を読んでいたら、案外スラスラ進めたかもしれません。

toyboot4etoyboot4e

push / pop の数を揃えてみた

Discard フラグが ON の場合は、スタックに値を残さないように調整してみました。

3 + 4;

式文も値を残さなくなりました。

  push 3
  push 4
  pop rdi
  pop rax
  add rax, rdi
  push rax
  # discard
    pop rax
toyboot4etoyboot4e

関数呼び出し

もう続きを作るつもりは無い のですが、スタックマシンについて知りたいので x86-64 を調べていきます。

スタックの使い方

こちらの記事が素晴らしかったです。以降では、記事中のコードを拝借して x86-64 の使い方を確認します。

https://qiita.com/tobira-code/items/75d3034aed8bb9828981

数個の引数を取る関数

レジスタで処理が終わってしまい、スタックの使い方が見れません。

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>: leardi+r9eax に代入します。 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 バイトじゃなく……? ほわぁあああああああ

toyboot4etoyboot4e

おおお (セキュキャン 2022 ログ)

https://sozysozbot.github.io/seccamp-2022-c-compiler-seminar/

面白かったです。 PDF にして 150 ページ! 皆さま非常に逞しく、講師の方は異常に強く (各種言語にも強く) 、魔法学校の件ではお世話になっております (?) 。また知識の深い人ほど意外とアウトプットしてくれない (本を読むだけ・ソースを読むだけとか言い出す) もので、 1 対 1 のコミュニケーションは貴重だなという気もしました。いいなぁ。

私用メモ
  • 教材のポインタ本 がめちゃめちゃ強いみたい
  • プリプロセッサとか大変そうだなと
  • 意味解析を追加したりパスを分けたり
  • そういえば関数呼び出しに関するアラインメントがあるので、それが構造体のアラインメントと噛み合って word 単位で読めるようになっている?
  • SSA (静的単一化代入) が非常に優秀な最適化らしいです。 MinCaml とは immutable なので話が違いそう
  • C (の GCC 拡張?) には文式というのがあるようで、 ({ .. }) の中の最後の文を式に変えます。えぇ……?
  • 『push と pop の対を消すのは peehole 最適化ででき』るそうです。
  • 『スカラ』というのは primitive + 組み込み演算子という感じみたいです。
  • 構造体の返し方が謎…… 関数呼び出しの ABI
  • スコープを作るときは、コールフレームのサイズは一番大きなスコープの分確保する。そうとは思っていましたが、 裏付けが取れると心強いものですね。
  • ピンポイントで標準言語仕様からポイントを見つけていて訳がわからないよ!
  • 出たよ mod と準同型
  • 小数型に関する ABI は地獄?
  • フランケンコンパイルとは
  • 壊れた SL の華

https://www.youtube.com/watch?v=m1qyzcyftDQ

なんか走るルーターの話はどこかで聞いたことありますね……。疲れたのでまた今度

このスクラップは2022/10/10にクローズされました