C语言可变参数多次转发的栈存储机制及示例代码问题咨询
示例代码
void fun2(char *format, ...){ va_list arg_list; va_start(arg_list, format); vprintf(format, arg_list); va_end(arg_list); } void fun1(char *format, ...){ fun2(format); } int main(){ fun1("test: %d", 100); }
运行结果
test: 100
问题答复
关于问题1:这段代码存在明确的未定义行为,属于严重不符合开发规范的错误写法。它能输出正确结果完全是特定编译环境、特定调用约定下的巧合,不属于合法可移植的C代码。
C标准规定可变参数的传递必须由函数调用方显式完成,fun1调用fun2时仅传入了固定参数format,没有将自身收到的可变参数透传给fun2,此时fun2内部通过va_start遍历可变参数的行为本身就是越界访问,不受标准保障。在x86 cdecl调用约定下参数按顺序从右往左压栈,fun1收到的参数100刚好留在栈上format相邻的位置,才被fun2误读到;换用x64等寄存器传参的调用约定、开高等级优化,这段代码大概率输出乱码甚至触发崩溃。
标准的可变参数透传写法如下,不存在未定义行为:// 接收va_list做透传,符合C标准要求 void fun2(char *format, va_list args) { vprintf(format, args); } void fun1(char *format, ...) { va_list arg_list; va_start(arg_list, format); fun2(format, arg_list); va_end(arg_list); }关于问题2:猜想不完全正确。调用
fun2(format)时,确实仅向fun2传递了第一个参数format(char*类型指针),但C语言不会自动把调用方(fun1)收到的可变参数隐式传给被调函数(fun2)。你观察到的“fun2能拿到100”不是传参的结果,是栈内存布局巧合导致的越界读。关于问题3:在你当前跑通代码的x86 cdecl环境下,整数100存储在fun1的栈帧参数区域。
该调用约定下参数从右往左压栈:调用fun1时先压入100,再压入format字符串地址,再压入fun1的返回地址,形成fun1的栈帧;fun1调用fun2时仅压入format副本、fun2返回地址,形成fun2的栈帧。fun2执行va_start时,会从自身栈帧内format参数的相邻高地址找可变参数,这个地址刚好落在之前调用fun1时压入100的位置,属于fun1栈帧的范围,不在fun2、vprintf的栈空间内。
注意这只是特定环境的现象,C标准没有规定参数的存储位置,换用寄存器传参的架构时100一开始就存在寄存器中,根本不在栈上,这段代码也读不到正确值。关于问题4:在你当前运行代码的特定环境下,这个描述是符合实际运行逻辑的。fun2本身没有收到100这个参数,它通过可变参数宏访问到100,本质就是越界访问了自身栈帧之外、fun1调用时预留的参数栈空间。再次强调:这是未定义行为,没有任何语法层面的保障,生产环境绝对不能使用这种写法。
内容的提问来源于stack exchange,提问作者Parminder Singh

