(最小サンプル main のアセンブリを読む)= # 最小サンプル `main` のアセンブリを読む ## この章で学ぶこと - リスト-2(`hello_mojo_minimal.asm`)の `main` が、スタック上で何をしているか - 文字列オブジェクトの組み立て、`print` 呼び出し、エラー/例外まわりの分岐、参照カウントと解放の流れ {numref}`Mojo とは何か・位置づけ` では、ソース(リスト-1)を示し **「見た目の違い」** に触れました。 この章では、そのコンパイル結果(リスト-2)を手がかりに、`hello_mojo_minimal::main()` の機械語を追います。 ## この章の3つのポイント アセンブリの細部に入る前に、この章で確認したい **3 つのことを先に示します**。 1. **文字列オブジェクトはスタック上に組み立てられる** Python のように全てがヒープのオブジェクトになるのではなく、スタック上にサイズ・ポインタ・フラグをレイアウトした構造体として組まれます。 2. **エラー状態は戻り値のビットフラグで表される** `print()` の結果ワードに特定ビット(`0x4000...`)が立っているかをチェックして、エラーあり/なしを判定しています。Mojo の例外処理が機械語レベルでどう実装されるかの一例です。 3. **参照カウントのデクリメントはアトミック操作で行われる** `lock xaddq` によって、マルチスレッド下でも安全に参照数を操作します。シンプルな `print("Hello, Mojo")` 一行でも、Mojo のメモリ安全の仕組みが機械語に反映されています。 以降は、この 3 点を念頭に置きながら、実際の命令列を確認します。 ```{literalinclude} ../../../src/part1/ch01/hello_mojo_minimal.asm ```
リスト-2: hello_mojo_minimal.asm
ここに載せるニモニックは、リスト-2 と同じく **AT&T 記法**(`objdump` の出力)です。 Mojo や LLVM のバージョンによって細部は変わり得ますが、**上記のダンプ**を前提に読み進めてください。 ## 関数プロローグ スタックに **136 バイト**を確保し、ローカル変数や一時データの置き場を作ります。 :::{note} 命令の詳細 ```text 0: subq $136, %rsp ``` ::: ## 文字列オブジェクトの構築 スタック上に、文字列長・データポインタ・タグビットを並べた構造体を組み立てます。 `leaq (%rip), %rax` はリンク前のプレースホルダーで、リンク時に実際のアドレスが埋まります。 タグビット(`0x2000000000000000`)は参照カウントまわりのフラグとして使われます(ビット割り当ては実装依存)。 :::{note} 命令の詳細 ```text 7: movq $11, 40(%rsp) ; 文字列長 "Hello, Mojo" = 11 文字 1a: leaq (%rip), %rax 21: movq %rax, 56(%rsp) ; 関数ポインタ的なもの① をスタックへ 26: leaq (%rip), %rax 2d: movq %rax, 64(%rsp) ; 関数ポインタ的なもの② をスタックへ 32: leaq (%rip), %rax 39: movq %rax, 32(%rsp) ; 文字列データへのポインタ("Hello, Mojo") 3e: movabsq $0x2000000000000000, %rax 48: movq %rax, 48(%rsp) ; タグ/フラグビット(後で参照される) ``` ::: ## 参照カウントの初期化 値として扱う型でも、ランタイム側で **参照カウント** を伴う経路が生成されることがあります。ここでは refcount を **1** で初期化しています。 :::{note} 命令の詳細 ```text 4d: movq %rsp, %rax 50: movq $1, (%rax) ; スタック先頭に refcount = 1 をセット ``` ::: ## `print()` の呼び出し準備 x86-64 Linux の **System V AMD64 ABI** に従い、整数・ポインタ引数を `rdi`, `rsi`, `rdx`, `rcx`, `r8`, `r9` の順に並べて `print` 本体を呼び出します。 :::{note} 命令の詳細 ```text 57: leaq (%rip), %rsi ; 引数: 文字列データポインタ("Hello, Mojo") 5e: leaq (%rip), %rcx ; 引数: メタデータ(vtable 等に相当し得るポインタ) 65: leaq 32(%rsp), %rdi ; 引数: 文字列オブジェクト本体へのポインタ 6a: movl $1, %r8d ; 引数: 要素数 = 1 70: xorl %r9d, %r9d ; 引数: 0(フラグ等) 73: movq %r8, %rdx ; rdx = 1 76: callq 0x7b ; print 本体の呼び出し(相対オフセットはリンク前の見え方) ``` ::: ## エラー/例外チェック(Mojo のエラー処理) `print()` の戻り値ワードに **エラー状態を表すビット** が埋め込まれています。 `0x4000000000000000` というマスクで特定ビットを取り出し、ゼロなら**エラーなし**としてエピローグへ、ビットが立っていれば**エラーあり**としてデストラクタ処理へ進みます。 :::{note} 命令の詳細 ```text 7b: movabsq $0x4000000000000000, %rax 85: andq 48(%rsp), %rax ; 先ほど保存したフラグビットと AND 8a: cmpq $0, %rax 8e: je 0x103 ; フラグが立っていなければ後処理なしで終了側へ ``` ::: ## エラーありの場合:参照カウントのアトミックデクリメント エラーが発生した場合、文字列オブジェクトの参照カウントを `lock xaddq` でアトミックにデクリメントします。 旧カウントが **1**(自分が最後の所有者)だった場合のみ、デストラクタ呼び出しへ進みます。 :::{note} 命令の詳細 ```text 90: movq 24(%rsp), %rax ; 文字列オブジェクトへのポインタ 9e: addq $-8, %rax ; ポインタを 8 戻す(実際の refcount の位置) c1: movq $-1, %rax c8: lock c9: xaddq %rax, (%rcx) ; アトミックに refcount -= 1(旧値を rax へ) dc: cmpq $1, %rax ; 旧 refcount が 1 だったか? e0: jne 0xff ; 1 でなければ他に参照があるので解放不要へ e2: jmp 0xe4 ; 1 なら解放処理へ e4: movq 16(%rsp), %rdi ; 解放対象のポインタを rdi へ f8: callq 0xfd ; デストラクタ/free に相当する呼び出し ``` ::: ## ランディングパッドと合流 `callq`(デストラクタ/解放に相当する呼び出し)の直後に、**4 本の短い `jmp`** が並んでいます。リスト-2 では次のとおりです。 ```text fd: eb 02 jmp 0x101 ff: eb 00 jmp 0x101 101: eb 02 jmp 0x105 103: eb 00 jmp 0x105 ``` これらは **ランディングパッド(landing pad)/例外処理用のスタブ** としてコンパイラが差し込んだコードです。 Mojo は LLVM を基盤にしているため、C++ と同様に **LLVM の例外処理機構**(Windows では SEH、ELF 系では **DWARF unwinding** など、ターゲットに応じたアンワインド情報)に沿ったコードが生成されます。`callq` で呼んだ関数が、例外・パニック・スタック展開時のクリーンアップを要する場合に、「制御がどのラベルへ戻るか」をアンワインダが解析できるよう、**合流点までの骨格**が置かれます。 構造としては次のように読めます。 - **`fd`〜`ff`** … 直前の `callq` が **正常終了した**あとにフォールスルーしてくる経路。いずれも **`0x101` へジャンプ**する。 - **`101`〜`103`** … さらに **追加の後処理なし**で、関数エピローグの **`0x105`**(`addq`/`retq`)へジャンプする。 `eb 02` と `eb 00` がペアで現れるのは、**2 バイト境界へのアライメント調整**と、unwinder がどちらのエッジから来ても **同じオフセットの合流点**へ到達できるようにするための、いわば冗長なジャンプ列です。 これらはリンク時に特別なシンボル解決で「意味が変わる」というより、**コンパイル時に LLVM が生成した例外処理・クリーンアップパスの骨組み**です。この最小サンプルでは実質的には **何もせずエピローグへ流れるだけ** の経路になっており、リスト上は短い `jmp` の並びに見えます。 **ランディングパッドを素朴に理解する** ### 例外とは 実行中に、確保に失敗する・前提が崩れるなど、**呼び出し元がその場で処理しきれない事態**が起きることがあります。 ```text main() └→ print() └→ (内部でメモリ確保やランタイム呼び出し) └→ 「続行できない」← ここでエラー ``` このとき、**どの関数から順に片付けをしてから**実行を止めるかを決める必要があります。 ### 呼び出しの積み重ねとスタック 関数は入れ子になって呼ばれます。 ```text main() が print() を呼ぶ print() が(内部で)さらに別の関数を呼ぶ … いちばん深いところでエラー発生 ``` スタックで表すと、**いま実行中のフレームが上**に積まれます。 ```text +-------------+ ← スタックの上 | 深い側の関数 | ← ここでエラー +-------------+ | … | +-------------+ | print() | +-------------+ | main() | +-------------+ ← スタックの下 ``` エラーが起きたあと、この積み重ねを **外側(呼び出し元)へ向かって順に巻き戻す** 処理が **スタックアンワインド(stack unwinding)** です。 ### なぜ巻き戻しが必要か 各フレームは、オブジェクトやバッファなど **解放が必要な状態** を持っていることがあります。エラーで途中打ち切りにすると **リークや不整合** が残るため、**各段で「後片付け」を挟んでから** 外へ戻る必要があります。 ### ランディングパッドの役割 アンワインド時、コンパイラは **「この呼び出しに対応する片付けの入口はここだ」** という情報を機械語に埋め込みます。その入口に相当するのが **ランディングパッド(landing pad)** です。通常の成功経路では通らず、**例外やランタイムが「ここへ一旦着陸して片付けろ」と指示したとき** に使われる、**後片付けコードへの玄関** だと捉えるとよいでしょう。 ### リスト-2 で見る位置づけ 概念の説明では「任意の `callq` の直後/近傍」と考えてかまいません。いま引用している **`fd`〜`103`** は、リスト-2 では **`f8: callq`(デストラクタ/解放に相当する呼び出し)の直後** から始まります。別の `callq`(たとえば `print` 本体)のまわりにも、LLVM は同様の骨組みを付けることがあります。 ```text f8: callq … ← ここから戻ってきた直後が fd fd: jmp 0x101 ff: jmp 0x101 101: jmp 0x105 103: jmp 0x105 ``` アンワインドが発生すると、**ランタイム側(アンワインダ等)** が「この `callq` に対応するランディングパッドはどこか」を DWARF 等の情報から調べ、**適切な `fd` 付近へ制御を移す**ことがあります。そのあと **後片付け(参照カウントや解放など)** を経て、**`0x105` のエピローグ**へ合流します。 ### なぜ `jmp` が複数並ぶのか LLVM は **正常に `callq` から戻ってきた場合** と **アンワインドで「着陸」した場合** の **入口の形をそろえつつ**、最終的に同じエピローグへ合流できるように、短い `jmp` を並べます。見かけ上は - **パスA** … `callq` が成功して戻り、機械語としては **`fd` 側から** `0x101` → `0x105` へ進む - **パスB** … アンワインドで **別エッジから** 合流点へ入り、同様に `0x105` へ向かう といった **二系統の入口を、同じオフセットに収束させる** ために、`eb 02` と `eb 00` で **2 バイト単位のアライメントを保ちながら** 冗長なジャンプを挟む、というイディオムが現れます(詳細は命令列ごとに異なります)。 `eb 02` は相対ジャンプで **2 バイト先** へ飛び、`eb 00` は **次の命令へそのまま進む**(相対 0)という形で、**アライメントを保ちつつ二つの入口を作る** LLVM のパターンとして説明されることがあります。 ### 用語の整理 | 用語 | 意味 | |------|------| | 例外 | 実行中に発生し、通常の戻り値だけでは処理しきれないエラーなどの事象 | | スタックアンワインド | エラー後に、内側のフレームから外側へ向かって順に巻き戻す処理 | | ランディングパッド | 各フレームまわりの **後片付けコードへの入口**(LLVM が生成する例外処理の骨格の一部) | `print("Hello, Mojo")` のように見える一行でも、内部でオブジェクトやランタイム呼び出しが伴うため、**ソース上は何も書いていなくても** LLVM がこのようなスタブを生成します。Mojo の **安全なメモリ管理や例外処理が、機械語側で自動的に組み込まれる** 一例です。 ## 関数エピローグ ```text 105: addq $136, %rsp ; スタック解放(プロローグの逆) 10c: retq ; 呼び出し元へ戻る 10d: 0f 1f 00 ; nopl (%rax) — 下記のとおりパディング ``` ### `retq` の直後の `nopl` について `nopl` は **アライメント用の NOP(パディング)** です。目的は **次の関数の先頭アドレスを、特定のバイト境界(多くの場合 16 バイト)に整える**ことにあります。 このダンプでは **`0x10d` の次のアドレスが `0x110`** になり、**ちょうど 16 バイト境界**になります。x86-64 では関数エントリを 16 バイト境界に整えると、命令フェッチや一部マイクロアーキテクチャ上で有利になることがあります。 バイト列 `0f 1f 00` は **マルチバイト NOP**(Intel が推奨する形式の長い NOP)です。単に `90`(1 バイト NOP)を 3 つ並べるより、**パイプラインへの影響が小さい**という理由でコンパイラが選ぶことがあります。 `retq` のあとに続く命令は **実行されません**。制御はすでに呼び出し元へ戻っているため、`nopl` は動作の正しさではなく **直後に配置される次関数のためのレイアウト**のために挿入されています。 ### まとめ(ランディングパッド列と `nopl`) | 箇所 | 目的 | |------|------| | `fd`〜`103` の 4 つの `jmp` | LLVM 例外処理機構に沿ったランディングパッドの骨格(このケースでは実質的にエピローグへ合流するだけ) | | `10d` の `nopl` | 次の関数を 16 バイト境界に整えるためのパディング(`retq` 後は未実行) | ## 全体の流れ(まとめ) ```text スタック確保 ↓ "Hello, Mojo" 文字列オブジェクトをスタック上に構築 ↓ print() 呼び出し ↓ エラーフラグチェック ├─ エラーなし → そのままエピローグ └─ エラーあり → 文字列オブジェクトの参照カウントをアトミックにデクリメント └─ 旧カウント == 1 → デストラクタ呼び出し → エピローグ スタック解放・return ``` 単純な `print("Hello, Mojo")` でも、**参照カウントの初期化**、**アトミックな減算**、**条件付き解放**まで機械語に落ちていることが、このダンプから読み取れます。Mojo が **メモリ安全** を機械側でどう支えているかの一端だと捉えるとよいでしょう。 ## 出典・参照 - [Quickstart](https://docs.modular.com/mojo/manual/quickstart/)(最小サンプルの出典)