x86-64 記憶體定址與堆疊框架
追蹤有效位址、對齊、堆疊成長方向與函式框架
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] ; 該位址的值
暫存器位元沒有「指標型別」標籤,除錯時要明確記錄每個值是位址還是資料。
陣列與結構
四位元組 int 陣列:
mov eax, dword ptr [rcx + rdx*4]
位址是 base + index * sizeof(int)。12 位元組結構無法只靠 scale 表達,因此會有額外計算。
lea rax, [rdx + rdx*2]
mov ecx, [rcx + rax*4 + 4]
結構欄位 offset 是 binary contract,不是欄位名稱。欄位順序、packing、繼承或 ABI 改變都可能讓硬編碼數字失效。
static_assert(offsetof(Player, health) == 12);
static_assert(sizeof(Player) == 16);
RIP-relative 與 little endian
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 容忍未對齊,但不代表所有情況都正確且免費。
- 跨 cache line 或 page 可能增加成本。
- 某些 SIMD 形式要求對齊。
- ABI 要求函式呼叫邊界的堆疊對齊。
- C/C++ 中解參考未正確對齊的 typed pointer 可能是未定義行為。
- 原子性取決於指令、寬度、對齊與邊界。
堆疊往低位址成長
push rbx ; rsp -= 8; [rsp] = rbx
pop rbx ; rbx = [rsp]; rsp += 8
call 儲存回傳位址並轉移控制;ret 取回位址。
call 前 RSP = S
callee 入口 RSP = S - 8
ret 後 RSP = S
即使現代 ABI 以暫存器傳多數參數,回傳位址仍由 call 放到堆疊。
堆疊框架
push rbp
mov rbp, rsp
sub rsp, 32
; body
add rsp, 32
pop rbp
ret
這段保存 caller 的 rbp、建立穩定基準、配置 32 位元組,再反向復原。但 frame pointer 並非必要;最佳化程式可把 rbp 當一般暫存器,甚至完全不配置堆疊框架。
框架可能包含區域陣列、register spill、保存的 nonvolatile 暫存器、stack 參數、Windows shadow space、對齊 padding、canary 與動態配置。因此不能看到 [rsp+40] 就武斷地說是「第五個區域變數」。
leaf 函式與 ABI 差異
add_three:
lea eax, [rdi + rsi]
add eax, edx
ret
不呼叫其他函式的 leaf 可能完全不用堆疊。Windows x64 caller 會預留 32 位元組 shadow space。System V 沒有 shadow space,但符合條件的 user-space leaf 可使用 rsp 下方 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 位元元素擴展到 64 位元。若 count > 0,caller 必須提供至少 count*4 位元組可讀記憶體。ASM 無法只從指標得知配置大小。
除錯工作表
| 時點 | rsp | [rsp] 意義 | 保存狀態 |
|---|---|---|---|
| 入口 | 基準 | 回傳位址 | 尚無 |
| prologue 後 | 基準 - N | local/save 區 | 列出使用項目 |
nested call 前 | 驗證 ABI 對齊 | outgoing 區 | 保護 live 值 |
| epilogue 後 | 入口值 | 回傳位址 | 全部復原 |
堆疊損壞常表現為 ret 跳到無關位址、caller 的 nonvolatile 值改變、只在例外時中斷 stack walk,或在對齊 SIMD 指令上 fault。
總結
x86-64 用 [base + index*scale + displacement] 建立有效位址。scale 常顯示元素大小,displacement 常顯示欄位 offset。call 把回傳位址放入堆疊,函式只在需要時配置 local、spill、保存與呼叫區。不要背一張萬用框架圖,而要逐指令追蹤 rsp。