Pointer Checker: check our pointers

    We all faced problems that occurred when pointers weren’t working properly: going out of the array and overflowing the buffer, accidentally writing to an unknown piece of memory, then reading this “garbage” in another place, and in some individual cases, the whole system could simply crash. Sometimes it's just "game", gentlemen! And you need to be able to deal with this "game" correctly - in time to find and correct such errors and problems. This is exactly what the Intel “plus” compiler did a few releases ago. In addition, many ideas went further and will be implemented in hardware through Intel Memory Protection Extensions . Let's see how all this works in the compiler.

    “It would be nice if there was such a compiler option that would allow finding errors with pointers immediately in the code, modify it and give a debugged working application at the output,” one developer dreamed. In fact, this is not, and, it seems, is not planned. The Intel compiler provides only a means of dynamically checking code. This means that, as usual, we 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 figure out. That's the whole way. In detail, it looks like this.

    Using the Pointer Checker feature, we can catch the work with memory through pointers throughout the application. To do this, each pointer has a lower and upper permissible boundary, which are checked when working with memory and guarantee correct operation. Naturally, this information is stored in a special plate at some address. In it, we can find the values ​​of the lower (lower bound) and upper (upper_bound) borders for any pointer .

    In the simplest case, if we allocated memory for p via malloc (size) , then in lower_bound (p) there will be an address (char *) p , and in upper_bound (p) it will be an address (lower_bound (p) + size - 1). And in the most trivial example, this will 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 when we try to write to an address that exceeds the allowable upper bound, we get a runtime error that says it goes out of bounds.
    It will look something like this:

    CHKP: Bounds check error ptr=0X012062ED sz=1 lb=0X012062E8 ub=0X012062EC loc=0X0
        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 . In doing so, we get a traceback and can easily find the problem spot. All this will happen under the condition that we compiled the application with the Qcheck-pointers key (Windows), which from Visual Studio can be set in the C / C ++ -> Code Generation -> Check Pointers tab . For the Linux case, use the -check-pointer switch . If you were not too lazy and honestly went to expose it through the VS interface under Windows, then you probably noticed that there are different Pointer Checker modes of operation:
    • Check bounds for reads and writes (/ Qcheck-pointers: rw)
    • Check bounds for writes only (/ Qcheck-pointers: write)
    • Check bounds for reads and writes, using Intel MPX (/ Qcheck-pointers-mpx: rw)
    • Check bounds for writes only, using Intel MPX (/ Qcheck-pointers-mpx: write)

    The last two options will not give anything at the actual launch of the application, because the hardware for its use is not yet available. Actually, it’s a common practice when functionality in software appears a little earlier. The same thing happens with other technologies, say AVX.
    Of interest to us is the ability to check pointers both during read and write operations, and only when writing. Say, using the Qcheck-pointers: write option , we will not get errors when going beyond the set bounds of the buffer pointer during a read operation. For example, in this case, provided that 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 a pointer to a 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 refer to the memory located in the valid region - p [-101] in this case is p [99] . Therefore, everything goes smoothly. The error of going out of bounds will happen only on the last line, because we are actually 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 together with Qcheck-pointers ). These are cases when the memory is already cleared, and we are persistently trying to do something through the pointer. If we continue our buffer example, something from this category:

    	printf("%c", buffer[2]);

    However, without additional installation of Qcheck-pointers-dangling, this case will not be considered an error. If you register 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 to 2 and the upper to 0. Thus, any attempt to work with memory through this pointer will result in an error. In the example considered, the error will look like this (traceback information 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 to work with memory, we can include it in a function test of hanging signs with a function call __chkp_invalidate_dangling , announced in chkp.h .
    An example of a function that clears memory will look like this:

    	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 possibilities and you need to try all this 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 is worth noting that Pointer Checker is supported only under Windows and Linux, on the Mac it is not.
    There is also the flip side of the coin - application execution slows down significantly (at least twice, but not more than 5). Naturally, the size of the code also increases. Nevertheless, for the purpose of debugging the application, the functionality is very interesting, and with its implementation in the hardware everything will be much more efficient.

    And finally, a small question. How do you think this whole thing works in multi-threaded applications? Given that each time we access the pointer, we read or write information about the boundaries, spending a few instructions, and the fact that different threads can try to write different information for the same pointer?

    Also popular now: