4

私は現在、スクリプトを解析してデータをロードし、単純な操作を実行し、結果などを出力する、90 年代後半に書かれたいくつかの C++ コードをデバッグしています。

コードを書いた人々は、ファンクターを使用して、解析しているファイル内の文字列キーワードを実際の関数呼び出しにマップし、ユーザーが要求する可能性のある無数の関数インターフェイスを処理するために (最大 8 つの引数で) テンプレート化されています。脚本。

ほとんどの場合、これはすべて正常に機能しますが、近年、一部の 64 ビット ビルド システムで segfault が発生し始めました。valgrind を実行すると、驚いたことに、前述のファンクターの 1 つである「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 節での操作のみを実行します。FType コンストラクター (戻り値の型を int としてテンプレート化する) を、printf のファンクター (デバッガーで検証済み) である "f" で呼び出します。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 が期待するとおり)。これはうまくいきます。

次に、変数を含まない置換フォーマット文字列 (Hello, world) で f1 を呼び出してみました。これもうまくいきます。

次に、変数の 1 つ (Hello, World2 %5.7f) を追加すると、上記のように valgrind エラーが表示され始めます。

このコードを 32 ビット システムで実行すると、valgrind はクリーンになります (それ以外の場合は、glibc、gcc の同じバージョン)。

いくつかの異なる Linux システム (すべて 64 ビット) で実行すると、segfault が発生する場合 (RHEL5.8/libc2.5 および openSUSE11.2/libc-2.10.1 など) と発生しない場合 (libc2.15 など) があります。 Fedora 17 および Ubunutu 12.04 を使用する) が、valgrind は常にすべてのシステムに対して同様の方法で文句を言うので、クラッシュするかどうかはまぐれだと思います。

これはすべて、64 ビットの glibc に何らかのバグがあるのではないかと私に思わせます。

私が持っていた 1 つの予感は、何らかの形で可変引数リストの解析に関連しているということです。これらはテンプレートでどのように機能しますか? 実行時までフォーマット文字列がわからないため、これがどのように機能するかは実際にはわかりません。コンパイル時に作成するテンプレートの特定のインスタンスをどのように知るのでしょうか? ただし、これは 32 ビットですべてが問題ないように見える理由を説明していません。

コメントに応じて更新

この有益な議論をありがとうございました。%al レジスタに関する awn からの回答は、まだ検証していませんが、おそらく正しい説明だと思います。とにかく、議論の利益のために、他の人が遊ぶことができる64ビットシステムでエラーを再現する完全で最小限のプログラムを次に示します. 一番上にいる場合#define _VOID_PTRは、void * ポインターを使用して、元のコードのように関数ポインターを渡します (そして、valgrind エラーをトリガーします)。をコメントアウトする#define _VOID_PTRと、WhosCraig が提案したように、代わりに適切にプロトタイプ化された関数ポインターが使用されます。このケースの問題は、単純に言えなかったということですint (*f)(const char *, double, double) = &printf;コンパイラはプロトタイプの不一致について不平を言うので(多分私はただ太っているだけで、これを行う方法がありますか?-これは元の作者が void * ポインタで回避しようとしていた問題だと思います)。この特定のケースに対処するためにwrap_printf()、正しい明示的な引数リストを使用してこの関数を作成します。このバージョンのコードを実行すると、valgrind はクリーンです。残念ながら、これは、それが 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 章「Parameter Passing」からの引用:

varargs または stdargs を使用する関数を呼び出す可能性のある呼び出し (プロトタイプなしの呼び出し、または宣言に省略記号 (...) を含む関数の呼び出し) では、使用されるベクトル レジスタの数を指定するための隠し引数として %al が使用されます。

さて、3つのステップシーケンス:

  1. printf(3) は、そのような可変引数関数です。したがって、%al レジスタが適切に埋められることが期待されます。

  2. FType::_f は、固定数の引数を持つ関数へのポインターとして宣言されています。したがって、コンパイラは %al を介して何かを呼び出す場合、%al を気にしません。

  3. printf() が FType::_f を介して呼び出されると、適切に埋められた %al (1 のため) が期待されますが、コンパイラはそれを埋めようとしませんでした (2 のため)。結果として、printf() は "ガベージ" in %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 に答える