---
title: 目标代码与 ABI
url: https://doc.liz6.com/compilers/06-code-generation/02-target-code-and-abi
locale: zh
area: compilers
tags:
- compilers
- 代码生成
date: 2026-06-30
modified: 2026-07-16
description: 函数调用不是几条指令的事——谁负责保存哪些寄存器、参数怎么传、栈帧怎么布局,全写死在 ABI 里,违反直接崩溃。x86-64 System V 和 Win64 的差异是跨平台编译最常见的坑。
---

# 目标代码与 ABI

> 函数调用不是几条指令的事——谁负责保存哪些寄存器、参数怎么传、栈帧怎么布局,全写死在 ABI 里,违反直接崩溃。x86-64 System V 和 Win64 的差异是跨平台编译最常见的坑。

## 概述

[指令选择](/compilers/06-code-generation/01-instruction-selection.md)把 IR 操作映射到目标机器指令。但函数调用不是几条指令的简单映射——涉及**谁负责保存哪些寄存器,参数怎么传,栈帧怎么布局,返回地址在哪**。这些约定全写在一份 ABI(Application Binary Interface)规范里,操作系统和编译器都要遵守。违反 ABI 程序直接 crash——不是逻辑错误,而是栈被污染,返回地址被覆盖。这篇以 x86-64 System V(Unix/Linux/Mac)为主,对照 Win64,把栈帧的每一块讲清楚。

## Calling Convention:寄存器与栈的协议

x86-64 System V 的参数传递:

```
参数顺序: rdi, rsi, rdx, rcx, r8, r9, [栈 (右压左)]
返回值:   rax (64-bit 以内), rdx:rax (128-bit 结构体)
调用者保存: rax, rcx, rdx, rsi, rdi, r8-r11   ← call 后可能被破坏
被调者保存: rbx, rbp, r12-r15                  ← 被调函数要用的话必须保存+恢复
```

一条 `fn(a, b, c, d, e, f, g)` 的调用在 x86-64 System V 下:

```
mov rdi, a          ; 参数 1
mov rsi, b          ; 参数 2
mov rdx, c          ; 参数 3
mov rcx, d          ; 参数 4
mov r8, e           ; 参数 5
mov r9, f           ; 参数 6
push g              ; 参数 7 → 栈 (在 call 之前推入)
call fn
```

Win64 不同:前 4 个参数用 `rcx, rdx, r8, r9`,后全走栈;**且调用方必须在栈上为前 4 个参数预留"shadow space"(32 字节)**——即使用不上也有这个位置。交叉平台编译时,这是个常踩的坑。

## 栈帧的解剖

<svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="x86-64 栈帧结构:从高地址到低地址的布局与 rbp/rsp 指向">
  <defs>
    <marker id="abiah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="420" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">函数栈帧解剖:从 rbp 出发,正偏移是调用方,负偏移是自己</text>

  <text x="315" y="48" text-anchor="middle" font-size="12" fill="#64748b">栈高位(调用方)</text>

  <rect x="190" y="58" width="250" height="34" fill="#e2e8f0"/>
  <text x="315" y="79" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">调用方推入的参数 7+</text>
  <text x="452" y="79" font-size="11" fill="#64748b">← 调用方在 call 前 push</text>

  <rect x="190" y="96" width="250" height="34" fill="#e2e8f0"/>
  <text x="315" y="117" text-anchor="middle" font-size="12" font-weight="700" fill="#334155">返回地址</text>
  <text x="452" y="117" font-size="11" fill="#64748b">← call 指令自动 push</text>

  <rect x="190" y="134" width="250" height="34" fill="#e0e7ff" stroke="#c7d2fe"/>
  <text x="315" y="155" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">旧的 rbp</text>
  <text x="452" y="155" font-size="11" fill="#64748b">← push rbp(prologue)</text>
  <text x="132" y="155" text-anchor="end" font-size="12" font-weight="700" fill="#4338ca">rbp →</text>
  <line x1="140" y1="151" x2="186" y2="151" stroke="#475569" stroke-width="1.6" marker-end="url(#abiah)"/>

  <rect x="190" y="172" width="250" height="34" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="315" y="193" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">局部变量</text>
  <text x="452" y="193" font-size="11" fill="#64748b">← sub rsp, N 分配的栈空间</text>

  <rect x="190" y="210" width="250" height="34" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="315" y="231" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">callee-saved 寄存器</text>
  <text x="452" y="231" font-size="11" fill="#64748b">← 保存被调者,如 rbx、r12</text>

  <rect x="190" y="248" width="250" height="34" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="315" y="269" text-anchor="middle" font-size="12" font-weight="700" fill="#115e59">spill slots</text>
  <text x="452" y="269" font-size="11" fill="#64748b">← 寄存器分配溢出位</text>

  <rect x="190" y="286" width="250" height="30" fill="#ccfbf1" stroke="#99f6e4" stroke-dasharray="4 3"/>
  <text x="315" y="305" text-anchor="middle" font-size="12" font-weight="700" fill="#0f766e">当前栈顶</text>
  <text x="132" y="305" text-anchor="end" font-size="12" font-weight="700" fill="#0d9488">rsp →</text>
  <line x1="140" y1="301" x2="186" y2="301" stroke="#475569" stroke-width="1.6" marker-end="url(#abiah)"/>

  <text x="315" y="332" text-anchor="middle" font-size="12" fill="#64748b">栈低位</text>

  <rect x="60" y="350" width="600" height="50" rx="8" fill="#eef2ff"/>
  <text x="80" y="370" font-size="12" fill="#3730a3">帧指针可选:frameless 函数只用 rsp 偏移访问局部变量,省一条 push/pop,但调试时难以回溯栈帧;</text>
  <text x="80" y="388" font-size="12" fill="#3730a3">调试信息(DWARF .debug_frame)负责记录从 rsp 恢复历史 rbp 的规则。</text>
</svg>

从 rbp(帧指针)出发,正偏移到返回地址和调用方参数;负偏移到自己函数的局部变量。**帧指针是可选的**——现代编译器常用 frameless:只用 rsp 偏移访问,不 push rbp(节省指令,但调试时难以回溯栈帧)。调试信息(DWARF `.debug_frame`)存着从 rsp 到历史 rbp 的恢复规则。

### Red zone(x86-64 System V)

rsp 以下 128 字节是 **red zone**——函数可以用这段区域而不用动 rsp。仅当该函数不调用其他函数时有效(leaf function)。Win64 没有 red zone——Windows 上 rsp 以下不保证不被信号处理程序/中断践踏。

## Prologue / Epilogue

每个函数在入口(prologue)和出口(epilogue)插入标准序列:

```
Prologue:
    push rbp              ; 保存帧指针
    mov rbp, rsp          ; 设新的帧指针
    sub rsp, N            ; 为局部变量 + spill + callee-saved 分配栈空间
    push rbx (etc.)       ; 保存被调者寄存器 (如果本函数用了)

Epilogue:
    pop rbx (etc.)        ; 恢复被调者寄存器
    mov rsp, rbp          ; 释放栈空间
    pop rbp               ; 恢复旧帧指针
    ret
```

编译器知道哪些寄存器本函数使用了——用不到 rbx 就不 push rbx。帧布局(哪个寄存器在栈上的哪个位置)是编译时确定的,prologue/epilogue 的 push/pop 和偏移是常量。

## Variadic Function(变参函数)

`printf(fmt, a, b, c, ...)` 的参数数量和类型在编译时不知道。x86-64 ABI 规定变参函数的参数传递和普通函数一样(前 6 个到寄存器,后走栈),但**`al` 寄存器记录浮点参数用了几个 XMM 寄存器**——这是让 `printf` 内部能检索 vararg 的关键。

对编译器的要求:在 vararg 调用点,编译器需要确保所有 XMM 寄存器里可能传的参数已被存到对应的 `va_list` 位置。`va_start`/`va_arg` 的实现完全基于 ABI 的寄存器+栈参数布局。

## PIC / PIE:位置无关代码

共享库和现代可执行文件需要 **PIC(Position-Independent Code)**:代码段可以在内存中任意位置加载,不需要重定位改代码本身(代码段是共享的,改了影响所有进程)。代价是全局变量和函数的访问多一层间接:

```
非 PIC:                           PIC:
  mov rax, [addr_of_x]              mov rax, [rip + offset_to_GOT_entry]
  call fn                           call [rip + offset_to_GOT_entry_for_fn]
                                     ; GOT = Global Offset Table, 在数据段中
```

编译器生成 PIC 时,所有"外部符号的地址"通过 GOT 间接——加载时动态链接器填 GOT 表项。`rip + offset` 是 PC 相对寻址(不需要绝对地址),保证代码段和地址无关。

## 参考

- **System V AMD64 ABI**: https://gitlab.com/x86-psABIs/x86-64-ABI — 权威规范
- **Microsoft x64 ABI**: https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions
- **Agner Fog**: "Calling Conventions" — 各平台 ABI 的工程化汇总

*Keywords: ABI, calling convention, System V, Win64, stack frame, prologue, epilogue, frame pointer, red zone, caller-saved, callee-saved, register passing, shadow space, variadic, va_list, AL register, PIC, PIE, GOT, PLT, PC-relative addressing, stack alignment*
