
We all faced problems arising from incorrect work with pointers: going beyond the array and buffer overflow, accidentally writing to an unknown piece of memory, followed by reading this “garbage” in another place, and in some individual cases simply dropping the entire system. Sometimes it's just a game, gentlemen! And you need to be able to deal with this “game” correctly - in time to find and correct such errors and problems. That's what they did in the "plus" compiler Intel a few releases ago. In addition, many ideas have gone further and will be implemented in hardware through the
Intel Memory Protection Extensions technology. Let's see how it all works in the compiler.
“It would be nice if there was such a compiler option that would allow to find errors with pointers right in the code, change it and produce a debugged working application at the output,” one developer dreamed. In fact, this is not, and, sort of like, is not planned. The Intel compiler provides only a means of dynamic code verification. This means that we, as usual, need to connect one of the magic options, collect code with it and run the application for execution, while receiving an error, the reasons for which will be easy to understand. That's the whole way. In detail, it looks like this.
With the help of the Pointer Checker feature, we can catch work with memory through pointers in the entire application. To do this, each pointer is determined by the lower and upper permissible limits, which are checked when working with memory and ensure correct operation. Naturally, this information is stored in a special plate at some address. In it, we can, for any pointer, find the values of the lower
(lower bound) and upper
(upper_bound) borders.
In the simplest case, if we allocate memory for
p through
malloc (size) , then the
lower_bound (p) will have the address
(char *) p , and the
upper_bound (p) will have the address
(lower_bound (p) + size - 1) . And in the most trivial example, this will allow to detect the problem:
')
char * buffer = (char*)malloc(5); for (int i = 0; i <= 5; i++) buffer[i] = 'A' + i;
There are only 5 elements in the array, and if we try to write to an address that exceeds the allowed upper limit, we will get a runtime error, which means that the limit is exceeded.
It will look something like this:
CHKP: Bounds check error ptr=0X012062ED sz=1 lb=0X012062E8 ub=0X012062EC loc=0X0 0131149 Traceback: wmain [0x131149] in file C:\ConsoleApplication1.cpp at line 12 __tmainCRTStartup [0x13F959] in file f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c at line 623 wmainCRTStartup [0x13FA9D] in file f:\dd\vctools\crt\crtw32\dllstuff\crtexe.c at line 466 BaseThreadInitThunk [0x76D3919F] RtlInitializeExceptionChain [0x77550BBB] RtlInitializeExceptionChain [0x77550B91] CHKP Total number of bounds violations: 1
Obviously, our pointer was out of range. The
ptr value is
0x012062ED , and the upper bound should be no more than
ub , which is
0x012062EC . At the same time we get a traceback and can easily find a problem place. All this will happen provided that we have assembled the application with the
Qcheck-pointers (Windows) key, which from Visual Studio can be set up in the
C / C ++ -> Code Generation -> Check Pointers tab . For the case of Linux, use the
-check-pointer switch. If you are not too lazy and honestly went to expose it through the VS interface under Windows, you probably noticed that there are different modes of operation for Pointer Checker:
- Check bounds for reads and writes (/ Qcheck-pointers: rw)
- Check bounds for writes only (/ Qcheck-pointers: write)
- Check bounds for reads and using Intel MPX (/ Qcheck-pointers-mpx: rw)
- Check bounds for writes only using Intel MPX (/ Qcheck-pointers-mpx: write)
The last two options so far will not give anything at the real launch of the application, because the hardware for its use is not yet available. Actually, the usual practice, when the functionality in the software appears a little earlier. The same happens with other technologies, say AVX.
Of interest to us is the ability to check pointers both for read and write operations, and only for writing. Say, using the
Qcheck-pointers: write option, we will not get errors when the
buffer pointer
exceeds the limits set during a read operation. For example, in this case, provided the array is correctly initialized:
for (int i = 0; i <= 5; i++) printf("%c", buffer[i]);
Having compiled with the
Qcheck-pointers: rw key, we will catch all cases, including reading. By the way, when passing the pointer to the function, information about the boundaries is also saved.
There is another interesting feature: you need to be able to distinguish between the concepts of working with memory and simple arithmetic with pointers. Example:
char *p = (char *)malloc(100); p += 200; p[-101] = 0; p[0] = 0;
In the first expression, we only move the pointer, and then we refer to the memory located in the valid domain -
p [-101] in this case is
p [99] . Therefore, everything runs smoothly. An error exceeding the boundaries will happen only on the last line, because we are, in fact, trying to write to
p [200] .
There is a special algorithm for finding dangling pointers for which the
Qcheck-pointers-dangling option is used (it must be specified along with
Qcheck-pointers ). These are the cases when the memory is already cleared, and we persistently try to do something through the pointer. If we continue our buffer example, something from this rank:
free(buffer); printf("%c", buffer[2]);
However, without additional installation of
Qcheck-pointers-dangling, this case will not be considered as an error. If you write
Qcheck-pointers-dangling , then the compiler will use a special wrapper for the
free functions and the
delete operator. It finds all pointers with cleared memory and sets the lower bound values to 2, and the upper to 0. Thus, any attempt to work with memory through this pointer will result in an error. In the above example, the error will look like this (the traceback information was removed for compactness):
CHKP: Bounds check error ptr=0X007F62EA sz=1 lb=0X00000002 ub=00000000 loc=0X00DB11D5
By the way, if we have our own implementation of the function for working with memory, we can include in it the function of checking hanging pointers by calling the function
__chkp_invalidate_dangling , declared in
chkp.h.An example of a function that performs memory cleanup will look like this:
#include <chkp.h> void my_free(void *ptr) { size_t size = my_get_size(ptr); // do the free __chkp_invalidate_dangling(ptr, size); }
In conclusion, I will say that Pointer Checker has a lot of opportunities and all this needs to be tried with pens. For example, it is possible to selectively compile one or more modules with pointer checking, and others without. In addition, many API functions are available for more flexible operation. It should be noted that Pointer Checker is supported only under Windows and Linux, on Mac it does not exist.
There is also a reverse side of the medal - application execution slows down significantly (at least twice, but not more than 5). Naturally, the size of the code also increases. However, for debugging purposes, the functionality is very interesting, and with its implementation in hardware, everything will be much more efficient.
And finally, a small question. How do you think, how does this whole thing work in multithreaded applications? Considering that every time we access a pointer, we read or write information about boundaries, spending several instructions, and the fact that different threads may try to write different information for the same pointer?