Featured Webinar: AI-Enhanced API Testing: A No-Code Approach to Testing | Watch Now
Jump to Section
How to Effectively Manage Pointers in C to Prevent Abuse
Although pointers let you use your creativity to solve a particular problem, they have several limitations that are among the hardest for programmers to deal with. Read on to learn how to use Insure++ to identify pointer-related issues automatically.
Jump to Section
Jump to Section
Pointers are both the strength and Achilles heel of programming in C and C++. While pointers allow you to be very creative and flexible in how you approach solving a particular problem. However, it’s easy to unwittingly introduce defects into your code. Problems with pointers are among the most difficult encountered by C programmers. A generation ago, brute-force techniques like inserting print statements into the code were often the best strategy for trying to find these problems.
Tools for Detecting Pointer Abuse in C
Insure++: A Tool for Dynamic Memory Allocation Checks
Today, memory error detection tools like Insure++ can detect pointer-related problems automatically as the code is executed, which saves a lot of time and headaches. Insure++ finds problems in the following categories:
- Operations on NULL pointers
- Operations on uninitialized pointers
- Operations on pointers that don’t point to valid data
- Operations that try to compare or otherwise relate pointers that don’t point at the same data object
- Function calls through function pointers that don’t point to functions
- Many other causes of possible undefined behavior or implementation-defined behavior
Insure++ uses a state-of-the-art code parser, along with hundreds of heuristics, to analyze the application code, during which it reports on several possible static violations. While analyzing the code, it writes a new source code file with appropriate instrumentation inserted in trouble spots, such as pointer dereference, scope exit, and so on. The resulting source file is automatically compiled, and all resulting object code files are linked into a new executable program.
Identifying the Address of a Variable: Best Practices
In many cases, when trying to solve memory management issues and figure out if a variable is corrupted, you’ll need to know the address of that variable and probably find out that it’s not just that variable but others as well. Using a debugger is often considered a best practice when it comes to identifying issues in your code, including understanding variable values and memory addresses.
Debuggers provide a powerful set of tools for inspecting and manipulating the execution of your program. However, it’s important to note that using a debugger is not mutually exclusive with other debugging techniques. Print statements and manual inspection can still be valuable tools, especially in situations where using a debugger might be impractical.
One simple approach that I have used for many years is to simply use the address-of operator ‘&’ in a printf statement. Take the following code example.
int myVariable = 77; printf("Address of myVariable is: %p\n", (void *)&myVariable);
The &myVariable expression returns the address of myVariable. The %p format specifier is used with printf to print the address in a pointer format. The (void*) cast is used to match the %p format specifier. Don’t forget that the actual memory address may vary each time the program is run.
Practical Example: Managing Pointers in a “Hello World” Program
Below is the code for a “Hello, World” program that uses dynamic memory allocation.
/* * File: hello.c */ #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char *string, *string_so_far; int i, length; length = 0; for(i=0; i<argc; i++) { length += strlen(argv[i])+1; string = malloc(length+1); /* * Copy the string built so far. */ if(string_so_far != (char *)0) strcpy(string, string_so_far); else *string = '\0'; strcat(string, argv[i]); if(i < argc-1) strcat(string, " "); string_so_far = string; } printf("You entered: %s\n", string_so_far); return (0); }
The basic idea of this program is that we keep track of the current string size in the variable length. As each new argument is processed, we add its length to the length variable and allocate a block of memory of the new size. Notice that the code is careful to include the final NULL character when computing the string length (line 14) and also the space between strings. Both of these are easy mistakes to make. It’s an interesting exercise to see how quickly you can find such an error with a memory error detection tool like Parasoft Insure++.
The code either copies the argument to the buffer or appends it, depending on whether or not this is the first pass round the loop. Finally, the pointer string_so_far points to the new longer string.
If you compile and run this program under Insure++, you’ll see “uninitialized pointer” errors reported for the code “strcpy(string, string_so_far)”. This is because the variable string_so_far hasn’t been set to anything before the first trip through the argument loop. In a small code sample like this such a problem is obvious, but even if the error is buried in a heap of hundreds of thousands of lines of code and much more subtle than the above error, Insure++ will find it every time.
Reports and Insights From Insure++
Insure++ reports any problems found. Insure++ reports contain detailed information like the type of bug, source file and line number, actual source code line contents, and expressions that caused the problem, with reports including:
- The type of bug, for example, EXPR_UNRELATED_PTRCMP
- The source file and line number, for example, foo.cc:42
- The actual source code line contents, for example, “while (p < g) {”
- The expression that caused the problem, for example, “p < g”
- Information about all pointers and memory blocks involved in the bug:
- Pointer values
- Memory blocks pointed to (if any) and any offset
- Block allocation information:
- Stack trace if dynamically allocated
- Block declaration location, (source file and line number), if allocated on the stack or globally
- Stack trace of deallocation of the block, if applicable
- Stack trace showing how the program got to the bug location
Next Steps: How to Keep Your Pointers in C Safe
Test coverage is critical for keeping pointers in C and C++ safe. Static analysis and dynamic analysis play significant roles.
The Importance of Test Coverage in Pointers
When it comes to working with pointers in C or C++, test coverage is important because they introduce risk. The risk may come in a subtle way and sometimes very difficult to track down, consuming high labor costs.
Employing a comprehensive set of testing methods to cover a list of pointer issues and scenarios is essential. For example, you’ll want to use a tool like Insure++ to help developers find erratic programming and memory-access errors, such as heap corruption, rogue threads, memory leaks, array out of bounds, and invalid pointers.
Static Analysis
One powerful way to get started is through using static analysis or compile time error detection, which catches potential issues related to pointers during the compilation phase. Issues like cast of pointer loses precision, mismatch in format specification or argument type, unused variables, dead code, divide by zero, and more.
Dynamic Analysis
Dynamic analysis testing is another potent and complementary approach to static analysis. Runtime error detection helps identify corrupt heap and stack memory, plus all types of memory leaks, memory allocation, free errors or mismatches, and array out of bound errors.
To ensure that you have tested all the code, code coverage analysis enables the visual identification of which sections of code have been executed and which have not. Leaving untested code in your application may cost you sleepless nights or come to bite you later.