gpt4 book ai didi

c++ - 如何在不更改/检测模块源的情况下对用户定义的 `operator new` 和 `operator delete` 进行单元测试?

转载 作者:行者123 更新时间:2023-12-02 10:33:07 25 4
gpt4 key购买 nike

由于这个问题不能用几行来解释,请耐心等待我和这个问题的大小。

情况

我们正在开发一个嵌入式系统,它需要通过替换操作符来自行管理其堆 new , new[] , delete , 和 delete[] .这些用户定义的替换功能在它们自己的模块中实现。

我们决定使用具有静态方法的类,我们也可以使用命名空间来保持全局命名空间整洁。

// 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
}

实际上,堆的 RAM 分配方式不同,但这在这里无关紧要。

为了对该模块进行单元测试,我们使用 GoogleTest ,但具体的测试框架并不重要。我们可以使用任何其他框架。

以下来源是我为调查问题而编写的测试框架的模拟。它使用全局运算符 newdelete , 当然,这些不应被上面的用户定义的运算符替换。否则,框架将尝试在尚不存在的堆上分配新对象。

// 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,因为我们绑定(bind)了这么古老的编译器。

用于测试的标准是 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

现在测试驱动程序可以简单地调用这些函数,因为它们不再是运算符。

但是我们的安全人员说“不可能!”我必须同意。在安全相关软件中测试仪器是危险的,因为它可能潜入最终产品。你根本不这样做。

第二种解决方案,由于对编译器及其版本的依赖而脆弱

我们在 MinGW64 的化身中使用 GCC,所以我想出了链接器选项 -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

这也奏效了。但它有点复杂和丑陋。而且它仅适用于 GCC,另外我不确定不同的版本会保留这些损坏的名称。他们很可能会这样做,以兼容,但谁知道呢。

我的一个问题

感谢您阅读所有这些,这是我的问题:

我还能尝试什么?

我正在寻找一种不改变模块源代码的解决方案,并且可以与(大部分)任何编译器一起使用。

最佳答案

好吧,伙计们,我并不懒惰和玩弄我的拇指。

我找到了一个既简单(非原始)又优雅的解决方案。它可能不适用于其他任何人,但它适用于我们。

GCC 的预处理器可以选择在处理实际源之前包含另一个文件。我用它来重新定义关键字operator并将其扩展到特定于类的运算符。

// instrumentation.h

#include <cstddef> // for size_t

class T
{
public:
void* operator new(size_t);
void* operator new[](size_t);
void operator delete(void*);
void operator delete[](void*);
};

#define operator T::operator

用这个编译被测模块:
g++ -Wall -Wextra -pedantic -std=c++11 -c -Wp,-include,instrumentation.h allocator.cpp -o allocator.o

现在,以前替换的运算符(operator)做 不是 不再替换全局运营商。它们被“包装”到一个类中,测试驱动程序可以在这个包装类的实例上调用它们。测试驱动程序取消定义 operator 当然很重要。在包含 instrumentation.h 之后,否则您将在测试驱动程序的其余代码中遇到大量错误。

// testdriver.cpp

#include <iostream>

#include "framework.h"

#include "allocator.h"

#include "instrumentation.h"
#undef operator

int main()
{
framework();

T* p1 = new T; // expected to be 0
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
T* p2 = new T[4]; // expected to be 0
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;

Allocator::setup();

p1 = new T;
std::cout << __func__ << " p1 = " << static_cast<void*>(p1) << std::endl;
p2 = new T[4];
std::cout << __func__ << " p2 = " << static_cast<void*>(p2) << std::endl;
delete p1;
delete[] p2;

framework();

return 0;
}

如果编译器的预处理器没有这样的选项,则可以使用小型包装器。

// allocator_wrapper.cpp

#include "instrumentation.h"

#include "allocator.cpp"

关于c++ - 如何在不更改/检测模块源的情况下对用户定义的 `operator new` 和 `operator delete` 进行单元测试?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61496172/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com