C++中虚函数的编译时静态类型检查

mgd*_*mgd 6 c++ linker static-analysis virtual-functions one-definition-rule

背景

最近,我的一位同事遇到了一个问题,其中使用了旧版本的库头文件.结果是,为在C++中调用虚函数而生成的代码引用了类(vtable)的虚函数查找表中的错误偏移量.

不幸的是,在编译期间没有捕获到此错误.

所有普通函数都使用其错位名称进行链接,以确保链接器选择正确的函数(包括正确的重载变量).同样,可以想象一个目标文件或库可以包含有关Ctable类的vtable中的函数的符号信息.

有没有办法让C++编译器(比如说g++或Visual Studio)在链接期间检查对虚函数的调用?

这是一个简单的测试示例.想象一下这个简单的头文件和相关的实现:

Base.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
Run Code Online (Sandbox Code Playgroud)

Derived.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();
  }

}
Run Code Online (Sandbox Code Playgroud)

现在,假设一个程序使用了错误/旧版本的头文件,其中缺少中间的虚函数:

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
Run Code Online (Sandbox Code Playgroud)

所以这里我们有主程序:

Main.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;
}
Run Code Online (Sandbox Code Playgroud)

当我们使用正确的头编译"库" ,然后使用错误的头编译和链接主程序...

$ g++ -c Derived.cpp 
$ g++ Main.cpp Derived.o -o Main
Run Code Online (Sandbox Code Playgroud)

...然后虚拟调用h()使用vtable中的错误索引,因此调用实际上转到g():

$ ./Main
Derived::f()
f() returned: 1
Derived::g()
h() returned: 2
Run Code Online (Sandbox Code Playgroud)

在这个小例子中,函数g()h()具有相同的签名,因此出错的"唯一"的东西是被调用的错误函数(这本身很糟糕并且可能完全被忽视),但如果签名不同,则可以(并且具有导致堆栈损坏 - 例如,在使用Pascal调用约定的Windows上调用DLL中的函数时(调用者推送参数并且callee在返回之前弹出它们).

Sto*_*row 0

对你的问题的简短回答是否定的。根本问题是被调用函数的偏移量是在编译时计算的;因此,如果您的调用者代码是使用包含“virtual int g() const”的(不正确的)头文件进行编译的,那么您的 main.o 将通过 g() 的存在来偏移对 h() 的所有引用。但是你的库是用正确的头文件编译的,因此没有函数 g(),因此 Derived.o 中 h() 的偏移量将与 main.o 中的不同

这不是对虚函数调用进行类型检查的问题 - 这是一个“限制”,基于 C++ 编译器进行编译时函数偏移计算,而不是运行时计算。

您可以通过使用 dl_open 而不是直接函数调用并动态链接库(而不是静态链接)来解决此问题。