0

Since this problem can not be explained in a handful of lines, please bear with me and the size of this question.

The situation

We are developing an embedded system, which needs to manage its heap on its own by replacing the operators new, new[], delete, and delete[]. These user-defined replacing functions are implemented in their own module.

We decided to use a class with static methods, we could as well have used a namespace to keep the global namespace uncluttered.

// allocator.h

#include <cstddef>   // for size_t

class Allocator
{
public:
    static void setup();
    static void* allocate(size_t size);
};

Since during run-time the allocated instances will not be deleted, we prohibit the rest of the application from calling delete by shutting down the system otherwise. The testing framework can intercept the shutdown, so this is already testable.

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

Actually the RAM for the heap is allocated differently, but this does not matter here.

To unit-test this module, we are using GoogleTest, but the specific testing framework does not really matter. We could use any other framework.

The following source is a simulation of a testing framework I made up to investigate the problem. It uses the global operators new and delete, and these shall not be replaced by the user-defined operators from above, of course. Otherwise the framework would try to allocate new objects on a heap that does not yet exist.

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

And of course, its header file, for your convenience.

// framework.h

void framework();

Here is the testdriver, simplified for this example. It calls framework stuff (in the real situation behind the scene), tests failing and succeeding allocations with the module-under-test, and calls framework stuff again.

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

The standard to use for the software to develop is C++98, as we are bound to such an ancient compiler.

The standard to use for the tests is C++11, as GoogleTest needs this as minimum.

These are the commands to compile and 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

First solution, polluting the module's source with testing artefacts

My first idea was to insert these conditionally compiled lines into the module's source.

// 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*)
{
    // ...
}

To compile for testing, I used:

g++ -Wall -Wextra -pedantic -std=c++11 -c -DTESTING allocator.cpp -o allocator.o

Now the testdriver can simply call these functions, because they are no operators any more.

But our safety staff said "No way!" And I have to agree. Testing instrumentation in safety-related software is dangerous because it could sneak into the final product. You simply don't do this.

Second solution, fragile due to dependencies on the compiler and its version

We are using the GCC in its incarnation of MinGW64, so I came up with the linker option -wrap. In a single sentence: This option makes the linker prepend __wrap_ to symbols at the calling site and to prepend __real_ at the called site.

So I looked up the mangeled names of the operators, since the linker does not know anything about C++; it just does not need to know. ;-) Well, G++ in the version we use has this "translation":

_Znwy := operator new(unsigned long long)
_Znay := operator new[](unsigned long long)
_ZdlPv := operator delete(void*)
_ZdaPv := operator delete[](void*)

Now I could extend the testdriver with replacements for the operators, working with the C memory allocators. (Thank you C++ guys, for leaving that stuff in the libraries!)

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

// ...

And these are the declarations of the real operators in the module-under-test, for the testdriver to call.

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

// ...

The command to link is now:

g++ -Wall -Wextra -pedantic -std=c++11 -Wl,-wrap,_Znwy,-wrap,_Znay,-wrap,_ZdlPv,-wrap,_ZdaPv testdriver.o framework.o allocator.o -o testdriver

This worked, too. But it is kind of complicated and ugly. And it works only with the GCC, additionally I'm not sure that different versions will keep these mangeled names. Most probably they do, to be compatible, but who knows.

My single question

Thanks for reading all of this, here comes my question:

What else can I try?

I'm looking for a solution that does not change the module's source, and that works with (mostly) any compiler.

the busybee
  • 10,755
  • 3
  • 13
  • 30
  • In Linux, you can "steal" (dynamically-linked) function calls with the help of `LD_PRELOAD` environment variable. Namely, you can create a library with the same function, put any testing code inside, and optionally call the original function. Not sure whether such an approach would work for you. – Daniel Langr Apr 29 '20 at 07:26
  • Currently we are bound to Windows, unfortunately. Thanks, anyway. – the busybee Apr 29 '20 at 07:32
  • There may be some solutions for Windows as well. Check, for example, [this question](https://stackoverflow.com/questions/1178257/ld-preload-equivalent-for-windows-to-preload-shared-libraries). – Daniel Langr Apr 29 '20 at 07:47

1 Answers1

0

Well, folks, I was not lazy and twiddling my thumbs.

I found a solution that is both simple (not primitive) and elegant. It might not work for anyone else, but it works for us.

The GCC's preprocessor can take an option to include another file before processing the actual source. I use this to re-define the keyword operator and extend it to a class-specific 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

Compile the module-under-test with this:

g++ -Wall -Wextra -pedantic -std=c++11 -c -Wp,-include,instrumentation.h allocator.cpp -o allocator.o

Now the formerly replacing operators do not replace the global operators any more. They are "wrapped" into a class, and the testdriver can call them on instances of this wrapper class. It is of course important for the testdriver to undefine operator right after inclusion of instrumentation.h, else you'll get tons of errors in the rest of the testdriver's code.

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

If a compiler's preprocessor does not have such an option, a small wrapper can be used instead.

// allocator_wrapper.cpp

#include "instrumentation.h"

#include "allocator.cpp"
the busybee
  • 10,755
  • 3
  • 13
  • 30