c++ - 使用模板化仿函数segfaults调用printf(仅64位,在32位中使用valgrind clean)

标签 c++ templates printf functor

我目前正在调试90年代后期编写的一些C++代码,这些代码可以分析脚本以加载数据,执行简单的操作以及打印结果等。

编写代码的人使用函子将要解析的文件中的字符串关键字映射到实际的函数调用,并且将它们进行模板化(最多包含8个参数)以处理用户可能在其请求中请求的各种函数接口(interface)。脚本。

在大多数情况下,这一切都可以正常工作,只是近年来它开始在我们的某些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);

};

然后调用它的代码在另一个模板类中-我展示了原型(prototype)和使用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所期望的那样。这很好。

接下来,我尝试使用其中没有变量的替换格式字符串(fello,world)调用f1。这也很好。

然后添加变量之一(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总是以类似的方式抱怨所有系统,这使我认为无论是否崩溃,这都是fl幸。

这一切都使我怀疑64位glibc的某种错误,尽管如果有人可以发现此代码有问题,我会更加高兴!

我有一种预感是,它某种程度上与可变参数列表的解析有关。这些如何与模板一起使用?我实际上还不清楚它是如何工作的,因为它直到运行时才知道格式字符串,那么它如何知道在编译时要制作模板的哪个特定实例?但是,这并不能解释为什么在32位中一切看起来都很好。

更新以回应评论

谢谢大家的有益讨论。我认为关于%al寄存器的问题可能是正确的解释,尽管我尚未验证它。无论如何,为了便于讨论,这里有一个完整的最小程序,可在我的64位系统上重现其他人可以使用的错误。如果您在顶部使用#define _VOID_PTR,它会像原始代码一样使用void *指针来传递函数指针(并触发valgrind错误)。如果注释掉#define _VOID_PTR,它将代替WhosCraig建议使用正确原型(prototype)的函数指针。这种情况的问题是,我不能简单地放入int (*f)(const char *, double, double) = &printf;,因为编译器抱怨原型(prototype)不匹配(也许我只是很胖,有办法做到这一点?-我猜这是原来的问题作者试图解决void *指针)。为了处理这种特定情况,我使用正确的显式参数列表创建了这个wrap_printf()函数。当我执行此版本的代码时,它是valgrind clean。不幸的是,这并没有告诉我们这是void * vs.函数指针存储问题,还是与%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;
}

最佳答案

由64位Linux(和许多其他Unix)使用的System V amd64 ABI,具有固定数量的参数和可变数量的参数的函数在调用方式上有很大不同。

引自“系统V应用程序二进制接口(interface)AMD64架构处理器增补”草案0.99.5 [2],第3.2.3章“参数传递”:

For calls that may call functions that use varargs or stdargs (prototype-less calls or calls to functions containing ellipsis (...) in the declaration) %al is used as hidden argument to specify the number of vector registers used.



现在,分三个步骤:
  • printf(3)是这样的变量参数函数。因此,期望%al寄存器正确填充。
  • 您的FType::_ f声明为带有固定数量参数的函数的指针。因此,编译器在通过%al进行调用时并不关心%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

    关于c++ - 使用模板化仿函数segfaults调用printf(仅64位,在32位中使用valgrind clean),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15341416/

    相关文章:

    c++ - 我已经使用 QtCreator 导入了一个现有项目。现在它提示 "No rule to make target ' 全部'。”

    c++ - win7 64 位上的正则表达式构建错误

    c++ - 运算符 << 重载

    c - 如何在 C 中创建 24 位无符号整数

    c++ - 内存分配和字符数组

    c++ - 在函数中创建的对象的范围

    C++ 微软 : How to associate uuid/guid with template specialization

    使用迭代器的 C++ 模板函数

    c++ - 使用 snprintf 填充结构体

    c++ - sprintf 为空字符字符串时的预期行为