← SEARCHABLE ARCHIVE
Assembly · 2026-08-15

x86-64のメモリアドレッシングとスタックフレーム

実効アドレス、アラインメント、スタックの成長方向、関数フレームを追跡する

INTERACTIVE READING MODE →

x86-64のメモリアドレッシングとスタックフレーム

本ブログの記事は講義を目的として作成しています。 例はIntel構文を使い、正確なスタック配置はABI、コンパイラ、最適化、デバッグ情報によって異なります。

アセンブリではplayer.healthが[rcx+12]に、values[i]が[rdi+rsi*4]になる。ローカル変数も固定された箱ではなく、その時点のrspまたはframe pointerを基準に見つける領域である。

実効アドレス

[base + index * scale + displacement]

scaleは1、2、4、8のいずれかで、他の項は省略できる。

mov eax, [rcx]
mov eax, [rcx + 12]
mov eax, [rcx + rdx*4]
mov eax, [rcx + rdx*4 + 12]

大括弧内をアドレスとして計算し、指定幅のメモリへアクセスする。leaだけは計算結果そのものを返す。

lea rax, [rcx + 8]    ; アドレス
mov rax, [rcx + 8]    ; その場所の値

レジスタのビットには「ポインタ」という型タグがないため、解析時はアドレスか値かを明示する。

配列と構造体

4バイトのint配列なら:

mov eax, dword ptr [rcx + rdx*4]

12バイト構造体はscaleだけで表せないため追加計算が入る。

lea rax, [rdx + rdx*2]
mov ecx, [rcx + rax*4 + 4]

構造体のoffsetはバイナリ契約であり、フィールド名ではない。

static_assert(offsetof(Player, health) == 12);
static_assert(sizeof(Player) == 16);

フィールド順、packing、継承、ABIが変わればハードコードしたoffsetの意味も変わる。

RIP-relativeとリトルエンディアン

lea rdi, [rip + message]
mov eax, dword ptr [rip + counter]

絶対アドレスを埋め込まず命令位置から対象を探す。動的リンクではELFのGOT/PLTやPEのimport tableによる間接参照が加わることがある。

0x1122334455667788はx86-64メモリで次の順序になる。

アドレス +0  +1  +2  +3  +4  +5  +6  +7
バイト   88  77  66  55  44  33  22  11

エンディアンは複数バイト値のメモリ表現順であり、算術の順序を反転するものではない。

アラインメント

x86-64の多くのload/storeはunalignedを許すが、常に正しく無料という意味ではない。

  • cache lineやpage境界をまたぐと高コストになり得る。
  • SIMD命令には整列を要求する形式がある。
  • ABIは呼び出し境界のスタック整列を定める。
  • C/C++で不正に整列したtyped pointerの参照は未定義動作になり得る。
  • atomicityは命令、幅、整列、境界に依存する。

スタックは低いアドレスへ成長する

push rbx       ; rsp -= 8; [rsp] = rbx
pop rbx        ; rbx = [rsp]; rsp += 8

callは復帰アドレスを保存し、retはそれを取り出す。

call直前        RSP = S
callee入口      RSP = S - 8
ret後           RSP = S

引数の多くがレジスタでも、復帰アドレスはスタックに置かれる。

スタックフレーム

push rbp
mov rbp, rsp
sub rsp, 32
; body
add rsp, 32
pop rbp
ret

呼び出し元のrbpを保存し、固定基準点を作り、32バイトを確保して逆順に戻す。ただしframe pointerは必須ではない。最適化コードはrbpを汎用に使い、ローカル値をすべてレジスタに置くこともある。

フレームにはローカル配列、spill、非volatileレジスタ、stack引数、Windowsのshadow space、padding、canary、動的領域などが置かれる。したがって[rsp+40]を根拠なく「5番目のローカル」と決めてはいけない。

leaf関数とABI差

add_three:
    lea eax, [rdi + rsi]
    add eax, edx
    ret

他関数を呼ばないleafはスタックを使わないことがある。Windows x64のcallerは32バイトshadow spaceを予約する。System Vにはshadow spaceはないが、ユーザー空間leafが条件付きで使える128バイトred zoneがある。Windowsにはこのred zoneはない。

バッファループを契約として読む

xor eax, eax
xor r8d, r8d
loop_begin:
    cmp r8, rdx
    jae loop_end
    movsxd r9, dword ptr [rcx + r8*4]
    add rax, r9
    inc r8
    jmp loop_begin
loop_end:
    ret

[rcx+r8*4]はvalues[r8]の候補アドレス、movsxdはsigned 32ビット要素の拡張である。count > 0ならcallerは少なくともcount*4バイトの読み取り可能領域を渡す必要がある。ASMはポインタだけから割り当てサイズを知ることができない。

デバッグ手順

時点rsp[rsp]の意味保存状態
入口基準復帰アドレスまだなし
prologue後基準 - Nlocal/save領域使用分を記録
nested call前ABI整列を確認outgoing領域live値を保護
epilogue後入口値復帰アドレス全て復元

スタック破壊は、無関係な場所へのret、呼び出し元の非volatile値の変化、例外時だけ壊れるstack walk、整列SIMD命令でのfaultとして現れる。

まとめ

x86-64は[base + index*scale + displacement]で実効アドレスを作る。scaleは要素サイズ、displacementはfield offsetの手掛かりになる。callは復帰アドレスを積み、関数は必要なlocal、spill、保存、呼び出し領域だけを確保する。万能なフレーム図を暗記せず、rspの変化を命令ごとに追跡する。

公式資料