6

バックグラウンド

最近、私の同僚が、ライブラリの古いバージョンのヘッダー ファイルが使用されているという問題に遭遇しました。その結果、C++ で仮想関数を呼び出すために生成されたコードが、クラスの仮想関数ルックアップ テーブル ( vtable ) 内の間違ったオフセットを参照していました。

残念ながら、このエラーはコンパイル中にキャッチされませんでした。

質問

すべての通常の関数は、正しい関数 (正しいオーバーロード バリアントを含む) がリンカによって選択されるように、マングルされた名前を使用してリンクされます。同様に、オブジェクト ファイルまたはライブラリには、C++ クラスのvtable内の関数に関するシンボリック情報が含まれていると想像できます。

g++リンク中に C++ コンパイラ (または Visual Studio など) が仮想関数の呼び出しを型チェックできるようにする方法はありますか?

簡単なテスト例を次に示します。この単純なヘッダー ファイルと関連する実装を想像してください。

ベース.hpp:

#ifndef BASE_HPP
#define BASE_HPP

namespace Test
{
  class Base
  {
  public:
    virtual int f() const = 0;
    virtual int g() const = 0;
    virtual int h() const = 0;
  };

  class BaseFactory
  {
  public:
    static const Base* createBase();
  };
}

#endif

派生.cpp:

#include "Base.hpp"

#include <iostream>

using namespace std;

namespace Test
{
  class Derived : public Base
  {
  public:
    virtual int f() const
    {
      cout << "Derived::f()" << endl;
      return 1;
    }

    virtual int g() const
    {
      cout << "Derived::g()" << endl;
      return 2;
    }

    virtual int h() const
    {
      cout << "Derived::h()" << endl;
      return 3;
    }
  };

  const Base* BaseFactory::createBase()
  {
    return new Derived();
  }

}

ここで、プログラムが間違った/古いバージョンのヘッダー ファイルを使用し、中央の仮想関数が欠落していると想像してください。

BaseWrong.hpp

#ifndef BASEWRONG_HPP
#define BASEWRONG_HPP

namespace Test
{
  class Base
  {
  public:
    virtual int f() const = 0;
    // Missing: virtual int g() const = 0;
    virtual int h() const = 0;
  };

  class BaseFactory
  {
  public:
    static const Base* createBase();
  };
}

#endif

ここにメインプログラムがあります:

メイン.cpp

// Including the _wrong_ version of the header!
#include "BaseWrong.hpp"

#include <iostream>

using namespace std;

int main()
{
  const Test::Base* base = Test::BaseFactory::createBase();
  const int fres = base->f();
  cout << "f() returned: " << fres << endl;
  const int hres = base->h();
  cout << "h() returned: " << hres << endl;
  return 0;
}

正しいヘッダーを使用して「ライブラリ」をコンパイルし、次に間違ったヘッダーを使用してメイン プログラムをコンパイルおよびリンクすると…</p>

$ g++ -c Derived.cpp 
$ g++ Main.cpp Derived.o -o Main

…その後、 への仮想呼び出しがvtableh()で間違ったインデックスを使用するため、呼び出しは実際には:g()

$ ./Main
Derived::f()
f() returned: 1
Derived::g()
h() returned: 2

この小さな例では、関数g()と関数h()は同じシグネチャを持っているため、「唯一の」問題は間違った関数が呼び出されていることです (これはそれ自体が悪いことであり、完全に気付かれない可能性があります)。たとえば、Pascal 呼び出し規則が使用されている Windows 上の DLL で関数を呼び出す場合 (呼び出し元が引数をプッシュし、呼び出し先が戻る前に引数をポップする)。

4

1 に答える 1

0

あなたの質問に対する短い答えはノーです。基本的な問題は、呼び出された関数へのオフセットがコンパイル時に計算されることです。したがって、呼び出し元のコードが "virtual int g() const" を含む (正しくない) ヘッダー ファイルでコンパイルされた場合、main.o には h() へのすべての参照が g() の存在によってオフセットされます。ただし、ライブラリは正しいヘッダー ファイルでコンパイルされているため、関数 g() はありません。そのため、Derived.o の h() のオフセットは main.o とは異なります。

これは、仮想関数への呼び出しの型チェックの問題ではありません。これは、C++ コンパイラがランタイムではなくコンパイル時に関数のオフセット計算を行うという事実に基づく「制限」です。

この問題は、関数を直接呼び出す代わりに dl_open を使用し、ライブラリを静的にリンクする代わりに動的にリンクすることで回避できます。

于 2016-02-24T05:38:54.827 に答える