我目前正在调试90年代后期编写的一些C ++代码,这些代码解析脚本以加载数据,执行简单操作和打印结果等。
编写代码的人使用仿函数将字符串关键字映射到它正在解析为实际函数调用的文件中,并且它们是模板化的(最多有8个参数)来处理用户可能无数的函数接口请求他们的脚本。
在大多数情况下,这一切都运行良好,但近年来它开始在我们的一些64位构建系统上出现段错误。通过valgrind运行,令我惊讶的是,我发现错误似乎发生在“printf”中,这是所谓的仿函数之一。以下是一些代码片段,用于说明其工作原理。
首先,正在解析的脚本包含以下行:
printf( "%5.7f %5.7f %5.7f %5.7f\n", cos( j / 10 ), tan( j / 10 ), sin( j / 10 ), sqrt( j / 10 ) );
其中cos,tan,sin和sqrt也是对应于libm的仿函数(这个细节不重要,如果我用固定的数值替换那些我得到相同的结果)。
在调用printf时,它按以下方式完成。首先,模板化的算子:
template<class R, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8>
class FType
{
public :
FType( const void * f ) { _f = (R (*)(T1,T2,T3,T4,T5,T6,T7,T8))f; }
R operator()( T1 a1,T2 a2,T3 a3,T4 a4,T5 a5,T6 a6,T7 a7,T8 a8 )
{ return _f( a1,a2,a3,a4,a5,a6,a7,a8); }
private :
R (*_f)(T1,T2,T3,T4,T5,T6,T7,T8);
};
然后调用它的代码在另一个模板类中 - 我展示了原型和使用FType的相关代码片段(以及我用于调试的一些额外代码):
template<class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8>
static Token
evalF(
const void * f,
unsigned int nrargs,
T1 a1,
T2 a2,
T3 a3,
T4 a4,
T5 a5,
T6 a6,
T7 a7,
T8 a8,
vtok & args,
const Token & returnType )
{
Token result;
printf("Count: %i\n",++_count);
if( _count == 2 ) {
const char *fmt = *((const char **) &a1);
result = printf(fmt,a2,a3,a4,a5,a6,a7,a8);
FType<int, const void*,T2,T3,T4,T5,T6,T7,T8> f1(f);
result = f1("Hello, world.\n",a2,a3,a4,a5,a6,a7,a8);
result = f1("Hello, world2 %5.7f\n",a2,a3,a4,a5,a6,a7,a8);
result = f1(fmt,a2,a3,a4,a5,a6,a7,a8);
} else {
FType<int, T1,T2,T3,T4,T5,T6,T7,T8> f1(f);
result = f1(a1,a2,a3,a4,a5,a6,a7,a8);
}
}
我插入了if(_count == 2)位(因为这个函数被多次调用)。在正常情况下,它只执行else子句中的操作;它使用“f”调用FType构造函数(将返回类型模板为int),这是printf的函子(在调试器中验证)。一旦构造了f1,它就会用所有模板化的参数调用重载的调用操作符,并且valgrind开始抱怨:
==29358== Conditional jump or move depends on uninitialised value(s)
==29358== at 0x92E3683: __printf_fp (printf_fp.c:406)
==29358== by 0x92E05B7: vfprintf (vfprintf.c:1629)
==29358== by 0x92E88D8: printf (printf.c:35)
==29358== by 0x5348C45: FType<int, void const*, double, double, double, double, void const*, void const*, void const*>::operator()(void const*, double, double, double, double, void const*, void const*, void const*) (Interpreter.cc:321)
==29358== by 0x51BAB6D: Token evalF<void const*, double, double, double, double, void const*, void const*, void const*>(void const*, unsigned int, void const*, double, double, double, double, void const*, void const*, void const*, std::vector<Token, std::allocator<Token> >&, Token const&) (Interpreter.cc:542)
因此,这导致了if()子句中的实验。首先,我尝试使用相同的参数直接调用printf(注意使用参数a1的类型转换技巧 - 格式 - 以便使其编译;否则它会抱怨模板的许多实例,其中T1不是(char * )如printf所期望的那样)。这很好。
接下来,我尝试使用一个没有变量的替换格式字符串调用f1(Hello,world)。这也很好。
然后我添加一个变量(Hello,World2%5.7f),然后我开始看到如上所述的valgrind错误。
如果我在32位系统上运行此代码,则它是valgrind clean(否则相同版本的glibc,gcc)。
在几个不同的Linux系统(全部64位)上运行,有时我得到一个段错误(例如RHEL5.8 / libc2.5和openSUSE11.2 / libc-2.10.1),有时候我没有(例如libc2.15与Fedora 17和Ubunutu 12.04),但valgrind总是以类似的方式对所有系统抱怨,让我认为它是一个侥幸,无论它是否崩溃。
这一切都让我怀疑64位的glibc存在某种错误,尽管如果有人发现这段代码有问题我会更开心!
我有一个预感是,它以某种方式与解析变量参数列表有关。这些模板究竟是如何运作的?我真的不清楚它是如何工作的,因为它直到运行时才知道格式字符串,那么它如何知道编译时模板的哪些特定实例呢?但是,这并不能解释为什么一切看起来都很好32位。
更新以回应评论
感谢大家的有益讨论。我认为awn关于%al寄存器的答案可能是正确的解释,尽管我还没有验证它。无论如何,为了讨论的好处,这里有一个完整的,最小的程序,可以在我的64位系统上重现其他人可以使用的错误。如果你在顶部#define _VOID_PTR
,它使用void *指针传递函数指针,就像在原始代码中一样(并触发valgrind错误)。如果您注释掉#define _VOID_PTR
,它将使用正确的原型函数指针,如WhosCraig所建议的那样。这种情况的问题是我不能简单地放int (*f)(const char *, double, double) = &printf;
,因为编译器抱怨原型不匹配(也许我只是很厚,有办法做到这一点? - 我猜这是原作者试图用void *指针解决的问题。为了处理这种特定情况,我使用正确的显式参数列表创建了这个wrap_printf()
函数。当我执行这个版本的代码时,它是valgrind clean。不幸的是,这并没有告诉我们它是否是一个void *与函数指针存储问题,或者与%al寄存器相关的东西;我认为大多数证据都指向后一种情况,我怀疑使用固定参数列表包装printf()
迫使编译器做“正确的事情”:
#include <cstdio>
#define _VOID_PTR // set if using void pointers to pass around function pointers
template<class R, class T1, class T2, class T3>
class FType
{
public :
#ifdef _VOID_PTR
FType( const void * f ) { _f = (R (*)(T1,T2,T3))f; }
#else
typedef R (*FP)(T1,T2,T3);
FType( R (*f)(T1,T2,T3 )) { _f = f; }
#endif
R operator()( T1 a1,T2 a2,T3 a3)
{ return _f( a1,a2,a3); }
private :
R (*_f)(T1,T2,T3);
};
template <class T1, class T2, class T3> int wrap_printf( T1 a1, T2 a2, T3 a3 ) {
const char *fmt = *((const char **) &a1);
return printf(fmt, a2, a3);
}
int main( void ) {
#ifdef _VOID_PTR
void *f = (void *)printf;
#else
// this doesn't work because function pointer arguments don't match printf prototype:
// int (*f)(const char *, double, double) = &printf;
// Use this wrapper instead:
int (*f)(const char *, double, double) = &wrap_printf;
#endif
char a1[]="%5.7f %5.7f\n";
double a2=1.;
double a3=0;
FType<int, const char *, double, double> f1(f);
printf(a1,a2,a3);
f1(a1,a2,a3);
return 0;
}
答案 0 :(得分:3)
由64位Linux(以及许多其他Unix)使用的System V amd64 ABI,具有固定数量的参数和可变数量的参数的函数具有略微不同的调用对象。
引用“System V Application Binary Interface AMD64 Architecture Processor Supplement”草案0.99.5 [2],第3.2.3章“参数传递”:
对于可能调用使用varargs或stdargs的函数的调用(无原型调用或在声明中调用包含省略号(...)的函数)%al用作隐藏参数来指定所使用的向量寄存器的数量。
现在,3步骤序列:
printf(3)是一个这样的变量参数函数。因此,期望正确填写%al寄存器。
您的FType :: _ f声明为具有固定数量参数的函数的指针。因此,当通过它调用某些内容时,编译器不关心%al。
当通过FType :: _ f调用printf()时,它期望正确填充%al(因为1),但编译器不关心填充它(因为2),因此, printf()在%al。
使用“垃圾”而不是正确初始化的值可能很容易导致各种不需要的结果,包括您观察到的段错误。
有关详细信息,请参阅:
[1] http://en.wikipedia.org/wiki/X86_calling_conventions#x86-64_calling_conventions
[2] http://x86-64.org/documentation/abi.pdf
答案 1 :(得分:1)
如果您的编译器兼容C ++ 11,因此可以处理variadic templates,并且可以重新排列参数的顺序,那么您可以执行以下操作:
template<typename F, typename ...A>
static Token evalF(vtok& args, const Token& resultType, F f, A... a)
{
Token result;
f(a...);
return result;
}
如果你看到例如,工作正常this example