Try English version of Quizful



Раздаем бесплатные Q! подробности в группе Quizful.Alpha-test
Топ контрибуторов
loading
loading
Знаете ли Вы, что

Свои вопросы для тестов можно добавлять на странице с информацией о тесте. При этом для некоторых тестов добавление вопросов закрыто

Лента обновлений
ссылка Oct 19 19:24
Комментарий от Astefia:
Пояснение точно надо переписать. Математика и программир...
ссылка Oct 19 19:09
Комментарий от log4456602:
Не аналогична- Integer, Double extends Number.
ссылка Oct 19 19:02
Комментарий от lazloSoot:
худшего теста я еще не видел....
ссылка Oct 19 16:52
Комментарий от log4456602:
Главный принцип для понимания в этой структуре:
1) с...
ссылка Oct 19 14:09
Добавлен вопрос в тест Python 3 Основы
Статистика

Тестов: 153, вопросов: 8597. Пройдено: 417812 / 2038561.

Техника безопасности при работе с таблицей виртуальных функций.

head tail Статья
категория
C++
дата08.07.2015
авторInstand
голосов19

Небольшое вступление перед статьей. Хотелось бы, чтобы читатели задались вопросом: нужно ли фотографу знать, как работает камера, чтобы сделать высококачественные фотографии? Например, понимает ли он, хотя бы, значение слов “диафрагма”, “отношение сигнал/шум”, “глубина резкости”? Как показывает практика, фотографии, сделанные такими знатоками, немногим лучше, чем фотографии, сделанные с помощью цифровой камеры мобильного телефона с разрешением 0,3 Мп.

С другой стороны, высококачественные фотографии могут быть выполнены за счет опыта и интуиции при отсутствии знаний (но обычно это исключение из правил). Маловероятно, что кто-либо оспорит тот факт, что профессионалы, желающие использовать все возможности своей камеры, должны обладать такими знаниями, в противном случае их просто нельзя называть профессионалами. Данное понятие справедливо не только к цифровой фотографии, но и к любой другой отрасли.

Естественно данная тематика справедлива для программирования и, в двойне, для программирования на языке C++. В данной статье мы обсудим важную особенность языка - указатель на таблицу виртуальных функций, используемый почти в любом нетривиальном классе, а также возможность данного указателя нанести вред реализуемой программе. Поврежденный указатель на таблицу виртуальных функций может привести к серьезным последствиям и большим трудностям в отладке программы. Для начала вспомним, что такое указатель на таблицу виртуальных функций, а затем рассмотрим что и как может повредить указатель.

К сожалению, в данной статье предостаточно объяснений, связанных с низким уровнем. Однако это единственная возможность проиллюстрировать данную проблему. В дополнение, код, написанный в данной статье, успешно работает на компиляторе Visual C++ 64 бит, результаты для других компиляторов и платформ могут отличаться.


Указатель на таблицу виртуальных функций

В теории сказано, что указатель на таблицу виртуальных функций (vptr, vpointer), хранится в любом классе, в котором присутствует хотя бы один виртуальный метод. Давайте разберемся, что это за указатель. Для этого напишем простую демонстрационную программу на C++.


#include <iostream>
#include <iomanip>
using namespace std;
int nop() {
  static int nop_x; return ++nop_x; // Don't remove me, compiler!
};
 
class A
{
public:
  unsigned long long content_A;
  A(void) : content_A(0xAAAAAAAAAAAAAAAAull)
      { cout << "++ A has been constructed" << endl;};
  ~A(void)
      { cout << "-- A has been destructed" << endl;};
 
  void function(void) { nop(); };
};
 
void PrintMemory(const unsigned char memory[],
                 const char label[] = "contents")
{
  cout << "Memory " << label << ": " << endl;
  for (size_t i = 0; i < 4; i++)
  {
    for (size_t j = 0; j < 8; j++)
      cout << setw(2) << setfill('0') << uppercase << hex
           << static_cast<int> (memory[i * 8 + j]) << " ";
    cout << endl;
  }
}
 
int main()
{
  unsigned char memory[32];
  memset(memory, 0x11, 32 * sizeof(unsigned char));
  PrintMemory(memory, "before placement new");
 
  new (memory) A;
  PrintMemory(memory, "after placement new");
  reinterpret_cast<A *>(memory)->~A();
 
  system("pause");
  return 0;
};

Несмотря на относительно большой размер кода, его логика предельно ясна: сначала, программа выделяет 32 байта на стеке, заполняет память значением 0x11 (данное значение будет означать “мусор” в памяти, то есть не инициализированную память). Затем, при помощи оператора “размещающее new” (placement new), программа создает объект обычного класса A. Наконец, выводится на экран содержимое памяти, уничтожается объект класса A и завершается программа. Во что выводит программа:


Memory before placement new:
11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11
++ A has been constructed
Memory after placement new:
AA AA AA AA AA AA AA AA
11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11
-- A has been destructed
Press any key to continue . . .

Как можно заметить, размер класса A в памяти составляет 8 байт и равен размеру единственного поля “unsigned long long content_A”. Немного усложним нашу программу, добавив ключевое слово ‘virtual’ в объявлении функции:


virtual void function(void) {nop();};

Теперь вывод программы будет таким (ниже и далее, фразы “Memory before placement new” и “Press any key to continue…”будут опущены):


++ A has been constructed
Memory after placement new:
F8 D1 C4 3F 01 00 00 00
AA AA AA AA AA AA AA AA
11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11
-- A ha sbeen destructed

На этот раз, размер класса A составляет 16 байт. Первые 8 байт содержат указатель на таблицу виртуальных функций. При данном запуске указатель равен значению 0x000013FC4D1F8 (указатель и поле ‘content_A’ инвертированы в памяти, это архитектура компьютера Intel64, формат слова с записью младшего байта по наименьшему адресу).

Таблица виртуальных функций - специальная структура в памяти, генерируемая автоматически и содержащая указатели на все виртуальные метода класса. При вызове function() класса A в каком-либо участке кода, вместо вызова A::function() напрямую, будет вызвана функция, размещенная в таблице виртуальных функций, такое поведение реализует принцип полиморфизма. Таблица виртуальных функций показана ниже (таблица получена при помощи компиляции /Faskey; заметим странное имя функции в ассемблерном коде – оно получено при помощи “декорирования имени”):


CONST SEGMENT
??_7A@@6B@ DQ  FLAT:??_R4A@@6B@   ; A::'vftable'
 DQ FLAT:?function@A@@UEAAXXZ
CONST ENDS

Атрибут __declspec (novtable)

Бывают ситуации, когда указатель на таблицу виртуальных функций совсем не нужен. Предположим, что мы никогда не будем создавать экземпляр объекта класса A. Такая ситуация часто встречается в абстрактных классах – как известно нельзя создать объект такого класса. На самом деле, если function() была объявлена в классе A как абстрактный метод, то таблица виртуальных функций будет выглядеть так:


CONST SEGMENT
??_7A@@6B@ DQ FLAT:??_R4A@@6B@ ; A::'vftable'
 DQ FLAT:_purecall
CONST ENDS

Попытка вызова функции все-равно что стрельба в собственную ногу. Возникает вопрос: если для класса никогда не создается экземпляр объекта, то какой смысл инициализации указателя на таблицу виртуальных функций? Для предотвращения генерации излишнего кода компилятором, программист может прописать атрибут __declspec(novtable) (специфика Microsoft). Перепишем наш пример:


class __declspec(novtable) A { .... }

Программа выведет на экран следующее:


++ A has been constructed
Memory after placement new:
11 11 11 11 11 11 11 11
AA AA AA AA AA AA AA AA
11 11 11 11 11 11 11 11
11 11 11 11 11 11 11 11
-- A ha sbeen destructed

Размер объекта не изменился, он все еще занимает в памяти 16 байт. После подключения __declspec(novtable), появилось два отличия: вместо указателя на виртуальную таблицу теперь неинициализированная память, а в ассемблерном коде отсутствует таблица виртуальных функций для класса A. Тем не менее, указатель на таблицу виртуальных функций присутствует и занимает 8 байт!


Наследование

Перепишем пример для реализации простейшей техники наследования от абстрактного класса с указателем на таблицу виртуальных функций.


class __declspec(novtable) A // I never instantiate
{
public:
  unsigned long long content_A;
  A(void) : content_A(0xAAAAAAAAAAAAAAAAull)
      { cout << "++ A has been constructed" << endl;};
  ~A(void)
      { cout << "-- A has been destructed" << endl;};
 
  virtual void function(void) = 0;
};
 
class B : public A // I always instantiate instead of A
{
public:
  unsigned long long content_B;
  B(void) : content_B(0xBBBBBBBBBBBBBBBBull)
      { cout << "++ B has been constructed" << endl;};
  ~B(void)
      { cout << "-- B has been destructed" << endl;};
 
  virtual void function(void) { nop(); };
};

Необходимо сделать так, чтобы вместо создания объекта класса A, программа создавала (и уничтожала) объект класса B.


....
new (memory) B;
PrintMemory(memory, "after placement new");
reinterpret_cast<B *>(memory)->~B();
....

Вывод программы:


++ A has been constructed
++ B has been constructed
Memory after placement new:
D8 CA 2C 3F 01 00 00 00
AA AA AA AA AA AA AA AA
BB BB BB BB BB BB BB BB
11 11 11 11 11 11 11 11
-- B has been destructed
-- A has been destructed

Посмотрим, что получилось. Был вызван конструктор B::B(). До выполнения тела конструктора B, вызван конструктор базового класса A::A(). Если бы отсутствовал атрибут __declspec(novtable), то конструктор A::A() инициализировал бы указатель на таблицу виртуальных функций. В нашем случае указатель не инициализирован. Затем конструктор присвоил значение 0xAAAAAAAAAAAAAAAAull полю content_A (второе поле в памяти) и вернул поток управления конструктору B::B().

Так как атрибута __declspec(novtable) у класса B нет, то конструктор установил указатель на таблицу виртуальных функций (первое поле в памяти) на виртуальный метод класса B и присвоил полю content_B значение 0xBBBBBBBBBBBBBBBBull (третье поле в памяти), а затем вернул управляющий поток основной программе. Мы видим, что объект класса B сконструирован правильно и логика программы совершенно верно опустила ненужную операцию. Под ненужной операцией подразумевается инициализация указателя на таблицу виртуальных функций в конструкторе базового класса.

Как видно, только одна операция опушена. Для чего необходимо ее удалять? Если программа содержит тысячи классов, наследуемых от одного абстрактного класса, то удаление одной автоматически генерируемой команды может значительно повлиять на производительность программы в целом. И оно будет влиять. Давайте проверим.


Функция memset

Основная идея функции memset() – заполнение полей памяти различными константными значениями (обычно это 0). В языке C такую функцию можно использовать для быстрой инициализации полей структуры. Так в чем же различие между простым классом на C++ без указателя на таблицу виртуальных функций и C структурой в размещениях в памяти? Что ж, здесь нет различий. Сырые данные C структуры, такие же как и сырые данные C++ класса. Для инициализации простых классов на C++ (в C++11 – стандартный формат типов) можно использовать функцию memset(). Также возможно использовать данную функцию для инициализации любого класса. Однако, каков будет результат? Неправильный вызов функции может повредить указатель на таблицу виртуальных функций. Возникает вопрос: можно ли использовать memset() для классов с атрибутом __declspec(novtable)? Да, можно, но с некоторыми мерами предосторожности.
Перепишем наш пример, добавив метод wipe():


class __declspec(novtable) A // I never instantiate
{
public:
  unsigned long long content_A;
  A(void)
    {
      cout << "++ A has been constructed" << endl;
      wipe();
    };
    // { cout << "++ A has been constructed" << endl; };
  ~A(void)
    { cout << "-- A has been destructed" << endl;};
 
  virtual void function(void) = 0;
  void wipe(void)
  {
    memset(this, 0xAA, sizeof(*this));
    cout << "++ A has been wiped" << endl;
  };
};
 
class B : public A // I always instantiate instead of A
{
public:
  unsigned long long content_B;
  B(void) : content_B(0xBBBBBBBBBBBBBBBBull)
      { cout << "++ B has been constructed" << endl;};
      // {
      //   cout << "++ B has been constructed" << endl;
      //   A::wipe();
      // };
 
  ~B(void)
      { cout << "-- B has been destructed" << endl;};
 
  virtual void function(void) {nop();};
};

Вывод программы:


++ A has been constructed
++ A has been wiped
++ B has been constructed
Memory after placement new:
E8 CA E8 3F 01 00 00 00
AA AA AA AA AA AA AA AA
BB BB BB BB BB BB BB BB
11 11 11 11 11 11 11 11
-- B has been destructed
-- A has been destructed

Тем не менее, если мы изменим вызов метода wipe(), закомментировав код конструкторов, уберем текущие комментарии, то процесс выполнения будет не верным. Первый вызов виртуального метода function() послужит следствием run-time ошибки из-за поврежденного указателя на таблицу виртуальных функций:


++ A has been constructed
++ B has been constructed
++ A has been wiped
Memory after placement new:
AA AA AA AA AA AA AA AA
AA AA AA AA AA AA AA AA
BB BB BB BB BB BB BB BB
11 11 11 11 11 11 11 11
-- B has been destructed
-- A has been destructed

Почему это произошло? Метод wipe() вызван после того как конструктор B проинициализировал указатель на таблицу виртуальных функций. Как следствие, wipe() и повредил этот указатель. Не рекомендуется создавать нулевой класс с указателем на таблицу виртуальных функций, даже если он обладает атрибутом __declspec(novtable). Полное обнуление уместно только в конструкторе класса, экземпляры которого никогда не будут созданы, однако даже в данной ситуации необходимо проявить осторожность.


Функция memcpy

Вся информация выше может быть применена и к функции memcpy(). На практике программисты часто используют эту функцию, где нужно и не нужно вовсе. Применение функции memcpy() в нестандартных форматах данных равносильно взбиранию по канату наверх Ниагарского водопада: одна ошибка будет смертельной, а допустить такую ошибку достаточно просто. Например:


class __declspec(novtable) A
{
  ....
  A(const A &source) { memcpy(this, &source, sizeof(*this)); }
  virtual void foo() { }
  ....
};
classB : public A { .... };

Копирующий конструктор может записать в указатель на таблицу виртуальных функций абстрактного класса все, что его “цифровой” душе угодно: конструктор наследуемого класса в любом случае проинициализирует указатель корректным значением. Однако, в теле оператора присваивания использование функции memcpy() запрещено:


class __declspec(novtable) A
{
  ....
  A &operator =(const A &source)
  {
    memcpy(this, &source, sizeof(*this));
    return *this;
  }
  virtual void foo() { }
  ....
};
class B : public A { .... };
 

Вспомним, что конструктор копирования и оператор присваивания имею почти похожие тела. На самом деле не все так плохо, как кажется на первый взгляд: на практике оператор присваивания может работать, так как от него требуется, но не в следствии корректности кода, а по чистой случайности. Данный код копирует указатель на таблицу виртуальных функций из другого класса и результат довольно непредсказуем.


PVS-Studio

В данной статье рассказано о таинственно атрибуте __declspec(novtable), о возможностях применения функций memset() и memcpy() в высокоуровневом коде и о нецелесообразности их использования в конкретных случаях. Время от времени разработчики спрашивают о том факте, что система PVS-Studio показывает так много предупреждений об указателях на таблицу виртуальных функций. Некоторые программисты считают, что если объявлен атрибут __declspec(novtable), то у класса нет ни таблицы виртуальных функций, ни указателей на таблицу виртуальных функций. Но на самом деле не все так просто, как выглядит.

Нужно иметь ввиду, что если классу объявлен атрибут __declspec(novtable), то это не означает отсутствия указателя на таблицу виртуальных функций. Инициализирует ли класс указатель или нет? В дальнейшем разработчики PVS-Studio планируют внедрить в анализатор возможность определения использования функций memset/memcpy для базового класса с атрибутом __declspec(novtable).


Заключение

К сожалению, в данной статье не освещено множество материала о наследовании (например, множественном). Тем не менее, автор надеется на полезность статьи и понимание всей проблематики вопроса. Стоит три раза подумать прежде, чем использовать низкоуровневые конструкции с высокоуровневыми объектами.

Источник: cplusplus.com Перевод: Instand

Если Вам понравилась статья, проголосуйте за нее

Голосов: 19  loading...
Staller   Seaside_   grandkarabas   Instand   ganzaev   neon009   wadoss   albatros78   nj7834   ingwarsmith   mad_rus   MeTrA   mutcher   Pinaeva   ViktotL   flkremin   fatalydag   Aburov   Kocok_Den23