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

Фундаментальные основы хакерства

Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.

Ссылки на другие статьи из этого цикла ищи на странице автора.

 

Объекты в куче

Конструктор в силу своего автоматического вызова при создании нового экземпляра объекта — первая по счету функция. Так какие же могут возникнуть сложности в его идентификации? Камень преткновения в том, что конструктор факультативен, то есть может присутствовать в объекте, а может и не присутствовать. Поэтому совсем не факт, что первая вызываемая функция — именно конструктор!

Заглянув в описание языка C++, можно обнаружить, что конструктор не возвращает никакого значения, что нехарактерно для обычных функций, однако все же не настолько редко встречается, чтобы однозначно его идентифицировать. Как же тогда быть?

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

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

Деструктор, как и конструктор, факультативен, то есть последняя вызываемая функция объекта может и не быть деструктором. Тем не менее отличить деструктор от любой другой функции очень просто — он вызывается только при результативном создании объекта (то есть успешном выделении памяти) и игнорируется в противном случае. Это документированное свойство языка — следовательно, обязательное к реализации всеми компиляторами. Таким образом, в код помещается такое же «кольцо», как и у конструктора, но никакой путаницы не возникает, так как конструктор вызывается всегда первым (если он есть), а деструктор — последним (опять-таки если он присутствует).

Особый случай представляет объект, целиком состоящий из одного конструктора (или деструктора), — попробуй-ка разобраться, с чем мы имеем дело. Но разобраться можно! За вызовом конструктора практически всегда присутствует код, обнуляющий this в случае неудачного выделения памяти, а у деструктора этого нет. Далее, деструктор обычно вызывается не непосредственно из материнской процедуры, а из функции-обертки, вызывающей помимо деструктора и оператор delete, который освобождает занятую объектом память. Так что отличить конструктор от деструктора вполне можно.

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

#include <stdio.h>

class MyClass {

public:
    MyClass(void);
    void demo(void);
    ~MyClass(void);
};

MyClass::MyClass()
{
    printf("Constructor\n");
}

MyClass::~MyClass()
{
    printf("Destructor\n");
}

void MyClass::demo(void)
{
    printf("MyClass\n");
}

int main()
{
    MyClass *zzz = new MyClass();
    zzz->demo();
    delete zzz;
}
Вывод приложения myClass1
Вывод приложения myClass1

В результате компиляции примера myClass1 с помощью VC++’17 с настройкой вывода проекта Release будет выглядеть так:

main proc near
  push rbx
  sub  rsp, 20h
  mov  ecx, 1           ; size
  call operator new(unsigned __int64)
  lea  rcx, _Format     ; "Constructor\n"
  mov  [rsp+28h+arg_0], rax
  mov  rbx, rax
  call printf
  lea  rcx, aMyclass    ; "MyClass\n"
  call printf
  lea  rcx, aDestructor ; "Destructor\n"
  call printf
  mov  edx, 1           ; __formal
  mov  rcx, rbx         ; block
  call operator delete(void *,unsigned __int64)
  xor  eax, eax
  add  rsp, 20h
  pop  rbx
  retn
main endp

Как раз упомянутый случай! Компилятор все сделал достоянием функции main, в отдельности не создав ни конструктора, ни деструктора, ни метода класса. После выделения памяти он тут же вызывает функцию для вывода строки символов! То же происходит с очисткой памяти. Вот это магия! Здесь интересным для нас может быть только оператор new: его устройство мы подробно рассмотрели в предыдущей статье.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии