如何在没有 changing/instrumenting 模块源代码的情况下对用户定义的“operator new”和“operator delete”进行单元测试?

How to unit-test user-defined `operator new` and `operator delete` without changing/instrumenting the module's source?

由于这个问题无法用几行文字来说明,还请大家多多包涵这个问题的大小。

情况

我们正在开发一个嵌入式系统,它需要通过替换运算符 newnew[]deletedelete[] 来自行管理其堆。这些用户定义的替换函数在它们自己的模块中实现。

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

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

void operator delete[](void*)
{
    // shutdown(); // commented out for Whosebug 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,因为我们被这样一个古老的编译器所束缚。

用于测试的标准是 C++11,因为 GoogleTest 需要它作为最低要求。

这些是编译命令 link:

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

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

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

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

我们在 MinGW64 的化身中使用 GCC,所以我想出了 linker 选项 -wrap。一句话:此选项使 linker 将 __wrap_ 添加到调用站点的符号,并在被调用站点添加 __real_

所以我查找了运算符的名称,因为 linker 对 C++ 一无所知;它只是不需要知道。 ;-) 好吧,我们使用的版本中的 G++ 有这个 "translation":

_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);

// ...

link 的命令现在是:

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 并将其扩展为特定于 class 的运算符。

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

现在以前替换的运算符不再替换全局运算符。它们 "wrapped" 变成了 class,测试驱动程序可以在这个包装器 class 的实例上调用它们。测试驱动程序在包含 instrumentation.h 后立即取消定义 operator 当然很重要,否则您将在测试驱动程序的其余代码中遇到大量错误。

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