MSVC环境下如何正确转发C/C++非模板可变参数
我太懂你现在的困扰了——要转发别人写的非模板可变参数函数,自己改不了函数签名,用va_list在GCC下跑的好好的,到MSVC就输出一堆随机栈值,试了调用约定、优化选项都没用,确实头疼。咱们先从你给的最小示例说起,把问题拆明白。
首先看你的测试代码:
#include <stdarg.h> int funcA(int a, void* b, ...) { va_list args; va_start(args, b); auto value = a + va_arg(args, int); va_end(args); return value; } int funcB(int a, void* b, ...) { va_list args; va_start(args, b); auto value = funcA(a, args); // 你这里故意去掉b参数测试,就算加回去也不对 va_end(args); return value; } int main() { return funcB(2, nullptr, 163); }
GCC下能得到正确的165,但MSVC返回的却是像7731509这种随机值。
问题出在哪?
你现在的写法本质是把va_list对象直接当成可变参数传给funcA,但funcA的最后一个参数是可变参数列表,不是va_list类型。GCC可能在__cdecl调用约定下刚好碰对了栈布局,但这属于标准未定义行为,MSVC的参数处理逻辑和栈布局不一样,自然就出乱子了——而且标准C/C++从来没规定过可以直接把va_list当作可变参数传递,这种写法本来就不合法。
你提到printf是通过调用接受va_list的变体(比如_vfprintf_s_l)来转发参数,但你说原函数没有提供这种版本,这条路走不通。
再看你真实的场景:函数挂钩,要把参数原封不动转发给DLL导出的可变参数函数,自己完全碰不到原函数的实现,这时候该怎么办?
靠谱的解决方案(针对MSVC)
在这种没法改原函数签名的场景下,最可靠的方式是直接操作栈和寄存器,绕过编译器的参数处理逻辑:
1. 使用__declspec(naked)裸函数+内联汇编
__declspec(naked)会让编译器不生成栈帧的初始化和清理代码,你可以完全手动控制参数传递,完美转发所有可变参数。
针对你的钩子场景,示例代码如下(以x86平台__cdecl调用约定为例):
// 原函数指针声明 extern int (*orig_exported_func)(int a, void* b, ...); __declspec(naked) int hook_exported_func(int a, void* b, ...) { __asm { // 保存需要用到的寄存器,避免破坏原函数的执行环境 push ebx push esi push edi // 直接调用原函数:当前函数的栈上已经包含了原函数需要的所有参数 call orig_exported_func // 原函数的返回值存在eax寄存器里,先保存起来 push eax // -------------------------- // 这里写你的钩子逻辑,比如: // - 记录返回值 // - 修改返回值 // - 做一些日志统计 // -------------------------- // 恢复原函数的返回值到eax pop eax // 恢复之前保存的寄存器 pop edi pop esi pop ebx // 返回给调用者 ret } }
这种写法完全绕开了编译器的参数处理,直接利用当前函数的栈帧传递参数,不管可变参数有多少个、是什么类型,都能原封不动转发给原函数,在MSVC下绝对可靠。
2. 若已知参数个数,用va_arg逐个提取后转发
如果你能提前知道原函数的可变参数个数和类型,可以用va_arg逐个提取参数,再传给原函数。比如知道可变参数是一个int:
int hook::exported_func(int a, void* b, ...) { va_list args; va_start(args, b); int arg1 = va_arg(args, int); // 提取第一个可变参数 va_end(args); // 调用原函数,传入所有参数 auto retVal = orig::exported_func(a, b, arg1); // 钩子逻辑 // ... return retVal; }
但这种方式只适用于参数个数固定且已知的场景,如果你做的是通用钩子,没法预判参数个数,还是第一种方法更靠谱。
总结
你之前的错误在于把va_list直接当作可变参数传递,这是未定义行为,GCC碰巧能工作但MSVC不行。在MSVC下处理这种无法修改原函数签名的可变参数转发,__declspec(naked)裸函数配合内联汇编是最可靠的方案,它能完全控制参数传递过程,确保所有参数原封不动转发给原函数。
备注:内容来源于stack exchange,提问作者Johny Siemano Kolano

