Memory Errors In C
By now, you guys have probably realized that the most incidious errors in C programs are very often memory-related problems. These problems are nasty because they are related to the language and environment -- not the problem that one is trying to solve.
We see memory errors in a lot of different ways, a few of which are listed below:
- Allocating too little space, as might happen if a string-length is wrong, an input exceeds the expected and allocated size, or the sizeof() the wrong type is used
- Walking past the end of an array (really a type of the above)
- Freeing memory, then continuing to use it. This can happen, for example, if an object is freed upon detecting an error, but a caller retries to operation, or if both the caller and the callee free the same object.
- "Leaking memory" by reassigning a pointer, without first freeing the object, or using malloc for a local purpose within a function, but not freeing it within that function.
So, it is pretty clear that if we use memory that is not properly allocated, one of three things can happen:
- Nothing, we wrote onto unused space. This might, for example, happen if our request was rounded up to some standard size by malloc, so there is some extra at the end of the array, anyway. This type of error might seem innoculous enough, but it can be trouble. After a different feature is exercised, a recompile with optimzation, a port to a different system, a new version of tools, or anything else -- the error can suddly morph into a different more potent form.
- We damage something. We scribble on top of something important. Later on our program relies on this damaged value and either crashes or generates incorrect results, or both. This situation is nasty, because the appearance of the errant behavior and the execution of the broken code are separated in time. This can make debugging challenging.
- A segment fault or bus error. The pointer is bogus and doesn't point to allocated space. As a result, the hardware catches us and the OS strikes us dead.
But, what if we "leak" memory? Well, the textbook answer is that eventually the system will run out of emmory and either malloc() will fail or the program will be killed by the OS for exceeding some resource limit. And, this can surely happen.
But, thee days most VM systems are backed by not only a large amount of RAM -- but a truly huge amount of disk. Well before malloc() fails or the system kills off a process, things are likely to slow down, perhaps exponentially, due to pagging
You'll learn about paging in 15-213 and, in depth, in OS. But, to make a long story short, when a computer doesn't have enough memory, it plays a shell game and temporaily frees some memory by writing pages of memory off to disk. Then, shoudl they be needed in the future, they can be read back in -- perhaps after writing out other pages to make room. This shell game dramatically hamper system performance because the disk, which is being used in place of RAM, is much, much, much, much slower.
Those of you who were in class got to hear a story about my master's project. For expediency on morning, I used a malloc in place of a static allocation. I knew I should remove it, but never got around to figuring out how big a buffer I needed. And, I never freed it, because, well, it was there only temporaily, anyway.
Well, I forgot about it and the software rolled out to our project's sponsor. And, with large enough inputs, my software became slow. I optimized the code. I added caching. I restructured large portions of the code. I tried to improve the algorithm.
Months after my graduation, my advisor took a look at it. Puzzled by the behavior, he started using some tools to analyze the situation. And, among those tools, he used strace -- which traces system calls. He found that in just a few seconds of exeuction, brk() was called some 20,000 times. You'll recall that brk() is the system call that malloc() uses when it runs out of memory to request more from the OS.
He replaced my sloppy malloc() call with a proper static allocation -- and the problem was gone. It would have been similarly fixed if he had simply freed the allocated space at the end of the work loop. But, in truth, malloc() should only be used when a static allocation won't do. Static allocaitons are "born" with the program. But malloc() is dynamic and wastes time during execution. And, in my case, it didn't make sense to free soemthing a the bottom of a loop only to reallocated it again moments later at the top.
Valgrind is a tremendous tool for finding memory problems in C programs. For those who might be familiar, it is similar to IBM's Rational Purify tool. Regardless, it can help you to find tons of different problems, and, of particular concern to us:
- Memory leaks
- The use of unallocated pointers
- Walking past the bounds of dynamically allocated arrays and other objects.
- The use of uninitialized variables
It is a dynamic, or runtime, analysis tool. This means that it analyzes your code while it is actually running. Basically, when you run a program using valgrind, it, at runtime, injects its code into your program (or vice-versa, really), so that it is able to trace your code.
But, like all runtime analysis tools, it checks only the code that actually runs -- not all paths. So, in any execution, it won't find problems, for example, in error handlers that don't happen to be exercised or in features that aren't invoked.
This is different than, for example, splint, which is a static tool. It analyzes the source code, rather than the execution. But, as it turns out, unless you program using a very formal and restricted style, runtime tools generally provide a better analysis.
In class we took a look at an excellent tutorial from the kind folks at cprogramming.com. I refer you there for a primer on valgrind:
Take a second. Try to call to mind the use we've made of the C preprocesssor. What have we done:
- We've #defined constants
- We've #included header files
- We've used #ifndef to guard against multiple inclusion.
- We've used #ifdef to include code intended only for debugging
Now, let's think for a moment about functions. Functions are great. They give us a way to reuse code -- we can write a function once and use them over-and-over again. This gets us much bang for the buck -- and makes the code maintainble by ensuring that corrections or changes need only be made in once place. And, it helps to make code meaningful by identifying segments of code with meaningful and descriptive names.
But, these benefits come at a cost. When you take 15-213, you'll learn a little bit about the mechanics of a function call. For now, let's just recall something we've said before -- there is overhead associated with making a function call and in returning from a function. It takes time.
Normally, for most functions, this small amount of overhead is no big deal. But, for really small functions, it can take longer to make the call and to return than to actually do the work. And, in certain essoteric types of programming, it can, under certain circumstances, be diifuclt or impossible to make function calls, because function calls change the stack.
We can get around this by using #define to create a function-like macro. The basic idea is that we define something that looks like a function within a #define. And then, after that, we can basically use it like a function. But, there are three big differences:
- The arguments to function-like macros are not typed, so they can be applied to multiple types -- this is, as you might guess, both a blessing and a curse.
- The function-like macro isn't called like a function, instead the preprocessor replaces the function-like macro with the body defined within the macro. There is no call -- the code is just placed in-line at the location of the macro use.
- Macros are used only for small, light-weight, mostly simple things. It is hard to wrtie complicated code cleanly within a macros. And, since it is placed in-line, large macros can increase code size and are usually better managed as functions.
Let's take a quick look at the following function-like macro and its use. We see that the macro si replaced and explanded by the preprocessor.
#define multiply(x,y) (x * y) ... int c = multiply(10,5); /* int c = (10 * 5);
Now, let's ocnsider the example below. It is broken. Do you see why? The extra space breaks the preprocessor -- the first space acts to separate the macro from its definition.
#define multiply (x,y) (x * y) /* ^ */ /* | */ /* Evil space */
Let's take a look at another interesting case. Here is an example that probably doesn't work as you'd expect:
#define multiply(x,y) (x * y) ... int c = multiply(10+5,5); /* int c = (10+5 * 5);
Take a seocnd look at the code above. Notice that it seems that we are asking to evaluate "((10+5)*5) = 75". But, it is expanded without regard to the grouping, so the order of oeprations is controlling -- and multiply beats add: "Please excuse my dear aunt sally". So, we get "(10+5 * 5) = (10 + (5*5)) = 250"
To fix this, when defining a macro, we always ()-parenthesize each-and-every use of the macro's arguments as follows. Notice how the parentheses force the right grouping:
#define multiply(x,y) ((x) * (y)) ... int c = multiply(10+5,5); /* int c = ((10+5) * (5));
Having said that, I'd like to reiterate and amplify what I've just said: Always parenthesize each and every use of a paramter within a macro -- whether you think you need it or not. I've shown you one alligator -- there are more in the swamp. Do go looking for them -- just stay safe in the boat. This is another example of defensive programming.
Toward This Week's Lab
This week's lab asks you to implement a memory trace utility that can detect memory leaks and the re-use of freed allocations. To do this, we'll need to be able to intercept calls to malloc and free. And, if we want to be rigorous, calloc(), realloc(), &c -- though this is not required for the lab.
The basic plan is this, we're going to put together a header file that uses #define to replace calls to malloc() and free() with calls own versions, mymalloc() and myfree(). Anywhere that this header file is included, malloc and free won't be called -- instead, the preprocessor will transform those calls into calls to mymalloc() and myfree()
Since we don't want to reimplement malloc() and free() ourselves -- we'll leave that for 15-213 -- we still want to call the "real thing" from our version. This isn't a problem -- our library doesn't include the header file that does the replacement, so we still have access to malloc() and free() in our code.
The only other detail is that, in the header file, we're going to "#undef malloc" and "#undef free". We're doing this just in case one of the included header files has #defined them as we are. #undef is the opposite of a #define -- it removes a previously defined preprocessor macro.
So, for a listing of the code that illustrates this, grab the exaples from class -- they are linked at the top of this page. Notice that main.c includes the memcheck.h header file -- this maps malloc() and free() over to our versions, mymalloc() and myfree(). Notice that memcheck.c does not include the header file -- this way, it can access the real malloc() and free().