(言語基礎(1)— コンパイル結果のアセンブリを読む)= # 言語基礎(1)— コンパイル結果のアセンブリを読む ## この章で学ぶこと - 前章リスト-1(`language_basics_minimal.mojo`)のコンパイル結果が、スタック上で何をしているか - 定数畳み込み、`List` 構築、文字列オブジェクト、引数テーブル、後片付けパターンの流れ 前章では、言語基礎の最小サンプル(リスト-1)を示しました。 この章では、そのコンパイル結果(リスト-2)を手がかりに、`main()` の機械語を段階的に追います。 このコードをコンパイルすると、実際には機械語へ変換されます。 ```{literalinclude} ../../../src/part2/ch05/language_basics_minimal.asm ```
リスト-2: language_basics_minimal.asm
## リスト-2 を段階的に読む 全体を段階的に解説します。オフセットや命令列は、手元の `objdump` 結果と Mojo / LLVM の版によってわずかに変わり得ますが、**リスト-2 のダンプ**を前提に読み進めてください。ニモニックは{numref}`最小サンプル main のアセンブリを読む` と同様に **AT&T 記法**です。 ### 関数プロローグ ```text 0: subq $776, %rsp ``` スタックに **776 バイト**を確保します。{numref}`最小サンプル main のアセンブリを読む` の **136 バイト**より大きいのは、複数の変数・文字列オブジェクト・引数用テーブルを **すべてスタック上に載せる**ためです。 ### ① 変数の初期化(コンパイル時定数の最適化) ```text ; var n: Int = 42 7: movq $42, 232(%rsp) ; var doubled = n * 2 13: movq $84, 240(%rsp) ``` 注目点は、`n * 2` の計算が **コンパイル時に完了** しています。`42 * 2 = 84` がそのまま即値として埋め込まれており、実行時に乗算命令は一切発生しません。Mojo コンパイラ(LLVM)の **定数畳み込み最適化**です。 ### ② `List[Int] = [1, 2, 3]` の構築 ```text ; 要素をスタックに配置 1f: movq $1, 272(%rsp) 2b: movq $2, 280(%rsp) 37: movq $3, 288(%rsp) ``` まず 3 つの整数をスタック上に並べます。 ```text ; 各要素へのポインタのテーブル 43: leaq 272(%rsp), %rax 4b: movq %rax, 296(%rsp) ; &xs[0] 53: leaq 280(%rsp), %rax 5b: movq %rax, 304(%rsp) ; &xs[1] 63: leaq 288(%rsp), %rax 6b: movq %rax, 312(%rsp) ; &xs[2] ``` 要素アドレスのテーブルを組み立てます。Mojo の `List` は、内部で **要素を指すポインタ列**を持つ形としてコードが生成されます。 ```text 73: movl $3, %esi ; 要素数 = 3 78: leaq 296(%rsp), %rdi ; ポインタテーブルの先頭 ``` ```text ; メタデータを 2 箇所にコピー(後で説明) 81: movq %rdi, 696(%rsp) 89: movq $3, 704(%rsp) 96: movq %rdi, 712(%rsp) 9e: movq $3, 720(%rsp) aa: leaq 248(%rsp), %rdx ; List 本体の格納先 b2: callq ... ; List 構築関数の呼び出し ``` ### ③ `List` 構築後の後処理 ```text b7: leaq 327(%rsp), %rdi bf: callq ... ; 内部初期化(アロケータ等) ``` `List` の内部状態を整える **追加の呼び出し**です。 ### ④ `String("count")` の構築 {numref}`最小サンプル main のアセンブリを読む` の `Hello, Mojo` と同型です。 文字列オブジェクトは **データポインタ・長さ・タグビット・関数ポインタ 2 本** というワード列でスタック上に現れます。 `"count"` 以降の `"n="`・`" sum="` なども、同じレイアウトで繰り返し構築されます。 ### ⑤ `print()` の引数構築(もっとも複雑な部分) `print(String("n="), n, String(" sum="), doubled, String(" "), label, String("="), len(xs))` の **8 引数**のために、広いアドレス範囲で準備が続きます。 `133`〜`2db` 付近では、次の文字列リテラルが **ほぼ同じ手順**で構築されます。 | アドレス範囲(目安) | 構築している文字列 | 長さ | |----------------------|-------------------|------| | `133`〜`186` | `"n="` | 2 | | `18e`〜`1f5` | `" sum="` | 5 | | `1fd`〜`264` | `" "` | 1 | | `26c`〜`2d3` | `"="` | 1 | 各オブジェクトは、第④と同様のレイアウト(データポインタ・長さ・タグ・関数ポインタ 2 本)で並びます。 ```text ; len(xs) の取得 2dc: movq 256(%rsp), %rax 2e4: movq %rax, 216(%rsp) ; len(xs) の結果を保存 ``` ここでは、直前の処理で取得した `len(xs)` の結果がスタック上の `256(%rsp)` に格納されており、それを別の場所へコピーしています。 ### ⑥ `print()` 本体の呼び出し x86-64 のレジスタ引数(`rdi`〜`r9` の 6 本)だけでは 8 引数を渡しきれないため、**スタック上に引数テーブルを組み立ててから呼び出す**経路が現れます。 ```text 368: leaq 368(%rsp), %rdi ; String("n=") 370: leaq 408(%rsp), %rdx ; String(" sum=") 378: leaq 448(%rsp), %r8 ; String(" ") 380: leaq 328(%rsp), %r9 ; label = String("count") 388: callq ... ; print() 本体 ``` ### ⑦ エラーチェックとデストラクタ処理(5 回繰り返し) `print()` のあと、**スタック上に作った文字列オブジェクトごとに**、第1部第2章と同型の **後片付けパターン**が繰り返されます(リスト上は次のような区間に現れます)。 - `38d`〜`45b` … `String("n=")` - `45d`〜`51f` … `String(" sum=")` - `521`〜`5e3` … `String(" ")` - `5e5`〜`6a7` … `String("count")` - `6a9`〜`76b` … `String("=")` 各ブロックは、おおむね次の骨格です。 ```text ; ① エラーフラグチェック movabsq $0x4000000000000000, %rax andq (フラグ保存場所), %rax cmpq $0, %rax je → エラーなし → スキップ ; ② エラーあり → refcount をアトミックにデクリメント movq $-1, %rax lock xaddq %rax, (%rcx) ; ③ 旧 refcount が 1 か(最後の参照か) cmpq $1, %rax jne → 他に参照あり → スキップ ; ④ 最後の参照 → 解放/デストラクタ movq ..., %rdi callq ... ; ⑤ ランディングパッド(4 つの jmp) ; eb 02 / eb 00 / eb 02 / eb 00 ``` 同じ形が **5 回分** 繰り返されるため、コードが非常に長くなります。 ### 関数エピローグ ```text 76d: addq $776, %rsp ; スタック解放 774: retq ; 呼び出し元へ戻る 775: nopw %cs:(%rax,%rax) ; アライメント用パディング ``` 末尾の `nopw` は、**次の関数の先頭を 16 バイト境界に合わせる**ためのパディングです。今回は **11 バイト**と長くなっていますが、これは必要なパディング量が多かったためです。 ### 全体の流れ(まとめ) ```text スタック確保(776 バイト) ↓ n = 42, doubled = 84 を配置(定数はコンパイル時に畳み込み済み) ↓ List[Int]{1,2,3} をスタック上に構築 ↓ String("count") をスタック上に構築 ↓ print 用の文字列リテラルを構築("n=", " sum=", " ", "=") ↓ len(xs) を取得 ↓ 引数テーブルを組み立てて print() 呼び出し 【後片付け × 5 回】各 String について:エラーチェック → refcount デクリメント → 必要なら解放 ↓ スタック解放・return ``` {numref}`最小サンプル main のアセンブリを読む` の `Hello, Mojo` と比べると、**同じ「文字列を組み立てる → 後片付けする」パターンが、引数の数だけ機械的に繰り返される**点が大きな違いです。Mojo が **文字列リテラルを含む引数ごとに独立したオブジェクトとして扱い、それぞれ参照カウントで管理する**様子が、ダンプから追いやすくなっています。