アセンブラ
目次
- 概要
- アセンブラとは何か
- なぜ今でも学ぶ価値があるのか
- 機械語とアセンブリの関係
- レジスタ
- メモリアクセス
- フラグレジスタと比較
- 制御フロー
- スタックと関数呼び出し
- プロローグとエピローグ
- 呼び出し規約ABI
- 具体的なコード例
- Linuxシステムコール例
- x86-64とARM64の見比べ方
- SIMD命令の入口
- 逆アセンブルとデバッグの見方
- 実際に組み立てて動かす最小サンプル
- Cからアセンブリ関数を呼ぶサンプル
- コンパイラ出力をどう読むか
- 実務での使いどころ
- x86-64アーキテクチャの詳細仕様
- 条件分岐とループ最適化
- SIMD命令とベクトル処理
- インライン アセンブリ
- まとめ
- 参考文献
概要
命令・レジスタ・スタック・呼び出し規約を具体的なコードで理解する
アセンブラは、CPUが実行する命令列を人間が読める形で記述するための低水準言語です。高級言語と機械語のちょうど間にあり、コンパイラ と CPU をつなぐ位置にあります。
アセンブラを学ぶと、関数呼び出し、スタック、レジスタ、条件分岐、メモリアクセスが具体的に見えるようになります。高級言語のコードが最終的にどう落ちるかを知ると、コンパイラやCPUの話が一気に現実的になります。
この章で重視すること
- アセンブラを「昔の人の言語」ではなく、現在の実行モデルを理解する窓として使う
- x86-64を中心に、必要に応じてARM64も対比する
- レジスタ、スタック、呼び出し規約、条件分岐を具体的なコードで理解する
- CやRustの関数が、どのような命令列に落ちるかを追う
アセンブラとは何か
アセンブラは、CPU命令をニーモニックで記述した低水準言語です。
たとえば
mov rax, 1
add rax, 2
は、
raxに1を入れるraxに2を足す
という命令です。
高級言語と違って、
- 変数の置き場所
- 一時値の扱い
- 分岐
- 関数呼び出し
がかなり露骨に見えます。
なぜ今でも学ぶ価値があるのか
普段はC、Rust、Go、Javaなどで十分に開発できます。それでもアセンブラを知る価値があるのは、次の問いに答えやすくなるからです。
- 関数呼び出しで何が保存されるのか
- 再帰でスタックがどう増えるのか
- 分岐予測ミスがなぜ痛いのか
- コンパイラの最適化が何をしているのか
- メモリ破壊がなぜ危険なのか
機械語とアセンブリの関係
CPUが本当に実行するのは機械語です。アセンブリは、その機械語を人間が扱いやすくした表記です。
コンパイラ の コード生成 は、まさにこの橋をどう渡るかを扱っています。
高級言語からこの形へ落ちる過程を先に見たいなら、コンパイラ の コード生成、レジスタ割付、関数呼び出しとABI が直接つながります。
レジスタ
レジスタは、CPUの中にある非常に高速な記憶領域です。
x86-64の代表例
raxrbxrcxrdxrsirdirsprbp
直感
- レジスタは「いま使う値の机の上」
- メモリは「少し遠い棚」
というイメージが役立ちます。
この 机の上 と 棚 の感覚を、CPUの実装側から見ると CPU の レジスタとISA、キャッシュとメモリ階層 に対応します。
役割の例
rax: 戻り値や計算用rsp: スタックポインタrbp: フレームポインタとして使われることがあるrdi,rsi,rdx,rcx,r8,r9: 引数渡しに使われることが多い
メモリアクセス
レジスタだけでは足りないので、メモリから値を読み書きします。
mov rax, [rbp-8]
mov [rbp-16], rax
これは、
rbp-8にある値をraxに読むraxの値をrbp-16に書く
という意味です。
アドレッシング
アセンブラでは
- 即値
- レジスタ
- メモリアドレス
- ベース + オフセット
を明示的に書くことが多いです。
スケール付きアドレッシング
x86-64では
mov rax, [rdi + rcx*8]
のように、base + index * scale の形がよく出ます。これは
rdi: 配列の先頭rcx: 添字8: 要素サイズ
という意味です。long 配列やポインタ配列を読むときに頻出します。
loadとstoreの直感
高級言語では代入一つに見えても、低水準では
- メモリから読む
- レジスタで計算する
- メモリへ書く
という3段階に分かれることが多いです。性能を考えると、この「読む回数」「書く回数」がかなり効きます。
フラグレジスタと比較
比較命令は、多くの場合「真偽値をそのままレジスタへ入れる」より、まずフラグレジスタを更新します。
cmp rax, rbx
je equal
ここでは cmp が内部的に引き算に近い比較を行い、その結果をフラグへ反映します。je はそのフラグを見て分岐します。
代表的なフラグ
ZF: ゼロフラグ。結果が0なら立つSF: 符号フラグCF: キャリーフラグOF: オーバーフローフラグ
なぜ重要か
アセンブラを読むとき、cmp や test の直後に来るジャンプは「どのフラグを見ているのか」を意識すると理解しやすくなります。
cmp と test
cmp a, b:a - b相当の比較をしてフラグだけ更新test a, b: AND的な判定をしてフラグだけ更新
null判定やビット判定で test がよく出ます。
制御フロー
高級言語の if や while は、最終的には比較とジャンプになります。
cmp rax, rbx
jl smaller
これは「rax < rbx なら smaller へ飛ぶ」という意味です。
代表的なジャンプ
jmp: 無条件ジャンプje: 等しければ飛ぶjne: 等しくなければ飛ぶjl: 小さければ飛ぶjg: 大きければ飛ぶ
ラベルを使って流れを読む
アセンブラでは、if や while のような構文はなく、ラベルとジャンプで制御フローを作ります。したがって読むときは
- 比較している場所
- どのラベルへ飛ぶか
- どこでループに戻るか
を先に見つけるのがコツです。
ループの形の見分け方
- 末尾で先頭ラベルへ戻るなら
while/forっぽい - 先頭で条件を見ずに本体へ入ってから末尾で条件を見るなら
do whileっぽい
といった読み方ができます。
スタックと関数呼び出し
関数呼び出しでは、戻り先、ローカル変数、一時退避領域などを管理する必要があります。そのために使われるのがスタックです。
よく出る命令
pushpopcallret
call は戻り先を積んでジャンプし、ret は戻り先を取り出して戻ります。
call や ret がCPUの内部でどう扱われるか、分岐予測やreturn stack bufferの観点まで追うなら CPU の パイプラインと分岐予測 が補助になります。
スタックが伸びる方向
x86-64では、多くの場合スタックは高アドレスから低アドレスへ伸びます。つまり push で rsp は小さくなり、pop で大きくなります。
再帰とスタック
再帰関数が深くなると、呼び出しごとに
- 戻り先
- 保存レジスタ
- ローカル変数
が積まれるため、スタック使用量が増えます。再帰の理解にはアセンブラのスタック像がかなり効きます。
プロローグとエピローグ
関数の先頭と末尾では、かなり定型的な命令列が出ます。
典型的なx86-64の形
push rbp
mov rbp, rsp
sub rsp, 32
これは関数の プロローグ で、
- 古い
rbpを保存 - 新しいフレーム基準を作る
- ローカル変数用の領域を確保
しています。
末尾では
mov rsp, rbp
pop rbp
ret
のような エピローグ が出ます。
なぜ今は省略されることがあるのか
最適化が有効だと、フレームポインタを省略して rbp を汎用レジスタとして使うことがあります。すると逆アセンブルは少し読みにくくなりますが、レジスタ資源は増えます。
例: ローカル変数を使う関数
long f(long a) {
long x = a + 1;
long y = x * 2;
return y;
}
最適化前のイメージでは、x や y がスタック上のスロットへ一度落ちることがあります。最適化後はレジスタだけで終わることも多いです。
呼び出し規約ABI
同じCPUでも、「引数をどのレジスタに置くか」「どのレジスタは呼び出し側が保存するか」は約束が必要です。これがABIの一部です。
System V AMD64 ABIの例
整数系の先頭引数はよく次で渡されます。
rdirsirdxrcxr8r9
戻り値は通常 rax に入ります。
caller save / callee save
- caller save: 呼び出し側が壊れて困るなら自分で保存する
- callee save: 呼び出された側が戻る前に元へ戻す
この違いは、コンパイラのレジスタ割付や関数呼び出しコストに効きます。
つまりABIは、アセンブラの約束であると同時に、コンパイラの最終判断でもあります。そこは コンパイラ の 関数呼び出しとABI と往復すると理解しやすいです。
FFIで壊れやすい場所
外部言語やライブラリとつなぐときは、
- 引数サイズ
- 構造体レイアウト
- アラインメント
- 戻り値の渡し方
がずれると壊れます。見た目の関数シグネチャが合っていても、ABIがずれていれば危険です。
具体的なコード例
例1: 足し算する関数
高級言語で
long add(long a, long b) {
return a + b;
}
と書いたときのイメージは、x86-64では次のようになります。
add:
mov rax, rdi
add rax, rsi
ret
引数 a, b が rdi, rsi に入ってきて、戻り値を rax に置いて返しています。
例2: if文
long max2(long a, long b) {
if (a > b) {
return a;
}
return b;
}
max2:
cmp rdi, rsi
jg .a_is_bigger
mov rax, rsi
ret
.a_is_bigger:
mov rax, rdi
ret
例3: whileループで総和
long sum_to_n(long n) {
long s = 0;
long i = 1;
while (i <= n) {
s += i;
i += 1;
}
return s;
}
sum_to_n:
mov rax, 0
mov rcx, 1
.loop:
cmp rcx, rdi
jg .done
add rax, rcx
add rcx, 1
jmp .loop
.done:
ret
ここでは
rax: 累積和rcx: ループ変数rdi: 引数n
という役割です。
例4: 配列の先頭から最大値を探す
; rdi = 配列先頭アドレス
; rsi = 要素数
; 戻り値rax = 最大値
find_max:
mov rax, [rdi]
mov rcx, 1
.loop:
cmp rcx, rsi
jge .done
mov rdx, [rdi + rcx*8]
cmp rdx, rax
jle .next
mov rax, rdx
.next:
add rcx, 1
jmp .loop
.done:
ret
これで、ポインタ、スケール付きアドレッシング、比較、分岐が一度に見えます。
例5: 再帰の骨格
long fact(long n) {
if (n <= 1) return 1;
return n * fact(n - 1);
}
イメージ上は次のような流れになります。
fact:
cmp rdi, 1
jle .base
push rdi
sub rdi, 1
call fact
pop rcx
imul rax, rcx
ret
.base:
mov rax, 1
ret
実際の最適化後コードはもっと違うことがありますが、call 前後で何を保存するかを見るにはよい例です。
例6: switchの骨格
switch は単純なif-else連鎖になることもあれば、値が密ならジャンプテーブルになることもあります。アセンブラを読むと、コンパイラがどちらを選んだかが見えます。
Linuxシステムコール例
ライブラリを経由せず、直接カーネルへ入る最小例もアセンブラ理解には有効です。
x86-64 Linuxで標準出力へ書く例
global _start
section .data
msg db "hello, world", 10
len equ $ - msg
section .text
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, len
syscall
mov rax, 60
xor rdi, rdi
syscall
これは
write(1, msg, len)exit(0)
に対応します。
何が見えるか
OS や CPU の話とここがつながります。
ユーザモードからカーネルモードへ
この例では、ライブラリやランタイムをほとんど介さず、CPU命令で直接カーネル境界を越えています。syscall 一発が、OSとCPUの境界そのものです。
x86-64とARM64の見比べ方
ISAが違うと見た目はかなり変わりますが、役割は似ています。
x86-64の足し算
mov rax, rdi
add rax, rsi
ret
ARM64の足し算
add x0, x0, x1
ret
ARM64では
x0,x1, … が引数や戻り値に使われる- 命令形式が比較的そろっている
という違いがあります。
Intel記法とAT&T記法
x86には資料によって2つの記法が出ます。
- Intel syntax:
mov rax, rbx - AT&T syntax:
movq %rbx, %rax
オペランド順や即値表記が違うので、最初は混乱しやすいです。Linuxの objdump 出力などではAT&Tを見ることがあります。
SIMD命令の入口
スカラー命令だけでなく、複数要素を一度に処理するSIMD命令も重要です。
例
addps xmm0, xmm1
は、xmm0 と xmm1 に入った複数の浮動小数点値をまとめて加算します。
なぜ見る価値があるか
- コンパイラの自動ベクトル化が見える
- 行列計算や画像処理の速さの理由が分かる
CPUのSIMD節と直接つながる
逆アセンブルとデバッグの見方
アセンブラを学ぶときは、自分で全部書くより、まず逆アセンブルを見る方が実務的です。
代表的な道具
objdumpotoollldbgdb- Compiler Explorer
読む順番
- 関数の入口を探す
- 引数レジスタを見る
- 比較とジャンプを見る
callの前後で保存しているものを見る- 末尾の
retへどう到達するかを見る
デバッグ時に効く観点
- スタックトレースの意味
- フレームポインタの有無
- 最適化で変数が消える理由
- インライン展開で見え方が変わる理由
lldb で最初にやること
macOSでは lldb が標準的です。最初は次の流れで十分です。
- 実行ファイルを読み込む
mainで止める- レジスタを見る
- 1命令ずつ進める
- スタックを見る
典型的には次のように使います。
lldb ./asm_hello_macos_arm64
(lldb) breakpoint set --name main
(lldb) run
(lldb) register read
(lldb) disassemble --frame
(lldb) ni
(lldb) register read x0 x29 x30 sp
ここで見るべきなのは、
x0に何が入っているかspがどう動くかblの前後でx30がどう使われるか
です。x30 はlink registerで、ARM64では戻り先アドレスを保持します。
実際に組み立てて動かす最小サンプル
読むだけでなく、実際に1本動かすと理解が一気に進みます。ここではmacOS ARM64でそのまま動く最小サンプルを使います。
サンプルファイル
何をしているか
このプログラムは、
- 文字列のアドレスを
x0に入れる - Cランタイムの
_putsを呼ぶ - 戻り値
0をw0に入れて終了する
という最小構成です。
ビルドと実行
clang Tech/ComputerScience/scripts/asm_hello_macos_arm64.s -o /tmp/asm_hello_macos_arm64
/tmp/asm_hello_macos_arm64
どこを見るとよいか
stp x29, x30, [sp, #-16]!フレームポインタと戻り先を保存するadrpとadd文字列リテラルのアドレスを組み立てるbl _puts関数呼び出しldp x29, x30, [sp], #16保存した値を戻す
最初は1命令ずつ暗記するより、「入口で保存」「引数を置く」「呼ぶ」「戻す」という流れで見る方が理解しやすいです。
Cからアセンブリ関数を呼ぶサンプル
アセンブリ単体より、Cとつないで1関数だけ自分で書く方がABIの理解には向いています。
サンプルファイル
アセンブリ関数がしていること
この関数は long add2(long a, long b) に相当します。
- 第1引数
aはx0 - 第2引数
bはx1 - 戻り値は
x0
なので、実装はほぼ1行です。
_add2:
add x0, x0, x1
ret
ビルドと実行
clang \
Tech/ComputerScience/scripts/asm_add2_main.c \
Tech/ComputerScience/scripts/asm_add2_macos_arm64.s \
-o /tmp/asm_add2_demo
/tmp/asm_add2_demo
このサンプルで見えること
これは コンパイラ の ABI や コード生成 が、最終的に何を意味しているかを具体化する最短ルートです。
コンパイラ出力をどう読むか
アセンブラを学ぶ最大の実利の一つは、コンパイラが吐いたコードを読めるようになることです。
見るポイント
-O0 と -O2 の違い
-O0: 元コードの形に近く、読みやすいが冗長-O2: レジスタ利用や分岐整理が進み、短くなるが追いにくい
最初は -O0 で構造を見て、その後 -O2 で最適化の差を見るのが学びやすいです。
この読み方は、コンパイラ の 最適化 と コード生成、CPU の パイプライン や 性能の見方 と強くつながります。
何が消え、何が残るか
最適化後に見えやすい変化は次です。
- 不要なload/storeが消える
- ループ変数がレジスタへ載る
- 定数計算が先に済む
- 関数呼び出しがinline化される
- 条件分岐が簡略化される
これを見ると、コンパイラが「ソースをそのまま翻訳しているだけではない」ことがよく分かります。
実務での使いどころ
- クラッシュダンプやバックトレースの理解
- パフォーマンスボトルネックの解析
- FFI境界の調査
- コンパイラ最適化結果の確認
- セキュリティ脆弱性の理解
セキュリティとの接点
バッファオーバーフロー、ROP、スタック破壊、シェルコードなどは、アセンブラとスタックの理解があるとかなり見通しが良くなります。
x86-64アーキテクチャの詳細仕様
汎用レジスタと命令セット
x86-64では16個の汎用レジスタが利用可能です。各レジスタは64ビット(8バイト)をサポート。
| 64-bit | 32-bit | 16-bit | 8-bit | 目的 |
|---|---|---|---|---|
| rax | eax | ax | al | 累算器(戻り値) |
| rbx | ebx | bx | bl | ベースレジスタ |
| rcx | ecx | cx | cl | カウンタ |
| rdx | edx | dx | dl | データ |
| rsi | esi | si | sil | ソースインデックス |
| rdi | edi | di | dil | 宛先インデックス |
| rbp | ebp | bp | bpl | ベースポインタ |
| rsp | esp | sp | spl | スタックポインタ |
| r8-r15 | r8d-r15d | r8w-r15w | r8b-r15b | 追加レジスタ(64ビット) |
; レジスタサイズの関係
mov rax, 0x123456789ABCDEF0 ; rax全体に64ビット値を設定
mov eax, 0xDEADBEEF ; eaxに32ビット値を設定(上位32ビトはゼロ)
mov ax, 0x1234 ; axに16ビット値を設定
mov al, 0x56 ; alに8ビット値を設定
32ビット操作で値をeaxに書き込むと、自動的に上位32ビットがゼロに設定されます。これは「ゼロ拡張」と呼ばれる重要な特性です。
関数呼び出し規約(Calling Convention)
System V AMD64 ABI(System V、Linux/BSD)では以下の規約を定義:
レジスタ使用
- rdi, rsi, rdx, rcx, r8, r9: 最初の6個の整数引数
- rax, rdx: 戻り値(raxは下位64ビット、rdxは上位64ビット)
- rbx, rbp, r12-r15: Callee-save(呼び出し側が保存を期待)
- rax, rcx, rdx, rsi, rdi, r8-r11: Caller-save(呼び出し側が保存)
スタック
- Return Address(呼び出し側のアドレス)がスタックトップに置かれ、rspが16バイト境界に揃う
; 関数呼び出しの例
mov rdi, 10 ; 第1引数
mov rsi, 20 ; 第2引数
mov rdx, 30 ; 第3引数
call add_three ; 関数呼び出し
; rax に戻り値が返される
一方Microsoft x64(Windows)では、rcx, rdx, r8, r9 を使用し、フローティングポイント引数はxmm0-xmm3を使用します。
ALU(算術論理ユニット)命令
基本的な演算命令:
; 算術命令
add rax, rbx ; rax += rbx
sub rax, 1 ; rax -= 1
imul rax, rcx ; rax *= rcx
idiv rbx ; rax /= rbx(結果はrax、余りはrdx)
; 論理命令
and rax, rbx ; rax &= rbx(ビット積)
or rax, rbx ; rax |= rbx(ビット和)
xor rax, rbx ; rax ^= rbx(排他的論理和)
not rax ; rax = ~rax(ビット反転)
; シフト命令
shl rax, 1 ; rax <<= 1(左シフト)
shr rax, 1 ; rax >>= 1(論理右シフト)
sar rax, 1 ; rax >>= 1(算術右シフト、符号拡張)
; 比較とテスト
cmp rax, rbx ; フラグを設定(rax - rbx の結果を基に)
test rax, rbx ; フラグを設定(rax & rbx の結果を基に)
各命令の実行後、条件フラグが設定されます:Zero Flag(ZF)、Sign Flag(SF)、Overflow Flag(OF)、Carry Flag(CF)。
メモリアドレス指定モード
x86-64の効率的なメモリアクセスパターン:
; ベースレジスタのみ
mov rax, [rbx] ; rbx の指す1バイトを読む(size ambiguous)
mov rax, [rbx] ; 文脈で決定
; 変位付きベース
mov rax, [rbx + 8] ; rbx + 8 のアドレスを読む
mov rax, [rbp - 16] ; rbp - 16 のアドレスを読む
; インデックス付きアドレス指定
mov rax, [rbx + rcx*8] ; rbx + rcx * 8 のアドレスを読む
; スケール(scale)は1, 2, 4, 8 のみ許可
; ベース + インデックス + 変位
mov rax, [rbx + rcx*8 + 16] ; 最も複雑な形式
複雑なアドレス指定は単一命令で計算され、複数の単純な命令より効率的です。
条件分岐とループ最適化
条件付きジャンプ
条件フラグに基づいて実行フローを変更:
cmp rax, 0
je zero_label ; rax == 0 なら分岐
jne nonzero_label ; rax != 0 なら分岐
jl negative_label ; rax < 0 なら分岐(符号付き)
jb borrow_label ; キャリーセットなら分岐(符号なし)
jo overflow_label ; オーバーフロー時に分岐
条件付きジャンプの種類
| ニーモニック | 条件 |
|---|---|
| je, jz | Zero Flag = 1(等しい) |
| jne, jnz | Zero Flag = 0(等しくない) |
| jl, jnge | Sign Flag != Overflow Flag(符号付き <) |
| jg, jnle | Sign Flag == Overflow Flag かつ ZF=0(符号付き >) |
| jb, jc | Carry Flag = 1(符号なし <) |
| ja, jnbe | Carry Flag = 0 かつ ZF=0(符号なし >) |
ループの最適化
; 単純なループ
mov rcx, 100
loop_start:
; ループ本体
dec rcx ; rcx をデクリメント
jnz loop_start ; rcx != 0 なら継続
ループアンローリング(Loop Unrolling)は、ループ本体を複数回展開して分岐オーバーヘッドを削減:
; ループアンローリング: 4ループをまとめて処理
mov rcx, 25 ; 100 / 4 = 25 回反復
loop_start:
; 処理1
; 処理2
; 処理3
; 処理4
dec rcx
jnz loop_start
条件移動命令(CMOV)
分岐予測ミスを避けるため、条件フラグに基づいてレジスタに値を移動:
; 分岐を使わない実装
cmp rax, rbx
cmovl rcx, rdx ; rax < rbx なら rcx := rdx(符号付き)
; そうでなければ rcx は変更なし
; 従来の分岐を使う実装(分岐予測ミスの可能性)
cmp rax, rbx
jge else_label
mov rcx, rdx
jmp endif_label
else_label:
endif_label:
CMOV は パイプライン停止を避け、性能が向上することが多い。
SIMD命令とベクトル処理
SSE/AVXレジスタと命令
SSE(Streaming SIMD Extensions)とAVX(Advanced Vector Extensions)は、複数の値を並列処理:
; SSEレジスタ(128ビット)
movaps xmm0, [rax] ; メモリからxmm0に128ビットロード
addps xmm0, xmm1 ; xmm0 の4つの単精度浮動小数点を加算
mulps xmm0, xmm2 ; xmm0 の4つの値に乗算
; AVXレジスタ(256ビット)
vmovaps ymm0, [rax] ; メモリからymm0に256ビットロード
vaddps ymm0, ymm0, ymm1 ; ymm0 = ymm0 + ymm1
vmulps ymm0, ymm0, ymm2 ; ymm0 *= ymm2
AVX-512(Skylake以降)では512ビット(64バイト)のレジスタがサポート。これにより8つの64ビット値や16個の32ビット値を並列処理できます。
SIMD活用の例
// C コード: スカラー版
void add_arrays(float *a, float *b, float *c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
}
AVX を使うと、1命令で8個の32ビット値を加算できるため、理論上8倍高速化が可能(ただしメモリアクセスがボトルネックになることが多い)。
インライン アセンブリ
GCC インラインアセンブリ構文
C言語内で直接アセンブリを埋め込む:
int add(int a, int b) {
int result;
asm ("addl %1, %0" : "=r" (result) : "r" (a), "0" (b));
return result;
}
制約(constraints)の意味:
Volatile と Barrier
volatile int hardware_register = 0;
asm volatile (
"mov %0, %%eax
" // hardware_register を eax に読む
"add $1, %%eax
"
"mov %%eax, %0
" // 結果をhardware_register に書く
: "=m" (hardware_register)
: "m" (hardware_register)
: "eax"
);
volatile キーワードはコンパイラに「この命令は副作用がある、最適化するな」と指示します。
まとめ
アセンブラは、レジスタ、スタック、分岐、呼び出し規約を具体的に見るための入口です。高級言語のコードが最終的にどう落ちるかを追うことで、コンパイラやCPUの話が現実の動きとして理解しやすくなります。