4. 関数呼び出しとスタック

4.1 なぜ戻り先を保存するのか

mainの途中でread_nameへ移ったCPUは、read_nameの後にmainのどこへ戻ればよいかを覚えておく必要がある。read_nameがgreetを呼ぶと、その戻り先も必要になる。

main の続きの位置
  └─ read_name の実行中に保存
       greet 呼び出し後は、その戻り先も追加で保存

後から呼ばれたgreetが先に戻り、次にread_nameが戻る。「後から入れたものを先に出す」スタックは、この入れ子構造と相性がよい。

4.2 RSP、call、ret

x86-64でRSPは現在のスタック先頭を指す。ここで「先頭」は最後に積んだ値の位置であり、図の上端とは限らない。

call target:
  RSP = RSP - 8
  [RSP] = callの次の命令アドレス
  RIP = target

ret:
  RIP = [RSP]
  RSP = RSP + 8

[RSP]は「RSPが指すメモリの内容」を意味する。callは戻り先をスタックへ書き、retはそれを命令位置として読み戻す。

call前、call後、呼び出し先実行中、ret後のRIPとRSP

図: スタック全体は残したまま、RSPより外側になった部分だけを薄く示す。

4.3 スタックフレームとRBP

このビルドでは、関数の入口で呼び出し元のRBPを保存し、新しいRBPを基準に局所領域を参照する。

高アドレス
┌──────────────────────┐
│ mainへの戻り先    │  [RBP+8]
├──────────────────────┤
│ 保存されたRBP     │  [RBP]
├──────────────────────┤  ← RBP
│ name[8..15]          │
├──────────────────────┤
│ name[0..7]           │  ← RBP-16
└──────────────────────┘  ← RSP
低アドレス

局所配列nameと戻り先は論理的に関係ないデータだが、この実行ファイルでは同じスタック上に連続して置かれる。この物理的な近さが後で効く。