MSVC:直接通过按值 returns 的 VTable 调用方法时崩溃 (C++)

MSVC: Crash when invoking a method directly via the VTable that returns by value (C++)

我试图理解为什么以下代码在 Windows 下的 MSVC 中无法正常工作。它通过value.

直接调用一个VTable方法returns
#include <iostream>

#define RETURN_BY_VALUE 1

struct Value {
  int Secret;
};

class IFoo {
public:
#if RETURN_BY_VALUE
  virtual Value GetValue() = 0;
#else
  virtual int GetValue() = 0;
#endif
};

class Foo : public IFoo {
public:
#if RETURN_BY_VALUE
  virtual Value GetValue() { return Value{42};  }
#else
  virtual int GetValue() { return 42; }
#endif
};

int main() {
  IFoo* f = new Foo;
  void* vtable = (void *)(*(std::intptr_t *)f);
  
#if RETURN_BY_VALUE
  std::cout << f->GetValue().Secret << std::endl;
  
  // The pointer to the GetValue function is at 0 byte offset in the Vtable
  auto method = (Value(*)(IFoo*)) (*(std::intptr_t *)(vtable));  
  std::cout << method(f).Secret << std::endl;
#else
  std::cout << f->GetValue() << std::endl;

  auto method = (int(*)(IFoo*)) (*(std::intptr_t *)(vtable));  
  std::cout << method(f) << std::endl;
#endif
  
  delete f;
  return 0;
}

输出为

> test.exe
42
1820312

如果我们将 RETURN_BY_VALUE 设置为 0,一切都会按预期进行。

> test.exe
42
42

运行godbolt中的示例表明gcc和clang没有这个问题https://godbolt.org/z/hn4MxK.

这在现实世界中的应用是在 d3d12 中挂钩 GetAdapterLuid

  1. 别忘了 __stdcall。没有 __stdcall,自由函数和方法的调用约定差异更大。我猜 GetAdapterLuid__stdcall
  2. 使用 Windows ABI returned 结构作为隐藏参数传递。看起来 this 和隐藏参数之间的顺序在方法和自由函数上是不同的。你可以定义你的函数,这样它就是真正的参数。

这似乎对我有用,但确实闻起来很香:

#include <iostream>

#define RETURN_BY_VALUE 1

struct Value {
    int Secret;
};

class IFoo {
public:
#if RETURN_BY_VALUE
    virtual Value __stdcall GetValue() = 0;
#else
    virtual int __stdcall  GetValue() = 0;
#endif
};

class Foo : public IFoo {
public:
#if RETURN_BY_VALUE
    virtual Value __stdcall GetValue() { return Value{42};  }
#else
    virtual int __stdcall  GetValue() { return 42; }
#endif
};


int main() {
    IFoo* f = new Foo;
    void* vtable = (void *)(*(std::intptr_t *)f);
  
#if RETURN_BY_VALUE
    std::cout << f->GetValue().Secret << std::endl;
  
    // The pointer to the GetValue function is at 0 byte offset in the Vtable
    Value v;
    auto method = (void(__stdcall *)(IFoo*, Value*)) (*(std::intptr_t *)(vtable));  
    std::cout << (method(f, &v), v.Secret) << std::endl;
#else
    std::cout << f->GetValue() << std::endl;

    auto method = (int(__stdcall *)(IFoo*)) (*(std::intptr_t *)(vtable));  
    std::cout << method(f) << std::endl;
#endif
  
    delete f;
    return 0;
}

gcc / clang 会 return 你的结构完全是整数,类 Unix 系统有不同的 ABI。


实现挂钩的最简洁的方法是将方法实现为方法。在你的 hook vtable 中定位方法,就像在目标 vtable 中一样,那么你就不需要使用自由函数来伪造方法。