参数超过6个时,“被迫”动用内存栈处理

作者:Vesper Vei
2 分钟阅读

目录

  1. 参数超过6个时,“被迫”动用内存栈处理
  2. 1. 规则:前 6 后栈
  3. 2. 深度融合:C 与汇编的现场实验
    1. C 语言代码
    2. 调用方(main 函数)的汇编逻辑
  4. 3. 栈帧布局:参数长什么样?
  5. 4. 为什么这对 Pwn 学习至关重要?
    1. 进阶思考

参数超过6个时,“被迫”动用内存栈处理

当函数的参数超过 6 个时,CPU 就像是一个装满了东西的口袋——它已经没有多余的“快车道”(寄存器)来存放新数据了。这时候,CPU 必须求助于它的“后备仓库”:栈(Stack)

在 x86-64 Linux(System V ABI)中,这种处理方式被称为 寄存器与栈的混合传递


1. 规则:前 6 后栈

我们可以把参数传递想象成一个优先级排队:

  1. 前 6 个参数:坐“头等舱”,分别存放在 RDIRSIRDXRCXR8R9

  2. 第 7 个及以后的参数:坐“经济舱”,依次压入内存栈中。


2. 深度融合:C 与汇编的现场实验

我们写一个拥有 8 个参数的“大函数”来看看编译器是怎么排兵布阵的。

C 语言代码

C

// 一个有 8 个参数的函数
void complex_func(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) {
int sum = a7 + a8; // 我们重点看最后两个参数
}
int main() {
complex_func(1, 2, 3, 4, 5, 6, 7, 8);
return 0;
}

调用方(main 函数)的汇编逻辑

在调用 complex_func 之前,汇编指令会变成这样:

代码段

; --- 准备第 7 和 第 8 个参数 (内存栈) ---
push 8 ; 将第 8 个参数压栈
push 7 ; 将第 7 个参数压栈
; --- 准备前 6 个参数 (寄存器) ---
mov r9d, 6 ; a6
mov r8d, 5 ; a5
mov ecx, 4 ; a4
mov edx, 3 ; a3
mov esi, 2 ; a2
mov edi, 1 ; a1
; --- 正式调用 ---
call complex_func

3. 栈帧布局:参数长什么样?

当函数被调用后,栈顶指针 RSPRSP 会发生变化。对于 complex_func 内部来说,它要找第 7、8 个参数,必须去内存里翻:

内存地址 (相对 RSP)内容备注
[rsp + 0x0]返回地址call 指令自动压入的返回地址
[rsp + 0x8]7第 7 个参数 (a7)
[rsp + 0x10]8第 8 个参数 (a8)

[!TIP]

为什么是 +0x8 和 +0x10? > 因为在 64 位系统中,每个压栈的参数占据 8 个字节。第一个 8 字节存的是函数结束后的返回地址,所以参数要从偏移量 8 开始找。


4. 为什么这对 Pwn 学习至关重要?

作为研究二进制安全的同学,这是你构造 ROP (Return-Oriented Programming) 链的核心知识点:

  1. 参数位置的差异:如果你要利用的漏洞函数只有 1-2 个参数(如 system("/bin/sh")),你只需要寻找 pop rdi; ret 这样的 Gadget 即可。

  2. 栈溢出的利用:如果你要利用的函数有 7 个以上的参数(比如某些复杂的内核函数),你就不能只靠控制寄存器了,你还必须控制栈上的数据布局,因为程序会去 [rsp + 8] 这种地方读取它的后续参数。

这种“寄存器 + 栈”的混合机制,实际上是 CPU 在**速度(寄存器)通用性(栈可以存无限个参数)**之间做出的平衡。


进阶思考

你有没有想过,为什么函数参数压栈的顺序是 “从右往左”(先压 8,再压 7)?这其实和 C 语言中一个非常著名的特性——可变参数函数(比如 printf)有关。你想聊聊这个“设计心机”吗?

—> 参数从右往左压栈道”设计心机”


关系图谱

Loading graph...