参数超过6个时,“被迫”动用内存栈处理
目录
参数超过6个时,“被迫”动用内存栈处理
当函数的参数超过 6 个时,CPU 就像是一个装满了东西的口袋——它已经没有多余的“快车道”(寄存器)来存放新数据了。这时候,CPU 必须求助于它的“后备仓库”:栈(Stack)。
在 x86-64 Linux(System V ABI)中,这种处理方式被称为 寄存器与栈的混合传递。
1. 规则:前 6 后栈
我们可以把参数传递想象成一个优先级排队:
-
前 6 个参数:坐“头等舱”,分别存放在
RDI,RSI,RDX,RCX,R8,R9。 -
第 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 ; a6mov r8d, 5 ; a5mov ecx, 4 ; a4mov edx, 3 ; a3mov esi, 2 ; a2mov edi, 1 ; a1
; --- 正式调用 ---call complex_func3. 栈帧布局:参数长什么样?
当函数被调用后,栈顶指针 会发生变化。对于 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-2 个参数(如
system("/bin/sh")),你只需要寻找pop rdi; ret这样的 Gadget 即可。 -
栈溢出的利用:如果你要利用的函数有 7 个以上的参数(比如某些复杂的内核函数),你就不能只靠控制寄存器了,你还必须控制栈上的数据布局,因为程序会去
[rsp + 8]这种地方读取它的后续参数。
这种“寄存器 + 栈”的混合机制,实际上是 CPU 在**速度(寄存器)与通用性(栈可以存无限个参数)**之间做出的平衡。
进阶思考
你有没有想过,为什么函数参数压栈的顺序是 “从右往左”(先压 8,再压 7)?这其实和 C 语言中一个非常著名的特性——可变参数函数(比如 printf)有关。你想聊聊这个“设计心机”吗?