Information Source Code Documentation Contact How to Help Gallery | |
COMPILE TIMEDebugging Memory Problemsby Steve BestMay 2003 Courtesy of Linux MagazineDynamic memory allocation seems straightforward enough: you
allocate memory on demand -- using For example, a memory leak occurs when memory is allocated but
never freed. Leaks can obviously be caused if you
Regardless of the root cause, memory management errors can have unexpected, even devastating effects on application and system behavior. With dwindling available memory, processes and entire systems can grind to a halt, while corrupted memory often leads to spurious crashes. System security is also susceptible to buffer overruns. Worse, it might take days before evidence of a real problem appears. Today, it's common for Linux systems to have a gigabyte of main memory. If a program leaks a small amount of memory, it'll take some time before the application and system show symptoms of a problem. Memory management errors can be quite insidious and very difficult to find and fix. This month, let's take a look at Valgrind, a tool that can help detect common memory management errors. We'll review the basics, write some "buggy" code, and then use Valgrind to find the mistakes. Valgrind was written by Julian Seward and is available under the GNU Public License. Dynamic Memory FunctionsOf all of the library calls in Linux, only four manage memory:
While the API for memory management is unusually small, the number and kind of memory errors that can occur is quite substantial, including reading and using uninitialized memory; reading/writing memory after it has been freed; reading/writing from memory past the allocated size; reading/writing inappropriate areas on the stack; and memory leaks. Luckily, Valgrind can detect all of those problems. When a
program is run under Valgrind, all memory reads and writes are
inspected and all calls to Installing ValgrindValgrind is closely tied to the architecture of the operating system. Currently, it's only supported on Linux x86 machines with kernels from the 2.2.x and 2.4.x series and glibc 2.1.x or 2.2.x. You can get the source for Valgrind at http://www.valgrind.org/. (At the time of this writing, the latest stable release of Valgrind is 1.0.4. The latest development release is 1.9.4.) Download the latest stable release (or the latest development release, depending on your sense of adventure) and build the software: % bunzip2 valgrind-1.0.4.tar.bz2 % tar xvf valgrind-1.0.4.tar % cd valgrind-1.0.4 % ./configure % make % make install One great feature of Valgrind is that it doesn't require you to build (or re-build) your application in any special way. Simply place valgrind right in front of the program you want to inspect. For example, the command % valgrind ls -all inspects and monitors the The output from Valgrind has the following format. ==20691== 8192 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==20691== at 0x40048434: malloc (vg_clientfuncs.c:100) ==20691== by 0x806910C: fscklog_init (fsckwsp.c:2491) ==20691== by 0x806E7D0: initial_processing (xchkdsk.c:2101) ==20691== by 0x806C70D: main (xchkdsk.c:289) The Losing Your MemoryLet's use Valgrind to find some common memory errors. The first sample program, sample1.c, shown in Listing One, has more than one memory leak. The code in Listing One allocates two 512-byte blocks of memory and then sets the pointer to the block to the second block. As a result, the address of the second block is lost, causing a memory leak. There are also 512 10-byte blocks of memory that are never freed. Listing One: sample1.c, a program with a memory leak #include <stdlib.h> #include <stdio.h> void get_mem() { char *ptr; ptr = (char *) malloc (10); /* memory not freed */ } int main(void) { int i; char *ptr1, *ptr2; ptr1 = (char *) malloc (512); ptr2 = (char *) malloc (512); ptr2 = ptr1; /* causes the memory leak of ptr1 */ free(ptr2); free(ptr1); for ( i = 0; i < 512; i++ ) { get_mem(); } } Compile and analyze sample1.c using the following commands: % gcc sample1.c -o sample1 % valgrind -v --leak-check=yes ./sample1 Valgrind produces the output shown in Figure One,
correctly identifying the 512-byte and the 512 10-byte memory
leaks. The Figure One: Valgrind output for Listing One ==2009== 512 bytes in 1 blocks are definitely lost in loss record 1 of 2 ==2009== at 0x400483E4: malloc (vg_clientfuncs.c:100) ==2009== by 0x80484D4: main (in /jfs/article/sample1) ==2009== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==2009== by 0x80483B1: free@@GLIBC_2.0 (in /jfs/article/sample1) ==2009== ==2009== 5120 bytes in 512 blocks are definitely lost in loss record 2 of 2 ==2009== at 0x400483E4: malloc (vg_clientfuncs.c:100) ==2009== by 0x80484A0: get_mem (in /jfs/article/sample1) ==2009== by 0x8048519: main (in /jfs/article/sample1) ==2009== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==2009== ==2009== LEAK SUMMARY: ==2009== definitely lost: 5632 bytes in 513 blocks. sample2.c, shown in Listing Two, demonstrates another common memory error: reading beyond the end of an array of bytes. Again, build the sample code and run Valgrind to analyze it. Listing Two: sample2.c, a program that tries to access memory beyond the end of an array #include <stdlib.h> #include <stdio.h> int main(void) { char *chptr; char *chptr1; int i = 1; chptr = (char *) malloc(512); chptr1 = (char *) malloc (512); for ( i; i <= 513; i++ ) { chptr[i] = '?'; /* error when i = 513 invalid write */ chptr1[i] = chptr[i]; /* error when i = 513 invalid read and write */ } free(chptr1); free(chptr); } % gcc sample2.c -o sample2 % valgrind ./sample2 As you can see from the output in Figure Two,
references to element 513 in the two arrays cause a write error,
a read error, and another write error. The message Figure Two: Valgrind output for Listing Two ==3016== ... ==3016== Invalid write of size 1 ==3016== at 0x80484DA: main (in /jfs/article/sample2) ==3016== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==3016== by 0x80483B1: free@@GLIBC_2.0 (in /jfs/article/sample2) ==3016== Address 0x40CA0224 is 0 bytes after a block of size 512 alloc'd ==3016== at 0x400483E4: malloc (vg_clientfuncs.c:100) ==3016== by 0x80484AA: main (in /jfs/article/sample2) ==3016== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==3016== by 0x80483B1: free@@GLIBC_2.0 (in /jfs/article/sample2) ==3016== ==3016== Invalid read of size 1 ==3016== at 0x80484EB: main (in /jfs/article/sample2) ==3016== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==3016== by 0x80483B1: free@@GLIBC_2.0 (in /jfs/article/sample2) ==3016== Address 0x40CA0224 is 0 bytes after a block of size 512 alloc'd ==3016== at 0x400483E4: malloc (vg_clientfuncs.c:100) ==3016== by 0x80484AA: main (in /jfs/article/sample2) ==3016== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==3016== by 0x80483B1: free@@GLIBC_2.0 (in /jfs/article/sample2) ==3016== ==3016== Invalid write of size 1 ==3016== at 0x80484EB: main (in /jfs/article/sample2) ==3016== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==3016== by 0x80483B1: free@@GLIBC_2.0 (in /jfs/article/sample2) ==3016== Address 0x40CA0454 is 0 bytes after a block of size 512 alloc'd ==3016== at 0x400483E4: malloc (vg_clientfuncs.c:100) ==3016== by 0x80484BF: main (in /jfs/article/sample2) ==3016== by 0x40271507: __libc_start_main (../sysdeps/generic/libc-start.c:129) ==3016== by 0x80483B1: free@@GLIBC_2.0 (in /jfs/article/sample2) Finally, to show how Valgrind finds invalid use of
uninitialized memory, let's look at the results of analyzing the
Journaled File System's (JFS) % valgrind -v -leak-check=yes \ fsck.jfs /dev/hdb1 Figure Three shows a snippet of the output. Figure Three: Valgrind output for the Journaled File System utility fsck.jfs ... ==12903== Conditional jump or move depends on uninitialised value(s) ==12903== at 0x8079FCC: __divdi3 (in /sbin/fsck.jfs) ==12903== by 0x805CB0E: validate_super (fsckmeta.c:2331) ==12903== by 0x805C266: validate_repair_superblock (fsckmeta.c:1833) ==12903== by 0x806E2B5: initial_processing (xchkdsk.c:1968) The Listing Three: A code snippet from fsckmeta.c int validate_super( int which_super ) { int64_t bytes_on_device; /* get physical device size */ vfs_rc = ujfs_get_dev_size(Dev_IOPort, &bytes_on_device); . . . dev_blks_on_device = bytes_on_device / Dev_blksize; /* Line 2331 */ if (sb_ptr->s_pbsize != Dev_blksize) { ... The output from Valgrind indicates that an uninitialized
variable is used on line 2331 -- that's the line that says,
Cache ProfilingValgrind can also perform cache simulations and annotate your source line-by-line with the number of cache misses. In particular, it records:
L1 is a small amount of SRAM memory that's used as a cache. L1 temporarily stores instructions and data, ensuring that the processor has a steady supply of data to process while memory catches up delivering new data. L1 is integrated or packaged within the same module as the processor. Level 2 caching is performed in L2. Valgrind's cachegrind tool is used to do cache profiling --
you use it just like valgrind. For example, the following command
looks at the % cachegrind fsck.jfs -n -v /dev/hdb1 The output of cachegrind is collected in the file
cachegrind.out. Sample output from analyzing
Figure Four: cachegrind.out, cachegrind's analysis of fsck.jfs ==11004== I refs: 99,813,615 ==11004== I1 misses: 4,301 ==11004== L2i misses: 3,210 ==11004== I1 miss rate: 0.0% ==11004== L2i miss rate: 0.0% ==11004== ==11004== D refs: 68,846,938 (65,916,678 rd + 2,930,260 wr) ==11004== D1 misses: 63,883 ( 37,768 rd + 26,115 wr) ==11004== L2d misses: 37,485 ( 14,330 rd + 23,155 wr) ==11004== D1 miss rate: 0.0% ( 0.0% + 0.8% ) ==11004== L2d miss rate: 0.0% ( 0.0% + 0.7% ) ==11004== ==11004== L2 refs: 68,184 ( 42,069 rd + 26,115 wr) ==11004== L2 misses: 40,695 ( 17,540 rd + 23,155 wr) ==11004== L2 miss rate: 0.0% ( 0.0% + 0.7% ) Events recorded abbreviations are: Ir : I cache reads (ie. instructions executed) I1mr: I1 cache read misses I2mr: L2 cache instruction read misses Dr : D cache reads (ie. memory reads) D1mr: D1 cache read misses D2mr: L2 cache data read misses Dw : D cache writes (ie. memory writes) D1mw: D1 cache write misses D2mw: L2 cache data write misses Next, you can annotate the output from cachegrind, by using
% vg_annotate
Figure Five: annotation of one entry of cachegrind for fsck.jfs ------------------------------ Ir I1mr I2mr Dr D1mr D2mr Dw D1mw D2mw ------------------------------ 88,405,584 23 23 61,740,960 14,535 98 576,828 9 9 fsckbmap.c:dmap_pmap_verify END For a complete description of cachegrind, see the Valgrind User's Manual in the Valgrind distribution. Some Limitations Of ValgrindThere are two issues that you should be aware of when analyzing an application with Valgrind. First, an application running under Valgrind consumes more memory. Second, your program will run slower. However, these two minor annoyances shouldn't stop you from using this powerful memory management debug tool. About the AuthorSteve Best works in the Linux Technology Center of IBM in Austin, Texas. He is currently working on the Journaled File System (JFS) for Linux project. Steve has done extensive work in operating system development with a focus in the areas of file systems, internationalization, and security. He can be reached at sbest@us.ibm.com. You can download the sample programs used in this article here. |
|
Copyright © 2000-2024 Valgrind™ Developers |