3.15 参数传递

x86-64 使用寄存器 + 栈的混合方式传递函数参数。前 6 个参数通过寄存器传递,第 7 个及以后的参数通过栈传递。

寄存器参数传递规则

前 6 个整数/指针参数依次放入:

参数序号寄存器说明
第 1 个参数%rdi第一个参数
第 2 个参数%rsi第二个参数
第 3 个参数%rdx第三个参数
第 4 个参数%rcx第四个参数
第 5 个参数%r8第五个参数
第 6 个参数%r9第六个参数

参数传递示例

// C 代码:6 个参数以内的函数
long func(long a, long b, long c, long d, long e, long f) {
    return a + b + c + d + e + f;
}
# 调用者的设置
    movq    $1, %rdi            # 第一个参数 a = 1
    movq    $2, %rsi            # 第二个参数 b = 2
    movq    $3, %rdx            # 第三个参数 c = 3
    movq    $4, %rcx            # 第四个参数 d = 4
    movq    $5, %r8             # 第五个参数 e = 5
    movq    $6, %r9             # 第六个参数 f = 6
    call    func
 
# 被调函数内部
func:
    addq    %rsi, %rdi          # a += b
    addq    %rdx, %rdi          # a += c
    addq    %rcx, %rdi          # a += d
    addq    %r8, %rdi           # a += e
    addq    %r9, %rdi           # a += f
    movq    %rdi, %rax          # 返回 a
    ret

栈参数传递

当参数超过 6 个时,第 7 个及以后的参数按从右到左的顺序压入栈中。

栈传参规则

  • 第 7 个参数在 栈顶上方(靠近调用者的方向)
  • 第 8 个参数在第 7 个参数上方的 8 字节处
  • 以此类推
  • 所有栈参数按 8 字节对齐

7 个及以上参数示例

// C 代码:8 个参数
long big_func(long a1, long a2, long a3, long a4, long a5, long a6,
              long a7, long a8) {
    return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;
}
# 调用者的设置
big_func:
    # 栈参数:从右到左压栈
    pushq   $8                  # 第 8 个参数(先压入)
    pushq   $7                  # 第 7 个参数(后压入,在栈顶)
                                # 注意:实际编译器常用 movq 到特定偏移位置
 
    # 寄存器参数
    movq    $1, %rdi            # a1
    movq    $2, %rsi            # a2
    movq    $3, %rdx            # a3
    movq    $4, %rcx            # a4
    movq    $5, %r8             # a5
    movq    $6, %r9             # a6
 
    call    big_func
 
# 编译器通常的优化做法(不 push,而是预分配空间后 mov):
#   subq    $16, %rsp
#   movq    $7, 8(%rsp)        # a7 在 rsp+8
#   movq    $8, (%rsp)         # a8 在 rsp
 
# 被调函数内部
big_func:
    # 访问寄存器参数
    addq    %rsi, %rdi          # a1 += a2
    addq    %rdx, %rdi          # a1 += a3
    addq    %rcx, %rdi          # a1 += a4
    addq    %r8, %rdi           # a1 += a5
    addq    %r9, %rdi           # a1 += a6
 
    # 访问栈参数(注意返回地址占 8 字节,所以从 rsp+8 开始)
    addq    8(%rsp), %rdi       # a1 += a7
    addq    16(%rsp), %rdi      # a1 += a8
 
    movq    %rdi, %rax          # 返回 a1
    ret

栈参数的内存布局

当被调函数刚执行时(call 之后,栈帧建立之前):

高地址
       ┌─────────────────────┐
       │        a8           │ ← 调用者栈帧
       ├─────────────────────┤
       │        a7           │
       ├─────────────────────┤ ← %rsp + 16
       │    返回地址         │ ← call 压入
       ├─────────────────────┤ ← %rsp + 8
       │  (被调函数的栈帧)    │
       └─────────────────────┘ ← %rsp
低地址

注意:%rsp 指向栈顶。返回地址在 %rsp,a7 在 %rsp + 8,a8 在 %rsp + 16

如果被调函数执行了 pushq %rbp

       ┌─────────────────────┐
       │        a8           │
       ├─────────────────────┤
       │        a7           │
       ├─────────────────────┤ ← %rbp + 24
       │    返回地址         │
       ├─────────────────────┤ ← %rbp + 8
       │    保存的 %rbp      │ ← %rbp 指向这里
       ├─────────────────────┤ ← %rsp

浮点数参数

浮点数参数使用不同的寄存器传递:

参数序号寄存器(浮点)
第 1-8 个%xmm0 ~ %xmm7

超过 8 个浮点参数时也通过栈传递。

返回值传递

返回值类型存放位置
整数/指针%rax
浮点数%xmm0
128 位整数%rdx(高 64 位)+ %rax(低 64 位)
大型结构体通过隐式指针参数返回

返回大型结构体

struct Big {
    long data[100];
};
 
struct Big create_big() {
    struct Big b;
    // ... 初始化 b ...
    return b;
}
# 实际调用时,编译器会传递一个隐藏参数:
# %rdi = 指向调用者分配的 Big 结构体的指针
# 被调函数将结果直接写入该内存位置
 
create_big:
    # %rdi 指向调用者提供的缓冲区
    # 将结果写入 (%rdi), 8(%rdi), ...
    movq    $0, (%rdi)
    # ...
    movq    %rdi, %rax          # 返回缓冲区地址
    ret

参数传递总结

参数范围传递方式寄存器/位置
第 1-6 个参数寄存器%rdi, %rsi, %rdx, %rcx, %r8, %r9
第 7+ 个参数返回地址上方,按从右到左的顺序
返回值寄存器%rax
浮点参数 1-8寄存器%xmm0 ~ %xmm7
浮点返回值寄存器%xmm0

相关笔记

    • 参数传递使用的寄存器
    • 栈帧中的参数区域
    • 调用过程中寄存器的保存策略

链接到