Так как эту проблему нельзя объяснить несколькими строчками, пожалуйста, не забывайте меня и размер этого вопроса.
Ситуация
Мы разрабатываем встроенную систему, которая нуждается в управлять своей кучей самостоятельно, заменяя операторов new
, new[]
, delete
и delete[]
. Эти определяемые пользователем замещающие функции реализованы в их собственном модуле.
Мы решили использовать класс с методами stati c, мы могли бы также использовать пространство имен, чтобы глобальное пространство имен не загромождалось.
// allocator.h
#include <cstddef> // for size_t
class Allocator
{
public:
static void setup();
static void* allocate(size_t size);
};
Поскольку во время выполнения выделенные экземпляры не будут удалены, мы запрещаем остальной части приложения вызывать delete
, иначе отключив систему. Среда тестирования может перехватывать завершение работы, поэтому это уже можно проверить.
// allocator.cpp
#include "allocator.h"
static char* baseAddress = nullptr;
static size_t spaceLeft = 0;
void Allocator::setup()
{
static char heap[1000]; // super-simple for StackOverflow example
baseAddress = heap;
spaceLeft = sizeof heap;
}
void* Allocator::allocate(size_t size)
{
void* p = 0;
if (size <= spaceLeft)
{
p = static_cast<void*>(baseAddress);
baseAddress += size;
spaceLeft -= size;
}
return p;
}
void* operator new(size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void* operator new[](size_t size)
{
void* p = Allocator::allocate(size);
return p;
}
void operator delete(void*)
{
// shutdown(); // commented out for StackOverflow example
}
void operator delete[](void*)
{
// shutdown(); // commented out for StackOverflow example
}
На самом деле ОЗУ для кучи выделяется по-разному, но здесь это не имеет значения.
Для модульного тестирования этот модуль мы используем GoogleTest , но конкретная c среда тестирования на самом деле не имеет значения. Мы могли бы использовать любой другой фреймворк.
Следующий источник представляет собой симуляцию фреймворка для тестирования, который я составил для исследования проблемы. Он использует глобальные операторы new
и delete
, и они, конечно, не должны заменяться пользовательскими операторами сверху. В противном случае фреймворк попытается разместить новые объекты в куче, которая еще не существует.
// framework.cpp
#include <cstdlib> // for malloc() and free()
#include <iostream>
#if 0 // For the example to compile and link, currently commented out
void* operator new(size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void* operator new[](size_t size)
{
void* p = malloc(size);
std::cout << __func__ << "(" << size << ") : " << p << std::endl;
return p;
}
void operator delete(void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
void operator delete[](void* block)
{
std::cout << __func__ << "(" << block << ")" << std::endl;
free(block);
}
#endif
void framework()
{
int* p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
}
И, конечно, для удобства, его заголовочный файл.
// framework.h
void framework();
Вот тестдрайвер, упрощенный для этого примера. Он вызывает структуру фреймворка (в реальной ситуации за кулисами), тестирует неудачные и последующие выделения с тестируемым модулем и снова вызывает фреймворк.
// testdriver.cpp
#include <iostream>
#include "framework.h"
#include "allocator.h"
int main()
{
framework();
#if 1 // possibility to comment out for experiments
int* p1 = new int; // expected to be 0
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
int* p2 = new int[4]; // expected to be 0
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
#endif
Allocator::setup();
#if 1 // possibility to comment out for experiments
p1 = new int;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
p2 = new int[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;
#endif
framework();
return 0;
}
Стандарт, используемый для программного обеспечения Разработка - это C ++ 98, так как мы связаны с таким древним компилятором.
Стандарт для использования в тестах - C ++ 11, так как это требуется GoogleTest как минимум.
Эти Вот команды для компиляции и компоновки:
g++ -Wall -Wextra -pedantic -std=c++11 -c allocator.cpp -o allocator.o
g++ -Wall -Wextra -pedantic -std=c++11 -c framework.cpp -o framework.o
g++ -Wall -Wextra -pedantic -std=c++11 -c testdriver.cpp -o testdriver.o
g++ -Wall -Wextra -pedantic -std=c++11 testdriver.o framework.o allocator.o -o testdriver
Первое решение, загрязняющее источник модуля артефактами тестирования
Моей первой идеей было вставить эти условно скомпилированные строки в источник модуля.
// allocator.cpp
//...
#if !defined(TESTING)
#define operator_new_single operator new
#define operator_new_array operator new[]
#define operator_delete_single operator delete
#define operator_delete_array operator delete[]
#endif
//...
void* operator_new_single(size_t size) // void* operator new(size_t size)
{
// ...
}
void* operator_new_array(size_t size)
{
// ...
}
void operator_delete_single(void*)
{
// ...
}
void operator_delete_array(void*)
{
// ...
}
Для компиляции для тестирования я использовал:
g++ -Wall -Wextra -pedantic -std=c++11 -c -DTESTING allocator.cpp -o allocator.o
Теперь тест-драйвер может просто вызывать эти функции, потому что они больше не являются операторами.
Но наша безопасность персонал сказал "Ни за что!" И я должен согласиться. Тестирование инструментов в программном обеспечении, связанном с безопасностью, опасно, потому что оно может проникнуть в конечный продукт. Вы просто не делаете этого.
Второе решение, например agile из-за зависимостей от компилятора и его версии
Мы используем G CC в его воплощении MinGW64, поэтому я придумал опцию компоновщика -wrap
. В одном предложении: эта опция заставляет компоновщика добавлять __wrap_
к символам на вызывающем сайте и __real_
на вызываемом сайте.
Поэтому я посмотрел искаженные имена операторов, так как компоновщик ничего не знает о C ++; это просто не нужно знать. ;-) Ну, G ++ в используемой нами версии имеет такой «перевод»:
_Znwy := operator new(unsigned long long)
_Znay := operator new[](unsigned long long)
_ZdlPv := operator delete(void*)
_ZdaPv := operator delete[](void*)
Теперь я могу расширить тест-драйвер заменой операторов, работающих с распределителями памяти C. (Спасибо вам, ребята из C ++, за то, что оставили эти вещи в библиотеках!)
// testdriver.cpp
#include <cstring> // for malloc() and free()
// ...
extern "C" void* __wrap__Znwy(size_t size)
{
return malloc(size);
}
extern "C" void* __wrap__Znay(size_t size)
{
return malloc(size);
}
extern "C" void __wrap__ZdlPv(void* block)
{
free(block);
}
extern "C" void __wrap__ZdaPv(void* block)
{
free(block);
}
// ...
И это объявления реальных операторов в тестируемом модуле для вызова тестдрайвера.
// testdriver.cpp
// ...
extern "C" void* __real__Znwy(size_t size);
extern "C" void* __real__Znay(size_t size);
extern "C" void __real__ZdlPv(void* block);
extern "C" void __real__ZdaPv(void* block);
// ...
Команда для связи теперь:
g++ -Wall -Wextra -pedantic -std=c++11 -Wl,-wrap,_Znwy,-wrap,_Znay,-wrap,_ZdlPv,-wrap,_ZdaPv testdriver.o framework.o allocator.o -o testdriver
Это тоже сработало. Но это довольно сложно и безобразно. И это работает только с G CC, кроме того, я не уверен, что разные версии будут держать эти искаженные имена. Скорее всего, они совместимы, но кто знает.
Мой единственный вопрос
Спасибо, что прочитали все это, вот мой вопрос:
Что еще можно попробовать?
Я ищу решение, которое не изменяет исходный код модуля и работает (в основном) с любым компилятором.