Переопределение оператора delete/новый для проверки правильной пары (отсутствия)вариантов выбора


Наследие проект я сохраню в моем парасэйлинг использует переопределения operator delete / operator new. Эти переопределения выделять некоторые байты, используя mallocfree) и лечить сказали байт в качестве префикса, наполненный подписи различных между new и new[]. Проверка подписи на удалять позволяет обнаружить случайные случаи "new ... delete[]" или "new[] ... delete". Возвращается Танос объед смещение указателя на размер префикса.

Эти переопределения ставятся за директиву #ifdef, которые не оценить в режиме релиза.

Я недавно переписал эту часть, используя руководящие указания из соответствующих cppreference страницы и найти, что мне осталось три вопроса у меня есть проблемы, чтобы удовлетворительно ответить. Вот мои вопросы:

  1. Этот код еще нужно или я могу заменить его с некоторыми встроенная функция или плагин? (В настоящее время используя последние номера-просмотр индекса MSVC++ toolchain и, возможно, переключиться к MinGW-w64 в какой-то неопределенный момент в будущем... в настоящее время проект не компилируется при помощи GCC, хотя.)
  2. Я не УБ, по крайней мере в патологических случаях использовать? (И даже если, примерами таких случаев являются добро пожаловать.)
  3. Оригинальный устаревший код реализует как operator delete и delete[] по телефону freeпо сути смысл их реализации (префикс обращения в сторону) идентична... если это должно быть так, то зачем вообще в первую очередь?

Конечно, соображения, кроме тех, относящихся к этому списку, также приветствуется.

Код должен быть рассмотрен:

#include <cstring>

constexpr char PREFIX_ARRAY [] = "STR2MED";
constexpr char PREFIX_OBJECT[] = "str1med";
constexpr auto OFFSET_ARRAY  = std::size(PREFIX_ARRAY);
constexpr auto OFFSET_OBJECT = std::size(PREFIX_OBJECT);
using OFFSET_TYPE = decltype(OFFSET_ARRAY + OFFSET_OBJECT);

template<OFFSET_TYPE OFFSET>
inline void * guardDelete(void * pointer, const char PREFIX[OFFSET])
{
  if(pointer == nullptr)
    return pointer;
  else
  {
    char * prefix = reinterpret_cast<char*>(pointer) - OFFSET;
    ASSERT(std::memcmp(prefix, PREFIX, OFFSET) == 0);

    return prefix;
  }
}

void __cdecl operator delete  (void * pointer) { std::free(guardDelete<OFFSET_OBJECT>(pointer, PREFIX_OBJECT)); }
void __cdecl operator delete[](void * pointer) { std::free(guardDelete<OFFSET_ARRAY> (pointer, PREFIX_ARRAY)); }

template<OFFSET_TYPE OFFSET>
inline void * guardNew(void * pointer, const char PREFIX[OFFSET])
{
  ASSERT(pointer != nullptr); // Out of virtual memory?
  std::memcpy(pointer, PREFIX, OFFSET);

  return reinterpret_cast<char*>(pointer) + OFFSET;
}

void * __cdecl operator new  (std::size_t size) { return guardNew<OFFSET_OBJECT>(std::malloc(size + OFFSET_OBJECT), PREFIX_OBJECT); }
void * __cdecl operator new[](std::size_t size) { return guardNew<OFFSET_ARRAY> (std::malloc(size + OFFSET_ARRAY),  PREFIX_ARRAY); }

Внешние зависимости (утверждать реализации; по-прежнему унаследованного кода; не рассматриваются):

// This spawns a handy Windows error message allowing one to start VS
// and attach the debugger while the context is still intact;
// with debugger already attached, it acts as a breakpoint.
void _AssertDebug(char * strAssertion, char * strFile, unsigned int uLine)
{
  static char str[1024];
  sprintf(str, "\n---  Assertion failed: %s file:%s line:%u  ---\nDo you still want to continue?", strAssertion, strFile, uLine);
  switch(MessageBox(GetActiveWindow(), str, "Fatal error!", MB_ABORTRETRYIGNORE|MB_DEFBUTTON3|MB_TOPMOST))
  {
    case IDABORT:
      std::abort();
      break;
    case IDRETRY:
      DebugBreak();
      break;
  }
}

#define ASSERT(f) \
  if (f)          \
  {}              \
  else            \
    _AssertDebug((#f), __FILE__, __LINE__)


241
5
задан 4 февраля 2018 в 06:02 Источник Поделиться
Комментарии
2 ответа

#include <cstring>

Также включают <cstdlib> (для std::malloc), и я включают <new> из паранойи каждый раз, когда я прикалываюсь operator newдаже если это может быть излишним в этом случае.

constexpr char PREFIX_ARRAY [] = "STR2MED";
constexpr char PREFIX_OBJECT[] = "str1med";
constexpr auto OFFSET_ARRAY = std::size(PREFIX_ARRAY);
constexpr auto OFFSET_OBJECT = std::size(PREFIX_OBJECT);

Это кажется мне чересчур сложным. Кроме того, такие похожие имена, приглашать случайных опечаток-ошибок (например, опечаток OFFSET_ARRAY как OFFSET_OBJECT). Кроме того, использование библиотеки C++14 алгоритм std::size ненужно — (sizeof будет делать именно то, что вы хотите); и использование auto это путанными (тип sizeof x неизменно size_t).

Если вы действительно хотели использовать std::sizeвы должны включить по крайней мере один из <array>, <deque>, <forward_list>, <iterator>, <list>, <map>, <regex>, <set>, <string>, <unordered_map>, <unordered_set>, <vector>.

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


using OFFSET_TYPE = decltype(OFFSET_ARRAY + OFFSET_OBJECT);

Это супер пупер путаешь, либо это одна из тех опечаток-ошибок я предупреждаю вас об. Здесь вы добавляете вместе OFFSET_ARRAY (т. е. size_t(8)) и OFFSET_OBJECT (т. е. size_t(8)), принимая decltype этого (т. е. size_t), и объявить typedef с именем OFFSET_TYPE. Если вы действительно хотели этого состоит в том, что вы должны написать его как

using OFFSET_TYPE = size_t;  // or std::size_t if you enjoy typing

По теме: я никогда не пишу std:: для того, что я знаю, идет из библиотеки libc, в любом случае. Но это личный бзик,. У меня создается впечатление, что многие люди не пишут std:: везде, даже там, где это технически необходимо.


template<OFFSET_TYPE OFFSET>
inline void * guardDelete(void * pointer, const char PREFIX[OFFSET])

Вот я бы не беспокоиться, что программист не знал, что массивы в C и C++ передаются "распад на указатель". Я предпочел бы написать это как

template<size_t Offset>
inline void *guardDelete(void *ptr, const char *prefix)

так что очевидно, что Offset это невыводимо. Также обратите внимание, что я постоянно использую snake_case для имена локальных переменных, и CamelCase для шаблона имена параметров (например, Offset); я нахожу, что это делает код легче расшифровать, чем при использовании смеси lower и UPPER случае.


void __cdecl operator delete  (void * pointer)

Если я заботился о переносимости номера-индекса MSVC цели, я бы превентивно вынести этого __cdecl в макро, что я могу легко отключить. Однако, мне кажется, что __cdecl сам - это идентификатор, который может быть определен на номера-индекса MSVC платформ; так уверен, давай оставим его в покое.


Итак, собираем все вместе, я бы:

#include <string.h>
#include <stdlib.h>
#include <new> // maybe

template<size_t N>
inline void *guardNew(void *pointer, const char (&guard)[N])
{
ASSERT(pointer != nullptr); // Out of virtual memory?
memcpy(ptr, guard, N);
return reinterpret_cast<char*>(ptr) + N;
}

template<size_t N>
inline void *guardDelete(void *ptr, const char (&guard)[N])
{
if (ptr == nullptr) {
return ptr;
} else {
char *prefix = reinterpret_cast<char*>(ptr) - N;
ASSERT(memcmp(prefix, guard, N) == 0);
return prefix;
}
}

void * __cdecl operator new(size_t size) {
return guardNew(malloc(size + 8), "str1med");
}
void * __cdecl operator new[](size_t size) {
return guardNew(malloc(size + 8), "STR2MED");
}
void __cdecl operator delete(void *ptr) {
free(guardDelete(ptr, "str1med"));
}
void __cdecl operator delete[](void *ptr) {
free(guardDelete(ptr, "STR2MED"));
}


Однако, потом я начинаю переживать... этот код даже делая разумные вещи? Обратите внимание, что если я попытаюсь new объект некоторого типа T где alignof(T) > 8тогда указатель вернулись из нашего operator new не будут выровнены для этого T. И тот факт, что он правильно выровнен для выравнивания <= 8 чувствует, как несчастный случай. Если мы изменили наш охранник строк из str1med и STR2MED (не понятно, почему мы выбрали эти строки, кстати — и опять неудачное сочетание lowercase и UPPERCASE), чтобы, скажем, hello и worldэтот код будет перерыв в несколько различных способов. Так что, возможно, нас хотят заставить компилятор, чтобы диагностировать эту поломку для нас:

inline void *guardNew(void *pointer, const char (&guard)[8])
{
ASSERT(pointer != nullptr); // Out of virtual memory?
memcpy(ptr, guard, 8);
return reinterpret_cast<char*>(ptr) + 8;
}

Теперь если мы изменим строку в operator new от str1med для helloмы получим сообщение об ошибке компилятора.

Но у нас еще есть два экземпляра магическое число 8 разделяя справедливое число строк кода. Давайте рядный этого guardNew помощник, так что мы передоза функции malloc и memcpy передоза в одном месте:

#include <stdlib.h>
#include <string.h>

void * __cdecl operator new(size_t size) {
void *p = malloc(size + 8);
ASSERT(p != nullptr); // Out of memory?
memcpy(p, "str1med", 8);
return (char *)p + 8;
}
void * __cdecl operator new[](size_t size) {
void *p = malloc(size + 8);
ASSERT(p != nullptr); // Out of memory?
memcpy(p, "STR2MED", 8);
return (char *)p + 8;
}
void __cdecl operator delete(void *ptr) {
if (ptr == nullptr) return;
char *p = (char*)ptr - 8;
ASSERT(memcmp(p, "str1med", 8) == 0);
free(p);
}
void __cdecl operator delete[](void *ptr) {
if (ptr == nullptr) return;
char *p = (char*)ptr - 8;
ASSERT(memcmp(p, "STR2MED", 8) == 0);
free(p);
}

Из 36 строк вплоть до 27 — и без загрязнения глобального пространства имен, нет шаблонов, и — главным образом в результате уплощения и встраивание вещи со сложными зависимостями друг друга — это просто немного сложнее сломать случайно.

4
ответ дан 5 февраля 2018 в 07:02 Источник Поделиться

От @Quuxplusone 'ы ответ я принял следующие рекомендации:


  • включает все используемые заголовки, не полагаться на заголовки и др.

  • удалить OFFSET_ константы и вместо того, чтобы пройти PREFIX_ константы правильно

  • полагаться на std::size_t вместо излишне определения размера тип (std::size_t сам выводимый тип)

  • использовать sizeof вместо std::size (сохраняет один заголовок)

  • последовательно использовать два разных именования между константами и переменными

  • порядок функций, замены оператора и реализации бизнес-логики группируются друг

Дальнейшее совершенствование на основе тестирования, комментарии @MartinYork и ответы и комментарии на этот вопрос являются:


  • тянуть звонков std::malloc и std::free в функции и переименовать соответствующим образом (немного больше сухости немного меньше ПСП... наверное дело вкуса)

  • проверить на delete является ли переданный указатель действительно компенсируется ожидаемой длины (получается, что например, дескрипторы файлов не обязательно)

  • static_assert это различные префиксы имеют одинаковую длину, поэтому вышеупомянутые смещения проверка будет всегда работать

  • вычислить смещение таким образом, чтобы не нарушать центровки (это вдохновило связан вопрос)

  • использовать std::byte * как указатель типа экспресс, который (за исключением префикса) указатель не должны содержать данные, предназначенные для печати

  • как на языке C++20, аннотировать заменить delete операторы с [[nodiscard]]

Создав в этот пересмотренный реализации:

#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>

constexpr char PREFIX_ARRAY [] = "STR2MED";
constexpr char PREFIX_OBJECT[] = "str1med";
static_assert(sizeof(PREFIX_ARRAY) == sizeof(PREFIX_OBJECT),
"Size of array and object prefixes must be equal to guarantee that a check always happens.");

constexpr std::size_t offset(std::size_t prefixSize, std::size_t alignment = alignof(std::max_align_t)) noexcept
{ return alignment + alignment * (prefixSize / alignment); }

template<std::size_t PREFIX_SIZE>
inline void guardedDelete(void * untypedPointer, const char (& prefix)[PREFIX_SIZE])
{
constexpr std::size_t OFFSET = offset(PREFIX_SIZE);
std::byte * pointer = reinterpret_cast<std::byte*>(untypedPointer);

if(OFFSET < reinterpret_cast<std::uintptr_t>(untypedPointer)) // Otherwise it's not one of ours. Yes, that *does* happen!
{ // Currently calling std::filesystem::current_path() triggers an example.
pointer -= OFFSET; // -Zsar 2018-02-08
ASSERT(std::memcmp(pointer, prefix, PREFIX_SIZE) == 0);
}

std::free(pointer);
}

template<std::size_t PREFIX_SIZE>
inline void * guardedNew(std::size_t size, const char (& prefix)[PREFIX_SIZE])
{
constexpr std::size_t OFFSET = offset(PREFIX_SIZE);
void * pointer = std::malloc(size + OFFSET);

ASSERT(pointer != nullptr); // Out of virtual memory?
std::memcpy(pointer, prefix, PREFIX_SIZE);

return reinterpret_cast<std::byte*>(pointer) + OFFSET;
}

void __cdecl operator delete (void * pointer) { guardedDelete(pointer, PREFIX_OBJECT); }
void __cdecl operator delete[](void * pointer) { guardedDelete(pointer, PREFIX_ARRAY); }
[[nodiscard]] void * __cdecl operator new (std::size_t size) { return guardedNew(size, PREFIX_OBJECT); }
[[nodiscard]] void * __cdecl operator new[](std::size_t size) { return guardedNew(size, PREFIX_ARRAY); }

0
ответ дан 17 февраля 2018 в 02:02 Источник Поделиться