GCC未对函数调用时的栈空间分配应用栈冲突保护是否为设计使然?
嘿,这个问题观察得太细致了!咱们先把背景和问题拆解清楚,再聊聊为什么会出现你看到的这种情况。
首先看你给出的C++代码:
struct Base { int x[10000]; }; void foo(Base base) {} int main() { Base base; foo(base); }
这段代码里foo是传值接收Base对象,所以main需要在自己的栈上创建一个Base的副本,再传给foo——这也就是汇编里那串memcpy和后续调用的原因。
接下来看你提到的两个关键汇编片段:
第一个是main开头的栈冲突保护逻辑:
leaq -36864(%rsp), %r11 .LPSRL0: subq $4096, %rsp orq $0, (%rsp) cmpq %r11, %rsp jne .LPSRL0
这部分是GCC启用-fstack-clash-protection后的标准操作:逐页分配栈空间,并且每分配一页就访问一次(orq $0, (%rsp)),这样如果分配过程中跳过了操作系统的栈Guard Page,会立刻触发页错误,避免攻击者利用栈冲突漏洞。这部分是为main函数自身的局部变量(比如你的Base base)分配栈空间时的保护。
然后是你关注的、为调用foo准备参数的片段:
subq $40000, %rsp movq %rsp, %rax movq %rax, %rcx leaq -40016(%rbp), %rax movl $40000, %edx movq %rax, %rsi movq %rcx, %rdi call memcpy call foo(Base)
这里直接一次性调整rsp分配了40000字节(正好是Base的大小:10000个int × 4字节),完全没走逐页分配+访问的栈冲突保护流程——这确实是GCC的设计使然,原因主要有这几点:
栈冲突保护的覆盖范围限定
GCC的-fstack-clash-protection特性从设计之初,就只针对函数入口处为自身局部变量分配栈空间的场景。它的核心目标是防范函数内部大型局部变量导致的栈冲突,而调用者为函数参数分配栈空间的操作,不在这个特性的覆盖范围内。参数分配的场景考量
虽然理论上参数空间也可能很大(比如你这里的40000字节),但GCC的实现团队认为,这类场景相对少见,且现有栈保护机制(比如栈金丝雀-fstack-protector-strong)已经能覆盖部分风险。另外,为参数分配添加逐页保护会增加调用函数的开销,对于大多数参数较小的场景来说得不偿失。现有实现的逻辑边界
从编译器的实现角度看,函数入口的栈帧分配是统一处理的,容易加入栈冲突保护;而参数分配是分散在各个函数调用点的,要逐个处理会增加编译器的复杂度,目前GCC还没有把这部分纳入栈冲突保护的范畴。
简单来说:你看到的差异不是编译器的bug,而是GCC当前对-fstack-clash-protection的设计选择——它只保护函数自身局部变量的栈分配,不管调用函数前的参数栈空间分配。
备注:内容来源于stack exchange,提问作者Ernie Sanderson

