14
Run-Time Debugging with Microsoft Visual Studio and Rational Purify Part II by Goran Begic Technical Marketing Engineer, Development Solutions Rational Software B.V. The Netherlands Debugging -- the extremely slow and expensive process of fixing defects -- will always be part of the larger software development process. And, as any "debugger" will tell you, locating the real cause of a defect is the hardest task of all; fixing a problem in the code is by far the easiest part of debugging. In Part I of this two-part series, which appeared in the April issue of The Rational Edge, we introduced you to the Microsoft Visual Studio Programming Environment and discussed ways to use the Microsoft Visual Studio Compiler for initial debugging. In Part II we will cover run-time debugging with the Microsoft Visual Studio Debugger and Rational Purify. Please refer to Part I for a description of the sample application and initial debugging procedures referred to in this article. Using a Debugger for Run-Time Debugging Let's suppose we have created some prototype code with the Microsoft Visual C++ compiler, as we described in Part I, and would now like to run it to see whether it works. We set up our project following the guidelines we described in Part I and tried to program as defensively as possible. When we run the application the first time, using the compiler, it works fine, but does that mean that it is bug free? We don't know! Even if the application executes fine on our machine, it may still contain errors. It is also possible for the errors to appear under special circumstances: if the application is run with certain scenarios or on certain machines. So now it is time to look for another, more powerful tool to help us with debugging: a debugger. Microsoft Visual Studio comes with a powerful debugger that provides extensive insight into the application internals, data stored in structures, content of registers, and even assembly- level instructions. There is only one problem with this debugger: It has to be told when to stop the execution. How do you know when to peek into the internals of the running program? It executes really fast! The simplest way is to use Just in Time (JIT) Debugging. JIT Debugging

The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

  • Upload
    others

  • View
    2

  • Download
    0

Embed Size (px)

Citation preview

Page 1: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

Run-Time Debugging with Microsoft Visual Studio and Rational Purify Part II

by Goran BegicTechnical Marketing Engineer, Development SolutionsRational Software B.V.The Netherlands

Debugging -- the extremely slow and expensive process of fixing defects -- will always be part of the larger software development process. And, as any "debugger" will tell you, locating the real cause of a defect is the hardest task of all; fixing a problem in the code is by far the easiest part of debugging. In Part I of this two-part series, which appeared in the April issue of The Rational Edge, we introduced you to the Microsoft Visual Studio Programming Environment and discussed ways to use the Microsoft Visual Studio Compiler for initial debugging. In Part II we will cover run-time debugging with the Microsoft Visual Studio Debugger and Rational Purify. Please refer to Part I for a description of the sample application and initial debugging procedures referred to in this article.

Using a Debugger for Run-Time Debugging

Let's suppose we have created some prototype code with the Microsoft Visual C++ compiler, as we described in Part I, and would now like to run it to see whether it works. We set up our project following the guidelines we described in Part I and tried to program as defensively as possible.

When we run the application the first time, using the compiler, it works fine, but does that mean that it is bug free? We don't know! Even if the application executes fine on our machine, it may still contain errors. It is also possible for the errors to appear under special circumstances: if the application is run with certain scenarios or on certain machines. So now it is time to look for another, more powerful tool to help us with debugging: a debugger. Microsoft Visual Studio comes with a powerful debugger that provides extensive insight into the application internals, data stored in structures, content of registers, and even assembly- level instructions.

There is only one problem with this debugger: It has to be told when to stop the execution. How do you know when to peek into the internals of the running program? It executes really fast! The simplest way is to use Just in Time (JIT) Debugging.

JIT Debugging

jprince
Copyright Rational Software 2001
jprince
http://www.therationaledge.com/content/may_01/t_debug2_gb.html
Page 2: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

Just In Time debugging allows a crashed program to be attached by the debugger. It enables the user to see a "frozen" profile of the program at the moment of the crash. Unfortunately, it may be very difficult to de-encrypt the information about the crash; furthermore, the main reason for the crash probably happened much earlier in the execution, and traces of the real cause may be long gone before JIT was engaged. JIT can sometimes be a great help, however, so let's see how it works on a buggy C++ program.

Our sample application project, which can be downloaded from the location specified in the Appendix of this article, is a simple console application with one class of Bears (yes, real ones) and a couple of member functions that interact with the MyBear object. The program has several severe errors that we'll try to debug one by one as we advance through the article.

If you compile and run the sample, Figure 1 is what you'll see:

Figure 1: Error Message

Looks bad. What happened? The program has just crashed, that's for sure.

Page 3: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

Figure 2: Windows When the Microsoft Visual Studio Debugger Starts UpClick here for full size image.

If you decide to engage the debugger at the moment of the crash, Figure 2 shows how the debugger windows might look like when the debugger starts and attaches to the crashing process. I like to have the Register Window and the Call Stack Window opened all the time. They display the content of the machine registers and the list of functions that have been executed, respectively. You can invoke the additional debugger windows if you go to the main menu -> View -> Debug Windows and than choose the windows you want.

The last function to be called before the crash was one of the functions from the C Run-Time Library: msvcrt.dll. Since we did not use the "Debug" version of the Library, the name of the function is not resolved and is therefore is not shown on the stack. Using the "Debug" version of the C Run-Time Library will give us more information, as shown in Figure 3:

Figure 3: Stack Window from "Debug" Version of C Run-Time LibraryClick here for full size image.

The last function to be called was strcat(), which really does live in mcvcrt.dll. We see from the trace message and from the Call Stack that the last action performed by the application was creation of the Bear object.

Let's look at some other information that is available in the debugger. The content of

Page 4: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

the register EDI is 0xCCCCCCCC. This register is used for memory move and compare operations. The hexadecimal value is the one that Visual Studio uses to automatically initialize all local variables. It also means that the program probably tried to use an uninitialized variable. A look at the Variables Window at the bottom of the screen shows that variable pBearFriend contains the above-mentioned value. Could this cause the application to crash?

The answer is "Yes, definitely!" A look at the source code shows that the author of the sample forgot to initialize the variable m_pBearFriend. Instead, he initialized m_pBearName twice while copy-pasting the lines of code.

Lines 45-49 in the source file bear.cpp:

m_pBearName = new char[strlen(pName)+1];

strcpy(m_pBearName, pName);

m_pBearName = new char[strlen(pFriend)+1]; //Copy-paste error!

// m_pBearFriend = new char[strlen(pFriend)+1]; //Correct allocation

strcpy(m_pBearFriend, pFriend);

Hey, that one was easy! Let's continue with the debugging of the Bear program. Fix the error by commenting out line 47 of the source file bear.cpp and remove the comments from the correct line that follows. Then rebuild the application and run it under the debugger.

Figure 4: Error Message from "Debug" Version of C Run-Time Library

Hmm, Figure 4 shows another nasty message. This time the system gave us a "Debug

Page 5: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

Error!" As we mentioned earlier, the "Debug" version of the Microsoft C Run-Time Library uses a special "Debug" version of the memory allocators that, among other things, allocate additional chunks of memory to report boundary violations of the newly- allocated memory blocks. This is exactly what has happened here. We can click on the button Ignore, and the application will execute completely. Please note that if we had linked the program against the release version of the Microsoft C Run-Time Library this error would not be displayed. So how can we find the cause of it?

Before the application terminates, we get an additional message -- and this time it is a warning:

0x80000003 EXCEPTION_BREAKPOINT (see Figure 5).

Figure 5: Warning Message from "Debug" Version of C Run-Time Library

This message is given when HeapFree() shows guard byte corruption.

Now that we've brought up the subject of breakpoints, let me explain this important feature, which is included in practically every debugging tool.

What Is a Breakpoint?

Breakpoints are one of the most frequently used features of the debugger. They can be set in the Visual Studio editor by pressing F9 on the selected line of the source code; this will mark the position in the program where you would like to stop the execution of the application in the debugger. If we manage to stop ("break") the program at the right moment, maybe it will give us a chance to catch memory reads or writes behind the structure boundaries. It may be a really difficult task to accomplish in the real world applications that are much more complicated than the Bear project.

On the assembly level, a breakpoint is a 1 Byte instruction (0xCC) inserted into the code. When 0xCC is passed to the processor, it interprets it as a special, high- priority interrupt, and the processor stops the execution of the application exactly at that instruction. Additionally, it saves the previous instructions (contents of two registers) so that these values can be re-established when the user decides to move on from the breakpoint, and the program will continue its execution.

Where should you set up the breakpoints? In this particular example, the "Debug" build at the moment of the crash gives us a hint as to where to look for the bug. The Call Stack window displays the list of functions and their parameters in the order in which they have been called and executed by the system. The very last instruction of the application that has been executed is displayed at the top of the stack. Often, the Call Stack will display the system functions, not the user ones. That is because these functions were called internally when the user functions were executed. In this

Page 6: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

particular example, the user-defined destructor for the MyBear object called the run-time function free() to free up the memory used by the object, which then internally called the "Debug" version of the operator that actually performs the cleaning of the used memory.

Figure 6: Call Stack from "Debug" Version of C Run-Time Library

As Figure 6 shows, the error happened while we called the destructor for the MyBear object. The functions at the top of the stack are "Debug" versions of the functions that are supposed to free the memory allocated for the object:

delete(m_pBearName);delete(m_pBearFriend);delete(m_pBearHobby);

Let's put one breakpoint at the line before the value is initialized (line 52 in the source file bear.cpp) and one at the line where delete() on m_pBearHobby is called (line 30 in the source file bear.cpp).

After setting up the breakpoints, we can rebuild the application and start it in the debugger. As Figure 7 shows, the execution stops exactly at the line where we specified the breakpoint (line 52). The main window in the Visual Studio Debugger displays several windows in the same order we used the last time the main window was closed. The window on the left half of the main window shows the source code with the error pointing at the line where we set up the breakpoint. The Call Stack window on the right side shows the last executed method; it is the constructor for the MyBear object.

Page 7: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

Figure 7: Microsoft Visual Studio Debugger at the First BreakpointClick here for full size image.

This time we will use the "Debug" Memory Window as well. It is displayed in the lower half of the main window. In the Address field of the Memory Window, enter the value stored in the m_pBearHobby pointer. That's the place where a chunk of memory for the value that m_pBearHobby is pointing to is allocated. Starting from the left in the Memory Window, there is a sequence of 10 bytes allocated for m_pBearHobby. It is marked with the pattern byte 0xCD. Additionally, there are 4 bytes that mark the end of the allocated memory and at the same time build the "security zone" created by the "Debug" version of the memory allocator (pattern byte 0xFD). The "Debug" allocator "sprays" the allocated, but not yet initialized, memory with the pattern of Bytes 0xCD, and the boundary zone around the array is marked with 0xFD.

Press the F5 key to continue the execution of the application in the debugger. Figure 8 shows what you will see after the array pointer m_pBearHobby is initialized:

Figure 8: Microsoft Visual Studio Debugger Screenshot at the Second BreakpointClick here for full size image.

Page 8: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

In the Variables Window on the very bottom of the main window, you can see the list of member variables of the created object. The m_pBearHobby variable is now pointing the string "Philosophy" assigned to it earlier in the application. The Memory Window is still showing the image of the string in the memory at the same location where we set it up in the earlier step. It is exactly the location where the string "Philosophy" is saved in memory.

If you carefully count the number of allocated bytes, you'll see that "Philosophy" has 10 characters (therefore 10 bytes as well), and that the last byte of the string -- string termination character (0x00) -- is written in the boundary zone after the string. Note that there are only 3 0xFD bytes left in the boundary zone. In the release version of this program there would be no additional zones between data structures, and the program could actually write over some valid data with unpredictable consequences for the execution of the application. Luckily, the example about bears is not a mission-critical application.

Breakpoints are powerful tools in the hands of a person who knows where to put them. What we have seen above is the simplest and the most straightforward way of setting up breakpoints in the application. The breakpoints can be engaged in a more advanced way. Pressing ALT+F9 in the Microsoft Visual Studio main window opens the Breakpoints Setting window, where it is possible to specify the location, the expression, the variable, or the message condition for the breakpoint. It is even possible to set the conditional breakpoints, although setting the correct condition may not be that easy.

For example, the advanced breakpoint setting can be used to instruct the debugger to stop whenever the address stored in a pointer variable is changed. It will enable you to check all the values a pointer is getting and lead you to the place in code when the illegal address is passed to the pointer.

Now, we can correct the error by allocating one more byte on line 50 of the source file bear.cpp and rebuild the application. The application executes fine now, so that means there are no more errors, right? Wrong!

Using an Automated Run-Time Debugger

By now we have discovered and fixed some errors that we discovered when the debug version of the application was test-run, but we still don't know whether this small program is bug free or not. There is simply no time to check every possible scenario in the application by setting up the breakpoints and looking at the application internals as it has been run. We still have to determine whether the software is ready to be shipped to the customer! In this situation, the safest bet is to use an advanced debugging tool that can perform this time-consuming run-time checking process for us.

There are a couple of such tools on the market. Some of them use the source code as the main source of information about the program execution, but the one we will use here works very much like an automated version of the debugger, even without the source code. It relies on the symbolic debugging information of the application and checks every memory allocation as well as function parameters, and it checks for memory leaks as the program is being run. The program requires one push on the button that will engage its integration in Microsoft Visual Studio, so we can leverage the expertise we've acquired in using the Visual C++ compiler and the Visual Studio debugger. The report presented after the run pinpoints the error locations and memory allocation locations, things that are very difficult to find by using only the standard methods and the debugger. I intentionally left the above-explained error in the program as well, because this is how I located it in the first place -- by running the

Page 9: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

sample in Rational Purify.

Figure 9 shows the first tab of the report:

Figure 9: Microsoft Visual Studio Main Window with Rational Purify ReportClick here for full size image.

The report says that even after we cleared the first error, there are still two errors and one memory leak left in the program.

Even if this is not a mission critical application, I would feel very uncomfortable about this article if we didn't investigate where these errors came from and whether they are real. Let's take a look at each of them.

The first error on the list is marked as ABW. The abbreviation stands for "Array Bounds Write" and means that the program violated the boundaries of the allocated array. In this case it marks the error that we already revealed by setting up the breakpoints at the correct location in the code and by observing the process of initialization of the variable in memory. I have to admit that I first used the above report to locate the error and that I used the following information to set the breakpoints and run the program in the debugger (see Figure 10).

Figure 10: Rational Purify Report for the Array Bounds Write ErrorClick here for full size image.

Page 10: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

If we expand the error message line, then Rational Purify will show us the Call Stack at the moment when the error happened, and it will also indicate the place in the code where the error happened. This is the same kind of information that was available in the debugger through the Call Stack Window, Memory Window, the Variables Window, and the Source Window. The big difference between the run in the Microsoft Visual Studio Debugger alone and the run with Rational Purify engaged is that in the latter case we don't have to set up the breakpoints or use the Just-In- Time debugger; Purify points us directly to the memory problem and provides extensive information about the occurrence, which is more than sufficient for any developer to fix the error.

What else do we have on the list of errors in the Purify report? The very next error is the Array Bounds Read, which also points to the problem of the array violation. This time, however, the application tried to read from the memory location that was outside the boundaries of memory allocated for the array. In Figure 11, the source of the problem this time is in the function that "calculates" the population of bears. It is a "feature" I decided to add in at the last moment, and it is not a particularly good one. It is important to note that this problem stayed undetected while the application was tested in the debugger, despite usage of the "Debug" C Run-Time Libraries.

Figure 11: Rational Purify Report for the Array Bounds Read ErrorClick here for full size image.

Although the array of integers was allocated for two elements, the population was given from the non-existing element of the array. This value is read right outside the boundaries of the array, and in the "Debug" version it holds the pattern used to mark the boundary of the allocated array of 4 Bytes. The boundary is the same size: 4 bytes. This "third element" of the array contains the following hexadecimal value: 0xFDFDFDFD, which is exactly the value that gets displayed by the program. No wonder the population numbers are the same for every run!

For the release version of the program the pattern is slightly different: 0xABABABAB (not the value of the boundary). The significant fact here is that nothing is preventing

Page 11: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

us from using this value in the application. This type of problem is very difficult to detect because the application may look like it is working properly. So unless the program is tested extensively prior to release, there is a good chance that these problems will not be detected until the most inconvenient time: when the system is installed at the customer's site.

Figure 12: Microsoft Visual Studio Debugger - Debug Memory Window Click here for full size image.

In Figure 12 you can see the first two elements of the array marked red and the following pattern understood as a valid number by the application. The funny thing about the value that was supposed to represent the population of bears is that it changes as we change the Microsoft C Run-Time that we use, or if we use Purify to detect the error, in which case the population would have the value 0xAEAEAEAE. The program didn't crash or cause system errors, but the number representing the population of the bears was false, based on the memory pattern byte that was on the location where memory was read. The biggest population was associated with the Rational Purify byte pattern we mentioned.

Memory Leaks

The final error message given by Purify is an MLK (Memory Leak) message. Memory leaks are particularly difficult to detect and can be very annoying if they are in an application that is supposed to run without restarting for a longer period of time. Such an application can eat all the memory resources and eventually stop all the processes running on the same machine. In the January issue of The Rational Edge, I provided a brief introduction to memory leaks on C++ as compared to Java memory leaks.

An MLK message will be displayed for all memory on the heap when there are not pointers to it -- or to anywhere within the blocks of allocated memory. Such memory areas are not used by the application, but they cannot be returned to the operating system, and continuous memory leakage can be a very serious problem even on the machines with gigabytes of available memory.

"Debug" Run-Time Libraries can be used to check memory leaks on the heap. The article from MSDN Library, "Detecting a Memory Leak," which is listed at the end of this article, explains how to use Debug C Run-Time Libraries to detect memory leaks. Purify approaches this problem in a slightly smarter way; its LeakCheck feature, which is applied by default just before the program is terminated, works in a similar way to the Garbage Collector in Java. Instead of freeing the memory, Purify displays a detailed report on the leaked memory blocks (Figure 13).

Page 12: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

Figure 13: Rational Purify Report on Leaked Memory

The report is correct; the memory allocated for pName has never been freed. The line of code where that was supposed to be done is commented out, but it could as well be forgotten.

One of the many advantages of having a tool like Rational Purify is that it gives you not only the ability to discover memory leaks, but also the ability to collect information about Memory In Use, and thus heap allocations to which the program has a valid pointer. Even when allocated blocks of memory are under control of the running process, if the program uses large amounts of memory, then the final result can be significant performance degradation. For example, the operating system may start using swap space on the disk to allocate additional memory, or it may even completely run out of memory. Memory blocks that have a pointer to them, or if there is a pointer to somewhere within the block of memory, cannot be considered a Memory Leak as we defined this term above, but they should be controlled as well, especially if their size may influence the performance of the program under test -- or maybe even of other programs running on the same machine.

For example, if the memory allocated with new() is declared as static, then Purify reports this memory as Memory In Use (MIU):

static char* pName = new char[MyBear.getLength()+1];

Page 13: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

Figure 14: Rational Purify Report for the Memory in Use

After we corrected all the errors shown in Figure 14, then we could mark this application as bug-free and ready to be released. Using an automated debugging tool provides the additional information we need to know about the quality of the developed application. It should be used as one of the main criteria for a decision to push a particular build to release.

Good Debugging Yields More Time for R&D

A combination of good project planning, defensive programming, and the right set of tools can make developers' lives much easier and leave more time for conducting research and implementing new features. Essential tools like a compiler and a debugger can be transformed into easy-to-use debugging aids and used in conjunction with an automated run-time bug-finder, like the one we described in the article.

Regardless of the techniques and tools you use to reach your final goal -- producing high-quality software -- keep in mind that the purpose of debugging is always the same: to discover "what lies beneath" in the application you are testing.

References

1. John Robins, Debugging Applications. Microsoft Press, 2000.

2. Chris H. Pappas and William H. Murray, III, Debugging C++, Troubleshooting For Programmers. Osborne/McGraw Hill, 2000.

3. Everett N. McKay and Mike Woodring, Debugging Windows Programs: Strategies, Tools and Techniques for Visual C++ Programmers. Addison-Wesley, 2000.

4. Matt Pietrek, "Peering Inside the PE: A tour of the Win32 Portable File Executable Format", MSJ March 1994, MSDN Library, 2001.

5. Microsoft MSDN Library, Detecting a Memory Leak.

Page 14: The Rational Edge -- May 2001 -- Run-Time Debugging with ... › developerworks › rational › ...hardest task of all; fixing a problem in the code is by far the easiest part of

6. Rational Purify documentation.

NOTE: Download a copy of the sample application (VC++ project) used in this article at: ftp://ftp.rational.com/outgoing/goran/RationalEdge/Edge_debugVCv1.zip

For more information on the products or services discussed in this article, please click here and follow the instructions provided. Thank you!

Copyright Rational Software 2001 | Privacy/Legal Information