MSVC: сбой при вызове метода непосредственно через VTable, который возвращает значение (C )

#c #visual-c #return #visual-studio-2019 #vtable

#c #visual-c #Возврат #visual-studio-2019 #vtable

Вопрос:

Я пытаюсь понять, почему следующий код неправильно работает в MSVC под Windows. Он напрямую вызывает метод VTable, который возвращает значение по значению.

 #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 .

Реальным применением этого было бы подключение GetAdapterLuid в d3d12.

Комментарии:

1. вы уверены, что это правильный макет виртуальной таблицы Visual Studio?

2. Ваш пример для godbolt кажется неправильным. Вы всегда выводите там путь по умолчанию.

3. @AlanBirtles Да, вот почему это работает, если вы установите значение RETURN_BY_VALUE равным 0

4. @Secundi Спасибо, виноват. Исправлено: godbolt.org/z/hn4MxK

5. при вызове неопределенного поведения часто кажется, что все работает

Ответ №1:

  1. Не забывай об __stdcall этом. Без __stdcall этого соглашение о вызове для свободной функции и для методов отличается больше. GetAdapterLuid это __stdcall я так думаю.
  2. В Windows ABI возвращаемые структуры передаются как скрытый параметр. Похоже, что порядок между 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, amp;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 вернет вашу структуру точно как целое число, Unix-подобные системы имеют другой ABI.


Самый чистый способ реализации перехватов — реализовать методы как методы. Найдите методы в вашей виртуальной таблице hook точно так же, как в целевой виртуальной таблице, тогда вам не нужно подделывать метод с помощью free function .

Комментарии:

1. Спасибо, кажется, это работает. Для общей реализации, я думаю, нам пришлось бы выделить объект, чтобы конструктор не вызывался дважды.

2. Размещение (C 11 без ограничений) union в стеке может быть лучше.