Aussie AI
Chapter 22. Intercepting Memory Primitives
-
Book Excerpt from "Advanced C++ Memory Techniques: Efficiency and Safety"
-
by David Spuler, Ph.D.
Chapter 22. Intercepting Memory Primitives
Interception Methods
Intercepts can be useful for both performance instrumentation and memory error detection or prevention. There are two main ways to intercept memory primitives in your C++ code.
- Preprocessor macro interception — used for functions like
mallocandfree. - Link-time interception — used for
newanddeleteoperators.
Once you have a successful intercept, it’s amazing the things you can do! Here are some ideas:
- Detect various types of memory errors
- Prevent some types of memory crashes
- Track statistics on memory allocations
- Detect and prevent errors in various non-memory functions
Preprocessor Macro Intercepts
There are different approaches to consider when wrapping system calls,
which we examine using memset as an example:
- Leave “
memset” calls in your code (auto-intercepts) - Use “
memset_wrapper” in your code instead (manual intercepts)
Macro auto-intercepts:
You might want to leave your code unchanged using memset.
To leave “memset” in your code, but have it automatically call “memset_wrapper”
you can use a macro intercept in a header file.
#undef memset // ensure no prior definition
#define memset memset_wrapper // Intercept
Note that you can also use preprocessor macros to add context information
to the debug wrapper functions.
For example, you could add extra parameters to “memset_wrapper”
such as:
#define memset(x,y,z) memset_wrapper((x),(y),(z),__FILE__,__LINE__,__func__)
Note that in the above version,
the macro parameters must be parenthesized even between commas, because there’s
a C++ comma operator that could occur in a passed-in expression.
Also note that these context macros (e.g., __FILE__) aren’t necessary
if you have a C++ stack trace library, such as std::stacktrace, on your platform.
Variadic preprocessor macros:
Note also that there is varargs support in C++ #define macros.
If you want to track variable-argument functions like sprintf, printf, or fprintf,
or other C++ overloaded functions, you can use “...” and “__VA_ARGS__” in preprocessor macros
as follows.
#define sprintf(fmt,...) sprintf_wrapper((fmt),__FILE__,__LINE__,__func__, __VA_ARGS__ )
Manual Wrapping:
Alternatively, you might want to individually change the calls to memset to call memset_wrapper
without hiding it behind a macro.
If you’d rather have to control whether or not the wrapper is called,
then you can use both in the program, wrapped or non-wrapped.
Or if you want them all changed,
but want the intercept to be less hidden (e.g., later during code maintenance),
then you might consider adding a helpful reminder instead:
#undef memset
#define memset dont_use_memset_please
This trick will give you a compilation error at every call
to memset that hasn’t been changed to memset_wrapper.
Link-Time Interception: new and delete
The idea of a tool to test memory allocations is to shine light on the hidden calls that create and destroy allocated memory. This helps examine how containers are using allocated memory, and it’s not usually pretty!
Macro interception does not work for the new and delete operators,
because they don’t use function-like syntax.
Fortunately, you can use link-time interception of these operators instead,
simply by defining your own versions.
This is a standard feature of C++ that has been long supported.
Note that defining class-level
versions of the new and delete operators is a well-known optimization
for a class to manage its own memory allocation pool,
but this isn’t what we’re doing here.
Instead, this link-time interception
requires defining four operators at global scope:
newnew[]deletedelete[]
There’s a pitfall in implementing our intercepted versions.
You cannot use the real
new and delete inside these link-time wrappers.
They would get intercepted again,
and you’d have infinite stack recursion.
However, you can call malloc and free instead,
assuming they aren’t also macro-intercepted in this code.
Here’s the simplest versions:
void * operator new(size_t n)
{
return malloc(n);
}
void* operator new[](size_t n)
{
return malloc(n);
}
void operator delete(void* v)
{
free(v);
}
void operator delete[](void* v)
{
free(v);
}
This method of link-time interception
is an officially sanctioned standard C++ language feature since the 1990s.
Be careful, though, that the return types and
parameter types are precise, using size_t and void*,
as you cannot use int or char*.
Also, declaring these intercept functions as inline gets a compilation warning,
and is presumably ignored by the compiler, as this requires
link-time interception.
Memory Debug Wrappers
I’ve always used this intercept method for some self-testing debug wrappers. Here’s an example of some ideas of some basic possible checks you can do in these intercepted operators:
void * operator new(size_t n)
{
if (n == 0) {
AUSSIE_ERROR("new operator size is zero\n");
}
void *v = malloc(n);
if (v == nullptr) {
AUSSIE_ERROR("new operator: allocation failure\n");
}
return v;
}
Note that you can’t use __FILE__ or __LINE__ as these are link-time intercepts, not macros.
However, you could use std::backtrace in C++23 instead.
Memory Performance Analysis
We can also use the idea of link-time interception to do performance improvement on memory allocation. This helps us find the slugs in both standard containers and our own code.
The modified version of these link intercepts is shown in the Appendix, with full source code. The idea is that you can examine the behavior of code by wrapping memory debug calls around it:
memory_reset_counters();
std::vector<int> v;
memory_report();
This allows investigation of the memory characteristics of any sequence of code. It’s quite enlightening to investigate what sort of actions in the standard C++ libraries will trigger memory allocations.
Unit Testing of Memory Allocation. Another useful idea is to add unit tests to your build, so as to ensure that nobody’s accidentally added some memory allocations to the code.
memory_reset_counters();
std::vector<MyClass> v;
TEST(s_new_count == 0); // No memory allocations!
You know what I mean: trust but verify!
|
• Online: Table of Contents • PDF: Free PDF book download • Buy: Advanced C++ Memory Techniques: Efficiency and Safety |
|
Advanced C++ Memory Techniques: Efficiency & Safety:
Get your copy from Amazon: Advanced C++ Memory Techniques |