4

我目前正在调试一些 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),“f”是 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;
}
4

2 回答 2

3

通过 64 位 Linux(和许多其他 Unix)使用的 System V amd64 ABI,具有固定数量参数和可变数量参数的函数具有稍微不同的调用约定。

引用自《System V Application Binary Interface AMD64 Architecture Processor Supplement》Draft 0.99.5 [2],第 3.2.3 章“参数传递”:

对于可能调用使用 varargs 或 stdargs 的函数的调用(无原型调用或对声明中包含省略号 (...) 的函数的调用)%al 用作隐藏参数以指定使用的向量寄存器的数量。

现在,3 步序列:

  1. printf(3) 就是这样一个可变参数函数。因此,期望 %al 寄存器被正确填充。

  2. 您的 FType::_f 声明为指向具有固定数量参数的函数的指针。因此,当通过它调用某些东西时,编译器并不关心 %al。

  3. 当通过 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

于 2013-03-11T16:10:59.877 回答
1

如果您的编译器与 C++11 兼容,因此可以处理可变参数模板,并且可以重新排列参数的顺序,您可能可以执行以下操作:

template<typename F, typename ...A>
static Token evalF(vtok& args, const Token& resultType, F f, A... a)
{
    Token result;

    f(a...);

    return result;
}

如果你看到这个例子,效果很好。

于 2013-03-12T07:43:27.203 回答