title author publisher isbn10 | asin print isbn13 ebook isbn13 language subject publication date lcc ddc subject
cover
...
328 downloads
2340 Views
2MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
title author publisher isbn10 | asin print isbn13 ebook isbn13 language subject publication date lcc ddc subject
cover
next page >
cover
next page >
: : : : : : : : : : :
< previous page
page_i
next page > Page i
Debugging Visual C++ Windows Keith Bugg
< previous page
page_i
next page >
< previous page
page_ii
next page > Page ii
Disclaimer: This netLibrary eBook does not include the ancillary media that was packaged with the original printed version of the book. R&D Books an imprint of Miller Freeman, Inc. 1601 West 23rd Street, Suite 200 Lawrence, KS 66046 USA Designations used by companies to distinguish their products are often claimed as trademarks. In all instances where R&D is aware of a trademark claim, the product name appears in initial capital letters, in all capital letters, or in accordance with the vendor's capitalization preference. Readers should contact the appropriate companies for more complete information on trademarks and trademark registrations. All trademarks and registered trademarks in this book are the property of their respective holders. Copyright © 1998 by Miller Freeman, Inc., except where noted otherwise. Published by R&D Books, an imprint of Miller Freeman, Inc. All rights reserved. Printed in the United States of America. No part of this publication may be reproduced or distributed in any form or by any means, or stored in a database or retrieval system, without the prior written permission of the publisher; with the exception that the program listings may be entered, stored, and executed in a computer system, but they may not be reproduced for publication. The programs in this book are presented for instructional value. The programs have been carefully tested, but are not guaranteed for any particular purpose. The publisher does not offer any warranties and does not guarantee the accuracy, adequacy, or completeness of any information herein and is not responsible for any errors or omissions. The publisher assumes no liability for damages resulting from the use of the information in this book or for any infringement of the intellectual property rights of third parties that would result from the use of this information. Cover art created by Robert Ward. Distributed in the U.S. and Canada by: Publishers Group West 1700 Fourth Street Berkeley, CA 94710 ISBN: 0-87930-545-2
< previous page
page_ii
next page >
< previous page
page_iii
next page > Page iii
This humble work is fondly dedicated to Glenn Leonard Fyfee III and Chadwick E. Tackett two rising stars of computer science.
< previous page
page_iii
next page >
< previous page
page_v
next page > Page v
Contents
Preface
ix
Nomenclature
x
1. Introduction and Scope
1
The Event-Driven Paradigm
The Bug Battle
Error Sources and States
Minimizing the Bug Count
Minimizing the Bug Cost
Chapter Summary 2. The Win32 Memory Management System
Virtual Address Space
Heaps
Heap Functions
Virtual Memory Functions
Chapter Summary 3. The Visual C++ Debugging Environment
Assertions
2
2
3
8
11
16
17 18
19
23
27
36
37 37
46
Access Violations
47
VERIFY
< previous page
page_v
next page >
< previous page
page_vi
next page > Page vi
The Debugging Environment
Debug versus Release
The Map File
C Run-Time Library Support
The Dump Function
Exceptions
Comparing Exception Handling: C++, MFC, and Win32
Return Values
Class CMemoryState
Hooking Memory Allocations
Casts GetLastError()
Validating Pointers and Strings
Chapter Summary 4. The Visual C++ Debugger
Overview
The Debug Toolbar
48
48
51
51
59
60
61
69
70
72
73
73
75
76
77 77
78
The Call Stack Window
Application Problems
DLLs
Just-in-time Debugging
Visual C++ Compiler Errors
Compiler Pragmas
Code Migration Issues
Chapter Summary 5. Additional Debugging Tools
MFC Tracer
Stress
Spy++
Browse
DDESpy
The Profiler
The Process Viewer
The ErrLook Utility
The WinDiff Utility
Chapter Summary
84
87
90
92
92
95
99
102
103 103
106
108
109
111
118
119
120
120
121
< previous page
page_vi
next page >
< previous page
page_vii
next page > Page vii
6. Commercial Debuggers and Project Tools
BoundsChecker, Visual C++ Edition
CodeWizard
Code Management Systems
Program Testing
Microsoft Visual Test
BugCollector Pro
Support Software
Chapter Summary 7. Debugging Database Applications
Database Design and Normalization
ODBC
DAO
Selecting the Database Class
Database Errors
SQL
SQL Debugging Limitations
SQL Data Types
123 124
125
128
130
132
134
135
138
139 139
144
145
146
147
147
148
148
Chapter Summary 8. Known Bugs and Special Problems
bool Size
Non-Integer Divide by Zero
Call_findclose() after Calling _findfirst() or _findnext()
C Runtime _expand() Function Returns NULL upon Failure
The Ternary Conditional
try Blocks and switch Statements
sizeof() and Arrays
URLMON.DLL
Access Violations
Typographical Error
Incorrect Documentation for the Clean Command
Windbg Cannot Use Visual C++ v5.0 Debugging Information
Error in Release Version of ATL
The /WS:AGGRESSIVE Linker Option
Option Precedence and the CL Environment Variable
The /Zm Option
151
153 153
154
154
154
155
156
157
157
158
158
159
159
159
160
160
161
161
Missing Type Definition Errors
162
Keywords _emul() and _emulu() Not Documented
< previous page
page_vii
next page >
< previous page
page_viii
next page > Page viii
The Linker's /OPT:ICF Option
Debugging Windows API Functions with NT Symbols Loaded
Resolving Error RC2104
Compiler Warning (Level 4) C4238
Compiler Warning (Level 3) C4800
Compiler Warning (Level 1) C4804
Compiler Warning (Level 1) C4806
Compiler Warning (Level 1) C4807
Compiler Warning (Level 1) C4808
Chapter Summary 9. Common Windows Errors
Bitmapped Buttons
Radio Button Member Variables
Linking with Libraries
Coordinate Systems
Window Handles and Device Contexts
Strings and Arrays
162
162
163
163
164
164
165
165
165
166
167 167
168
168
169
169
169
170
Trapping WM_HELP
171
Chapter Summary Appendix A ODBC Error Codes
173
Appendix B SQLState Values
185
Appendix C DDEML Error Codes
189
Index
193
< previous page
page_viii
next page >
< previous page
page_ix
next page > Page ix
Preface This book covers one of the most crucial and under-acknowledged phases of the software development life cycle for WindowsTM applications: debugging. However, this book is not simply an explanation of how to use a debugger or strictly a theoretical discussion of the types of faults you might encounter. Rather, it also seeks to enhance your understanding of how bugs are introduced into an application, how you can minimize them with proper design techniques, and how you can detect and correct them. But don't worry; all the "techno-mumbojumbo" is still there. I've tried to aim this book at the beginner-to-intermediate Windows developer that is, I assume you're comfortable with the Visual C++ Workbench and "know your way around." As I was outlining my ideas for this book, I viewed the reader as someone who was knowledgeable enough not to need debugging help but who saw its utiliy and wished to learn more. In other words, you're probably already a darn good software developer, or you're on your way to being one. This book is written for Microsoft's® Visual C++TM development tool, which is the most common platform in use today and the dominating force in mainstream Windows development. The book is organized as follows: Different types of bugs Error minimization through design and analysis Memory and memory allocation Using the Visual C++ debugger Using commercially available debuggers
< previous page
page_ix
next page >
< previous page
page_x
next page > Page x
As you work through the book, you'll be exposed to a wide variety of bugs and their causes, along with suggested solutions. Known bugs in the compiler will be given due attention, and you'll find a compendium of resources in the last chapter to help you if you get stuck. In addition to the subject of debugging, you'll also find some useful hints, tips, and tools to facilitate basic project management. While some of these topics have no direct relationship to fault detection and isolation, they can indirectly influence your debugging effort. For example, in Chapter 4, there is a discussion of the pragmas. One useful pragma is #message(string)
which lets you output a message to the build window. For instance #message("Date-validation not finished in method GetDate()!");
If you've got a function working that is not completely road-worthy, a reminder like this can save you some embarrassment at productdelivery time. Nomenclature To make this book more readable, I've adopted the following font usage. All functions, API calls, arguments, Windows messages, source files, modules, and so on are shown in monospaced typeface, matching their counterparts in the code. I hope this book helps you bring your MFC/Visual C++-based applications to market as quickly and as bug-free as possible.
< previous page
page_x
next page >
< previous page
page_1
next page > Page 1
Chapter 1 Introduction and Scope Anybody who has developed Windows applications for any length of time will tell you that writing the application is only half the battle; the other half is getting it to work properly. Indeed, software development has been described as a three-step process: 1. Make it work 2. Make it work right 3. Make it work fast This book is all about getting applications to work how to prepare yourself, how to implement a plan, and how to use the tools at your disposal. Inasmuch as Microsoft's Visual C++ is swiftly emerging as the de facto standard for developing Windows applications, this book will use it as the development platform. In addition to exploring the purely mechanical aspects of debugging, another goal of this book is to expand your awareness regarding how bugs come into being. For example, bugs can be introduced into the system by poor or contradictory design goals, incomplete or inadequate testing (especially for different target platforms), and so forth. Proper debugging strategies, tools, and techniques are a critical part of any software development
< previous page
page_1
next page >
< previous page
page_2
next page > Page 2
effort because errors cost time, money, and can greatly effect current and future sales. With large and complicated software products, the public has come to accept a certain amount of bugs. But releasing software with bugs beyond the public's threshold of acceptance makes it all the more difficult to sell new versions and new products. The Event-Driven Paradigm Compounding the problem of debugging Windows applications is the simple fact that Windows applications are based on the model of messages or events. Unlike procedural-based programs that flow in a linear fashion, Windows responds to messages. Since there is no way of knowing what message will arrive next, you lose the comfort of knowing ''where to look." For debugging an application based on events, it is essential that the developer know how to use one or more debugging tools. Mercifully, the Professional Edition of Visual C++ includes several tools, each providing a specialized function for fault isolation. Forget to delete the memory allocated for a CBrush object? Not to worry the memory allocation information returned by STRESS.EXE will show you that the GDI heap is shrinking. This at least will point you in the right direction. Not having tools such as these and the know-how to use them makes debugging Windows applications an exercise in frustration, if not futility. The Bug Battle The war against software bugs can be viewed as three distinct battles: 1. Prevent bugs 2. Detect bugs 3. Resolve or handle bugs Preventing bugs through proper software design and analysis is the easiest battle to fight the bug never enters the system. The next two battles the main topic of this book are fought using tools and utilities provided by Visual C++, in addition to those you can purchase from third-party vendors. Bug detection and resolution can be accomplished by using these tools and solid, methodical testing. It will be up to your code to detect some program anomalies, whether they are actual bugs or not. Regardless, it is
< previous page
page_2
next page >
< previous page
page_3
next page > Page 3
always good design policy to provide the user with a graceful recovery path from the error or condition. Error Sources and States Errors can appear at different places and times in the development cycle, and they must be recognized and sorted into something manageable. Machine error (which I'll get to shortly) is a classic example if you don't know about it, all the watch windows and debuggers in the world won't explain why some variable has a value you know is mathematically wrong. For our purposes, there are four main types of bugs that can arise during the software development cycle. 1. Machine errors 2. Compile-time errors 3. Run-time errors 4. Logic and design errors The first type, machine errors, is a topic that many developers seem either not to understand or to discount. The next two types are, for the most part, easy to detect and correct. The last type, logic and design errors, is much more insidious because your program will "work" but will produce incorrect and/or unpredictable results. There is no debugger available for these types of problems; only solid, meticulous design and analysis techniques will help you avoid these bugs. Machine Errors While mainstream pop culture touts the precision of computers, we know it just ain't so. Those little silicon chips unavoidably create errors some numbers simply can't be accurately expressed. The most common variety of this condition is called roundoff error. Simply stated, roundoff error is the error produced by not using all the bits of a binary value. When you use numerical variables in your application, their values are converted to binary numbers. Since there is always a finite number of bits available for the processor to load or store a number, it "chops off" the end of the number, and you lose accuracy. The same situation exists for us, because we use a base 10 system of mathematics. Try dividing 6.0 into 1.0. The answer is 1.66666 . . . the trailing sixes never end.
< previous page
page_3
next page >
< previous page
page_4
next page > Page 4
Now, for a demonstration, consider the following piece of code float fSum= 0.0; for(int i = 0; i < 1000000; i++) fSum += .000001;
Adding 1/100,000 to itself 10,000 times is exactly 1. But because of machine error, the value of fSum is either slightly more or less than 1.0. My test machine, a 166MHz Pentium, reported fSum as 1.009039. Changing fSum from a float to a double yields the correct result. But in severe number-crunching cases, imprecise results will be returned. Computers don't like floating-point numbers, and they don't like adding large numbers to small numbers. These operations can give rise to errors in your software, producing surprising and unpredictable results. For the most part, machine errors are generally a problem only for computationally intensive applications such as weather models, least-squares curve fitting, and so on. Compile-Time Errors These bugs are the easy ones to find and fix the compiler does all the work for you. The following is an example. CString csStr; int len = csStr.GetLengthh();
When you try to compile this, you'll get the following error error C2039: 'GetLengthh' : is not a member of 'CString'
Here, a simple typographical error ("typo") was made the class method GetLength() was misspelled 'and thus not recognized. As you'll see later on, you can tailor the Visual C++ compiler's settings to assist you with error detection and resolution. Run-Time Errors Run-time errors occur when you execute your program. The cause may or may not be a logic error, but the gosh-darn thing just won't run. Your
< previous page
page_4
next page >
page_5
< previous page
next page > Page 5
program compiles without errors or warnings but "blows up" at some point. The following is a simple example of a run-time bug int k = 0, i = 100; for(int j = 9; j < 1; j--) k = i/j;
This little snippet of code will compile; the loop will even execute nine times. But on the last iteration, there is an attempt to divide "i" by zero, which generates the run-time error. Actually, a divide by zero is more accurately called a hardware exception. While the end result is the same as far as the developer is concerned, exceptions are detected and resolved in a different manner than errors. Exceptions come in two flavors: hardware exceptions and software exceptions. These will be covered in considerable depth in the next chapter. One of the biggest sources of run-time bugs are variables that are used without proper initialization. Take a look at this example. for(int j; j < 10; j_++) { // do something }
Here, the variable j has not been initialized, although the programmer probably intended j initially to be zero. If the address where the compiler stuck j already has some value, this loop may not even run. Serious problems develop when the uninitialized variable is a pointer. Run-time errors can also be created as a result of bugs in the compiler itself. Although these are covered later in the book, let me give you an example now. int i,j = 0; for (i = 0; i < 5; i++) { j = (j < 3) ? j++ }
: 0;
// j
never incremented!
This is completely valid and legal code. However, because of a bug in the Visual C++ compiler, the variable j never gets incremented three times, as expected. Although compiler bugs are probably the least common cause of problems, they do exist and should be considered when valid code doesn't seem to work.
< previous page
page_5
next page >
< previous page
page_6
next page > Page 6
Logic and Design Errors As stated earlier, logic and design errors are difficult to correct. To begin, you must recognize that the software under development may contain contradictory requirements or design goals. For example, suppose you're developing an inventory-control software package for a client. The accounting department wants to see a report of all inventory marked as "Received" for the current month in order to pay for the goods within, say, 30 days. However, another department must inspect all incoming goods to ensure that nothing is missing or damaged. If the inspection department holds an item longer than 30 days, and your software doesn't have any way of handling such a case, inventory begins to back up, and vendors start demanding payment. In situations like these, it is vital that the person or persons performing the system analysis understand the business and its rules. It has been my experience that a lot of logic bugs are the result of "incomplete if statements" that is, the developer creates an if statement with no matching else block. This can occur as a result of some particularly nasty business rule or complicated procedure. The tendency is to focus on writing code to handle such a case while forgetting to write code to process the data when it is something else (that is, the else portion). A classic example is a date-handler. You want your code to handle leap years, so you write a code that says "if year is divisible by 4, then could be a leap year." But that isn't exactly right you have to check if the year is divisible by 400, too. Simple oversights like this can lead to bugs downstream. De Morgan's Theorem Logic errors in programming can also be caused by not using De Morgan's theorem. This theorem has to do with the way you perform Boolean operations on two negative variables or logical clauses and basically states the following (using standard logic notation) !p AND !q = !p OR !q !p OR !q = !p AND !q
< previous page
page_6
next page >
< previous page
page_7
next page > Page 7
In general, De Morgan's theorem means if you're ANDing two NOT conditions, you must replace the AND with an OR; if you're ORing two NOT conditions, you must replace the OR with an AND. Let's look at an example using C.
int x = 1; int y = 2; if(!(x == 1) && !(y == 3)) printf("The right answer!"); else printf("The wrong answer!");
If you run this code, the string "The wrong answer!" will be printed, even though x is really and truly 1, and y is really and truly not 3. Using De Morgan's theorem, the expression must be changed to the following if(!((x == 1) && (y == 3 ))) printf("The right answer!"); else printf("The wrong answer!");
Now the string "The right answer!" will be printed. Now for a more realistic example. Suppose your application is monitoring a process in a chemical plant; if the temperature is not greater than 150 degrees and the pressure is not less than 3 atmospheres, the plant will become unsafe. Look at this code int temp = 175; // assume analog-to-digital sensors report int pressure = 5; // these conditions, which make the plant unsafe if(!(temp > 150) && !(pressure < 3)) PrintAlarm("The plant is unsafe!\n"); else PrintAlarm("The plant is safe.\n");
Here, the first part of the test is true the temperature is greater than 150, but the second part is false the pressure is actually greater than 3. But if you step this code with a debugger, you'll find the else portion is called,
< previous page
page_7
next page >
< previous page
page_8
next page > Page 8
reporting that the plant is safe when it truly is not. Again, using De Morgan's theorem, the correct code becomes if(!(temp > 150) || !(pressure < 3) PrintAlarm("The plant is unsafe!\n"); else PrintAlarm("The plant is safe.\n");
This is an easy mistake to make, and no debugger in the world will find it for you. Try to get in the habit of inspecting compound logic conditions that involve double negatives. Minimizing the Bug Count To write a solid, bug-free program, you must start by reducing the possibility of introducing bugs. From a tactical point of view, there are a few common-sense things you can do to minimize bugs, or at least make them easier to find. In this section, I'll discuss some simple changes to work habits that can reduce the introduction of bugs. Naming Conventions and Commenting Styles Standardize a variable-naming convention, above and beyond the Hungarian system. For example, besides prefixing all your CString variables with cs, include more information about what the variable does. The variable csNameRemoteDept tells you a lot more than just csDept. The first cues you that not only is this a Department name, but there are explicit remote departments, too. If you saw this variable for the first time, you'd suspect the source code would contain a variable called csNameLocalDept, or something similar. Sprinkle meaningful comments throughout your code. Using the previous example, you might want to mention something about Remote versus Local department names in your comments. Every developer tends to craft his or her own commenting style that evolves over time. Give yourself a reality check and ask a qualified co-worker or friend to critique a sample of your comments.
< previous page
page_8
next page >
< previous page
page_9
next page > Page 9
Customize Your Workbench It may or may not seem obvious to you, but customizing the Visual C++ workbench can help you prevent bugs, if only in subtle ways. For example, you can color-code variables that appear in your source code window (and others). In this way you can quickly and easily spot things visually, which can help you find or prevent bugs. To customize colors, start VC++ and choose "Options . . ." under "Tools" on the menu bar, click on the '"Format'' tab, and the dialog box shown in Figure 1.1 appears.
Figure 1.1 The Formatting Options Screen Select "Source Windows" from the "Category" list box. Scroll down through the "Colors" list box to find the item you want to color-code, and then pick the foreground and background colors. Don't get carried away too much color can cause eyestrain. As you can see from Figure 1.1, you can also set fonts and point sizes to please your individual taste. The point is, get comfortable and consistent in your workspace you may be there a while!
< previous page
page_9
next page >
< previous page
page_10
next page > Page 10
Array Errors You should use arrays only when appropriate and exercise caution with the index. Arrays can be the source of insidious bugs in an MFCbased program like those created with Visual C++. MFC is written in C++, and C++ is nothing more than a superset of C. As you may know, C really doesn't have arrays in the classical sense. If you define an array of 10 integers, the compiler gives you an address and reserves memory for 10 integers starting at that address. So far, no problem. But bounds checking is not enforced, and thus you can index any element of the array! For example int iAges[10]; // declare an array of 10 integers int k = 0, index = 0; // // do something else here . . . // k = iAges[index]; // no problem UNLESS index becomes > 9
In this over-simplified example, if index should ever become greater than nine as a result of being incremented elsewhere in your program, the variable k will contain garbage. By garbage, I mean whatever is stored at the address pointed to by iAges[index]. This is not to suggest you should never use arrays, but you must exercise the greatest caution with the index into the array. For small, common data components, such as the months of the year (there are only 12), arrays are just fine. But for large data domains, especially those in which the number of elements you'll need to store are not known "up front," you should consider replacing arrays with some other data structure, such as a linked list. There is now a CList class, implemented as a template. While using a CList object entails a little more work (that is, you must use a variable of type POSITION to index the list), there are plenty of member functions to help you out. Some of the advantages of using a CList object over an array are as follows. The list can be dynamically resized. New items can be inserted at the head, tail, or middle of the list. There are many member functions available for manipulating the list. Consult the Visual C++ documentation for class CList for more information on the nuances of this class.
< previous page
page_10
next page >
< previous page
page_11
next page > Page 11
Minimizing the Bug Cost The most cost-effective way to write a solid, bug-free program is to prevent bugs or to find and correct them as early in the process as possible. These issues are closely related to data integrity, system testing, and the coding skills of the development staff. In this section, I'll explore the techniques and the processes of preventing, or at least minimizing, the introduction of bugs into a software product release. Design, Analysis, and Review To minimize bugs, everyone involved must understand the nature of the software under development. Good project management skills, interaction with and feedback from the customer, and well-written specifications will go a long way toward creating a robust application free of bugs. Unfortunately, management often takes the short view in this regard they see their programmers "wasting time" in meetings when they "should" be coding. As the old saying goes, "There is never enough time to do it right, but there is always enough time to do it over." The best defense here is a strong personality willing to stand up to this myopic perspective. This task falls to the project leader, and it requires tact, diplomacy, and assertiveness to make sure design and analysis are performed properly. In addition, standard techniques exist that can help ensure that project goals are clearly understood and that the best solution has been reached. Examples of such techniques include database normalization rules and software metrics.
< previous page
page_11
next page >
< previous page
page_12
next page > Page 12
Figure 1.2 shows the cost of errors as a project moves through the development cycle. As you can see, it is much cheaper to fix errors earlier than later.
Figure 1.2 The Cost of Errors in Software Development Rapid Prototyping Try to develop a rough prototype of the project early in the design cycle, and show it to the client early on. This prototype needn't actually work, or even do anything; it simply serves as a feedback model to make sure the project needs are understood and that the customer is satisfied with the preliminary interface. The prototype will most likely be revised several times as input is collected from the customer. There are several pitfalls to watch for when using rapid prototyping techniques. One of these is to let minor details become the focus of a meeting. For example, if your prototype has a dialog box that uses check boxes and the client indicates a preference for radio buttons, don't let the meeting devolve into a discussion of the virtues of radio buttons versus check boxes. This is a minor detail the larger issue is to make sure that the purpose of the dialog box is appropriate. Another danger of rapid prototyping is that the customer gets a false sense of progress. The project leader must exert gentle pressure when this happens and remind the customer of the nature and purpose of the prototype. In a way, the project leader should strive to lower the expectations of the client. Periodically remind the client that you're still in the design and
< previous page
page_12
next page >
< previous page
page_13
next page > Page 13
analysis phase of the project and that the client's reward for patience will be a software tool that meets or exceeds their needs. Point out that any time "lost" on the front end will be more than made up for on the back end of the project because there will be fewer bugs to fix, design changes to implement, and so forth. Remember, the person you're dealing with on the behalf of the customer is, like you, accountable to someone else, so try to develop a sense of partnership. Give this person ammunition to fight the battles likely to be held with superiors, and a win-win situation will be created. Testing More will be said about this subject later in the book, particularly the automating of test scripts and procedures. However, testing should not be deferred till the end of the project but incorporated into the overall work flow. If you perform even rudimentary testing on your code as you work, you'll reap two rewards. 1. There will be fewer bugs to fix at the end of the project. 2. You can safely build on top of the debugged code. Once you know you've got a module working properly (especially an important one), you can be fairly confident that it will not be the source of errors downstream. For example, consider a database application. You might want a module whose only function is to accept a table name, and open it for a given access type (read, write, and so on). Now, it makes sense you'd not only want to get this part working first but you'd also want it working properly, since all your subsequent code will depend on being able to open a database table. Here, testing as you code in the only way to go. While we're on the subject of testing, don't overlook the need for maintenance testing. Software maintenance activities can account for as much as two-thirds of the total cost of production. One type of this activity is called regression testing. This is the process of validating the modified components of the software to make sure no new errors have been introduced to the (previously debugged) code. A key difference between regression testing and development testing is that regression testing supports reusable tests: you can elect to rerun all your test scripts. This has the benefit of making sure your code is stable, but it does cost time and resources. An alternative approach is to use selective testing. In this type of testing, you run only a subset of the entire test script(s) against the modified program.
< previous page
page_13
next page >
< previous page
page_14
next page > Page 14
Project Management Good project management techniques can prevent some software errors. For large projects, especially those being developed on a network, a Code Management System (CMS) is essential. This type of system will be covered in more detail later in this book, but for now, suffice it to say that a CMS is much like a librarian, except it tracks who has a file/ module instead of books. Recovery points can thus be established, as well as the knowledge of who changed what, and when. Good project management includes access control, critical path management, disaster planning and recovery, personnel scheduling, reporting, and lots of other things that contribute to the delivery of software with a minimum of errors. Data Domains As part of your design effort, you will naturally define data domains for your application. Simply stated, a data domain defines the type and range of some data. For example, suppose your application needs to track an employee's age. The data domain for this would probably be of type INTEGER and would have a range of say, 1870 (assuming the company does not hire anyone younger than 18 and has mandatory retirement at age 70). By gathering all the data domains and recording them in your design document, you directly affect not only what you'll code but how you'll code it. With the data domain being a reflection of the enterprise's business rule(s), your application can automatically reject invalid data values, which in turn obviates error propagation. Error propagation is like a linked list one error causes another one somewhere else in the program. (The easiest way to kill a snake is to cut off its head. . . .) Data Input While we're on the subject of data, be aware that the data input you solicit from the user can be a source of software failure. For example, suppose you build a Visual C++ application that allows the user to save data to a file. Somewhere, you'll prompt the user to supply a file/project name, tack on an extension, and serialize your CDocument. You just can't accept any filename, especially if you later parse this for subsequent operations. For example, if you allow the user to enter something like "*.*" as a filename, and later give the user the opportunity to delete this "file," the entire disk will have
< previous page
page_14
next page >
< previous page
page_15
next page > Page 15
been wiped out! You just can't assume the user is going to give you something "safe." Related to this issue is the concept of forcing the user to select data instead of entering it. While this isn't practical in all cases, for those input values with small data domains it can be very useful. For example, instead of having the users enter an abbreviation for a U.S. state via an edit box, give them a combo or list box and let them pick the state. Now, you have only to validate the abbreviations once when you add them to the combo/list box. I've found this approach to be especially useful when asking the user to supply a date. Dates can get pretty complicated, what with all the various combinations, and then there are leap years. But there are still only 12 months, and the most a month can have is 31 days. I build a "date gadget" using spin controls for the day and year, and a combo box for the month. This is shown in Figure 1.3.
Figure 1.3 A Date Input Gadget An array, maxdays[], holds the maximum number of days for each month, with a special test for February.
if((nYear % 4 == 0 && nYear % 100 != 0) || nYear % 400 == 0) maxdays[1] = 29; // it's a leap year else maxdays[1] = 28; // not a leap year
Peer Review The old adage "two heads are better than one" is appropriate when it comes to devising a plan of attack. Peer review is simply the process of having your peers review your design/plan. Generally, you'll take a "first cut" at a plan and divide the tasks among different work groups. Once each group finishes its plan, consolidate the plans, and then circulate a master plan among the work groups. This will help them spot flaws in both their own plans and those of the other groups. Repetition of this process will help eliminate flaws until you're left with a viable, working strategy.
< previous page
page_15
next page >
< previous page
page_16
next page > Page 16
Intelligent Data The use of intelligent data, data that is composed of other data elements should be eliminated early in the design cycle. For example, consider a student enrollment system in which a field called COURSE_ID might look like the following.
MAJH201
Here, the first two characters, MA, mean this is a math course. The next two, JH, indicate that the course is taught in Jones Hall, and the last three characters, 201, indicate the room number. If such fields go into a database, and the course changes venue, you've got a real nightmare on your hands updating them! Chapter Summary This chapter has given you a quick introduction to the various types of bugs that can affect a project, as well as a small crash course in project management. You've seen that some errors are unavoidable, because of the nature of binary numbers. Getting the project off on a solid foundation is the single most important thing you can do when developing bug-free software. All the debugging tools and techniques won't be worth a hill of beans if errors or inconsistencies have been "designed into" the system. Be generous with spending time and resources at the beginning of a project you may have to live with it for a long, long time.
< previous page
page_16
next page >
< previous page
page_17
next page > Page 17
Chapter 2 The Win32 Memory Management System It's been estimated that about 90 percent of all computer bugs are somehow related to a problem with memory. With the advent of 32-bit Windows, the contemporary software landscape has changed dramatically from past versions. Developers can get a leg up on debugging by first getting a good understanding of how Win32 memory is managed, as well as the various APIs available. Memory errors usually occur when one of the following conditions exist. Memory is allocated but not freed. Memory is overwritten. An attempt is made to allocate memory already allocated. An attempt is made to free memory already freed or never allocated. In the old days, Windows gave you 16 chunks of memory, each one 64Kb in size. These were accessed through a segment (4 bits) and an offset (16 bits) for an apparent 20-bit address space. With the 32-bit version, each process gets its own 32-bit address space of virtual memory. With 232 memory
< previous page
page_17
next page >
< previous page
page_18
next page > Page 18
locations, that works out to about 4Gb, which is highly impractical for even midrange and mainframe computers, let alone the humble PC. To achieve utilization of this address space, the computer's hard drive is used as if it were part of the memory. Free space is divided into pages, and parts of the program are swapped out between the actual physical memory and the disk for each process. However, you don't get all those nice fat 4Gb for yourself. Only the lower 2Gb (0x00 through 0x7FFFFFFF) are available for your use. The operating system kernel uses the upper 2Gb (0x80000000 through 0xFFFFFFFF). When a process is loaded, it tries to maximize as much of the physical memory as possible. But for big programs on machines without much memory, it must swap between the disk and memory. So how does the kernel know where to find the program page it needs? It uses a data structure called a page map that converts the virtual addresses into actual, physical addresses. When the program needs a page that is not already in physical memory, it must fetch it from the disk. This generates what is called a page fault. While a page fault isn't a bug per se, too many of them can slow your program down. Page faults will be discussed later in this chapter when we look at virtual memory functions. Virtual Address Space The amount of memory available to your process is the sum of the host machine's physical memory and its free disk space. The virtual memory addresses are mapped to the physical addresses by using a special disk file called the paging file. This mapping occurs using a discrete size dependent on the host computer. (The x86 family of computers uses a page size of 4Kb.) The process of moving pages of physical memory to the paging file is called swapping: when a page is swapped in from disk, the kernel updates the page maps for that process. When it needs to swap out to disk, it looks for the page that has not been used for the longest time, and sends the page out to the paging file. Pages can be in one of three states: free, reserved, or committed. A free page is one that isn't available at the moment but that can be committed or reserved. A reserved page is one that has been reserved for impending use. The process cannot access it, and it has no physical storage associated with it. For this reason, you cannot use any of the "standard" memory allocation functions [for example, malloc(), new, and so on] on it. You can, however, use the VirtualAlloc() function to reserve a page or pages. Finally, a committed page has physical storage (in memory or on disk) allocated and can be assigned properties such as read only, read-write, and so forth. There is one caveat to remember, especially when using the memory
< previous page
page_18
next page >
< previous page
page_19
next page > Page 19
allocation functions (which will be covered shortly): virtual address zero is never allocated. In this way, zero is not confused with the NULL value, which indicates an error, thus making it easy to detect a NULL pointer. Heaps As you probably already know, C and C++ have lots of functions for managing memory. These functions return, based on their prototype, either a handle or a pointer to a block of memory. The allocation occurs at run time and is managed by something called the heap. The heap's main purpose is to manage memory and address space for a process. That's quite a job, when you realize the heap has no idea what the process is going to do with the allocated block of memory. Because of heaps, you don't have to be concerned with whether a page is reserved, free, or committed; you can stay focused on building your application. And, heaps can conserve memory; you don't have to reserve a 4Kb page when you need only, say, 80 bytes. Your Visual C++ application can have several heaps of different types, but the total size limits for all heaps is about 105Mb. Type of Heaps Every process running in a 32-bit environment has at least one heap called the default heap. You can also create dynamic heaps by using the heap memory routines. Note that the type of function you use affects the source from which the memory is taken! The Win32 subsystem and the C run-time library allocate memory from the default heap. You can obtain a handle to the default heap by using the GetProcessHeap() function. Default Heap Management The default heap (and dynamic heaps, too) has a certain amount of reserved and committed pages associated with it. These pages are defined when the application is linked and is embedded in the header of the executable image. The default heap size is set at 1Mb by the linker, but you can modify this by using the /HEAP option in the link options (in Visual C++, select Settings under Project on the main menu bar, and click the Link tab). You add the
< previous page
page_19
next page >
page_20
< previous page
next page > Page 20
new heap specification to the Project Options box. The /HEAP command has the following syntax. /HEAP: (New Heap Size) /HEAP: reserve[,commit]
The reserve argument designates the total heap allocation in virtual memory, rounded up to the nearest 4 bytes. The optional commit argument is interpreted by the host operating system. Under NT, it means the amount of physical memory to allocate, which affects the space reserved in the paging file. A higher commit value can save time when the calling process needs more heap space but can increase memory requirements as well as start-up time. Knowing the nature of your application can help you decide if you need to adjust the heap. Figure 2.1 shows the heap being set to 2Mb in Visual C++.
Figure 2.1 Modifying the Heap Size Dynamic Heaps As we've already learned, you create a dynamic heap when you allocate memory at run time. And, the minimum memory doesn't have to be 4,096 bytes (4Kb). The heap manager commits pages as needed to fulfill your requests. For example, if you allocate 80 bytes using a call to
< previous page
page_20
next page >
< previous page
page_21
next page > Page 21
GlobalAlloc(), the heap manager gives you 80 bytes from within its committed region. If you had previously requested 4,050 bytes, the heap manager would simply allocate another page, since 4,050 + 80 is greater than 4,096. In this case, 46 of the 80 bytes would go on the last page (4,050 + 46 = 4,096), and the other 34 bytes (80 46 = 34) would go on the next page. You still get your 80 bytes they just span two pages instead of one. Additional requests would now start incurring against the current page's 4,062 available bytes (4,096 34 = 4,062). So, if you repeatedly allocate 80 bytes (maybe for some type of line editor, for instance), the heap commits an additional page upon every 52nd request (51 * 80 = 4,080; next request is 4,080 + 80, which exceeds 4,096). Upon hitting the 52nd request, the heap manager sees there is insufficient memory committed to satisfy the request, so it commits another page in order to fulfill the request. Local and Global Memory Functions Allocating memory can be a double-edged sword. On the one hand, you want to make sure you've got memory assigned to important data structures (like arrays, strings, and so on). But, on the other hand, you can get in trouble if you don't use memory allocation routines properly. Fortunately, most of the trouble here comes from simple failures on the programmer's part, such as forgetting to free memory after it's been allocated. The debugger and other monitoring tools can help you find these types of errors. Memory is allocated by a process using the GlobalAlloc() and LocalAlloc() functions, but in the flat 32-bit address space of Win32, there is no difference between the local and global heap. The prototypes are HGLOBAL GlobalAlloc(UINT uFlags, DWORD dwBytes);
and HLOCAL LocalAlloc(UINT uFlags, UINT uBytes);
where the uFlags parameter specifies how to allocate the memory (shared, fixed, and so on), and the second parameter indicates how many bytes to allocate. For a complete rundown on the uFlags parameter, consult the Visual C++ documentation. These functions allocate memory in private (and committed) pages with read-write access. Also, other processes cannot access this memory. With these functions, you can allocate memory blocks of any size represented with 32 bits. The only limitation is the amount of available storage, which means RAM and the paging file.
< previous page
page_21
next page >
< previous page
page_22
next page > Page 22
These two functions, and the others that we'll get to later, are compatible with 16-bit segmented memory, but some of their functions and properties are unnecessary or superfluous. For example, you don't have to declare pointers using the near or far type, since both functions return a 32-bit virtual address. Note that the actual size of the memory block allocated by these functions can be greater (but not less) than the requested size. If you need to know the actual number of blocks, use the GlobalSize() and LocalSize() functions, respectively. Should the amount allocated be greater than what you asked for, you can go ahead and safely use it. Fixed versus Movable Objects By using the uFlags parameter, you can specify the memory you allocate with the previous functions as either fixed or movable. Movable memory can also be marked as discardable. When more physical storage is needed, the system can free discardable memory by using a "least recently used" algorithm. Thus, if you're writing a memory-intensive application, try to mark the memory as discardable whenever you can. Locking Memory You can prevent memory from being moved or discarded by using the GlobalLock() and LocalLock() functions, even if you marked it as movable when it was allocated. The prototype for G1obalLock() is LPVOID GlobalLock(HGLOBAL hMem);
where hMem is the address of the memory block returned by GlobalAlloc(). For LocalLock(), the prototype is LPVOID LocalLock (HLOCAL hMem);
Both of these functions lock the memory object and return a pointer to the first byte of its memory block. When memory is allocated using the GMEM_MOVEABLE or LMEM_MOVEABLE flag, the function increments a lock count (which is initially zero) associated with the memory. Memory can be unlocked by using the GlobalUnLock() and LocalUnLock() functions, which decrement the lock count by one. The only way to move locked memory is by reallocating it. This is accomplished by using the GlobalReAlloc() and LocalReAlloc() functions. These functions can increase or decrease the size,
< previous page
page_22
next page >
page_23
< previous page
next page > Page 23
and change the attributes, of the allocated memory. The global variety is prototyped as HGLOBAL GlobalReAlloc(HGLOBAL hMem, DWORD dwBytes, UINT uFlags);
where hMem is the handle to the allocated memory, dwBytes is the new size, and uFlags describes how to reallocate the memory. Should the call to GlobalReAlloc() fail, the memory is not freed, and the original handle and pointer are still valid. Freeing Memory When you're finished using allocated memory, always free it using either GlobalFree() or LocalFree(). Failure to do so can lead to some nasty bugs, especially if the piece of code that allocates it is called often (as in a loop). Eventually, you'll eat up all available memory, and your program will crash. Remember, the compiler won't give you any warning like "Hey, you allocated memory but never freed it!" It's up to you to make sure every allocation call has a matching free. Although it's covered later, Visual C++ includes a tool called STRESS.EXE, which reports available memory (from the GDI, and so forth). You can use this tool before running your program to see how much memory you have, and run it again when it ends. If the numbers don't match, you've allocated memory somewhere, but it has not been freed. Start looking . . . Heap Functions MFC includes heap functions that allow a process to create a private heap. A private heap is simply a block of one or more pages in the address space of the process that created it. The process can then use a different set of functions to manage the private heap. Setting up a private heap involves first creating the heap, then allocating memory from it, and then freeing or destroying it. The following is a quick look at these functions. A private heap is created by using the HeapCreate() function. HANDLE DWORD
HeapCreate(DWORD
flOptions,
DWORD
dwInitialSize,
dwMaximumSize);
< previous page
page_23
next page >
< previous page
page_24
next page > Page 24
The first parameter specifies the options to use when creating the heap. These are HEAP_GENERATE_EXCEPTIONS HEAP_NO_SERIALIZE
These values function as Boolean switches and can be ORed together. Setting the first value causes an exception to be raised during a failure, and is thus highly recommended. Setting the second values serializes access to the heap, which means two or more threads can access it simultaneously. If your application is not multithreaded, you can gain a slight performance increase by not specifying this flag. The second parameter to HeapCreate() determines the initial size of the heap. The last parameter specifies the maximum size of the heap. If this is nonzero, the heap cannot grow. Once the heap is safely created, the calling process can allocate memory blocks from it by using the HeapAlloc() function. But there is a potential for trouble here, because the system "steals" some of the memory from the heap to provide for its management overhead. For example, if a private heap was created of 32Kb and you tried to use HeapAlloc() to allocate a 32Kb block, you really wouldn't get the full 32Kb. So, always ask for more than you really need. In contrast, trying to allocate a very small size (that is, less than 12 bytes) returns 12. Take a look at the following code. // // get the size of a memory page from by system // SYSTEM_INFO lpSystemInfo; GetSystemInfo(&lpSystemInfo); // HANDLE hMem; // handle to the heap void* lpMem; // pointer to heap memory hMem = HeapCreate(HEAP_GENERATE_EXCEPTIONS, lpSystemInfo.dwPageSize, lpSystemInfo.dwPageSize); // // now allocate just 1 byte // lpMem = HeapAlloc(hMem, HEAP_GENERATE_EXCEPTIONS |HEAP_NO_SERIALIZE | HEAP_ZERO_MEMORY, 1);
< previous page
page_24
next page >
< previous page
page_25
next page > Page 25
DWORD dwSize; dwSize = HeapSize(hMem, HEAP_NO_SERIALIZE, lpMem); HeapFree(hMem, HEAP_NO_SERIALIZE, lpMem)
If you examine the variable dwSize returned from the call to HeapSize(), you'll find it is 12, not 1. So things are not always as they seem. Furthermore, the return value from the HeapSize() call will not include the size of the allocation minus the overhead. It just returns the size you requested, which isn't really any help at all. On the machine I'm using, a page is 4,096 bytes. I've found that if I create a heap of that size, the maximum I can allocate is 4,096 124 bytes (when the dwMaximumSize parameter is non-zero). new and delete In C, you allocated and freed memory using malloc() and free(). In C++, these functions have been replaced with the new and delete operators, respectively. new allocates fixed-size memory blocks on the heap (or free store), as in this example. char * myString = new char[128];
Here, 128 bytes have been reserved for the variable myString. If you need resizable memory blocks, you must use the C run-time functions malloc(), realloc(), and free() to manage the heap. Don't try to use realloc() on memory that has been allocated with new. Likewise, don't use free() on memory allocated with new, or use delete with memory allocated via malloc(). Or, more simply, don't mix the C calls with the C++ ones you will corrupt memory, and this will lead to bugs. Return Values Normally, new returns a pointer to the type of object allocated (for example, a pointer to char in the preceding example). If the allocation is for an array, it returns a pointer to the first element in the array. If there isn't enough memory available to satisify the new allocation request, the return value is NULL. This default behavior can be modified by writing a custom exception-handling routine. (Exceptions are covered in the next chapter.)
< previous page
page_25
next page >
< previous page
page_26
next page > Page 26
The _heapchk() Function You can use the _heapchk() function to check the heap for consistency in Windows NT applications only. The following is an example of this function. #include <malloc.h> // must include this! // int heapstatus; char *buffer; // Allocate and deallocate some memory from the default heap if((buffer = (char *)malloc( 256 )) != NULL) free(buffer); // Check heap status heapstatus = _heapchk(); switch(heapstatus) { case _HEAPOK: TRACE(" OK - heap is fine\n" ); break; case _HEAPEMPTY: TRACE(" OK - heap is empty\n" ); break; case _HEAPBADBEGIN: TRACE( "ERROR - bad start of heap\n" ); break; case _HEAPBADNODE: TRACE( "ERROR - bad node in heap\n" ); break; }
Note that the function is being used here in debug mode, as reflected in the TRACE statements. Most likely, you will just want to use the _heapchk() function during development.
< previous page
page_26
next page >
< previous page
page_27
next page > Page 27
Virtual Memory Functions There is another variety of memory-management functions that can be a source of angst to the developer. These are the virtual memory functions, which, as their name implies, allocate memory from the virtual address space. As usual, there are functions for allocating memory, freeing it, locking it, and so on. In this section we'll take a look at these functions and examine their behavior. In particular, we'll be examining the following functions. VirtualAlloc() VirtualProtect() VirtualProtectEx() VirtualAllocEx() VirtualLock() VirtualUnlock() VirtualQuery() VirtualQueryEx() VirtualFree() VirtualFreeEx() The virtual memory functions have more significance for Windows NT applications than those developed for other platforms. Before we proceed, I need to mention something that can cause problems with virtual memory allocation the guard page. Guard Pages A guard page is like a fence around a house with a big dog in the yard. But in this case, the yard is a region in memory. Attempting to access an address within a guard page raises a STATUS_GUARD_PAGE (0x80000001) exception. The operating system then clears the PAGE_GUARD flag and will not stop a subsequent attempt to access the memory in the guard page. The PAGE_GUARD flag, and some others, are set in the allocation functions VirtualAlloc(), VirtualProtect(), and VirtualProtectEx(). This flag can be used with any other page-protection flag, with the exception of the NO_ACCESS flag. Because the guard page exception occurs only once, it is a one-shot alarm. Nevertheless, it can be useful in preventing bugs, especially for applications that use large dynamic data structures, which are
< previous page
page_27
next page >
< previous page
page_28
next page > Page 28
subject to resizing (for example, as in database record retrieval). The following code example demonstrates the one-time properties of guard pages. // local variables LPVOID lpvAddr; DWORD dwSize; BOOL bLock; LPVOID commit; // amount of memory we'll allocate dwSize = 512; // try to allocate some memory lpvAddr = VirtualAlloc(NULL, dwSize, MEM_RESERVE, PAGE_NOACCESS); // if we failed . . . if(lpvAddr == NULL) fprintf(stdout, "VirtualAlloc failed on RESERVE with %ld\n", GetLastError()); // try to commit the allocated memory commit = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_READONLY|PAGE_GUARD); // if we failed . . . if(commit == NULL) fprintf(stderr, "VirtualAlloc failed on COMMIT with %ld\n", GetLastError()); else // we succeeded fprintf(stderr, "Committed %lu bytes at address %lp\n", dwSize,commit); // try to lock the committed memory bLock = VirtualLock(commit,dwSize);
< previous page
page_28
next page >
< previous page
page_29
next page > Page 29
// if we failed . . . if(!bLock) fprintf(stderr, "Cannot lock at %lp, error = %lu\n". commit, GetLastError()); else // we succeeded fprintf(stderr, "First lock set at %lp\n", commit); // try to lock the committed memory again bLock = VirtualLock(commit, dwSize); // if we failed . . . if(!bLock) fprintf(stderr, "Cannot get 2nd lock at %lp, error = %lu\n", commit, GetLastError()); else // we succeeded fprintf(stderr, "Second lock set at %lp\n", commit);
The output from this program looks something like this (the memory addresses may be different on your machine). Committed 512 bytes at address 003F0000 Cannot lock at 003F0000, error = 0x80000001 Second lock set at 003F0000
Here, the first attempt to lock the memory fails, raising a STATUS_GUARD_PAGE exception. The second attempt succeeds, because the memory block's guard page protection has been removed by the first attempt. Now that we've got that out of the way, let's move on to the virtual memory functions themselves. VirtualAlloc() The VirtualAlloc() function reserves or commits a region of pages in the virtual address space of the process that called it, and initializes the allocated memory to zero. The prototype is LPVOID VirtualAlloc(LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect);
< previous page
page_29
next page >
< previous page
page_30
next page > Page 30
The parameters are the starting virtual address, size, type of allocation, and protection flags, respectively. An example can be seen in the preceding section on guard pages. VirtualProtect() This function allows a process to change the access protection of any committed page in the address space of the process. The prototype is BOOL VirtualProtect(LPVOID lpAddress, DWORD dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
Parameter lpAddress points to the base address of the region whose pages are to be changed. All the pages in this region must have been allocated using a single call to VirtualAlloc() or VirtualAllocEx(). The second parameter is the size of the region to be changed. The affected pages includes all pages that contain one or more bytes in the range indicated by the lpAddress parameter up to the address lpAddress+dwSize. Thus, if this combination (of address + size) ends up straddling two pages, both pages have their protection changed. This function can help eliminate bugs by preventing accidental changes to data. For example, a process could set some pages to readwrite, load in some necessary data that you wouldn't want changed, and then change the protection to read-only to prevent changes. VirtualProtectEx() This virtual memory function is identical to VirtualProtect() except for one important difference it changes the access protection on a specified process, not just the one that called it. The prototype is BOOL VirtualProtectEx(HANDLE hProcess, LPVOID lpAddress, DWORD dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
The hProcess parameter is the handle of the process to be changed. This is set in the PROCESS_INFORMATION structure and is filled in by the
< previous page
page_30
next page >
< previous page
page_31
next page > Page 31
CreateProcess() function when a new process is created. The entire structure is typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId; } PROCESS_INFORMATION;
Parameters lpAddress and dwSize are the same as for the VirtualProtect() function. The flNewProtect parameter is the new protection scheme. The lpflOldProtect must point to a variable that receives the previous access protection of the first page in the specified region of pages. If this is NULL or does not point to a valid variable, the function fails. VirtualAllocEx() This function is identical in every respect to VirtualAlloc(), except that it can allocate memory in the virtual address space of another process that you specify, and not just the address space of the calling process. All allocated memory is initialized to zero; the prototype is as follows. LPVOID VirtualA1locEx( HANDLE hProcess, LPVOID lpAddress,
// process within which to allocate // desired starting address of allocation DWORD dwSize, // size, in bytes, of region to allocate DWORD flAllocationType, // type of allocation DWORD flProtect // type of access protection );
< previous page
page_31
next page >
< previous page
page_32
next page > Page 32
VirtualLock() This function locks a region in the virtual address space of the calling process into memory. With the page locked, swapping will not occur, which means no page faults will be generated. The prototype is BOOL VirtualLock( LPVOID lpAddress // address of first byte of range to lock DWORD dwsize // number of bytes in range to lock );
VirtualLock() is basically the equivalent of the Windows v3.1 function GlobalPageLock(). When would you use VirtualLock()? One example might be when you want maximum performance for some operation. Suppose you have a large lookup table used in a number-crunching exercise. By locking it in memory, you're guaranteed that it won't be swapped out, so you'll never have to wait for the disk I/O to complete. There is an interesting comment about VirtualLock() in the on-line documentation: Locking pages into memory may degrade the performance of the system by reducing the available RAM and forcing the system to swap out other critical pages to the paging file. By default, a process can lock a maximum of 30 pages. The default limit is intentionally small to avoid severe performance degradation. Applications that need to lock larger numbers of pages must first call the SetProcessWorkingSetSize function to increase their minimum and maximum working set sizes. The maximum number of pages that a process can lock is equal to the number of pages in its minimum working set minus a small overhead. This suggests that SetProcessWorkingSetSize() and VirtualLock() have linked functionality. You may wish to experiment with adjusting the working set for your application to see if you can gain some advantage there.
< previous page
page_32
next page >
< previous page
page_33
next page > Page 33
VirtualLock() This function unlocks the pages locked by a call to VirtualLock(). The prototype is BOOL VirtualUnlock( LPVOID lpAddress, // address of first byte of range DWORD dwSize // number of bytes in range );
Call VirtualUnlock() as soon as you can in order to free up memory. Memory will be unlocked automatically when the process terminates. VirtualQuery() and VirtualQueryEx() In view of the fact that each process has a 2Gb address space at its disposal, virtual memory management would be fairly difficult without some way of examining an address. The VirtualQuery() function and its cousin, VirtualQueryEx(), perform this task. Again, the difference between the two is simple: the first can query only the calling processes address space, while the second can access any process to which it has security access. Querying virtual memory is really just a matter of examining the data structure that maintains its state. Remember that virtual addresses aren't "real" in the sense they aren't tied to some actual, physical address. The prototypes for the functions are DWORD VirtualQuery( LPCVOID lpAddress, // address of region PMEMORY_BASIC_INFORMATION lpBuffer, // address of information buffer DWORD dwLength // size of buffer );
and DWORD VirtualQueryEx( HANDLE hProcess, // LPCVOID lpAddress, // PMEMORY_BASIC_INFORMATION lpBuffer, // DWORD dwLength // );
< previous page
handle of process address of region address of information buffer size of buffer
page_33
next page >
< previous page
page_34
next page > Page 34
The query information is returned into the MEMORY_BASIC_INFORMATION structure, which is prototyped as follows. typedef struct _MEMORY_BASIC_INFORMATION { // mbi PVOID BaseAddress; // base address of region VOID AllocationBase; // allocation base address DWORD AllocationProtect; // initial access protection DWORD RegionSize; // size, in bytes, of region DWORD State; // committed, reserved, free DWORD Protect; // current access protection DWORD Type; // type of pages } MEMORY_BASIC_INFORMATION;
The State member tells you if the pages are marked as committed, reserved, or free. The Protect member indicates the current access protection; the AllocationProtect member reflects the protection when the memory was allocated. VirtualFree() This function is used to release, decommit, or release and decommit a region of memory pages belonging to the calling process. It is prototyped as follows. BOOL VirtualFree( LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType );
// base address of region of committed pages // size of region // type of free operation
The lpAddress parameter points to the base address from where the freeing is to occur; the dwSize parameter is the size of this region. The dwFreeType parameter can be either MEM_RELEASE or MEM_DECOMMIT. If the MEM_RELEASE value is specified, the parameter lpAddress must be the base address returned by the VirtualAlloc() function. Likewise, when this value is specified, the dwSize parameter must be zero. When the size given by the dwSize parameter causes the region to straddle a page boundary, both pages are freed. As you would expect, releasing memory involves a lot more work than simply decommitting it. Attempting to decommit an uncommitted page will not cause VirtualFree() to fail, so you won't have to do any heavy errorchecking in this situation. But attempting to release memory without making sure the dwSize parameter is zero
< previous page
page_34
next page >
page_35
< previous page
next page > Page 35
will cause a failure. A return value of zero indicates a failure; anything else is a success. You can call GetLastError() to get more diagnostic information. When releasing memory, you need to be aware of a couple of things. For openers, all the pages must be in the same state (reserved or committed). Secondly, the entire region must be released at the same time. In other words, if just a part of the pages from the original memory region is committed, you need to call VirtualFree() to decommit the pages, and then call it again to release the entire memory region. Once the pages have been released, you cannot read or write to them without another allocation. Doing so results in an access violation. The VirtualFree() function can decommit a page that is not marked as committed without causing a problem such as an exception or access violation. A decommitted page releases its associated physical storage, whether it be in memory or in the disk paging file. If it is decommitted but not marked as released, VirtualFree() changes its state to reserved; it can then be committed again by VirtualAlloc(). Reading from or writing to a reserved page generates an access violation. VirtualFreeEx() VirtualFreeEx() is related to VirtualFree(). The main difference is that the latter one frees memory within the address space of the calling process, while VirtualFreeEx() lets you specify the process. The function is prototyped as follows. BOOL VirtualFreeEx( HANDLE hProcess, LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType );
// // // //
process within which to free memory starting address of memory region to free size, in bytes, of memory region to free type of free operation
The hProcess argument is the handle of the process that owns the memory; the other arguments are the same as for VirtualFree (). The only caveat is that you must have PROCESS_VM_OPERATION access to this process. Otherwise, the function fails.
< previous page
page_35
next page >
< previous page
page_36
next page > Page 36
Chapter Summary This chapter offers insight into how Win32 memory management works, and its potential affect on debugging. The good news is, memory bugs can usually be isolated easily with both the debugger that comes with Visual C++ and thirdparty tools. Nevertheless, the developer must take a proactive role in the use of memory, since this is generally the primary culprit when bugs rear their ugly heads in your code in the form of exceptions and violations. In the upcoming chapters, we'll be looking at how you can resolve these problems.
< previous page
page_36
next page >
< previous page
page_37
next page > Page 37
Chapter 3 The Visual C++ Debugging Environment Beginning with this chapter, we start getting down to the nitty-gritty of debugging. Here we'll examine all the software diagnostic tools, macros, and so on that will help you find and prevent bugs from cropping up. We'll also be looking at various types of anomalies. These are not necessarily bugs in the strictest sense of the word, but responses to coding errors in development. Anomalies include things like assertions, exceptions, access violations, and so forth. You'll be pleasantly surprised to learn there are a lot of weapons in the Visual C++ debugging arsenal. Let's start by looking at assertions. Assertions An assertion is somewhat difficult to define precisely, but basically it's a way of testing if a piece of code is valid. This is accomplished using the ASSERT macro, and you can (and should) be generous with its usage. This macro is valid only when you are running a debug version of your program; it's
< previous page
page_37
next page >
< previous page
page_38
next page > Page 38
ignored completely under release. Hence, unlike regular error-checking statements, it adds no overhead, since the compiler just ignores it. The basic form of the ASSERT statement is as follows. ASSERT(some condition);
For example, consider the following code segement. CWnd* pWnd = GetParent(); ASSERT(pWnd != NULL);
If you ran this, and for some reason pWnd was indeed NULL, you'd get a dialog box similar to the one shown in Figure 3.1.
Figure 3.1 The ASSERT dialog box. The assertion message box contains the name of the source file and the line number at which the assertion failed. The main causes of assertion failures are memory corruption and invalid parameters being passed to a class member or Windows API function. The ASSERT macro is generally used to check function parameters and return values. Improper use of the ASSERT macro can lead to problems when you build your program in release mode. Consider the following code snippet. char *buffer; ASSERT(buffer = (char *) calloc(20, sizeof(char))); strcpy(buffer, "Hello world!"); free(buffer);
< previous page
page_38
next page >
< previous page
page_39
next page > Page 39
This will work just fine in debug mode, but not in release. Why? Well, notice that the memory allocation call [that is, calloc()] is inside the ASSERT statement. In release mode, the call to calloc() will never be made, and this will be followed by an attempt to free memory that was never allocated. Also, the strcpy() function will copy the string "Hello world!" into a random memory location, resulting in a crash. To correct this problem, change the code as follows. char *buffer; buffer = (char *) calloc(20, sizeof(char)); ASSERT(buffer != NULL); strcpy(buffer, "Hello world!"); free(buffer);
Assertions are an important development aid if only for this one reason: they let you find problems early in the development cycle. Not only should you use ASSERT statements in code you write, but you'll most likely encounter assertions in the MFC library in the general course of developing an application. For this reason, the following section takes a look at some common assertions you're likely to encounter and offers some suggestions as to their remedy. Common MFC Assertion Errors The source code for MFC contains lots of ASSERT statements. When your code generates an assertion from the MFC library, the assertion dialog box (refer to Figure 3.1) is opened. This box reports the name of the module and the line number of the assertion. This is great now you can simply use the Visual C++ editor to open the file and go directly to the offending line. This gives you your first clue as to what is going on. Please observe that not all the source modules are in your \vc\mfc\src directory; some of the modules that have the extension .inl, for example, are in the \vc\mfc\include directory. You may wish to set a break point on the opening curly bracket of the function BOOL AFXAPI AfxAssertFailedLine(LPCSTR lpszFileName, int nLine)
which you will find in AFXASERT.CPP in your \vc\mfc\src directory. This will allow you to get a head start, so to speak, and uncover the offending line before the dialog box appears. Regardless, here are some of the most
< previous page
page_39
next page >
< previous page
page_40
next page > Page 40
common assertions you're likely to encounter. (Note: The references to line numbers are for VC++ v5.0.) FreeMemoryDebug() Found at line 67 of AFXMEM.CPP, this assertion can be caused by attempting to free memory with the wrong operator for example, using delete on memory allocated with malloc(). It can also be caused by using the global delete operator on a CObject class instead of the delete method of class CObject. Other causes include attempting to delete an object created on the frame (stack), or previously corrupted memory. CWnd::Attach() This common assertion failure is found at line 314 in WINCORE.CPP. ASSERT(m_hWnd == NULL; // only attach once, detach on destroy
This assertion is commonly caused by attempting to use the same CWnd object to subclass another window; i.e., trying to use the same window object twice. Another assertion is found at line 315. ASSERT(FromHandlePermanent(hWndNew) == NULL);
This assertion can be caused by attempting to attach a window to some other object, and the window has already been attached. Only one MFC object can be attached to a window otherwise, the message maps cannot function properly. CWnd::Create() The assertion ASSERT ((dwStyle & WS_POPUP) == 0);
at line 708 of WINCORE.CPP is caused by attempting to assign the WS_POPUP style when using the Create() function, which is not supported. Correct this by using the CreateEx() function instead. CWnd::DestroyWindow() The assertion ASSERT(pMap != NULL);
< previous page
page_40
next page >
< previous page
page_41
next page > Page 41
at line 930 of WINCORE.CPP can be caused by closing, or destroying, a window before a message pump has been activated. Check your InitInstance() function. CWnd::PreCreateWindow() At line 694 in WINCORE.CPP, the assertion ASSERT(cs.style & WS_CHILD);
is generated because the WS_CHILD style was omitted. For non-child windows, use the CFrameWnd classes instead of CWnd. CBitmapButton::DrawItem() ASSERT(m_bitmap.m_hObject != NULL);
This assert occurs at line 117 in WINBTN.CPP as a result of failing to draw a bitmapped command button before its bitmap image is loaded. For dialog boxes, this is easily handled in the OnInitDialog() function, as in this example. BOOL CSomeDlg::OnInitDialog() { m_Ok.AutoLoad(IDOK, this); m_Cancel.AutoLoad(IDCANCEL, this); m_Help.AutoLoad(IDHELPBUTTON, this); CDialog::OnInitDialog(); // // rest of function follows . . . // } // end of OnInitDialog()
CListBox::DrawItem(), CListBox::MeasureItem(), and CListBox::CompareItem() All three of these assertions occur at the same place and mostly for the same reason. These assertions occur at line 105 in WINCRTL1. CPP for owner-drawn list boxes. For these assertion failures, you must remember to override these message handlers.
< previous page
page_41
next page >
< previous page
page_42
next page > Page 42
CObject::IsKindOf() The assertion ASSERT(this != NULL);
occurs at line 43 of OBJCORE.CPP whenever IsKindOf() is called for an invalid CWnd-derived object. This means the object was not initialized (for example, a NULL pointer), or that memory was corrupted previously. void AFXAPI DDX_Control() The assertion ASSERT(!pDX->m_bSaveAndValidate);
occurs at line 575 of DLGDATA.CPP as a result of calling UpdateData() or UpdateData(TRUE) without calling UpdateData (FALSE) at least once first. PrepareCtrl(int nIDC) A common assertion for controls is found in DLGDATA.CPP HWND CDataExchange::PrepareCtrl(int nIDC)
The full details are HWND hWndCtrl; m_pDlgWnd->GetDlgItem(nIDC, &hWndCtrl); if (hWndCtrl == NULL) { TRACE1("Error: no data exchange control with ID 0x%04X.\n", nIDC); ASSERT(FALSE); AfxThrowNotSupportedException(); }
The most common cause is deleting a control associated with a data exchange variable(s).
< previous page
page_42
next page >
< previous page
page_43
next page > Page 43
Void AFXAPI DDX_Radio() This method is found in DLGDATA.CPP and is associated with radio buttons. The assertions are ASSERT (::GetWindowLong(hWndCtrl, GWL_STYLE) & WS_GROUP); ASSERT(value == -1); // only set once
This can be caused if the call is made to a radio button other than the first one in a group, or if the call is made more than once for any button in the group. Only the first button should have the WS_GROUP window style. Using Class Wizard, the only way to assign member variables to radio buttons other than the first one in a group is to first assign them the WS_GROUP property, then assign the variables, and then remove the WS_GROUP style. If you forget to remove this style, you'll get the assertions. CEditView::PaginateTo() The assertion ASSERT(nPageSave > 1);
which is in module VIEWEDIT.CPP, is generally caused by overriding your view's OnPrepareDC() without first calling the base class's OnPrepareDC(). It's best to set the number of pages to print in OnPreparePrinting() or OnBeginPrinting(). AFXAPI AfxMessageBox() This assertion is found in the following code from APPUI.CPP. if (!string.LoadString(nIDPrompt)) { TRACE1("Error: failed to load message box prompt string 0x%04x.\n",nIDPrompt); ASSERT(FALSE); }
The problem can usually be traced to one of two causes: 1. The string resource doesn't exist. 2. The resource is incorrectly referenced (that is, a bad number, and so on).
< previous page
page_43
next page >
< previous page
page_44
next page > Page 44
CDC::SelectObject() There are several similar assertions for each type of object (pen, brush, and so forth). The general form of the assertion(s) is ASSERT(m_hDC != NULL);
This can be caused by attempting to use the object in question without first calling GetDC(), CreateDC(), or Attach(). Essentially, the assertion is saying it doesn't have a device context. CGdiObject::FromHandle() The assertion in this method, found in WINDGI.CPP, is ASSERT (pObject == NULL || pObject->m_hObject == h);
Creating an object before deleting a previous one can cause this assertion for example, by calling CreateFontIndirect() twice without calling DeleteObject() between the calls. CGdiObject::Attach() You'll find this in WINDGI.CPP right after the method FromHandle(). The assertion is ASSERT(m_hObject == NULL);
This assertion can be caused by not selecting drawing objects (pens, brushes, and so on) out of the device context that have been selected in to it [as with SelectObject(), and so forth]. CString::GetBufferSetLength() The assertion ASSERT(nNewLength >= 0);
is found in this method in module STRCORE.CPP. Class CString is notorious for being finicky. One cause of this assertion can be using GetWindowText(CString&) for a window that has no title. If the window is a combo box (remember, all controls are child windows derived from CWnd), GetWindowTextLength() returns 1 for "Drop List" combo boxes. Try using CWnd:: GetWindowText(LPSTR, int max) instead.
< previous page
page_44
next page >
< previous page
page_45
next page > Page 45
CScrollView::SetScrollSizes() The assertion ASSERT(sizeTotal.cx >= 0 && sizeTotal.cy >= 0);
is found in module VIEWSCRL.CPP and appears mostly when you are using CFormView and CScrollView. The cause is improper initialization of the scroll sizes, or forgetting to pass the second parameter. AFXAPI AfxFormatStrings() There is an assertion in this method, found in APPHELP.CPP, caused by a failure in calling LoadString(). A frequent cause of this assertion is not including required resources, such as those for print preview. CTime::CTime() This method, which is in TIMECORE.CPP, has several asserts. The entire method is CTime::CTime(int nYear, int nMonth, int nDay, int nHour, int nMin, int nSec, int nDST { struct tm atm; atm.tm_sec = nSec; atm.tm_min = nMin; atm.tm_hour = nHour; ASSERT(nDay >= 1 && nDay <= 31); atm.tm_mDay = nDay; ASSERT(nMonth >= 1 && nMonth <= 12); atm.tm_mon = nMonth - 1; // tm_mon is 0 based ASSERT(nYear >= 1900); atm.tm_year = nYear - 1900; // tm_year is 1900 based atm.tm_isdst = nDST; m_time = mktime(&atm); ASSERT(m_time != -1); // indicates an illegal input time }
The main villain here is the last assert, caused by mktime() returning -1. The other asserts before this check only if the parameters are valid but don't catch all possible bad dates or times (for example, 30-Feb-1999). You
< previous page
page_45
next page >
< previous page
page_46
next page > Page 46
should build this sort of date-checking into your program before calling CTime(). CStdioFile::Open() This method, in FILETXT.CPP, is full of assertions. These trap missing filenames, filenames that are not NULL-terminated, and so on. CMenu::Attach(HMENU()) The assert in this method, found in WINMENU.CPP, is ASSERT(m_hMenu == NULL);
This can be caused by doing two calls to LoadMenu() without calling DestroyMenu() between them. That's it for the most common assertions. Of course, there are many more, but the preceding material gives you a feel for what to look for when they do occur, and how to avoid them. The bottom line: it's better to deal with an assertion during development than have a user report it in production! Access Violations Another type of exception is the access violation, which occurs whenever a thread attempts to read or write data at an address that is not accessible to the process. There are several general causes. Recall that in Win32 you have a 32-bit address space, or 4Gb. But only 2Gb of that is accessible by your application. Attempting to access the system's 2Gb will result in an access violation. Also, attempting to read or write to your 2Gb using a bad or NULL pointer will cause this exception. Not only that, but attempting to access a virtual address that has been marked as READONLY or NOACCESS will generate this exception. If your program runs without exceptions under the debugger, but you get an access violation when you run it as a stand-alone release program, you can use just-in-time debugging. This is covered in Chapter 4, but basically, it is a way to start the debugger while running your application. You can also use the Call Stack window to check for corrupted data as the cause of the access violation. A likely culprit is a bogus value being passed to a function. With the debugger, you can set a breakpoint at some location just before the access violation, and examine the data at that point. You can then step
< previous page
page_46
next page >
< previous page
page_47
next page > Page 47
toward the location where the fault occurs, and use the Watch window to keep an eye on your parameters. If you're able to identify some action that leads to the violation, such as clicking a button, you can set a breakpoint between the action (for instance, the button click) and the access violation. You can then examine the state of your process just before the point where it nosedives into chaos. VERIFY This routine is similar to ASSERT, except that it tests the condition in both the debug and release environments. The main difference is that VERIFY prints and exits (if necessary) only under debug. In release mode, the expression is executed but not checked for validity. VERIFY is frequently used to wrap function calls that return a pointer. The following section takes a look at two common verifications you're likely to encounter and offers recommendations on the most effective use of them. CObject::AssertValid() This function is used to see if the associated object is internally valid. The prototype is virtual void AssertValid() const;
The definition of what is valid depends on the object's class. As a general rule, you should use this function for ''high-level" validation. For example, if an object contains pointers to other objects, you should make sure they're not NULL but don't perform validity checks on the objects themselves. You need to override AssertValid() when writing a new class in order to provide diagnostic services for the class. Your new diagnostic functions should not throw exceptions, but use ASSERT to detect invalid data. Here is an example of AssertValid() void CMyDates::AssertValid() const { CObject::AssertValid(); ASSERT(m_Month > 0); ASSERT(m_Month < 13);
// keep month in range 1-12
}
< previous page
page_47
next page >
< previous page
page_48
next page > Page 48
The ASSERT_VALID Macro The Microsoft Foundation Class library uses the ASSERT_VALID macro to force a call to an object's AssertValid() member function. Generally speaking, whenever a function is expecting a valid CObject or pointer to a CObject as a parameter, it should use this macro to validate the object. This macro is called only when in the debug environment. The following is an example. CMyDates* pMonth = GetMonth(); ASSERT_VALID(pMonth);
That's a wrap on debugging routines and macros. This section has laid the foundation for exploring the debugging environment in closer detail. The Debugging Environment As you know, there are two kinds of executables you can create with Visual C++: a debug version and a release version. Each of these versions uses different DLLs, and only the release version can be distributed, owing to licensing restrictions on the debug DLLs. When you create a project in Visual C++, the Developer Studio starts you off by building a debug version of your program. By default, no optimizations options are turned on, but you can change this by modifying your settings (under Project on the main menu bar). However, optimization can make debugging an application more difficult and should not be used. Debug versus Release There are considerable differences between the debug and release versions of a Visual C++-generated application. If you've worked with the Visual C++ compiler before, you may have had the unsettling experience of discovering that your program works just fine in debug, but fails in release. This problem is generally caused by access violations and can generally be traced to one of the following factors. The Heap Compilation Pointers Optimizations
< previous page
page_48
next page >
< previous page
page_49
next page > Page 49
Before we go into these topics, a brief word is in order regarding the Trace tool. This is a debugging utility you get with Visual C++. You'll find it in the \bin directory in your \vc installation directory; it's called TRACER.EXE. Double click it, and you'll see the dialog box shown in Figure 3.2. To turn on tracing, make sure the "Enable tracing" check box is checked. This lets you embed statements similar to the printf() function in C. The contents of the TRACE statement will appear in your output window when you run the Visual C++ debugger. In the following sections, you'll see several references to Trace in the code examples.
Figure 3.2 The Trace Dialog Box The Heap This factor is the cause of about 90 percent of the problems you'll encounter. In the debug version, memory is allocated differently from the release version. The debug memory allocator places guard bytes around allocated memory. These guard bytes serve to protect the allocated memory from an overwrite (access violation), which is not available under the release version. So how do you determine if the guard bytes have been changed? First, you'll need to build a debug version. Go to the start of your application's InitInstance() method and add the following code. afxMemDF |= checkAlwaysMemDF;
This action will force a call to the AfxCheckMemory() function every time a call to new or delete is made. This will also have the unpleasant side effect of slowing down your program, so you may choose not to set the afxMemDF
< previous page
page_49
next page >
< previous page
page_50
next page > Page 50
flag in your InitInstance() method. Instead, you can make explicit calls to AfxCheckMemory() as needed. For example ASSERT(AfxCheckMemory()); CallSomeFunctions(); DoMoreStuffEtc(); ASSERT(AfxCheckMemory());
// your code here // your code here
If the first ASSERT succeeds but not the second, you know the problem is somewhere in between. Move the first ASSERT closer to the second one until you find the problem. If an overwrite does occur, it will generate a Trace message that looks something like Damage Occurred! Block 0x5fff
Well, that's a start. At byte 0x5fff in the memory block you allocated, something bad happened. You'll need to step through your code to find the exact point at which the damage occurred. Compilation Another cause of a release mode failure is the compiler itself. Much of the way MFC works changes under release, as do many of the macros. For example, the ASSERT macro is commented out in release, so if you've put some important code (like a call to allocate memory) inside an ASSERT block, you'll get a failure. Also, a different memory allocator is used in debug than in release. Pointer Support When you build a release version of your application, the guard bytes are removed. Pointers that have not been properly initialized are more likely to point to uninitialized memory than the debug information that was added in the debug version. Optimizations By default, a release build is optimized for speed and size, and this can cause the compiler to generate unexpected code. And, some MFC routines are defined as inline, which can also give rise to problems. While optimization is generally the least cause of a release-build problem, it does happen. Try
< previous page
page_50
next page >
< previous page
page_51
next page > Page 51
turning off the optimizations in the build settings. You can also try searching the help file for a solution (see "Optimization Problem"). The Map File Another weapon you can use in the war on bugs is the linker's .map file. You specify this option with the check box Generate mapfile on the Link tab of the Project Settings dialog box. The map file is a plain, ASCII text file that contains the following information. The module name that is, the base name of the file. A timestamp from the program file header (not from the system clock). A list of all the groups in the program, with their start address, length, name, and class. A list of public symbols with their addresses, name, and the object file in which they're defined. The program entry point. A list of any fixups. The fact that the map file will give you all the addresses is a big help. I recently changed a dialog box in a project from having owner-drawn buttons to non-owner-drawn, and I deleted the bitmaps. When I ran the application, I got an ASSERT failure in a completely different part of the program. But, I had the address, so I rebuilt it using a map file. I found the address and saw my program was failing in a call to DrawState(). I then realized I shouldn't have deleted those button bitmaps, as they were used by more than one dialog box! Without the map file, it would have taken hours and hours to isolate this bug. C Run-Time Library Support A little-known component of Visual C++ is the Debug Run Time Library. Using this library, you can step into run-time functions with the debugger. This debugging tool is probably a best-kept secret, since by default, it is turned off. Once you switch it on, it offers a lot of useful features like checking for memory overwrites, memory leaks, attempting to access freed memory, and so on. There are also reporting macros for generating error reports, and many of the Debug Run Time Library's features are extensible. You'll
< previous page
page_51
next page >
< previous page
page_52
next page > Page 52
find the C run-time library useful for non-Windows applications built with Visual C++, such as console applications. A lot of the heap-checking tools in the debug version of the C run-time library have been removed from MFC. To use these, your application must be linked with a debug version of the library. This library also contains powerful functions, as shown by the following list of examples. Debug versions of malloc(), free(), calloc(), realloc(), new, and delete. _CrtDbgReport and _CrtIsValidPointer macros. _ASSERT and _RPTn that use a debug heap. Heap-monitoring functions [for instance, _CrtCheckMemory() and _CrtDumpMemoryLeaks()]. To use these routines, the _DEBUG flag must be defined, and you must include the header file CRTDBH.H. Just before #include, you need to add a #define for _CRTDBG_MAP_ALLOC. This maps the allocation routines to special versions that record the source and line of the call, which lets you home in on where a problem occurred. Once you get the debug run-time library included, you have to enable it using the _CrtSetDbgFlag() function. _CrtSetDbgFlag() This function allows an application to control how the debug heap manager tracks memory allocations by modifying the bit fields of the _crtDbgFlag flag. This flag, of type integer, is passed to the function. Table 3.1 describes the various bit fields.
< previous page
page_52
next page >
page_53
< previous page
next page > Page 53
Table 3.1 The _crtDbgFlag Bits Bit Field
Value
Description
_CRTDBG_ALLOC_MEM_DF
ON
Enable heap allocation and memory block identifiers.
OFF
Add new allocations to heap's linked list and set block type to _IGNORE_BLOCK.
ON
Call _CrtCheckMemory() at every allocation and deallocation.
OFF
_CrtCheckMemory() must be explicitly called.
ON
Include _CRT_BLOCK types in leak detection and memory state difference checks.
OFF
Memory used internally by the run-time library ignored by these operations.
ON
Freed blocks remain in heap's linked list; assign value 0xDD.
OFF
Do not keep freed blocks in heap's linked list.
ON
Automatic leak checking on exit, generate report if application failed to free all allocated memory.
OFF
No automatic leak checking on exit.
_CRTDBG_CHECK_ALWAYS_DF
_CRTDBG_CHECK_CRT_DF
_CRTDBGDELAY_FREE_MEM_DF
_CRTDBG_LEAK_CHECK_DF
_CrtDbgReport() This function generates a report with a diagnostic message and sends it to as many as three different places that you specify. It's a lot like using printf(), except that it is far more flexible because it doesn't need to be inside #ifdef statements to keep it from being called in release mode (again, _DEBUG must be defined). The prototype is int _CrtDbgReport(int reportType, const char *filename, int linenumber, const char *moduleName, const char *format [, argument] . . .);
< previous page
page_53
next page >
< previous page
page_54
next page > Page 54
Besides including CRTDBH.H, you'll need to link with one of the following libraries. LIBCD.LIB Single thread static library LIBCMTD.LIB Multithread static library MSVCRTD.LIB Import library for MSVCRTD.DLL The return value is 0 if no errors occur, or 1 if an error occurs. The only exception is when the report's destination is a debug message window and the user clicks the Retry button. The return value in this case is 1. If the Abort button is clicked, this function aborts and does not return a value. The parameter reportType can be one of three values (these can be ORed together): _CRT_WARN Warnings, messages, and information that do not require immediate attention. _CRT_ERROR Errors, unrecoverable problems, and issues that require immediate attention. _CRT_ASSERT Assertion failures (asserted expressions that evaluate to FALSE). The filename is a pointer to the name of the source file in which the report occurred, and linenumber is the line number in the source file in which the assert/report occurred. The moduleName is the name of the .EXE or .DLL in which the assert/report occurred, and format is the format control string for your message. The last parameter, argument, is for optional substitution arguments used by the format argument. You can send the output from _CrtDbgReport() to a file, the Visual C++ debugger, or a debug message window. The destinations can be separately controlled using the _CrtSetReportMode() and _CrtSetReportFile() functions. As an example, you could set reportType to _CRT_WARN and send all warnings to a file, while sending all _CRT_ASSERT types to the Visual C++ output window. Table 3.2 lists the different report modes and the resulting behavior of _CrtDbgReport().
< previous page
page_54
next page >
page_55
< previous page
next page > Page 55
Table 3.2 _CrtDbgReport() Report Modes Report Mode
Report File
Behavior
_CRTDBG_MODE_DEBUG
N/A
Writes message to Windows OutputDebugString() API.
_CRTDBG_MODE_WINDOW
N/A
Writes to Windows MessageBox()API.
_CRTDBG_MODE_FILE
_HFILE
Writes to a file using WriteFile()API.
_CRTDBG_MODE_FILE
_CRTDBG_FILE_STDERR
Writes to stderr.
_CRTDBG_MODE_FILE
_CRTDBG_FILE_STDOUT
Writes to stdout.
When you are using report mode _CRTDBG_MODE_FILE, it's up to your application to open a valid file handle and to close it. See the WriteFile() documentation for more information. _CrtSetReportMode() This function sets the general destinations for reports generated with _CrtDbgReport(). The _CrtSetReportMode() function assigns a new report mode specified in reportMode to the report type specified in reportType and returns the previously defined report mode for reportType. The prototype is int _CrtSetReportMode(int reportType, int reportMode);
where reportType can be _CRT_WARN, _CRT_ERROR, or _CRT_ASSERT. The parameter reportMode controls the output of _CrtDbgReport(). The possible values, which can be ORed together, are as follows. _CRTDBG_MODE_DEBUG writes the message to an output debug string. _CRTDBG_MODE_FILE writes the message to a specified file handle. _CrtSetReportFile() should be called to define the specific file or stream to use as the destination. _CRTDBG_MODE_WNDW creates a message box that displays the message along with the Abort, Retry, and Ignore buttons. _CRTDBG_REPORT_MODE indicates that _CrtDbgReport() is not called and the report mode for reportType is not modified. _CrtSetReportMode() simply returns the current report mode for reportType.
< previous page
page_55
next page >
page_56
< previous page
next page > Page 56
_CrtSetReportFile() This function specifies the file or stream to be used by _CrtDbgReport() when sending output to a file. The prototype is _HFILE _CrtsetreportFile(int reportType, _HFILE reportFile);
If the call is successful, _CrtSetReportFile() returns the previous report file defined for the report type specified in reportType. If an error occurs, the report file for reportType is unchanged, and the return value is _CRTDBG_HFILE_ERROR. The parameter reportType can be _CRT_WARN, CRT_ERROR, or _CRT_ASSERT, as discussed previously. The parameter reportFile is the new report file; the values and behavior are listed in Table 3.3. Table 3.3 _CrtSetReportFile() Report Modes reportFile
Behavior
_HFILE
Output message is written to a file you specify and open.
_CRTDBG_FILE_STDERR
Output goes to stderr.
_CRTDBG_FILE_STDOUT
Output goes to stdout.
_CRTDBG_REPORT_FILE
_CrtDbgReport() is not called; return value is current reportType.
Report types can be sent to different destinations; for example, _CRT_WARN can be sent to a file, while _CRT_ASSERT can be sent to the debugger window. _CrtDumpMemoryLeaks() This function is used to determine if a memory leak has occurred since the program started; it dumps all the memory blocks from the debug heap when such a leak occurs. When _DEBUG is not defined, calls to this function are removed during preprocessing. This function is generally called at the end of program execution to confirm that all allocated memory was freed. It can be called automatically at program termination by turning on the _CRTDBG_LEAK_CHECK_DF bit field of the _crtDbgFlag flag using the _CrtSetDbgFlag () function described previously.
< previous page
page_56
next page >
< previous page
page_57
next page > Page 57
_CrtIsValidHeapPointer() This run-time function allows you to verify that a given pointer is in the local heap. The prototype is int _CrtIsValidHeapPointer(const void *userData);
where userData is the pointer to the beginning of an allocated memory block. The return value is TRUE if the pointer is in the local heap, FALSE if it is not. The return value makes this function an ideal candidate for use with the _ASSERT or _ASSERTE macros. For example _ASSERTE(_CrtIsValidHeapPointer(userData));
The following is a simple console application that demonstrates some of the C run-time library functions. All this program does is create a memory leak. [Note that the call to free(pLeak) is commented out.] #include <stdio.h> #include <malloc.h> #include void main() { _CrtMemState memState1,memStates2,memStates3; _CrtSetReportMode(_CRT_WARN,_CRTDBG_MODE_FILE); _CrtSetReportFile(_CRT_WARN,_CRTDBG_FILE_STDOUT); _CrtSetReportMode(_CRT_ERROR,_CRTDBG_MODE_FILE); _CrtSetReportFile(_CRT_ERROR,_CRTDBG_FILE_STDOUT); _CrtSetReportMode(_CRT_ASSERT,_CRTDBG_MODE_FILE); _CrtSetReportFile(_CRT_ASSERT,_CRTDBG_FILE_STDOUT); _CrtMemCheckpoint(&memStates1); void *pLeak = malloc1024); _CrtMemCheckpoint(&memStates2); // // free(pLeak); //
< previous page
page_57
next page >
page_58
< previous page
next page > Page 58
if(_CrtMemDifference(&memStates3,&memStates1, &memStates2)) _CrtMemDumpStatistics(&memStates3); _CrtDumpMemoryLeaks(); }
When you run this program, you get the output shown in Figure 3.3. (Note that your memory addresses will vary.)
Figure 3.3 Output from the Console App with a Memory Leak The _ASSERT and _ASSERTE Macros These macros are used to evaluate an expression and generate a report when the value of the expression is FALSE. Besides the necessity of defining _DEBUG and including CRTDBG.H, you need to link with one of the following libraries based on your application. LIBCD.LIB Single thread static library LIBCMTD.LIB Multithread static library MSVCRTD.LIB Import library for MSVCRTD.DLL There are some subtle differences between these macros. The _ASSERT macro just prints a simple diagnostic message, while _ASSERTE includes a string that displays the failed expression along with the diagnostic message. This lets you see the problem without looking at the source code. The _ASSERTE macro also calls _CrtDbgReport(), and if the return value is 1, the
< previous page
page_58
next page >
< previous page
page_59
next page > Page 59
debugger is launched (assuming that just-in-time debugging has been enabled). The Dump Function The CObject::Dump() function is a debugging aid that lets you display the internal state of an object in the debugger's output window. When running in debug mode, MFC will automatically call Dump() for any CObject-derived object that is not properly destroyed when the program terminates. You should thus always override Dump() for any base class you write and distribute. Just don't add a newline character at the end of the output. The overridden Dump() function calls the Dump() of its base class before it displays any data members unique to the derived class. Dump() will print the class name if your overridden class uses the IMPLEMENT_DYNAMIC or IMPLEMENT_SERIAL macros. When you call the Dump() function, you pass it a single argument of type CDumpContext; the global object afxDump is provided for this purpose. For example void CMyDate::Dump(CDumpContext &dc) const { CObject::Dump(dc); dc << "Month = " << m_Month; }
A similar example using afxDump is CMyDate myDate = new CMyDate; myDate->m_Month = 2; myDate->m_Day = 14; myDate->m_Year = 1999; // // now dump the contents // #ifdef _DEBUG afxDump << "Dumping myDate:\n"; myDate->Dump(afxDump);
< previous page
page_59
next page >
< previous page
page_60
next page > Page 60
afxDump << "\n"; #endif
Exceptions An exception is some abnormal, unexpected event that changes the normal flow of control and signals the operating system to intervene. Exceptions are raised (or thrown) as a result of some undetectable hardware or software event beyond the control of your program. Exceptions come in two flavors: hardware and software. An example of a hardware exception is divide-by-zero. Passing a bad parameter to a function, and not having sufficient memory to complete an operation, are examples of software exceptions. Exception handling allows you to respond to exceptions. Exception Handling To manage exception handling, the running process and the operating system work together to resolve the exception. Exceptions are raised, and handled, on a thread-by-thread basis. When an exception is raised, the operating system kernel finds the right handler, and passes control to it. Either the handler resolves the exception, or the process terminates. If no handler is found (that is, an unhandled exception), a predefined run-time handler function called terminate() gets called, which in turn calls abort(). You can override this by using the set_terminate() function to create a custom handler. Exceptions are trapped in your code using any one of several mechanisms, the most popular being try-catch. Your process "tries" to execute a command, and any raised exceptions are caught. Here is an example of the database class exception, CDBException.
try { dbMyDb.ExecuteSQL(lpSql); // execute an SQL command }
< previous page
page_60
next page >
< previous page
page_61
next page > Page 61
catch(CDBException* e) { AfxMessageBox(e->m_strError, MB_ICONEXCLAMATION); }
If the SQL command contained in the string lpSql fails, the reason for the exception is "caught" in the catch block, and the error message is printed out in a message box (although you could use TRACE here, too). Once the exception is caught, a process called stack unwinding occurs. Essentially, this is a process whereby local objects on the stack are destroyed a form of stack hygiene to allow the catch handler to work without terminating your program. Comparing Exception Handling: C++, MFC, and Win32 Each programming environment (C++, Win32, and MFC) supports exception handling. Each has its own particular syntax, ''costs and benefits," and other considerations, all of which will be examined and compared in this section. The C++ Handlers C++ includes built-in language support for exceptions. The C++ exception handlers are now the most popular method used to develop Visual C++ applications using MFC. The C++ handlers involve the use of the following statements. try throw catch The try block is any section of code starting with the try statement and surrounded by the curly braces. The throw statement is used to transfer control to a handler of the type specified in the throw argument. The catch handler performs the following duties.
< previous page
page_61
next page >
< previous page
page_62
next page > Page 62
Defines which exceptions should be handled. Deletes the exception object if it was dynamically created. The only way to enter a catch block is through an exception; you can never get there using a goto statement or as part of a case in a switch statement, for instance. Because the code contained in a try block might cause several different exceptions, you can have more than with one catch block. Also, try blocks can be nested. Here is an example that uses each of these handlers. try { throw CMyException(); } catch( . . .) // Handle all exceptions { // Respond to exception (in whole or part) // throw; // Pass exception to another handler }
A throw statement without an operand rethrows the exception that is currently being handled and can appear only in the catch block or in a function called from there. The object that is rethrown is the original exception object, and not a copy. There are two advantages gained from using the C++ exception handlers. You can handle exceptions of varying types. You can automatically call the object's destructor during stack unwinding for all local objects created on the stack before the exception was thrown. Please note that objects created on the heap (that is, memory allocated using the new operator) must still be destroyed (or freed) with delete.
< previous page
page_62
next page >
< previous page
page_63
next page > Page 63
Win32 Structured Exception Handling Microsoft has added several Win32 exception handling extensions to C that enable applications to gain control of events that would normally cause program termination. These are as follows. _try/_except _try/_finally Although not specifically designed for C++, these handlers work with C and C++ source files. Destructors for local objects are called if you use the /GX compiler option (that is, Enable Exception Handling). You can enhance the portability of your code by using the C++ exception handlers instead of these extensions. MFC Exception Handling The Microsoft Foundation Class library has had exception handling since version 1. The following macros are included. TRY CATCH AND_CATCH END_CATCH THROW THROW_LAST These macros originally supplied exception handling in environments that lacked intrinsic support, such as in Windows 3.1. Starting with MFC v3, the MFC exception macros have been changed to use the C++ exception handling mechanism. These macros are still supported in MFC v4, but should be considered obsolete and may be removed in future releases of MFC. MFC Exception Classes If you use the C++ exception handlers in an MFC application, you'll write handlers that use pointers to CException objects that are capable of being thrown by either your application or the MFC framework. These handlers are polymorphous; a handler for the base class type will catch the handlers of a derived type. Using the MFC exception classes, you can extend the
< previous page
page_63
next page >
page_64
< previous page
next page > Page 64
exception handling technique. The MFC exceptions are outlined in Table 3.4. The following pages offer a closer look at each of these exception classes. Table 3.4 The MFC Exceptions MFC Exception
Application
CException
The base class for all exceptions
CArchiveException
Exceptions for class CArchive
CDaoException
Exceptions for DAO database operations
CDBException
Exceptions for ODBC database operations
CMemoryException
Exception for out-of-memory
CFileException
Exceptions for file operations
CNotSupportedException
Exception for an unsupported feature
CResourceException
Exception resulting from failure to load a Windows resource
CUserException
Exception used to stop a user-initiated operation
COleException
Exception from OLE processing
COleDispatchException
Exception resulting from an error during automation
CInternetException
Exception resulting from the Internet classes
CException This is the base class from which all the others are derived. CArchiveException This exception class is used to trap the exceptions raised when you are attempting to perform a CArchive operation. Class CArchive is used to serialize data, most often from the application's CDocument class. The class includes a data member, m_cause, which indicates the cause of the exception.
< previous page
page_64
next page >
< previous page
page_65
next page > Page 65
CDaoException This class handles the exceptions thrown by using the DAO database class. These can be caught using TRY/CATCH logic, or you can throw CDaoException objects with your code using the AfxThrowDaoException() global function. DAO exceptions generally provide more information than ODBC ones. You can retrieve error information using three data members of class CDaoException. m_pErrorInfo contains a pointer to a CDaoErrorInfo object. m_nAfxDaoError contains an extended error code from the MFC DAO classes. These error codes, which have names of the form AFX_DAO_ERROR_XXX, are found in the documentation. m_scode contains an OLE SCODE from DAO, if applicable. You'll seldom need to work with this error code, as the other two will give you more information. Here is an example of using this exception class. CDaoRecordset* CSectionView::OnGetRecordset() { if (m_pSet != NULL) return m_pSet; // Recordset already allocated m_pSet = new CSectionSet(NULL); try { m_pSet->Open(); } catch(CDaoException* e) { AfxMessageBox(e->m_pErrorInfo->m_strDescription, MB_ICONEXCLAMATION); // Delete the incomplete recordset object delete m_pSet; m_pSet = NULL;
< previous page
page_65
next page >
< previous page
page_66
next page > Page 66
e->Delete(); } return m_pSet; }
CDBException This exception class is for database access using ODBC. Besides the return codes defined by the framework (which have the form AFX_SQL_ERROR_XXX), some CDBExceptions are based on ODBC return codes, which have the form SQL_ERROR_XXX. You'll find both flavors documented under the m_nRetCode data member of class CDBException. The following is an example of using this exception class. CRecordset* CSectionView::OnGetRecordset() { if (m_pSet != NULL) return m_pSet; // Recordset already allocated m_pSet = new CSectionSet(NULL); try { m_pSet->Open(); } catch(CDBException* e) { AfxMessageBox(e->m_strError,MB_ICONEXCLAMATION); // Delete the incomplete recordset object delete m_pSet; m_pSet = NULL; e->Delete(); } return m_pSet; }
< previous page
page_66
next page >
< previous page
page_67
next page > Page 67
CMemoryException This exception represents an out-of-memory condition and is used by lots of other classes (for example, for strings and collection classes). These are also thrown automatically by the new memory allocator. CFileException This class is great for finding and handling file errors. The following is a simple example. CFile fp; CFileException e; // if(!fp.Open("foo. bin", CFile::modeReadWrite | CFile::typeBinary, &e)) { AfxMessageBox("Failed to open file foo.bin.",MB_OK); // take alternative action here }
Here, an attempt was made to open a binary file for read-write. The exception is caught in the variable e. The default constructor for class CFile doesn't throw an exception, so it doesn't make sense to use TRY/CATCH. Instead, use the Open() member and test for exception conditons. The class contains the following data members. m_cause contains portable code corresponding to the exception cause. m_10sError contains the related operating-system error number. m_strFileName contains the name of the file for this exception. The first two are especially useful for isolating and reporting the cause of the file-operation exception. CNotSupportedException This exception represents the result of a request for an unsupported feature. For example, consider the class CStdioFile, which is derived from CFile. The CFile member functions Duplicate(), LockRange(), and
< previous page
page_67
next page >
< previous page
page_68
next page > Page 68
UnlockRange() are not supported for CStdioFile and will throw this exception. CResourceException This exception is thrown when a Windows resource could not be loaded; examples are objects of class CPen(), CBrush(), and so forth. It's also thrown by a failed call to GetDC() from CPaintDC. CUserException This exception class is used to stop the operation currently being executed by the application. Think of it as "the user did something wrong or unexpected, and I have to handle it." The exception is raised by calling AfxThrowUserException(), which is generally called after displaying a message box to the user. For example TRY { // Assume next call could throw a CUserException. SomeUserOperation(); } CATCH(CUserException, e) { AfxMessageBox("This user operation has failed!"); AfxThrowUserException(); } END_CATCH
COleException A COleException condition is thrown as a result of some OLE operation. The COleException class has a public data member called m_sc that holds the status code indicating the reason for the exception. In general, you should not create a COleException object directly; instead, you should call AfxThrowOleException(). COleDispatchException The COleDispatchException class handles exceptions specific to the automation portion of OLE. COleDispatchException can be used with the THROW, THROW_LAST, TRY, CATCH, AND_CATCH, and END_CATCH macros. The documen-
< previous page
page_68
next page >
< previous page
page_69
next page > Page 69
tation recommends that you should not create a COleDispatchException object but instead call AfxThrowOleDispatchException() to handle the exception. CInternetException This exception is related to an Internet operation. The class includes two public data members m_dwError, which is the error code that caused the exception, and m_dwContext, which is the context value associated with the operation. That's a wrap on the exception classes. Now let's take a closer look at reporting these exceptions. Exception Reporting Class CException has two member functions to facilitate the reporting of exceptions: ReportError() and GetErrorMessage (). Use ReportError() to report an error to the user. The prototype is virtual int ReportError(UINT nType = MB_OK, UINT nMessageID = 0);
The return value is any one of the AfxMessageBox values if the call is successful, or zero if there is not enough memory to display the message box. The parameter nType indicates the style of the message box; the default is MB_OK. The parameter nMessageID is the string table entry of the error message to display. If zero, the message "No error message is available" will be displayed. GetErrorMessage() is a lot like ReportError(), except it puts the error text into a buffer; you can then do what you want with it. The prototype is virtual BOOL GetErrorMessage(LPTSTR lpszError, UINT nMaxError, PUINT pnHelpContext = NULL);
The return value is non-zero if successful, or zero if an error occurs. The parameter lpszError is a pointer to a buffer that will receive the error message, and nMaxError is the maximum number of bytes the buffer will hold. The pnHelpContext parameter is the address of a UINT that receives the help context ID. If NULL, no ID will be returned.
< previous page
page_69
next page >
< previous page
page_70
next page > Page 70
Return Values A program error can occur as a result of improper error-checking of return values from MFC functions. A classic example is CMyDoc* pDoc = GetDocument();
This can return a NULL pointer, which if not trapped, can lead to subsequent program failure. Fortunately, many MFC functions return special return values to reflect some error condition. Your application needs to anticipate these by properly checking return values and taking appropriate action. Make sure your mission-critical functions return error codes, and test for them accordingly. Usually, you'll use return values like FALSE,1, or some other easily recognized value. You can also use global error codes. Most Win32 functions call SetLastError() to set a global error code when a failure occurs. Others call SetLastError(0) when they succeed and clear the error code that was set last. Thus, you can call GetLastError() whenever the return value indicates that such a call will return useful information. Try to maintain a good balance too much error-checking can impair your application's execution speed. Class CMemoryState MFC includes the class CMemoryState for assisting in detecting memory leaks. It works only for finding leaks that use the new allocator (that is, memory allocated but never freed with the delete command). One of the class members is Checkpoint(). This is a way of taking "snapshots" of memory at different places in your code stream. Another class member, Difference(), tells you if the state of the memory between the checkpoints has changed. If so, you have a leak. The memory allocated for p[1] never gets freed and is reported by the Difference() method. Here is a less simple, more insidious form of a memory leak. afxMemDF |= checkAlwaysMemDF; CMemoryState first, second, diff; char* buffer[10]; first.Checkpoint(): // take first memory snapshot for (int j =0; j < 10; j++) // allocate using new . . . buffer[j] = new char[10];
< previous page
page_70
next page >
< previous page
page_71
next page > Page 71
for (j = 9; j > 0; j--) delete buffer[j];
// error! buffer[0] never freed!!
second.Checkpoint();
// take second memory snapshot
if (diff.Difference(first, second)) { TRACE("Dumping Difference stats between CheckPoints\n"); diff.DumpStatistics(); TRACE("\nDumping all objects from now back to first Checkpoint\n"); first.DumpAllObjectsSince(); }
The following output is produced; the unfreed block is reported on the third line (I've added an arrow and a comment). Dumping Difference stats between CheckPoints 0 bytes in 0 Free Blocks. 10 bytes in 1 Normal Blocks. ← this says there is 1 block unallocated! 0 bytes in 0 CRT Blocks. 0 bytes in 0 Ignore Blocks. 0 bytes in 0 Client Blocks. Largest number used: 11 bytes. Total allocations: 100 bytes. Dumping all objects from now back to first Checkpoint Dumping objects -> C:\Program Fi1es\DevStudio\MyProjects\HopCalc\HopCalcDlg.cpp(124): {48} normal block at 0x00E10380, 10 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD Object dump complete. Detected memory leaks! Dumping objects -> C:\Program Files\DevStudio\MyProjects\HopCalc\HopCalcDlg.cpp(124) : {48} normal block at 0x00E10380, 10 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD
< previous page
page_71
next page >
< previous page
page_72
next page > Page 72
Object dump complete. The thread 0xFFFA7F45 has exited with code 1 (0xFFFFFFFF). The thread 0xFFC6CODD has exited with code 0 (0x0). The program 'C:\Program Files\DevStudio\MyProjects\HopCalc\Debug\HopCalc.exe' has exited with code 0 (0x0).
Inside the loop, a simple error with the loop index resulted in buffer[0] never being freed. Class CMemoryState has several other useful methods. DumpAllObjectsSince() dumps a summary of all allocated objects from a previous checkpoint. DumpStatistics() displays memory allocation statistics for a CMemoryState object. The hardest part about using this class is deciding where to insert the Checkpoint() calls. Objects, which allocate memory in their own constructors, are especially difficult to deal with; an example is CString. This memory isn't de-allocated until the CString destructor is called. So, if you place checkpoints at the beginning and end of a function that uses a CString object, Difference() will report this as a memory leak even though it really isn't. You can work around this problem (for CString) by calling the CString's Empty() function before the second checkpoint. This will release the memory. Hooking Memory Allocations Under debug, the Microsoft Foundation Class Library debug-memory allocator can call a user-defined hook function to allow you to monitor memory allocation and to control whether the allocation is permitted. This is called hooking and is another way to observe memory operations. As a handy helper for tracking memory allocation, use the function AfxSetAllocHook(), prototyped as follows. AFX_ALLOC_HOOK AfxSetAllocHook(AFX_ALLOC_HOOK pfnAllocHook);
What makes this function unique is that it displays information before an allocation is made, not after. The argument pfnAllocHook is the name of a function that is called before each allocation. You should save the old hook before installing a hook function, and restore it when you're finished. The
< previous page
page_72
next page >
< previous page
page_73
next page > Page 73
return value is non-zero if you allow allocation, or zero otherwise. The pfnAllocHook parameter is the name of the hook function to call. The hook allocation function given by pfnAllocHook is prototyped as follows. BOOL AFXAPI AllocHook(size_t nSize, BOOL bObject, LONG lRequestNumber);
where nSize is the size of the requested memory allocation. If the allocation is for a CObject-derived object, set the parameter bObject to TRUE; otherwise FALSE. The third parameter is the memory allocation's sequence number, lRequestNumber. The return value uses the AFXAPI calling convention, so the hook allocation function must remove the parameters off the stack. Casts You should strive to avoid using casts as much as possible in your applications for a variety of reasons. Mainly, using cast defeats the compiler's ability to perform all kinds of type checking for you, which in turn can lead to the introduction of bugs. And, casting from a ''wide" data type, like a double, down to a "narrow" type, like a float, results in a loss of accuracy. Whenever possible, avoid the use of casts. GetLastError() Many of the Win32 API functions return a last-error code value when an error is encountered. This value can be retrieved by your application and used to display an error message. The prototype is DWORD GetLastError(VOID)
The return value is the code; you can find all these in the file WINERROR.H in your Visual C++ \INCLUDE directory. This function works on both single and multithreaded applications. Multiple threads do not overwrite each other's last-error code, so you can always be assured your thread has the right error code. This function works whenever an API uses the SetLastError() function to set the error condition. This function is prototyped as VOID SetLastError(DWORD dwErrCode)
< previous page
page_73
next page >
page_74
< previous page
next page > Page 74
where dwErrCode is the same as the return value for GetLastError() described previously. You can use these two functions to create your own custom error codes that apply to your application. The only caveat is you must set bit 29 to 1 to prevent conflict; bit 29 is a reserved bit that is 0 for error codes defined by the operating system. Retrieving the Error Message Text Once you have the error code, the next thing you'll want is the text that goes along with it. For this, use the FormatMessage() function, prototyped as follows. DWORD FormatMessage( DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageId, DWORD dwLanquageId, LPTSTR lpBuffer, DWORD nSize, va_list *Arguments );
// // // // // // //
source and procressing options pointer to message source requested message identifier language identifier for message pointer to message buffer maximum size of message buffer address of array of message inserts
Here is an example that illustrates how to use FormatMessage(). First, an error code is deliberately set. Next, a call to GetLastError() is made followed by a call to FormatMessage(). The output is displayed in a TRACE message. char * lpMsgBuffer; DWORD dwErr = ERROR_FILE_NOT_FOUND;. SetLastError(dwErr); // set the error code here to file not found dwErr = GetLastError(); // now get the error code and format it . . . FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) & lpMsgBuffer, 0, NULL); TRACE("%s\n",lpMsgBuffer);
When this code is executed, the message The system cannot find the file specified.
< previous page
page_74
next page >
< previous page
page_75
next page > Page 75
is displayed by the TRACE statement. If necessary, you could display these error messages in an AfxMessageBox() function. Validating Pointers and Strings MFC includes an API for verifying that the new diagnostic allocator has properly allocated a memory block. This function is BOOL AfxIsMemoryBlock(const void* p, UINT nBytes, LONG* plRequestNumber = NULL);
The argument p is a pointer to the memory block allocated by a call to new. The argument nBytes is the length of this block, and plRequestNumber points to a longer integer that receives the memory block's allocation sequence number. This gets set only if the call is successful. The function AfxIsMemoryBlock() makes use of AfxIsValidAddress() to determine if p is valid. If memory tracking is disabled, only AfxIsValidAddress() is called, and the allocation number is set to zero. The function AfxIsValidAddress() is prototyped as follows. BOOL AfxIsValidAddress(const void* lp, UINT nBytes, BOOL bReadWrite = TRUE);
This function also checks memory pointers (the lp argument). It can't guarantee that the pointer is correct, but it can verify that there's nothing wrong with it (that is, that the address lies within the application's memory space). The memory does not have to have been allocated with new. You can also check if a pointer to a string is valid. The function for this is BOOL AfxIsValidString(LPCSTR lpsz, int nLength = 1);
where lpsz is the pointer to test and nLength is the length of the string to be tested, in bytes. If set to 1, it means the string will be null-terminated. The return value is non-zero if the pointer is valid, or zero if it is not.
< previous page
page_75
next page >
< previous page
page_76
next page > Page 76
Chapter Summary We've covered a lot of ground here, but now you should have a keen insight into using debugging macros such as ASSERT. You've also seen how you can monitor the memory operations in your program as it executes and get the results "real time" in the debug window. The use of ASSERT macros will plug those memory leaks and put the wind back in your sails. Now let's look at the next subject, using the Visual C++ Debugger, and apply some of these functions we've covered.
< previous page
page_76
next page >
< previous page
page_77
next page > Page 77
Chapter 4 The Visual C++ Debugger Up to now, this book has taken a look at bugs and some of their causes. Beginning with this chapter, we'll start looking at the tools that allow us to find and correct them. First and foremost among these is the debugger that comes with Visual C++. Although there are other third-party debuggers, most developers will spend their time using this "stock" debugger. When used in conjunction with the other tools that ship with Visual C++, the debugger can quickly become the developer's best friend. Overview The Visual C++ debugger has all the features expected of a working-class debugger. For example, you can use the debugger to perform the following tasks. Step through your code line-by-line. Set breakpoints to stop execution. Open watch windows to view the values of variables. Change the values of variables. Trace the functions called by your program (stack unwinding).
< previous page
page_77
next page >
< previous page
page_78
next page > Page 78
As you can see, the debugger can do a lot of things for you and let you get down to the nitty-gritty. Much of your interaction with the debugger will take place in the Output window. The Output Window The debug output window is found in the docking toolbar that contain your Build and Find in Files tabs. You open the Output window by choosing Output under View on the main menu bar. This is where you'll get the results of the memory-monitoring functions discussed earlier, as well as the output from your TRACE statements. It behaves like any other source window you can copy text from it, and so on. The Debug Toolbar The Visual C++ debug toolbar is shown in Figure 4.1. Each of the debugging commands is explained in greater detail in the following section, moving from left to right on the toolbar shown in Figure 4.1.
Figure 4.1 The Debug Toolbar Restart This command lets you start over. It reloads the program into memory, discarding the present value of all variables. Your breakpoints and watch expressions will still be valid. The debugger will automatically stop at the main() or WinMain() function.
< previous page
page_78
next page >
< previous page
page_79
next page > Page 79
Stop Debugging Terminates the debugging session and returns to the Visual C++ workbench. Stop Program, Start Debugger This is a dangerous, yet powerful, option. It lets you attach to a running process and launch the debugger. Take care when exercising this option don't attach to a process unless you know what it is! However, this option offers a handy way to regain control of your application if it slips into some unpleasant condition (for example, an infinite loop or a nonterminating wait state). Start/Continue Click this button to start the debugger. If you've already started the debugger and paused (for instance, at a breakpoint), clicking this icon continues to the next breakpoint. Show Source Line for Instruction Pointer Clicking this toolbar icon displays the source line pointed to by the instruction pointer when you are working in the disassembly window (which displays the assembly-language code from your compiled program). If you've scrolled the pointer out of view, clicking this icon scrolls it back into view. Essentially, a quick way to recover where you were. Step into Function Most debugging involves single-stepping through a program using the Step Over command (see the following section). But sometimes you want to take a "detour" into a function and examine what is going on there. This toolbar
< previous page
page_79
next page >
< previous page
page_80
next page > Page 80
option allows you to do that. Consider the following code example. char title[80]; memset(title, 0, sizeof(title)); title = MyTextFunction(); // one of your functions m_pMainWnd->SetWindowText(_T(title)); m_pMainWnd->ShowWindow(SW_SHOWMAXIMIZED); m_pMainWnd->UpdateWindow();
If you were having a problem with the value of title, you would set a breakpoint on the third line. Now instead of stepping over this function and examining the value of title, you could use Step Into and walk through the function MyTextFunction() to isolate the problem. Step Over This is probably the command most widely used. It allows you to walk through each line in your program, or to "step over" them. This is also referred to as single-stepping the debugger. If you use this command on a function call, the function is executed without stepping through its code use the Step Into command instead. Step Out The Step Out command is just the opposite of the Step Into command. If you have stepped into a function and determined that the bug you're looking for isn't there, you can click this toolbar command to exit the function. The debugger will automatically stop on the next line, even if no breakpoint has been set at that location. Run to Cursor Sometimes you don't want to set a break point, you just want to run to a line. You can do this by clicking on the desired line and then using this command. The program will run to this line and stop.
< previous page
page_80
next page >
< previous page
page_81
next page > Page 81
Set/Clear Breakpoint This command does double duty. It lets you set a break point and also allows you to clear it. To set a break point, simply click on the desired line to establish the insertion point, then click this command. A break point is indicated by a dot in the left margin of the window, as shown in Figure 4.2.
Figure 4.2 The Break Point Symbol Clear All Breakpoints This command simply clears all breakpoints you have defined for the current debugging session. This includes breakpoints in other modules as well, not just the current one. Show Variable/Expression
This is also known as the QuickWatch window. It allows you to examine and change variables "on the fly." To use QuickWatch, simply highlight or double click the variable you wish to watch, then click this command button. The QuickWatch window, shown in Figure 4.3, will appear.
< previous page
page_81
next page >
< previous page
page_82
next page > Page 82
Figure 4.3 The QuickWatch Window There are some situations in which you can't use the QuickWatch window. One of these is when you try to watch a global variable defined as const. Attempting to do so will generate compiler error CXX0017. For example #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif const int iGlobal=0; // define global const variable at top of file
When you try to watch iGlobal with QuickWatch, you'll get the following message iGlobal CXX0017: Error: symbol " iGlobal " not found
The storage space for MFC global constants is optimized to save space; thus, no debugging information is generated. However, you can still examine global const variables inside TRACE statements. Watch This command is similar to the QuickWatch command in that it allows you to specify which variables and expressions to add to the Watch window. The Watch window normally appears at the bottom of the workbench, although you can change this by changing its docking properties. The Watch window
< previous page
page_82
next page >
< previous page
page_83
next page > Page 83
has four tabs: Watch1, Watch2, Watch3, and Watch4. Each tab is used to display a list of variables or expressions in a spreadsheet-like field. This allows you to group variables according to their functionality. You can also modify variable values in the Watch window. The Watch window, like the QuickWatch window, will display a plus sign (+) or minus sign () in the Name column when watching an array, object, or structure. Clicking one of these allows you to expand/ collapse your view of the variable. Registers Using this command will display the contents of the CPU registers, flags, and floating-point stack. You can also change the value in a register or set one of the flags while debugging your program. Memory Some data types, such as large buffers and strings, are not well-suited for display in the Watch or Variables window. Instead you can use the Memory command for displaying these in the Memory window. You can change the format of the Memory window display by changing options on the Debug tab in the Options dialog box (under the Tools menu option). This dialog box is shown in Figure 4.4; the Memory window is shown in Figure 4.5.
Figure 4.4 The Debug Options Dialog Box
< previous page
page_83
next page >
< previous page
page_84
next page > Page 84
Figure 4.5 The Memory Window Call Stack Use this command to unwind the call stack. While you are running the debugger, the Call Stack window shows you the stack of currently active calls to functions. When a function is called, it is pushed onto the stack and popped off when the function returns. The currently executing function is displayed at the top of the stack, with older function calls beneath it. This lets you ''follow the trail" of what functions have been called by your program. Function parameter types and values can be displayed or hidden by using the check boxes in the Call Stack window section of the Debug tab on the Options dialog box. Or, you can simply use the context menu by right-clicking your mouse in the Call Stack window. The call stack is discussed in greater detail in the following section. So, that's a rundown on the basic debugger commands on the toolbar. Using the Visual C++ workbench, you can customize toolbars or even build your own. This is often preferable to using the stock toolbar, as you may wish to add/ remove commands to suit your style and needs. The Call Stack Window In this section, we'll take a closer took at the call stack and how you can use it to assist your debugging efforts. The stack is a reserved memory region, several tens of kilobytes in size that acts like a "scratch pad" for the CPU. The stack acts just like a plate stacker used in a cafeteria the last plate pushed onto it is the first one that comes off. Thus, the data most recently added is what's accessible. An application causes the CPU to push data onto the stack or to pop some data off. For Intel-based platforms, the stack begins at a specific address and grows downward from that address. Pushing items onto the stack just means that data is added to the stack, and
< previous page
page_84
next page >
< previous page
page_85
next page > Page 85
popping means it is removed. For example, when a parameter is passed to a function, that parameter is pushed onto the stack. The function that is called then pops it off. Problems are caused when an errant pointer corrupts the stack, or an invalid data type gets passed, or some other misfortune occurs. The CPU has a special register called the stack pointer (abbreviated SP) that it uses to reference the memory where the stack data resides. When an item is pushed onto the stack, the SP is decremented; when popped, it is incremented. By moving the contents of the SP to another register and subtracting some offset from it, and finally de-referencing the resulting address, the other values on the stack can be retrieved. Using the Call Stack Window When the debugger hits a breakpoint, it requests information about the state of the process under execution. This lets the debugger know the contents of the instruction pointer (IP), which is the address of the next instruction to be executed. The address in the SP is also retrieved the values in the IP and SP registers, along with the debugging symbols, tell the debugger what entries are on the stack, as well as their meaning to the application. By using the Call Stack window, shown in Figure 4.6, you can gather information about the stack.
Figure 4.6 The Call Stack Window The first line in the Call Stack window is the name of the function that is currently executing. The next line is the function that called the current function. Each succeeding line represents another level in the calling hierarchy. Looking at the call stack window allows you to follow the flow of control and ascertain how you came to this point in your program. By watching for functions that are listed more than once, or by observing the patterns of function calls, you can determine if there's a problem in your program's flow of control.
< previous page
page_85
next page >
page_86
< previous page
next page > Page 86
Stack Errors The stack can be a source of errors for your program. The stack overflow is the most common cause. An overflow occurs when a process uses more stack space than was allocated when the application was built. The default size of the stack is 1Mb, but you can change this as follows. 1. Click "Settings.." on the Project menu bar. 2. Click the Link tab in the Project Settings dialog box. 3. Click Output in the Category box. 4. Set the stack size in the box marked Reserve. The linker rounds this value up to the nearest 4 bytes. Figure 4.7 shows the dialog box for adjusting the stack size.
Figure 4.7 Setting the Stack Size It's easy to generate a stack overflow. For example, the statement int x[2000000];
will do the trick. The variable x is created on the stack, which by default is 1Mb in size. Here, x tries to gobble up 2Mb. The output obtained when this
< previous page
page_86
next page >
< previous page
page_87
next page > Page 87
line is encountered is shown in Figure 4.8.
Figure 4.8 The Stack Overflow Dialog Box Stack Probes The size of the stack can be adjusted at run time by using stack probes. A stack probe is a block of code inserted into every function call by the compiler. The /Gssize option must be enabled in the compiler options (that is, on the Link tab in the Project Settings dialog box). The net effect is that the amount of space required to store the function's local variables is checked against the stack size. If more space is needed, the stack probe increases the size of the stack by the amount size specified in the /Gs option. Stack probes can be toggled on or off by using #pragmacheck_stack. Stack Corruption Another stack error occurs when the values on the stack are corrupted with bad data. Generally, this happens because of a wild pointer that ends up poking some value into the memory region reserved for the stack. Or, it can occur when memory is allocated using a value in, or near, the stack. You can spot such errors by examining the contents of the stack pointer register and the starting address of the stack. Application Problems We've now covered enough ground to start looking at specific application problems and what you can do about them with the help of the debugger. In this section, we're going to examine some of the different kinds of problems associated with programs that refuse to start or terminate. These can be especially frustrating because there doesn't seem to be any place to begin. We'll examine the way an application interacts with the operating system and the run-time library, from a conceptual view. This overview should give you some insight into how things work, which in turn can give you a leg up
< previous page
page_87
next page >
page_88
< previous page
next page > Page 88
on tracking down bugs. Let's start by looking at how the kernel executes your program. Birth of an Application An entry point is the first place in your executable that the operating system calls to start your program. Every Windows application has an entry point, which is controlled by the linker. You can specify the entry point by using the /ENTRY option for the linker. If you don't define an entry point, the linker selects one for you according to the type of program you're building; the starting address is a function name from the C run-time library. The default entry points are outlined in Table 4.1. Table 4.1 Entry Point Defaults Program Type
Starting Address
Entry Point
An application using /SUBSYSTEM:CONSOLE
mainCRTStartup (or wmainCRTStartup)
main (or wmain)
An application using /SUBSYSTEM:WINDOWS
WinMainCRTStartup (or wWinMainCRTStartup)
WinMain (or wWinMain) Must be defined with _stdcall
A DLL
_DllMainCRTStartup
DllMain Must be defined with _stdcall, if it exists
If the /DLL or /SUBSYSTEM option is not specified, the linker will select a subsystem and entry point depending on whether the application is Windows or console-based. Thus, the functions main, WinMain, and DllMain are the three forms of user-defined entry points. The compiler and run-time library work together to initialize your application. Even though mainCRTStartup() and WinMainCRTStartup() take different parameters and are used in entirely different types of applications, the work they do is similar. For example, both begin by initializing the run-time library, which gives you a default heap. They do some other things, too, like checking the locale of your process (for international languages, times, and so on). Also, the command line is parsed, and the environment goes into a table of strings. This happens for a Windows application the same as for a console. Many developers don't know they can reference the _argv[] and
< previous page
page_88
next page >
< previous page
page_89
next page > Page 89
_argc variables from anywhere in their program, so long as it was built using a Microsoft tool, which provides this capability. Once the run-time library has been initialized, it next constructs any static objects you've declared in your application. During compilation, the compiler builds a table of these objects; the run-time library has code that knows how to find and read this table. For Windows applications built with MFC, you'll always have at least one static object: an instance of CWinApp. When the initialization finishes, the run-time library will call the entry point either WinMain() or main(). Ostensibly, this lets your program come to life, assuming there were no problems with the initialization process. Problems like insufficient memory, which prevent allocating copies of the environment or command line, represent one example. Should this occur, you might get an error message from the run-time library, as follows. R6008 - not enough memory for arguments
If there isn't enough memory for the error message, you won't even get this, and your program won't get out of the gates. Since it can't even communicate with you, you're left with no idea as to what went wrong. Don't despair: there are a couple of tricks you can try. One remedy is to run your program in debug mode with a break point set at the runtime library's error reporting routine, _amsg_exit(). When the break point is hit, you can then use the Call Stack window to determine why the error message was called. Another solution would be to place a break point on your entry point and step through the run-time library's initialization code to find the problem. This can be time consuming and laborious, so you might want to skip past some of the internal initializations that the run-time library does for itself. Death of an Application Sometimes your application will start and run, but problems occur when you try to shut it down. In this section, we'll look at what happens when you shut down, or try to shut down, your application. The emphasis here will be on Windows-based programs built with MFC. A program is finished executing when its entry point function [in this case, WinMain()] has returned. When this happens, control passes to the run-time libraries, which start calling the destructors for the static objects found in the compiler-built table discussed previously. What comes after this destruction depends on whether you're in debug or release mode. In debug,
< previous page
page_89
next page >
< previous page
page_90
next page > Page 90
the libraries go through the application's heap, looking for memory leaks. Any that are found are reported in the Output window, as we have seen. But eventually, the entry point does return, and control returns to the operating system, which shuts down the process and removes it from memory. The run-time libraries are responsible for most of the cleanup on behalf of your (dying) application. MFC-based applications don't quit until they see a WM_QUIT message, at which time ExitInstance() is called. This is where you do your final cleanup for anything you might have done in InitInstance(), which lies at the opposite end of the cycle of reincarnation. But before WinMain() returns, MFC calls AfxWinTerm() for some lastminute cleanup. At this point you might encounter an assertion, or throw an exception. The best way to handle these problems is to use the debugger to halt execution when an unhandled exception is thrown. You can then examine the call stack to find the module that threw the exception, and resolve the problem. DLLs Dynamic Link Libraries (DLLs) have an initialization process similar to applications. When a DLL is loaded via a call to LoadLibrary(), and an error occurs, this API will return an error code that you can trap. If a DLL calls other DLLs, initialization is resolved recursively starting with the last DLL in the call chain. For an example, look at Figure 4.9.
Figure 4.9 A DLL Call Chain
< previous page
page_90
next page >
< previous page
page_91
next page > Page 91
In this example, Windows will first try to load and initialize DLL_3, then DLL_2, and finally, DLL_1. Only when all three have been successfully loaded and initialized will Windows return from the call to LoadLibrary(). Things are a little different for your application if the DLLs are linked via an import library. In this case, Windows won't attempt to initialize your application until all the DLLs have been initialized. This is just the opposite of what happens when the application has no DLLs to deal with. DLLs also have an entry point called DllMain(). But as a regular application does, the run-time libraries provide their own entry point a function called _DllMainCRTStartup(). This is conceptually similar to a regular main() function in that it takes care of all the initialization and shutdown procedures. However, a DllMain() function differs from a main() function in that it communicates with the run time library. For example, DllMain() is called to let the library know if a thread has been created or terminated, or if a new process has been spawned. Unlike main(), the operating system expects that DllMain() will return quickly. When main() returns, the application has been terminated, but DllMain() might run and return several times. These late returns cause DllMain() errors. The runtime libraries perform some cleanup for any previous initializations and are still responsible for creating and destroying static objects. Any problems encountered calling a DLL's entry point are pretty much the same as those for an executable. The probable cause is with your code for static objects, or with the implementation of the call to the entry point. DLL Initialization Steps The act of initializing a DLL can be broken down into the following functional steps. 1. Windows attempts to find and open the DLL's executable file. 2. Windows next attempts to locate the executable in memory. 3. An import table is built that is referenced by the program(s) that link to the DLL. 4. Windows tries to load all other DLLs. 5. Windows attempts to find the entry points for the DLLs. 6. Windows calls the entry point.
< previous page
page_91
next page >
< previous page
page_92
next page > Page 92
Any number of things can go wrong throughout this process. For example, in Step 1, the DLL has to exist and you have to have read/ execute access to it. Should you encounter a DLL that will not load, then the problem is somewhere in Steps 15. If it loads but fails to call the entry point in Step 6, there is a problem with the DLL itself, or a resource it requires is missing. You can identify the problem by putting break points on all DllMain() functions. But if a secondary DLL's initialization fails while you are trying to load the primary DLL, there is no simple cure, because the operating system gives you no access to these secondary DLLs. About all you can do is step through them until you find the failure. If you are porting an MFC AppWizard application v4.1 to v5 that enables OLE Automation or an OLE Control, there is a bug when running under Windows NT v3.51 when attempting to load the DLL URLMON.LIB. Please refer to Chapter 8 for more information. Just-in-Time Debugging Beginning with Visual C++ v5, developers now have just-in-time debugging, which allows your program to run outside of the debugger; if the program fails, the debugger will automatically start. The syntax for the command to get or set just-in-time debugging is object.JustInTimeDebugging [=boolean]
where object is an object expression that evaluates to an object in the Applies To list, and boolean is a flag that specifies if just-intime debugging is available. A value of TRUE turns it on, and FALSE turns it off. Using just-in-time debugging makes debugging available for Remote Procedure Calls (RPC). RPC is a message-passing technique that allows a distributed application to call services that are physically located on other computers in a network. Just-in-time debugging can also be set by selecting the check box on the Debug tab in the Options dialog box under the Tools menu option). Visual C++ Compiler Errors In this section, we'll look at some common errors reported by the compiler and what you can do if you encounter them. While some errors are obvious (like misspelling a data type), some are a little more confusing.
< previous page
page_92
next page >
< previous page
page_93
next page > Page 93
Curly Brace Mismatch Failing to end a block with a closing curly brace (the } character) can cause all sorts of errors, none of which simply says "Hey, you left out a closing curly brace!" If you add one too many closing braces, the compiler might catch it and report the offending line; it just depends on the location within the source code. The best defense against this problem is to simply add a closing brace when you start a code block, then go back and fill in the middle. For example, suppose you're going to set up a big while loop. Go ahead and close the loop before you start putting in the code. while() { }
// end while loop
Use white space and indents, too. You can customize the Visual C++ Workbench to use auto-indenting by selecting Options from the Tools menu bar and clicking on the Tabs tab, as shown in Figure 4.10.
Figure 4.10 The Dialog Box for Auto-Indenting Source Code
< previous page
page_93
next page >
< previous page
page_94
next page > Page 94
This makes it tremendously easy to spot code blocks and missing/extra braces. For example BOOL bLoop = TRUE; while(bLoop) { if(something) { } else { }
// end if(something)
}
// end while loop
Here, you can quickly spot the blocks of code. I also recommend adding a comment on the closing brace line of each block; when you start nesting blocks, this makes it easy to read your source code and follow the flow. Fatal Error C1001 This is one of the most dreaded errors, and determining the cause can be a real headache. The form of the error is INTERNAL COMPILER ERROR (compiler file 'file', line number)
If you get build errors before seeing this message, make sure you fix these first, because they can contribute to this message. Manually delete all your .OBJ files, precompiled headers (if any), and perform a Rebuild All. If that doesn't fix the problem, it's possible this error was caused by an optimization option. Disable all optimizations and try again. If no errors were reported before this error, the next step is to ascertain which pass of the compiler generated the message. This is accomplished by recompiling with the /Bd option, which causes each pass to print its name and arguments when invoked. The last pass that is invoked before receiving the error message is the one responsible. If the reported pass is P1, then the problem is probably still error recovery, but it occurs before the compiler gets the chance to post the error message just discovered. In this case, check
< previous page
page_94
next page >
< previous page
page_95
next page > Page 95
the line reported for error C1001. It might still contain an unreported syntax error, and correcting it generally will fix the problem. In extreme cases, you might want to completely delete the line and retype it. If the pass is P2, then optimization options are likely to be the source of the problem. Try removing these one at a time until the error disappears, or try using a different code generator in the project settings. If none of these suggestions works, try running ScanDisk on your disk, and check the amount of memory available. (I once experienced this problem when a SIMM failed during a compile. I had booted to 32Mb, but when I checked memory, I had only 24Mb. Hey, it happens . . . ) If there doesn't seem to be a hardware problem, your last resort is to call Microsoft's Technical Support for assistance. Compiler Pragmas The Visual C++ compiler includes several useful pragmas that can influence your debugging efforts as well as provide additional support functions. Every implementation of a C/C++ compiler offers features unique to its host platform while maintaining compatibility with the C/C++ languages. This functionality is provided by #pragma directives, which are platform-specific features that are generally different for every compiler. The syntax of a #pragma directive is #pragma token
where token is a string containing the name and body of the action to be performed. In this section, we'll look at some of the pragmas that can be used for a variety of purposes. Warning() As you may know, you can specify the level of warnings the compiler emits by adjusting the project settings. It is a good idea to set the level as high as you can tolerate; the warnings can give you insights into potential errors. The Warning() pragma allows you to change the behavior of compiler warning messages in either a general or a specific manner. The prototype is #pragma warning(warning-specifier : warning-number-list [,warning-specifier : warning-number-list . . .])
The values for warning-specifier are described in Table 4.2.
< previous page
page_95
next page >
page_96
< previous page
next page > Page 96
Table 4.2 The Warning Pragma Specifiers warning-specifier
Action
once
Display the specified message(s) only once.
default
Apply the default compiler behavior to the specified message(s).
1, 2, 3, or 4
Apply the given warning level to the specified warning message(s).
disable
Disable the specified warning.
error
Report the warning as an error.
The warning-number-list parameter can contain any warning numbers. Multiple options can be specified in the same #pragma directive by using a semicolon delimiter, as shown in the following example #pragma warning(disable : 4507 34; once : 4385; error : 164)
Here, warning numbers 4507 and 34 are disabled, 4385 is shown only once, and 164 is reported as an error. Code-generation warnings are those that start at 4700 and above. The Warning() pragma takes effect for these only when it is placed outside function definitions. Message() This pragma is not so much a debugging aid as it is a project management tool. It offers a handy way to post notes in the output window during a build. For example, suppose you're working on a function that accepts a date from the user. You get it working to the point that you can accept a date in a given format, but you don't validate the data. You could use the Message() pragma to output a message reminding you that you have to add date-validation to the function. The format of the message pragma is #pragma message(messagestring)
< previous page
page_96
next page >
page_97
< previous page
next page > Page 97
where messagestring is the text of the message in double quotes. For example #pragma message("Date validation not finished in function getdate()!")
The messagestring parameter can also be a macro that expands to a string literal, and you can concatenate such macros with string literals in any combination. For example, the following statements display the name of the file being compiled and the date and time when the file was last modified. #pragma message("Compiling " __FILE__ ", modified on:" __TIMESTAMP__)
Comment() Closely related to the Message() pragma is the Comment() pragma. The difference is that the latter pragma inserts a comment record into an object or executable file. The syntax is #pragma comment(comment-type [, commentstring])
where comment-type can be one of the five values shown in Table 4.3. Table 4.3 The Comment() Pragma Values comment-type
Action
compiler
Places compiler name and version in the object file.
exestr
Places comment string in the executable file at link time.
lib
Places a library-search record in the object file.
linker
Places a linker option in the object file.
user
Places the comment string in the object file.
Here is an example of using the Comment() pragma. #pragma comment( user, " Compiled on " __DATE__ " at " __TIME__ )
< previous page
page_97
next page >
page_98
< previous page
next page > Page 98
Pack() When you write code that includes structures or unions, the compiler will align them on certain byte boundaries (the default is 8 bytes) in memory. For structures, the alignment is based on the size of the largest member of the structure. For unions, it is based on the first member declared in the union. This ''packing alignment" is controlled for the entire build with the /Zp option, but you can use the Pack () pragma to change this at the data-declaration level. The syntax of the pragma is #pragma pack([ n ])
where n can be 1, 2, 4, 8, or 16. This pragma takes effect at the first structure or union encountered after it is seen by the compiler. The main concern of the Pack() pragma is when you are porting code from 16-bit Windows to Win32; you'll probably need to change or remove any packs from the code. Another form of the Pack() pragma is the following syntax. #pragma pack([ [ { push | pop }, ] [ identifier, ] ] [ n ])
This lets you combine program components into a single token if they use Pack() pragmas to alter the alignment scheme. A Pack() pragma that uses push stores the current alignment on an internal compiler stack, and the list is read from left to right. If push is used with a value n, then n becomes the new packing value. The token identifier, which is a name you select just like a variable name, becomes an alias for the new packing value. If pop is used, the value at the top of the compiler's stack is retrieved and becomes the new packing alignment value. If you use pop with n, the value for n becomes the new packing value. When you use pop with identifier, all values on the compiler's stack are removed until a matching identifier is found. Then, the packing value that existed before pushing identifier becomes the new packing value. Simple, huh? A known bug occurs when you use this pragma to pop an ID that has not been pushed after successfully popping an ID that was pushed. This bug generates the C1001 fatal error described earlier. For example, the following code snippet produces the error. #pragma #pragma #pragma #pragma
pack(push, a) pack(push, b, 4) pack(pop, b) pack(pop, value)
< previous page
← here comes the problem!
page_98
next page >
< previous page
page_99
next page > Page 99
The last line is the culprit; comment it out, and the code works just fine. To work around this bug, you should pop only IDs that have been pushed onto the compiler's internal stack. Code Migration Issues This section is a catchall for those of you who have 16-bit Windows applications and migrate to 32 bits. There are a lot of little details that you need to be aware of in porting code developed for 16 bits. For the most part, a project built with Visual C++ v1.5x can be opened and built under version 5.x with only a few problems. Since most of these are related to memory and memory management, this is a good place to start. Memory The first common problem is the NEAR and FAR addresses. So long as your project includes WINDEF.H (where these keywords are defined), Win32 will simply ignore them. However, it is best to remove them for the sake of clarity and ambiguity in the maintenance cycle. If you do not include WINDEF.H, a simple solution is to use the /D command-line option to replace the keywords with empty strings. For example /D_near= /D_far= /D__near= /D__far=
replaces the keywords with empty strings. Data Types Data types and their default behavior change when you move from 16- to 32-bit Windows. For example, 16-bit code frequently uses the WORD type interchangeably with other types, such as HWND and HANDLE. Consider the following 16-bit code. HWND hWnd; hWnd = (WORD) SendMessage(hWnd, WM_SETFOCUS, 0, 0L);
In Win32, this code becomes HWND hWnd; hWnd = (WORD) ::SendMessage(hWnd, WM_SETFOCUS, 0, 0L);
< previous page
page_99
next page >
< previous page
page_100
next page > Page 100
This code generates an error when compiled for Win32 error C2440: '=' : cannot convert from 'unsigned short' to 'struct HWND__ *' Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or function-style cast
To correct this, change the cast from WORD to HWND hWnd = (HWND) ::SendMessage(hWnd, WM_SETFOCUS, 0, 0L);
In general, it is best to use the most specific type possible. Replace all generic handle types, such as HANDLE, with a more specific type, such as HPEN. Data Sizes Migrating 16-bit code to 32-bits requires you to understand some of the basic changes that result from all these extra bits. Here is a quick rundown on the most fundamental data types. A WORD is still 16 bits, but integers are 32. Structure alignment, as we have seen, is different for Win32. For example, if you write a structure out to disk in 16-bit Windows, you have to pick it apart to read it into Win32. NULL is not 0; it is now ((void*)0). All HANDLEs are now 32 bits. This includes HICON, HBRUSH, and HINSTANCE. The BOOL is no longer an integer but a stand-alone data type of 1 byte. (See Chapter 8 for more details.) In addition, the GetWindowWord(), SetWindowWord(), GetClassWord(), and SetClassWord() functions cannot be used to access window and class information. You'll need to use the GetWindowLong(), SetWindowLong(), GetClassLong(), and SetClassLong() functions instead. For example Listing 4.1 16-Bit Code // Retrieve application instance handle and module handle. hInstance = GetWindowWord (hWnd, GWW_HINSTANCE); hModule = GetClassWord (hWnd, GCW_HMODULE);
< previous page
page_100
next page >
< previous page
page_101
next page > Page 101
Listing 4.2 32-Bit Code // Retrieve application instance handle and module handle. hInstance = GetWindowLong (hWnd, GWL_HINSTANCE); hModule = GetClassLong (hWnd, GCL_HMODULE);
The unsigned integer, which was 16 bits in 16-bit Windows, is now 32 bits wide. The same goes for the UINT and int data types. If you've used these data types in such a manner that your code relies on the value returning to zero when incremented past the maximum size, you'll need to address this. For example Listing 4.3 16-Bit Code unsigned int i= 65535; ++i; // new value now 0
Listing 4.4 32-Bit Code unsigned int i= 65535; ++i; // new value now 65536
int to enum Another problem you might encounter in porting code up from C to C++ involves the enum type. C++ does not provide implicit conversion from int to enum. For example enum Fruit { apples, oranges, pears }; Fruit fruit = apples; fruit = fruit + 1; // error, no implicit conversion from int to Fruit
The last line causes the compiler to emit the following error (for an application called Test). error C2440: '=' : cannot convert from 'int' to 'enum CTestApp::OnAppAbout::Colour' Conversion to enumeration type requires a static_cast, C-style cast or function-style cast
< previous page
page_101
next page >
page_102
< previous page
next page > Page 102
To correct this, you'll have to cast the type as fruit = (Fruit)(fruit + 1);
// OK
Structure Scope Any data types declared within a structure in C++ are not exported to global scope. For example, in C, the following structure is legal. struct Location { struct Pixel { int x,y; }; }; struct Pixel pixel;
The Visual C++ compiler will emit the following error message. error C2079: 'pixel' uses undefined struct 'Pixel'
To correct this, move Pixel outside the braces of the structure Location. Shift Escape and WM_CHAR In Windows 3.x and NT, pressing Shift+Esc generates a WM_CHAR message. The same application running under Windows 95 doesn't generate this message, because the key table that TranslateMessage() uses under Windows 95 doesn't include the Shift+Esc key combination. If you need to use this key combination, use the WM_KEYDOWN or WM_KEYUP messages. Chapter Summary This chapter covered the basics of the standard Visual C++ debugger. I hope that you've been exposed to something new here, or, if you're just starting out, that you've learned the basic capabilities and operations of the debugger. The debugger, whether it be the one discussed in this chapter or an after- market product, is an indispensable tool when you are developing quality software. In the next couple of chapters, I'll be looking at debuggers from other vendors as well as additional tools that can make bug zapping a snap.
< previous page
page_102
next page >
< previous page
page_103
next page > Page 103
Chapter 5 Additional Debugging Tools In this chapter, we're going to examine some of the additional debugging tools that ship with Visual C++. Each of these performs specialized roles that will allow you to quickly get a handle on your debugging problems. In addition, some of these tools will help you determine just what resources are being consumed by your application, which can help you write better code. Finally, some of them are simply good project management utilities (for example, Windiff.exe) that help you monitor and manage your workload. MFC Tracer First and foremost among the tools is Tracer, which I mentioned earlier. This utility lets you display messages, the values of variables, and so forth, in a special output window while running your program in debug mode. It's a lot like the printf() function in C. The program, which is installed in your BIN directory, is called TRACER.EXE. This utility program allows you to set the options in AFX.INI. See Figure 5.1 for an illustration of the Tracer window.
< previous page
page_103
next page >
< previous page
page_104
next page > Page 104
Figure 5.1 The Tracer Window The output goes to the output window. You open this window by selecting Output under View on the main menu bar, or by pressing Alt+2. Figure 5.2 shows how to open the Output window.
Figure 5.2 Opening the Trace Output Window The AFX.INI file is used to store information about diagnostic messages and other options. This file needs to be in your \WINDOWS directory; running the Tracer program will create this file for you. The most important option shown in Figure 5.1 is the first one: Enable tracing. You must click the check box "on" in order to use Trace messages in your source code. This output is available only in a debug version; the Trace statements are ignored in release mode.
< previous page
page_104
next page >
page_105
< previous page
next page > Page 105
Besides specifying options in the dialog box shown in Figure 5.1, you can control them from inside your program by modifying the global integer afxTraceFlags. For example afxTraceFlags = 4 + 8;
// windows message dumping
You'll find the flags and their meanings in file AFXWIN.H. enum AfxTraceFlags { traceMultiApp = 1, traceAppMsg = 2, traceWinMsg = 4, traceCmdRouting = 8, traceOle = 16, traceDatabase = 32, traceInternet = 64, };
// // // // // // //
multi-app debugging main message pump trace (includes DDE) Windows message tracing Windows command routing trace special OLE callback trace special database trace special Internet client trace
As you can see, these values all represent a bit in a word. In the preceding example, setting afxTraceFlags to 4+8 enables control notification messages. The Trace message actually comes in four varieties: TRACE0 is the same as TRACE but takes a format string with no arguments. TRACE1 is the same as TRACE but takes a format string with a single argument. TRACE2 is the same as TRACE but takes a format string with two arguments. TRACE3 is the same as TRACE but takes a format string with three arguments. These macros are recommended for debugging Unicode, because you don't need the _T macro. One of the advantages of using the TRACE macro(s) is that you don't have to set break points with the debugger once you've gotten on the trail of a bug. You can simply output the variables, messages, and so on, to the Output window and move on.
< previous page
page_105
next page >
< previous page
page_106
next page > Page 106
Stress Another invaluable tool is STRESS.EXE, found in your BIN\WIN95 directory. This utility is mainly a tool to be used in testing your application. It provides acquisition of system resources (for instance, the global heap, user heap, GDI heap, disk space) for low resource stress testing. In addition, it provides fixed, random, and message-dependent allocation of the resources and gives you a logging option that helps you locate and reproduce bugs. The window displayed by Stress is shown in Figure 5.3.
Figure 5.3 The Stress Output Window Fixed Allocations The option Fixed Settings is found under Settings on the main menu bar. Clicking this allows you to specify allocation values for any of the resources. The dialog box for this option is shown in Figure 5.4. There are eight edit fields that let you specify the amount of resources to leave available for the heap, the 32-bit heap, and drive specifications. Entering a value of zero will consume all the resources, while a 1 will free any allocations made by Stress for that resource. The button labeled Free All is just a quick way to set all these fields to 1. The Set button does any allocation you specify in the edit fields.
< previous page
page_106
next page >
< previous page
page_107
next page > Page 107
Figure 5.4 The Fixed Settings Dialog Box The Executer The Executer allows applications to be tested, while Stress dynamically allocates resources in the background. This option is also under Settings on the main menu bar. Figure 5.5 shows the dialog box for this option. The Advanced button gives you more options for specific resources.
Figure 5.5 The Executer Dialog Box
< previous page
page_107
next page >
page_108
< previous page
next page > Page 108
Log Settings This option, under Settings on the main menu bar, lets you log events and messages to a log file. The dialog box for this option is shown in Figure 5.6. The log file lets you keep track of stress allocations, and so on and can be helpful in reproducing errors.
Figure 5.6 The Log Settings Dialog Box Spy++ Spy++ is another utility for debugging your Win32 applications. This tool produces a graphical view of the system's windows, messages, processes, and threads. It lets you ''spy" on these objects to perform the following tasks. Create a graphical tree of the relationships among system objects. Search for windows, messages, and other objects. View the properties of these objects. Select a window to spy on. Specify message options. Figure 5.7 shows the Spy++ window.
< previous page
page_108
next page >
< previous page
page_109
next page > Page 109
Figure 5.7 The Spy++ Window Browse Another useful weapon in the debugging arsenal is the Browse window, used to monitor information about your program's symbols, such as variables, classes, data, functions, or even macros. This information is stored in a file having the extension .BSC and is created by using the BSCMake utility. The .BSC file is generated by combining a set of files that have the extension .SBR. While most debugging tools, and even the debugger itself, let you "zoom in," the beauty of Browse is that it lets you "zoom out" and get the big picture as to what is going on inside your program. Creating a Browse File You create a Browse file from the Settings dialog box under the Project menu from the workbench toolbar. Click on the C/C++ tab, and, from the General category, click the check box labeled Generate browse info. You'll then need to do a (clean) rebuild. The compiler will create an .SBR file for each of your project's files. All that's left to do is run the BSCMake utility to assemble these into a single .BSC file. You can either run this from the DOS shell (which gives you more flexibility type BSCMAKE/? for a list of
< previous page
page_109
next page >
< previous page
page_110
next page > Page 110
options) or simply choose Source Browser . . . from the Tools command. This will automatically generate the .BSC file. Using Browse When you open Browse, you'll see the dialog box shown in Figure 5.8. The OK button is initially disabled you'll need to type in whatever it is you're looking for and select one of the categories from the list box shown Figure 5.8. For example, I have a CRect object in one of my project files called rClient, and I browsed for this. The results are shown in Figure 5.9.
Figure 5.8 The Browse Dialog Box
Figure 5.9 A Sample Browse
< previous page
page_110
next page >
page_111
< previous page
next page > Page 111
As you can see, Browse tells you the name of the file(s) that contain the symbol and the line number of where it can be found. This can come in mighty handy when you are tracking down logic design bugs. All in all, Browse allows you to examine the following items. Symbols. Source code lines where a symbol is defined. Code lines that reference a symbol. Relationships between base and derived classes. Relationships between called and calling functions. DDESpy DDESpy was designed specifically for monitoring dynamic data exchange (DDE) events in your application. This debugging tool should provide you with everything you need to watch your DDE activities. Should you decide to write your own someday, bear in mind that such a custom application should not perform DDE server or client communications. There is a potential for problems when the application intercepts its own communications. Figure 5.10 shows the DDESpy window and menu bar.
Figure 5.10 The DDESpy Window and Menu Bar DDESpy provides a plethora of information about DDE messages and errors. You can monitor messages that are sent, callbacks, string data, and more.
< previous page
page_111
next page >
< previous page
page_112
next page > Page 112
The Output Menu The Output menu lets you determine where DDESpy sends its output; this output can be sent to a window, a second monitor/debugging terminal, or to a file. The menu is shown in Figure 5.11. When the File . . . command is selected, a dialog box appears as shown in Figure 5.12.
Figure 5.11 The DDESpy Output Menu
Figure 5.12 The Output Menu File Command Dialog Box When a valid filename is accepted, DDESpy will prompt you for an output filename whenever the application is restarted. The rest of the Output menu options are pretty straightforward. Select output destination (Debug Terminal or Screen). Clear the display window. Add text markers to the display/output file. This makes finding a DDE event in the output file much easier.
< previous page
page_112
next page >
< previous page
page_113
next page > Page 113
The Monitor Menu This menu is where you specify the types of DDE information for display or file storage. The full menu is shown in Figure 5.13.
Figure 5.13 The DDESpy Monitor Menu The Monitor menu gives you access to five types of DDE data. 1. String handle data 2. DDE messages sent 3. DDE messages posted 4. Callbacks 5. DDE Errors DDESpy has an interesting scheme for monitoring. The DDE protocol exchanges information via shared memory, with the memory contents dependent on the transaction in question. This shared memory forms the basis of the Dynamic Data Exchange Management Library (DDEML) protocol. There are several internal structures that allow DDE-based applications to display the contents of these structures, which is how the information gets back to the developer. String Handle Data Sometimes you just want to peek at the string that DDE is passing. For this, DDESpy uses a dedicated structure called MONHSZSTRUCT. This structure is
< previous page
page_113
next page >
< previous page
page_114
next page > Page 114
used to pass string handle data, from which DDESpy reports the following information. The string handle. The contents of the string. The application instance. The "time" at which the DDE event occurred. The structure is prototyped as typedef struct tagMONHSZSTRUCT { UINT cb; BOOL fsAction; DWORD dwTime; HSZ hsz; HANDLE hTask; TCHAR str[1]; } MONHSZSTRUCT;
The members are pretty basic. The cb member is the size of the structure, in bytes. The fsAction member specifies the action taken on the string. This can be one of the following values. MH_CLEANUP indicates the application is freeing its DDE resources. MH_CREATE indicates the application is creating a new string handle. MH_DELETE indicates the application is deleting a string handle. MH_KEEP indicates the application is increasing the usage count of a string handle. The dwTime member reflects the "time" at which the fsAction event occurred. The "time" is measured in the number of milliseconds that have elapsed since the system was last booted. The hsz member identifies the string. The hTask member is the application instance that launched the fsAction event. Lastly, the str member points to the string itself (that is, the hsz member).
< previous page
page_114
next page >
< previous page
page_115
next page > Page 115
Here is an example of DDESpy output using this structure. Task:0x94f, Time:438700, String Handle Created: c0fa(my string 1) Task:0x94f, Time:438920, String Handle Created: c0fa(my string 2)
Monitoring Sent and Posted DDE Messages DDESpy uses the MONMSGSTRUCT structure to display information about DDE messages that are sent. This structure is prototyped as follows. #include typedef struct tagMONMSGSTRUCT { /* mmst */ UINT cb; HWND hwndTo; DWORD dwTime; HANDLE hTask; UINT wMsg; WPARAM wParam; LPARAM lParam; } MONMSGSTRUCT;
The cb member is the size, in bytes, of the structure. The hwndTo member is the handle of the window that is receiving the DDE message. The Windows time at which the message was sent is in the dwTime member. The hTask member is the application instance that owns the window receiving the DDE message; the message itself is in the wMsg member. The wParam and lParam members are the corresponding components of the DDE message. Using this structure, DDESpy reports to you all the salient facts about the posted DDE message. The following is an example of the output Task:0x8df Time:342402 hwndTo=0x38dc Message(Sent)=Initiate: hwndFrom=9224, App=0xc35d("Server") Topic=* Task:0x94f Time:342457 hwndTo=0x2408 Message(Sent)=Ack: hwndFrom=9396, App=0xc35d("Server")status=c35d(fAck fBusy) Topic=Item=0xc361("System")
This structure is also used for monitoring posted DDE messages.
< previous page
page_115
next page >
< previous page
page_116
next page > Page 116
Monitoring Callbacks Since DDEML has application programming interface (API) elements such as DdeCreateDataHandle() and DdeGetLastError (), the API functions can be used to write "helper functions" that monitor DDE activity on the host machine. If you plan to do this, then your monitoring application will have a DDE callback function. This function gets a message whenever a DDE event occurs, so you can definitely get debugging information in a hurry. When monitoring callback functions, DDESpy (and your custom application) uses the MONCBSTRUCT structure to report data about the current transaction the task, time, transaction type, and more. This structure is prototyped as follows. #include typedef struct UINT WORD DWORD HANDLE DWORD UINT UINT HCONV HSZ HSZ HDDEDATA DWORD DWORD } MONCBSTRUCT;
tagMONCBSTRUCT { /* mcbst */ cb; wReserved; dwTime; hTask; dwRet; wType; wFmt; hConv; hsz1; hsz2; hData; dwData1; dwData2;
The cb member is the size of the structure, in bytes. The wReserved member is reserved for Microsoft-specific purposes. The dwTime member is the Windows time at which the transaction occurred. The hTask member is the application instance that contains the callback function receiving the transaction, and dwRet is the return value from the callback function. The wType member is the transacton type, and wFmt is the format of any data exchanged during the transaction. The hConv member is the identifier of the conversation in which the DDE transaction occurred. A conversation is simply a
< previous page
page_116
next page >
page_117
< previous page
next page > Page 117
full-duplex data exchange (that is, data flows in both directions simultaneously). The hsz1 and hsz2 members are string identifiers. The hData member is the identifier of any data exchanged during the conversation. Notice that it has the data type HDDEDATA. This is the handle of the DDE object that gets passed. Finally, the dwData1 and dwData2 members piggyback any additional data. Here is a sample of callback activity. Task:0x8df Time:1572158 Callback: Type=Advstart, fmt=0x1("CF_TEXT"), hConv=0xc24b4, hsz1=0xc361("System") hsz2=0xc4df("xxcall"), hData=0x0, lData1=0x83f0000, lData2=0x0 return=0x0
DDE Errors DDESpy, and any custom monitoring functions you write, use the MONERRSTRUCT structure to give you feedback. This structure is probably the one you'll use the most in tracking down DDE bugs. When a DDE transaction error occurs, an error code with additional information gets written to this structure. DDESpy uses this to report the following about the error in question. The handle of the application that generated the error. The time of the error. The error code and name. The structure is prototyped as typedef struct tagMONERRSTRUCT UINT cb; UINT wLastError; DWORD dwTime; HANDLE hTask; } MONERRSTRUCT;
{ /* mest */ // size of the structure // error code // Windows "time" of error // app. instance
The important member here is wLastError; the rest are identical to the other structures covered previously. When a DDEML function fails, the DdeGetLastError() function returns the error value and then resets the error condition flag to DMLERR_NO_ERR. Here's a closer look at this useful debugging aid.
< previous page
page_117
next page >
< previous page
page_118
next page > Page 118
DdeGetLastError() The function prototype is UINT DdeGetLastError(DWORD idInst)
As you see, it returns a UINT and takes the application-instance identifier as a parameter. The error code returned by DdeGetLastError() must be obtained by first calling DdeInitialize(). Appendix C lists the DDEML error codes returned by DdeGetLastError(). The Track Menu The Track menu allows you to specify which DDE you wish to track using DDESpy. You can track via: String handles Links Conversations Services As you specify these activities from the menu bar, DDESpy creates a child window for the activity being tracked and displays the information therein. One caveat: any DDE events that occur before the window gets created will not show up in the tracking window. You can also sort the data in the tracking window by selecting the desired column heading. This can make it easier to find a particular event or string handle. The Profiler Strictly speaking, the Profiler is not a debugging tool. It's actually a powerful analysis tool that allows you to examine the run-time behavior of your application. By using this information, you can ascertain which portions of your code are working efficiently and which ones need closer scrutiny. It can also show you ''orphan code" that is, code that is not even being executed. This diagnostic information alone makes it worthy of mention as a resource for writing solid, bug-free code. For example, by eliminating orphan code, you can significantly reduce maintenance time, especially if
< previous page
page_118
next page >
< previous page
page_119
next page > Page 119
someone other than yourself maintains the code. Get rid of it; it's one less thing that can go wrong. Running the Profiler should be deferred until late in the development cycle to determine if an algorithm is effective and is covering all possible data cases. As such, it should be used in conjunction with scripted test programs and so forth. You enable profiling by checking the check box Enable profiling on the Link tab of the Project Settings dialog box. The Process Viewer This tool is used to view information about all the running processes, single or multithreaded. It provides a safe way to kill a process that "hangs," as in an infinite loop or some other abnormality. The utility is in your BIN\WIN95 directory and is called PVIEW95.EXE. Doubleclick it, and you'll see a window similar to the one shown in Figure 5.14.
Figure 5.14 The Process Viewer Window
< previous page
page_119
next page >
< previous page
page_120
next page > Page 120
The ErrLook Utility Visual C++ v5.x includes another tool called ErrLook. You will find ErrLook.exe in your \vc\bin\win95 directory. Although it isn't exactly a debugging tool in the sense that it helps you find bugs, it does provide a useful function. This tool is a quick error lookup utility: you enter the value of the error, and it returns descriptive text for that error number. The ErrLook window is shown in Figure 5.15.
Figure 5.15 The ErrLook Window In addition to retrieving system error messages, the tool can retrieve module error messages. Either you can manually enter the value from the keyboard, or you can paste it in from the Clipboard. Also, you can drag and drop the value (hexadecimal or decimal) from the Developer Studio debugger or any other application that supports OLE. You can use the standard Clipboard accelerator keys for Cut (Ctrl+X), Copy (Ctrl+C), and Paste (Ctrl+V) inside the edit box for the value or for the text retrieved by ErrLook. The WinDiff Utility Visual C++ includes a utility called WinDiff that is used to find the differences between two files. While this falls more within the genre of project management instead of an actual debugging tool, you're sure to find it useful. This tool's window and menu bar are shown in Figure 5.16. With WinDiff, you can compare and modify the contents of two files, or two directories, using a graphical interface. The utility prompts you to enter the names of the files/directories using the File Open common dialog. You can
< previous page
page_120
next page >
< previous page
page_121
next page > Page 121
Figure 5.16 The Windiff Window and Menu Bar view the files with or without line numbers. Notice that one of the options on the menu bar is Expand, and there is a toolbar button with the same label. When you compare two files and click the Expand button, the files' contents are expanded, and the button label changes to Outline. When one file is expanded, differences between the two files show up in color, which makes it easy to find where and how the files differ. This can be invaluable if you don't have Code Management System software, and you're backing up your files manually. Should any discrepancies occur, you can use WinDiff to find the correct version of the file to use. Chapter Summary As you have seen, there are a lot of additional tools available to assist you in managing your project and for finding bugs. These include Trace, Stress, Spy++, Browse, DDESpy, and others. You should become familiar with them and their capabilities, because they are wonderful time-savers when it comes to finding and minimizing program bugs. In the next chapter I'll give you a look at commercial debuggers, plus some handy software that eases the burden of managing your projects and bugs.
< previous page
page_121
next page >
< previous page
page_123
next page > Page 123
Chapter 6 Commercial Debuggers and Project Tools In this chapter we're going to take a look at some additional software to help in the fight against bugs, as well as some of the software that often accompanies product delivery help files, tutorials, documentation, setup kits, and so on. Other topics to be discussed include project management and testing software. These tools can be invaluable in disaster recovery, as well as provide an audit trail of activity during the development life cycle. Such audit trails can be useful in determining when a problem was first introduced into the program. Finally, we'll look at code inspection tools; these are used to provide hints and suggestions that can improve your code. The overall goal of these tools is the same. Prevent failure. Reduce development time. Increase portability. Increase customer satisfaction. Lower development and maintenance costs.
< previous page
page_123
next page >
< previous page
page_124
next page > Page 124
As a disclaimer: my direct, extended experience with commercial tools has been limited, so I really can't (and wouldn't) make any recommendations regarding these products. My intention is rather to make you aware of various types of products, as well as vendors who can support your debugging objectives. BoundsChecker, Visual C++ Edition BoundsChecker, from NuMega Technologies, is a smart debugger that integrates seamlessly into the Visual C++ workbench. Once it is installed, you'll find a menu option called BoundsChecker on your menu bar. One of the features of BoundsChecker is called ActiveCheck. You still run your code as before under Developer Studio, but with this feature enabled, ActiveCheck runs in the background and examines how your code calls APIs and uses memory and COM interfaces and methods. All this is accomplished without any special compiling, linking, or specialized operations. It also checks APIs obtained from other vendors, even without the source code. This debugger offers the developer quite a bit of horsepower when you stop and think about it. BoundsChecker boasts that it delivers more API validation than any other debugging tool. The types of errors ActiveCheck will catch include: Invalid and out-of-range parameters Invalid number of parameters Invalid return values Invalid and/or conflicting flags Uninitialized fields Bad pointers When an error is trapped, BoundsChecker opens a window that gives you detailed information about the error, and it highlights the source code line that originated the error. There are five buttons in this window: Acknowledge, Halt, Debug, Explain, and Suppress. The Acknowledge button tells BoundsChecker you recognize the error, but you wish to continue finding more. The Halt button stops the bugfinding process, and Debug starts up the Visual C++ debugger and goes right to the offending line. The Explain button gives you more information about the error and its causes. It even gives you a brief snippet
< previous page
page_124
next page >
< previous page
page_125
next page > Page 125
of code that shows you how to do it right. Finally, the Suppress button lets you ignore the error and customize the error categories you wish to check. Another nice feature is the FinalCheck option. This bug-finder is used: Prior to a build When source code is checked back into the CMS On subcomponent testing When project milestones are reached This feature is great for preventing errors from being introduced into the overall project. Another nice touch is BoundsChecker's error-reporting capabilities. You can view errors during the debugging session in a pop-up window as they occur, or you can defer reporting until the end of the session. The Error Detection settings let you customize the level of error detection, which errors to search for, what to suppress, and so on. This extends not only to your source code, but even to DLLs and executeables. Also, you can have BoundsChecker profiled for either Windows 95 or NT (which have different Win32 behavior and implementation) by using the Event Compliance and Program Compliance reporting options. This goes a long way toward giving you that warm feeling that your code will work on both platforms. CodeWizard CodeWizard, a product from ParaSoft, Inc., is a code-inspection utility. It's not a compiler, parser, or syntax checker; instead, it looks at your code and offers recommendations for improvement. You might think of it as a specialized form of the old Lint program. For example, CodeWizard will emit messages regarding the use of the new operator but assumes you will consult the documentation for a full explanation of how to use new. It will not report rule violations if your code is syntactically incorrect. CodeWizard is available for a number of platforms, including Visual C++. The main advantage of using CodeWizard is that it gives the programmer guidelines that make C++ programs more understandable and less error-ridden. These guidelines focus mainly on effective code design strategies and the misuse of specific language features. In this sense, it can be thought of as a CASE (Computer-Aided Software Engineering) tool for software design. And, as I have mentioned, design is the single most important step in the overall development cycle. There is a vast difference between knowing what you want your product to do and how you plan to do it.
< previous page
page_125
next page >
< previous page
page_126
next page > Page 126
Another feature of CodeWizard is that it encourages you to use only logical, portable C++ coding techniques that are compliant with ANSI/ISO standards. While this might seen a bit restrictive, there are benefits. For example, consider the case of passing an object by reference instead of by value, wherein the object is a parameter and a return value. Now, both of the object's copy constructors are called, and when the function returns, both destructors will be called, too. If the original object has inherited objects that have their own constructors and destructors, these must be called in their turn. But if the object is passed by reference, no constructors and destructors are called, because no new objects are created. This is the sort of thing that CodeWizard will point out to you. Other problems it catches are those related to memory (heap) corruption, dangling pointers, and ambiguous initializations. To be used effectively, ParaSoft recommends that CodeWizard be integrated throughout the entire development life cycle. The earlier it is used, the greater the benefits. Using CodeWizard The CodeWizard installation procedure for Visual C++ v5.0 does everything for you except for two small steps. 1. You must export your project's makefile by selecting Project:Export Makefile from the menu bar. 2. You should install the CodeWizard toolbar. The first step is mandatory. You can automatically force your makefiles to be exported by specifying a setting in your Visual C++ workspace. First select Tools:Options . . . from the main menu. Then click on the Build tab, and check the box labeled Export makefile when saving project file, as shown in Figure 6.1. The second step is optional but recommended, since it provides a shortcut to the CodeWizard commands. The toolbar is shown in Figure 6.2. There are four commands that you can run, corresponding to the icons on the CodeWizard toolbar. These are as follows: 1. Analyze File, which inspects the current source file. 2. Analyze Modified, which inspects all files modified since the last build. 3. Analyze All, which inspects all the files in the project. 4. Control Panel, which lets you modify the CodeWizard settings.
< previous page
page_126
next page >
< previous page
page_127
next page > Page 127
Figure 6.1 The Build Dialog Box
Figure 6.2 The CodeWizard Toolbar To add the CodeWizard toolbar to your Workbench, simply follow these steps. 1. Select Tools:Customize . . . from the main menu bar. 2. Select the Add-ins and Macro Files tab. If your property sheet has only three pages and not five, you are using Developer Studio and not Visual Studio 97 and will not be able to add the new icons. 3. Choose Browse . . . and load the CWStudioBar.dll file from the CodeWizard installation directory. Then click Close. The new dockable toolbar with the four icons will appear.
< previous page
page_127
next page >
< previous page
page_128
next page > Page 128
CodeWizard also installs several new tabs in your Output window; when you use CodeWizard, the output will appear in these tabbed windows. Code Management Systems For team development in a network environment, a Code Management System (CMS) is essential for maintaining the integrity of the source code. Using a CMS is like going to the library: you check out a book, read it, and return it. No one but you has access to the book while it is checked out. A CMS is the same, except instead of managing books, it manages source files. The process of "checking out" the file generates an audit trail; the project manager knows who has the file, or who had it last. Versions can be established to maintain safe "fall back" points. One of the leading CMS tools for Visual C++ is Visual SourceSafe, a product from Microsoft that integrates nicely into the Workbench. SourceSafe Most code management systems treat a project as a set of individual source files but sometimes fail to establish relationships between them. For large projects that might consist of several executables and DLLs that might be used by several additional applications, managing the source code can be a problem. SourceSafe addresses this problem by combining source code control with the basic tasks of project management. It stores files on the network in a central database rather than a DOS directory. This database contains all the source files and their histories, organized into project hierarchies. When someone "checks out" a source code file, it is marked as such in the database, and other developers are blocked from updating the file until it is "checked in." At this time, SourceSafe updates its database and marks the file's access privilege on your machine back to read-only. As this process continues over the life cycle of the project, and files are added, modified, deleted, and so on, the file and the project's history are updated in the database. This gives you a complete audit trail of what has happened. Having a handle on the project's history allows you to accomplish the following tasks. View the file status of a project and its subordinate projects before building. Maintain shared files across applications. Recreate any previous version of the application.
< previous page
page_128
next page >
< previous page
page_129
next page > Page 129
Ascertain which projects are affected by shared files. Manage specialized versions of an application. Performing these tasks using a file-oriented system would be a huge undertaking, but they are easily handled by SourceSafe. Building the Project With a CMS, you must ascertain that no files are checked out before you build a project. Visual SourceSafe makes this fairly easy. It can create a report of all files that are checked out from the repository. This is a nice feature when there are subprojects that constitute the current project. Using the generated report, you can quickly and easily determine with whom you'll need to speak to get the files checked back in and then proceed with the build. Regressions The regressive quality of SourceSafe is an excellent feature for tracking down bugs introduced between builds. For example, suppose a program feature was working properly last week but breaks in the current build. Clearly, someone has changed something that introduced this bug. But where? With most CMS systems, you would create a history report on a suspected file, see if it has been changed since the last build, and see if this change was the source of the bug. You might have to repeat this several times, and even then, you may not find the bug (perhaps the bug was caused by someone adding or deleting a file). But SourceSafe generates a report on the project, not just on files. For example, it might report that FILE2.CPP was just changed and before that, FILE1.CPP; and before that, FILE3.CPP was added to the project. SourceSafe collates all these changes that would normally have to be manually sorted, saving you time and effort. Creating Previous Versions In any project, it is often necessary to be able to recreate a previous version. For example, if a bug is reported in version 3.02 of your software, and you're preparing to ship version 3.03, you can still rebuild the earlier version to confirm the problem. With other CMS systems, you usually have to archive the source code for the complete version(s) or track the specific file versions for each release. In either case, this is a tedious chore that is time-consuming and resourceintensive.
< previous page
page_129
next page >
< previous page
page_130
next page > Page 130
Maintaining Reusable Code Many projects are organized around a central core of files that form the basis of the application. These files evolve over time and are reused over and over again. Regardless if the change is made to fix a bug, enhance performance, or add a new feature, managing the code is a vital part of your operations. You need to know which applications are using which files and make sure changes are made in all the right places. This isn't so difficult, as long as only a small number of applications reuse one file; but when 20 or 30 applications reuse 20 or 30 files, you have a real problem on your hands. Again, SourceSafe makes light work of this problem. It stores each file only once in its internal database, and each project that uses the file has a pointer to it in this database. All versions of the file are also available to each project, and the version can be ''frozen" to avoid introducing bugs while someone else works on the reused code. This also works in reverse: a bug fix in one project will be propagated to all the other projects that use the file. For example, suppose you have a source file that handles all your report printing; all your applications that print reports thus share this file. A bug fix in this one common file is instantly reflected in each project that uses the file. SourceSafe can report which projects use the file, so you immediately know what projects must be rebuilt. Program Testing Program testing is one of the final steps in the application development cycle. Testing validates the quality of the application and helps you find bugs that might otherwise be found by the user community. This helps increase the consumer's confidence in the application and can help to establish market dominance. Thorough testing before shipping the product pays dividends by lowering overall product cost; it saves time and money by reducing the need to rework the product many times. However, developers are, well, developers. They're not all that interested in testing, preferring to focus instead on the nuts and bolts of the project. Also, applications almost never meet design specifications on the first cut, so testing must be repeated. As bugs are found, you have to devise new tests to verify that your bug fixes work, and previous tests must be run to confirm that your bug fixes haven't broken the code in some other place. As you can see, testing and subsequent retesting can eat up a lot of time and money. Automating the test process is a cost-effective alternative, as it
< previous page
page_130
next page >
< previous page
page_131
next page > Page 131
offers advantages over traditional testing methods. The traditional approach usually relies on someone sitting down and going through the program, trying out various keyboard and mouse commands in a somewhat random fashion. This can cause the tester to miss potential problems and discrepancies between actual results and expected results. Another testing technique is beta testing. In this scenario, the application is tested by a diverse group of users who work in real-world situations but who are unfamiliar with the application. While this generally does uncover more bugs, it too is a random process, and there is often a large time gap between the finding of a bug by beta testers and when these findings are returned. For these and other reasons, automated testing offers the most bang for the buck. Advantages of Automated Testing There are several compelling reasons to make the decision to use automated testing. First, it saves you time; in fact, it can even buy you time. Developer manpower can be allocated to fixing bugs or writing documentation instead of spending time testing. Traditional testing is labor intensive, and each test repetition takes the same amount of time to complete. But automated test scripts, while they take longer to create, can be executed without human intervention. Another advantage of automated testing is cost savings. Typically, software products require multiple repetitions of the testing. With automated testing, you will generally pass the break-even point in labor costs after two to three repetitions of the testing cycle. This reduction in costs means you can test more things more often, and this improves quality. Also, automated testing can be configured to run unattended at night, freeing up machines for use during the day. Another advantage offered by automated testing is that it produces results faster. The basic reasons are as follows. Machines can enter keystrokes faster than a human. Machines can compare results faster than a human. Machines can run 24 hours a day. Also, a test suite can be spread across multiple machines and run in parallel. Thus, a test that once took three to four weeks can be completed in two to three days.
< previous page
page_131
next page >
< previous page
page_132
next page > Page 132
Drawbacks of Automated Testing Automated testing does have some drawbacks, although they are outweighed by the advantages. The main drawbacks are as follows. 1. A testing suite must be designed. 2. You have to learn how to use the testing software. 3. You must have a functional product to test. 4. Automated testing software is slightly expensive. On the bright side, you have to deal with items 1 and 3 whether you use automated testing or not. Also, you'll always have to do some beta testing just to get some human response. For example, a machine can't tell you it doesn't like a certain color scheme or button layout. Microsoft Visual Test Visual Test from Microsoft is one of the premier software-testing packages for Windows applications. In fact, Microsoft uses it to test its own products. To use it, you'll need the following system requirements. 386 or better processor running Windows v3.1, v3.11, Windows 95, Windows 98, or NT v3.x 2Mb of free memory 4Mb of free disk space 1.44Mb floppy drive or a CD-ROM drive Mouse or compatible device (trackball, pointing stick, touchpad) Visual Test can also run on a RISC machine under NT, on MIPS, and on the DEC Alpha AXP. It also supports any NetBIOS-compatible network. Visual Test can be used to test any Windows application or component such as DLLs, APIs, DDE, and OLE. It requires no special hooks, debug code, or special hardware. It can test any application created by other languages and by tools from various vendors, just so long as they conform to Windows conventions.
< previous page
page_132
next page >
< previous page
page_133
next page > Page 133
Capabilities The Windows environment is unique in that more than one window can be open, and each window can be resized, moved, and so forth. And the programs can be manipulated with either a pointing device or the keyboard. Visual Test addresses these operating conditions by its ability to simulate keyboard input and mouse movement. It can also cope with subtle differences between machines such as monitor resolution and windows that change their size, location, and color. Visual Test can intercept general-protection faults and other unexpected messages from other applications or from Windows itself. The test author can indicate what the test script should do when such things happen for example, log and continue or end the test. The ability to log testing events allows you to find bugs and make corrections, and recovering from unexpected events allows the test to proceed unattended. Another capability of Visual Test is that it gives you control over the timing of events; that is, it can verify that an event occurred within a given time. For instance, if you want to be sure that a dialog box opens within 10 seconds of an event (keystroke or mouse click), you can configure the test script to wait 10 seconds and then log a failure. The logging information can include an error code that identifies the error. This timing feature can also be used to adjust the speed of the test. For example, you can prevent the input buffer of the application from being flooded with events during the test. Customization Customized functions can easily be added to extend the capabilities of Visual Test by using DLLs. For example, you can write your test scripts in C instead of BASIC. Any language capable of calling a DLL can be used as a front-end for Test. Summary of Microsoft Test Microsoft Test has many appealing features. It's easy to learn and use. It supports unattended testing. It does not require a separate test script for screens of different resolutions.
< previous page
page_133
next page >
< previous page
page_134
next page > Page 134
If you're planning on writing professional, high-quality software, you will need testing software. It more than pays for itself in terms of saved time and improved product quality. BugCollector Pro Not only do you have to detect, isolate, and resolve bugs during software development, you must also track and manage them. BugCollector Pro v2 from Nesbitt Software (www.nesbitt.com) is a tool that allows you to do just that. Figure 6.3 illustrates the main window of the program.
Figure 6.3 The BugCollector Pro Window The program uses the Microsoft DAO Jet engine to store the information you collect on bugs in a database. The features of the tool are customizable and extendable. The program works pretty much as follows.
< previous page
page_134
next page >
< previous page
page_135
next page > Page 135
1. Open a project. 2. Assign bug IDs and descriptions. 3. Assign additional properties like severity, date reported, steps-to-duplicate, and so on. While the program comes with most of the properties predefined (such as bug severity), you can add your own. Another great feature, especially from the perspective of project management, is the reporting capability of the tool. You can also create graphs of such things as: Time lapse between bug assignment and resolution Status Priority Fixed Verified Reports are generated using the Crystal ReportsTM engine and can be exported to a variety of formats, including HTML. These features go a long way toward better management of your entire project. But the best feature of the program, in my opinion, is its ease of use. You should be able to start using the tool right out of the box, because the product is very intuitive. Plus, there is "bubble help" on virtually all the controls/components, and many contextsensitive menus are provided. Having a single repository of all your bugs and feature requests should help you sleep better, especially if you're the project leader. A single-user license sells for $149.00 (U.S. dollars in 1998), and multipleuser versions are available. Support Software In this section, I want to review some of those oft-forgotten or neglected items that accompany virtually every software product: Setup kit User's Guide Help file Tutorial
< previous page
page_135
next page >
page_136
< previous page
next page > Page 136
Just as you must debug your application, you must ensure that these support files are accurate and bug-free. You may write a great software package, but if the installation setup doesn't work properly, or the Help file has the wrong image in a topic, the end result is the same. As the old saying goes, perception is reality. If your support software looks bad, your product will be perceived in the same way. Common Errors Mercifully, most of the errors you might encounter with support software are easy to find and remedy. With setup disks, the biggest culprit is forgetting a file. If you build under Visual C++, don't forget you'll need the various MFC run-time libraries. Whenever possible, you should test your setup on an independent platform. It's best even to format the disk and reinstall the operating system; if you're missing a file, you'll hear about it! The User's Guide is a horse of a different color only laborious proofreading works here. When feasible, have someone with only basic computer skills go through the setup and the User's Guide. If they can do it, you can safely assume you got it right. Finally, the Help files and tutorials (which can sometimes be the same) can be a pain. All the links have to jump to the right topic, all the topics need the right image, and so on. To help me with this situation, I update my Help file with a dummy topic whenever my code accesses the Help file. Later, when the project has finished changing (if that ever happens), I or the technical writer can put in the Help topic material. I use a header file (usually called helpids.h) for this, and define all my topics in it. #define #define #define #define #define #define
IDH_CONTENTS IDH_EDIT_PROJ IDH_NEW_PROJ IDH_SUPPORT IDH_GEN_DATA IDH_CUSTOMER
100 210 220 230 240 250
Whenever I add a Help button to a dialog box, I add a message handler for a mouse click and call WinHelp with the appropriate topic. For example, if I have a Help button in a dialog box that prompts the user to supply data about a customer, I insert the following boilerplate code AfxGetApp()->WinHelp(IDH_CUSTOMER, HELP_CONTEXT);
< previous page
page_136
next page >
< previous page
page_137
next page > Page 137
Next, I build the dummy topic for IDH_CUSTOMER in the .RTF file with an "under construction" message. This makes it easier to come up with the text for that particular Help topic, because you can look at the dialog and understand how it relates to the big picture. And should a last-minute change be made that affects IDH_CUSTOMER, you can easily add a hypertext line (like those famous "See also:" jumps). The User's Guide There are a couple of common-sense guidelines you might want to follow when writing your User's Guide. From the outset, I favor making this an extension of the product's help file. As an example, you'd probably want some sort of brief overview of what the product does. Except in cases of large and complex programs, the User's Guide should be small. When possible, outline the subject matter of the User's Guide before the project kicks off. A boilerplate outline might look something like this: 1. Overview 2. Setup 3. Features and Operations 4. Troubleshooting 5. Technical Support This outline leads the user through using the product in a logical fashion. It starts with what the software does and how to install it. The end is help information, and the middle is where you'll add the material that is related to your specific application. Try to do a "mini-outline" on this section, too; that will help you stay focused on just the basic information required for successful operation. The final, printed format should be black text on white paper; an independent proofreader should go over it before the final printing. Again, try to match the User's Guide to the on-line Help file and HTML documents. Don't reinvent the wheel. Programmer/Project Documentation I've saved the best for last. People who love to write code hate writing documentation. But for large and complex projects, it is vital that the project manager convey to the team that on-going documentation must be
< previous page
page_137
next page >
< previous page
page_138
next page > Page 138
maintained. I've found that if I write out the major subtasks of my portion of the project, I get a clearer view of "the big picture." The main thing is to identify what needs to be done and the approach that will be taken to achieve the goal. Other team members should have access to this type of information, so they can ensure their code will interface with yours, and vice-versa. You should make liberal use of the #pragma message()
compiler directive throughout your source files to alert others to important factors regarding your code. These will change and evolve over time; the project manager may wish to directly authorize the use of this pragma so that all developers working on the project can see these "notes" in the build window of the compiler. Chapter Summary This chapter covered commercial debuggers and project software. If you're seriously considering making a purchase, shop around for a demo disk; most software shops have them on the Internet. The main point of this chapter has been to stimulate your thinking about a lot of the other aspects of project development things like testing and support software. Debugging a project is only one aspect of product delivery, so don't lose sight of that.
< previous page
page_138
next page >
< previous page
page_139
next page > Page 139
Chapter 7 Debugging Database Applications Of all Windows applications in production today, it is estimated that 90 percent interface or interact with databases. The rising proliferation of client-server applications and use of the Internet suggest that using Windows programs to access databases is likely to continue. Visual C++ supports two major database access technologies: Open Database Connectivity (ODBC) and Data Access Objects (DAO). Developing and debugging database applications present unique challenges to the developer. In this chapter, we'll take a look at these issues and explore ways to prevent bugs as well as find and correct them. Although databases are categorized as being either relational or hierarchical, we'll consider only the relational model. But before we get into that, I think it's wise to first take a look at database design. Database Design and Normalization Once again, the best way to debug an application is to make sure you get the design right; that is, don't let bugs get a grip on your application in the first place. First and foremost among the features of database design is the
< previous page
page_139
next page >
< previous page
page_140
next page > Page 140
concept of normalization. Normalizing a database basically means organizing the data (usually by breaking the entire schema up into separate tables) such that the resulting model has the following properties. Redundant data is eliminated. No inconsistencies are produced when the database is updated. Whenever the same data appears more than once in a database, you're wasting disk space, degrading performance, and setting yourself up for a maintenance nightmare: data changed in one place must be changed in all other places, and you have to be sure the data is the same in all locations. This is not a good situation to be in, because it can quickly lead to the worst kind of bug known to man: invalid data. Thus the first goal of normalization is to eliminate redundant data. The second goal of normalization is the elimination of inconsistent dependencies. In most business applications, the value of one column will depend on that of another. For example, suppose your database has two tables: one containing information about your customers and the other containing data about your account representatives who service the customers. You wouldn't put the salary of your account reps in the table containing the customer information, because this could easily lead to inconsistencies. (For example, what if one of your account reps leaves the company and his customers get re-assigned?) Another example might be a table that maintains information about your employee's dependents. This information should be kept in a separate table, and the employee's internal ID (badge number, for example) would be used as the key into the dependents table. So, the goal of normalization should be fairly obvious, as are the benefits. Normal Forms Basic rules regarding normalization have evolved along with the increasing use of databases. Each of these rules is called a normal form. A form of shorthand notation has also been assigned to these forms: 1NF (first normal form), 2NF (second normal form), 3NF, and so on up to the fifth normal form. Most of the time it is sufficient to normalize the database to 3NF. Actually, it may even be possible to ''over-normalize" a database in some situations. Since normalization basically causes a few "wide" tables to be broken up into many "narrow" tables, you could theoretically end up with a performance degradation as more and more tables are joined by foreign keys.
< previous page
page_140
next page >
< previous page
page_141
next page > Page 141
As the rules of normalization are applied, you move to subsequent levels of normalization. Thus, if the first rule is met, the database is said to be in 1NF, If the first two rules are met, it is in 2NF, and so forth. In the following section, I'll examine these rules of normalization and provide examples as necessary. 1NF To get a database in first normal form, you need to enforce these three rules. 1. Remove any repeating groups in individual tables. 2. Place all related data in a separate table. 3. Use a primary key to identify each set of related data. Now for an example. Suppose you're tracking sales by geographical region: North, South, East, and West. Your sales record may contain a field for one of these four regions. But what will happen if your company grows, and you add a Northeast region, or Southwest? Adding a field would require both program and database modifications, and things would get worse if you ever went international. Instead, you should place all regions in their own table, then link all your sales staff to this table via a region key. 2NF A database is moved to second normal form if it is in 1NF and the following rules are applied. 1. Separate tables are created for values that apply to multiple records. 2. These tables are indexed via a foreign key. Continuing with the previous example of a sales force database, consider the customer's shipping address. Your customer's address is obviously needed by a CUSTOMER table but also by your ORDER-ENTRY, SHIPPING, and BILLING tables. There's no use duplicating this address over and over again; just store it in the CUSTOMER table and use a customer code as the key. Now, this key will appear in your ORDER-ENTRY, SHIPPING, and BILLING tables, and any new tables you may create. And, the value (that is, the address) depends only on this customer code (a foreign key).
< previous page
page_141
next page >
< previous page
page_142
next page > Page 142
3NF A database is moved to third normal form if it is in 2NF and the following rule is applied. Eliminate any columns (that is, fields) that do not depend on the key. Any fields that are not part of the record's key don't belong in the table; these should be moved to a separate table. As an example, consider a database used to scout high school athletes. You might thus have a table called PROSPECT with the student's name, high school's name, address, and so on. Now suppose you need a report that lists all the high schools. If the school information is stored with the prospect's information, you can't list all the schools having no prospects for the time being. Here, you would create two tables, PROSPECT and SCHOOL, and link them together via a school code key. Performance Issues As I touched on earlier, you can over-normalize your data in such a way that performance is affected. As a rule of thumb, you should focus your normalization on data that changes frequently. For example, suppose sales reports are sent to the corporate office on a weekly basis. The sales data changes daily, but the customer data, store data, and so forth, don't really change that much. So while you could get carried away creating tables for cities, ZIP codes, sales regions, and so on, you may find it better to devote your efforts to the data that changes the most frequently. Even if you end up with some dependent fields in your database, you can always have your application ask the user to verify this type of data whenever it's changed. Normalization Examples Let's take a look at a real-world scenario and apply some normalization guidelines. We'll consider a student registration database, and look at the database from both the unnormalized and unnormalized perspectives. This will be like a "before" and "after" picture. Students have ID numbers and are assigned an advisor who has an office. Students can take one of three classes: math, physics, or chemistry. The unnormalized table would appear as follows.
< previous page
page_142
next page >
page_143
< previous page
next page > Page 143
Student_ID
Adv_Name
Adv_Office
Math
Physics
1000
Johnson
500
M-101
P-101
2009
Wilson
660
3005
Adams
105
Chemistry
P-200 M-105
C-102
Let's get this in 1NF by moving all the repeating groups out to a separate table. This means the classes that the student can take need to be moved. So, we'll keep the Student_ID, Adv_Name, and Adv_Office fields, and add a new one called Class_No.
Student_ID
Adv_Name
Adv_Office
Class_No
1000
Johnson
500
M-101
1000
Johnson
500
P-101
2009
Wilson
660
P-200
3005
Adams
105
C-102
Now we've got a better arrangement: we got rid of three columns and replaced them with one. Now let's go for 2NF by eliminating redundant data. In the preceding table, there can be multiple Class_No values for each Student_ID field; that is, the same student can sign up for three different classes. Because Student_ID is the primary key, but Class_No is not functionally dependent on it, the table is clearly not in 2NF. To make it 2NF, split this table into two separate ones such that the first table contains the columns Student_ID, Adv_Name, and Adv_office, and the second table has Student_ID and Class_No.
STUDENTS TABLE Student_ID
Adv_Name
Adv_Office
REGISTRATION TABLE Student_ID
< previous page
Class_No
page_143
next page >
page_144
< previous page
next page > Page 144
To normalize this database into 3NF, it's necessary to eliminate data not dependent on the key. Now, the Adv_Office column is functionally dependent on the Adv_Name column. The solution is to create a table for the faculty. You may have noticed a pattern here: we keep moving those items that are functionally related into their own table. So, the FACULTY table becomes as follows. FACULTY Adv_Name
Adv_Ofice
Dept
There are actually six primary normal forms; these are the first through the fifth normal forms plus another form called BoyceCodd. A form of shorthand has evolved for these six forms: 1NF, 2NF, 3NF, 4NF, 5NF, and BCNF. Consult any good book, or even the Visual C++ on-line help, for more information on these additional normal forms. In the real world, you'll be lucky to get your customer's data into 3NF, so the others won't be covered. ODBC ODBC stands for Open Database Connectivity. It is a set of application programming interfaces (APIs) which allow you to access any database management system (DBMS) for which an ODBC driver exists. The interface is based on the Call Level Interface of the SQL (Structured Query Language) Access Group and hence provides an open, vendor-independent method for accessing data. This interface obviates the need to learn multiple APIs on behalf of the programmer. Your application simply uses this API to call the ODBC Driver Manager to request an operation, and the Driver Manager passes the request on to the driver. It's the driver that executes the SQL command. By using ODBC, an application can concurrently access, view, and update data for multiple, and even different, databases. An ODBC-based application can literally "unplug" from one database (for example, Oracle) and "plug in" to another (for instance, Access) so long as the database schemas are identical (and of course, an ODBC driver exists for the target database). This process effects a substantial reduction in effort on the behalf of developers.
< previous page
page_144
next page >
< previous page
page_145
next page > Page 145
DAO DAO stands for Data Access Objects and is a proprietary technology developed by Microsoft for its Jet database engine. DAO interacts with the Jet database engine to give you a set of data access objects such as database objects, tabledef objects, recordset objects, and so on. It works best with .MDB files such as those used by Microsoft Access; however, you can still access ODBC data sources using DAO and the Jet database engine. The CDaoErrorInfo Structure When you use DAO for database operations, a CDaoErrorInfo structure contains information about an error. Instead of encapsulating DAO error objects in a class, class CDaoException provides an interface for accessing the Errors collection found in the DAO DBEngine object. When a CDaoException is thrown that your code catches, MFC fills in this structure and stores it in the exception object's m_pErrorInfo member. In contrast, if you call DAO directly, you'll have to call GetErrorInfo() yourself to fill in the m_pErrorInfo member. The structure is defined as struct CDaoErrorInfo { long m_lErrorCode; CString m_strSource; CString m_strDescription; CString m_strHelpFile; long m_lHelpContext; };
The members are as follows. m_lErrorCode, a numeric DAO error code. m_strSource, the name of the object or application that originally generated the error. The Source property specifies a string expression representing the object that originally generated the error; this usually is the object's class name. m_strDescription, a descriptive string associated with an error. m_strHelpFi1e, a path to a Microsoft Windows Help file. m_lHelpContext, a context ID for a topic in a Microsoft Windows Help file.
< previous page
page_145
next page >
< previous page
page_146
next page > Page 146
Comments Information retrieved by the CDaoException::GetErrorInfo() member function is stored in a CDaoErrorInfo structure. Examine the m_pErrorInfo data member from a CDaoException object that you catch in an exception handler, or call GetErrorInfo() from a CDaoException object that you explicitly create to check errors that might have occurred as a result of a call to the DAO interfaces. CDaoErrorInfo() also has a Dump () member function in debug builds. You can use this to dump the contents of a CDaoErrorInfo object. Selecting the Database Class Selecting which MFC class to use, ODBC or DAO, depends on the nature of your application and its specific requirements. Guidelines to assist you in selecting which of the two classes to use include the following. Use ODBC if you are working strictly with ODBC data sources that is, where an ODBC driver is known to exist, or, more simply, where your data source does not support DAO. ODBC is also well-suited for client-server applications. If you are working strictly with Jet databases (for example, .MDB files), you should use DAO, because it delivers superior performance. While the list of DAO-based databases is ever-growing, the following are some of the databases that you can use with DAO. Microsoft Access Installable ISAM databases like FoxPro, dBASE, Paradox (later than version 3) Excel worksheets (versions 3, 4, 5, and 7) Lotus WKS, WK1, WK3, and WK4 spreadsheets Text files For the most part, DAO classes provide more extensive support than ODBC. This includes the ability to directly manipulate the structure of your database directly from the MFC classes instead of calling the underlying implementation (DAO or ODBC).
< previous page
page_146
next page >
< previous page
page_147
next page > Page 147
Database Errors For the most part, regardless of what MFC database class you're using, most database errors fall into one of the following categories. Access violations (e.g., user has no access to the database or table) Syntax errors (e.g., invalid query statement) Data mismatch (e.g., treating a DATE column as TEXT) Invalid query criteria (e.g., no records returned) Invalid joins across multiple tables SQL SQL stands for Structured Query Language. This language can be used with any SQL-compliant database such as Access, Oracle, and so on. SQL calls made inside your application can be caught with try/catch blocks. Consider this example, which assumes a hypothetical database containing customer information. CString csSqlCmd; // assume this has some SQL command CDatabase dbCust; // the database object try { dbCust.ExecuteSQL(csSqlCmd); // execute an SQL command } catch(CDBException* e) { AfxMessageBox(e->m_strError, MB_ICONEXCLAMATION); }
The interesting thing here is the catch block. Class CDBException contains a data member called m_strError, which contains the error message text. If there is an error in the SQL command csSqlCmd, you'll see the error message appear in the message box. In the case of long commands subject to syntax errors, you can also write them out to an error log file and examine them more closely.
< previous page
page_147
next page >
< previous page
page_148
next page > Page 148
SQL Debugging Limitations Now for the bad news you cannot step into SQL code with the debugger unless you have the Enterprise Edition of Visual C++. Even then, the SQL debugger has the following limitations. You cannot debug SQL statements outside a stored procedure. You cannot debug extended stored procedures. These are actually DLLs that are not SQL-based. Real-time modification of stored procedures is not supported. When debugging SQL procedures, do not modify them, since this can produce unpredictable results. You must have a Data View. You cannot use complex break points only the location break point is supported. Because SQL is interpreted, you cannot display register values. You cannot use SQL memory windows. These are reserved for the native application. Cached values cannot be automatically modified, and you can't force a refresh of cached values. As you can see, debugging SQL is a lot more difficult than debugging native applications. You can lessen this difficulty somewhat by testing your SQL statements separately from the code, where possible. This will at least help you find syntax errors and the like. Also, you may be able to create your own error log file and write values to it at different points in the code stream. While this is a "poor person's debugger," it is better than nothing. SQL Data Types Before we get into the nuts and bolts of SQL and database debugging, it may be wise to review some of the core data types used. These are defined in SQL.H; Table 7.1 describes the ones you see and use most frequently.
< previous page
page_148
next page >
page_149
< previous page
next page > Page 149
Table 7.1 The Core SQL Data Types SQL Definition
ODBC Definition
C Definition
SQL_C_CHAR
UCHAR FAR *
unsigned char FAR *
SQL_C_SSHORT
SWORD
short int
SQL_C_USHORT
UWORD
unsigned short int
SQL_C_SLONG
SDWORD
long int
SQL_C_ULONG
UDWORD
unsigned long int
SQL_C_FLOAT
SFLOAT
float
SQL_C_DOUBL
SDOUBLE
double
There is one caveat when passing CString objects to ODBC functions: CString objects are signed but in ODBC functions objects are unsigned. Passing a CString object to an ODBC function without first casting it will generate a compiler warning. SQLError() Chapter 3 discussed an error-reporting function called GetLastError(). There is a similar function for SQL operations called SQLError(). This function returns error, or status, information about the most recent SQL call. The function's prototype is RETCODE SQLError(henv, hdbc, hstmt, szSqlState, pfNativeError, szErrorMsg, cbErrorMsgMax, pcbErrorMsg)
The following list explains the arguments. henv is an input argument of type HENV and is the environment handle. It can also have the value SQL_NULL_HENV. hdbc is an input argument of type HDBC and is the connection handle. It can also have the value SQL_NULL_HDBC. hstmt is an input argument of type HSTMT and is the statement handle. It can also be SQL_NULL_HSTMT. szSqlState is an output argument of type UCHAR FAR*. SQLError() returns the SQL state into this argument. For a list of values, see Appendix A, ODBC Error Codes.
< previous page
page_149
next page >
< previous page
page_150
next page > Page 150
pfNativeError is an output argument of type SDWORD FAR *. It is the native error code (specific to the data source). szErrorMsg is an output argument of type UCHAR FAR *; it's the pointer to the error message text. cbErrorMsgMax is an input argument of type SWORD. This is the maximum length of the szErrorMsg argument and must be less than or equal to SQL_MAX_MESSAGE_LENGTH -1. pcbErrorMsg is an output argument of type SWORD FAR *. This is a pointer to the total number of bytes available to return into the szErrorMsg argument. The szErrorMsg argument is truncated to cbErrorMsgMax-1 if this value is greater than or equal to cbErrorMsgMax. The return values for SQLError() can be one of the following. SQL_SUCCESS SQL_SUCCESS_WITH_INFO SQL_NO_DATA_FOUND SQL_ERROR SQL_INVALID_HANDLE SQLError() does not post errors for itself; that is, you can't use it to find out if there is a problem in your SQLError () call itself. It returns SQL_NO_DATA_FOUND when no error information can be retrieved. In this event, szSqlState equals 00000, which indicates success, so be careful. If it is unable to access error values for any reason that would normally return SQL_ERROR, it still returns SQL_ERROR but does not post any error values (that is, set the szSqlState argument). If cbErrorMsgMax is too small to hold the error message, the return value is SQL_SUCCESS_WITH_INFO, but no error value is posted. You can then ascertain if truncation occurred by comparing cbErrorMsgMax with the length of the text contained in pcbErrorMsg.
< previous page
page_150
next page >
< previous page
page_151
next page > Page 151
Chapter Summary Because something like 90 percent of all applications use databases in one form or another, it's important to be able to prevent, detect, and remedy database-related bugs. Unfortunately, this can be difficult for a variety of reasons, not the least of which is the fact that you're always encountering kernel and database engine functions that you can't ''step into" with a debugger. Again, the best cure here is good design work implemented with a lot of ASSERT and exception trapping. Maybe you can help yourself out with the next chapter, which deals with known bugs.
< previous page
page_151
next page >
< previous page
page_153
next page > Page 153
Chapter 8 Known Bugs and Special Problems It's not a perfect world; there will be times when your code is in compliance with the documentation, but it still won't work. Unfortunately, there are errors in the Visual C++ documentation, the compiler, and the source files. In this chapter, I've put together a list of those that I've encountered, along with suggested workarounds. These errors are based on Visual C++ v5.0. Because a new release is sure to be released before this book goes to press, you should check the Visual C++ readme file for any information regarding the latest fixes. bool Size You might hit this problem if you're porting code to Visual C++ v5 from v4.2. In v4.2, the Standard C++ header files contained a typedef that equated the Boolean data type with an integer, or 4 bytes. In v5.0, this data type is implemented as a stand-alone data type with a size of 1 byte. So, any code that contains a call to sizeof(bool) would return misleading results:
< previous page
page_153
next page >
< previous page
page_154
next page > Page 154
In v4.2 sizeof(bool) = 4
but in v5.0 sizeof(bool) = 1
This can result in memory corruption problems if you have defined structure members of type bool in a Visual C++ v4.2 project and are mixing object files (OBJ) and/or DLLs built with the v4.2 and v5.0 compilers. This has to do with the way the compiler assigns and aligns memory for structures. Non-Integer Divide by Zero Attempting to divide by zero with integer variables throws an exception as advertised, but this is not the case for floats and doubles. Try running the following code snippet through the debugger and see what happens. float x = 10.0; float y = 0.0; x = x/y; TRACE("x = %f\n",x);
Call _findclose() after Calling _findfirst() or _findnext() Always call _findclose() after you are finished using either the _findfirst() or _findnext() file functions. This frees up resources used by these functions in your application. C Runtime _expand() Function Returns NULL upon Failure The memory function _expand() returns NULL when an error is detected during operation. This may happen when there is sufficient memory. The documentation, quoted below, suggests that failures occur only when there is insufficient memory to expand the memory block:
< previous page
page_154
next page >
< previous page
page_155
next page > Page 155
_expand() returns NULL if there is insufficient memory available to expand the block to the given size without moving it. The item pointed to by memblock is expanded as much as possible in its current location. Actually, this is only half true. A NULL return value would be returned if _expand() was used to shrink a memory block when passed an invalid block pointer, or if it detected corruption in the small block heap. The Ternary Conditional Another known bug involves using the ternary conditional with the increment operator (++). The incrementing variable never gets incremented, leaving you to wonder what went wrong. The following code will produce this error. int i,j = 0; for (i = 0; i < 5; i++) { j = (j < 3) ? j++ : 0: TRACE("j = %d\n ", j);
// j never incremented! // watch it in the debugger
}
When you run this code with in the debugger, you'll see that j always stays zero, when in fact it should be incremented three times. The assembly language code generated by the compiler loads the value of j into a register, increments the variable, then incorrectly moves the original value from the register back into the memory address for j. To correct this problem, get rid of the ternary operator altogether.
< previous page
page_155
next page >
page_156
< previous page
next page > Page 156
int i,j=0; for (i = 0; i < 5; i++) { if(j < 3) ++j; else j=0; TRACE("j = %d\n", j);
// watch it in the debugger
}
try Blocks and switch Statements A bug occurs when you use a try block inside a switch statement. The following code illustrates the problem. int i = 0; try { switch (i) try { case 0: throw i; } catch(int) { // Should be caught here but isn't TRACE("You won't see this!\n"); } } catch (int) { // Is caught here incorrectly TRACE("Should not see this, but you do!\n"); }
< previous page
page_156
next page >
< previous page
page_157
next page > Page 157
The compiler doesn't recognize catch blocks when they're inside a switch statement, and thus tries to link the try block with the next catch it sees outside the block. sizeof() and Arrays The sizeof() operator returns a non-zero value for a zero-length array of any data type. A zero-sized array is legal only when the array is the last member of a structure or union. The following is an example of the error. struct MyStruct { int a[0]; }; if((int)(sizeof(struct MyStruct)) != 0) TRACE("Failed-size reported as non-zero!\n"); else TRACE("Passed-size reported is zero\n");
URLMON.DLL This DLL is reported "not found" when you port a Visual C++ v4.1 application with OLE automation or OLE control. AppWizard adds AFXDISP.H or AFXOLE.H to the STDAFX.H header file, and these header files contain the following directive. #pragma comment(lib, "urlmon.lib")
This brings in the URLMON.LIB during linking, and the error message occurs when running on Windows NT v3.51. Commenting the #pragma out will not solve the problem. This is a bug in the debug version of the MFC static library (not the MFC DLL). To solve the problem, use the DLL version of MFC for debug builds and the release version of the static MFC library for release.
< previous page
page_157
next page >
< previous page
page_158
next page > Page 158
Access Violations A handled exception will occur in certain pointer functions for huge memory blocks. The specific functions are IsBadHugeReadPtr() IsBadHugeWritePtr() IsBadStringPtr() IsBadWritePtr() The exception, which is actually an access violation, occurs when the specified memory region is not accessible to the calling process when any of the previous functions try to access it. When executed from the debugger (configured to get the first shot at handling exceptions), the debugger traps the access violation and stops the program. You can then continue stepping through your code, and these functions will return the appropriate value. This can be useful in isolating potential access violations in your code. Typographical Error There is a typographical error in the documentation for the tangent and hyperbolic tangent functions. The sample code in the documentation reads printf("tan( %f) = %f\n", x, y); printf("tanh(%f) = %f\n", y, x);
It should be printf("tan(%f) = %f\n", pi/4, x); printf("tanh(%f) = %f\n", x, y);
The correct output is tan (0.785398) = 1.000000 tanh(1.000000) = 0.761594
< previous page
page_158
next page >
< previous page
page_159
next page > Page 159
Incorrect Documentation for the Clean Command There is also an error in the documentation regarding the "Clean" command. The documentation states: You can remove all files from the intermediate directories in any project configuration in your project workspace. The Clean command does not remove the .PCH file (precompiled header) from your project workspace. You should always delete this file when you really and truly want to do a complete rebuild. This is critical if the project is a server application that has been registered on the system; not deleting the .PCH files means the registry entries will not be cleaned up, too. By design, the .PCH file is not removed, since it is somewhat time-consuming to produce originally. Windbg Cannot Use Visual C++ v5.0 Debugging Information If you're using the Windbg debugger that comes with the NT v4.0 Win32 SDK, you cannot use the debugging information created by Visual C++ v5.0. This is due to a change in the formatting of the debugging information that Windbg cannot read. Error in Release Version of ATL When building a release version of an ATL project and using any of the CRT functions that require CRT startup code, you will get the following error during the link phase. LIBCMT.LIB(crt0.obj) : error LNK2001: unresolved external symbol _main
The error is due to the fact that the release configuration defines _ATL_MIN_CRT, which excludes CRT startup code from being linked to the executable or DLL file. To avoid this problem, you can do one of the following.
< previous page
page_159
next page >
< previous page
page_160
next page > Page 160
Remove _ATL_MIN_CRT from the preprocessor #define statements so that CRT startup code will be included. Use the following steps. 1. Click Settings on the Project menu bar. 2. Choose Multiple Configurations from the Settings For drop down list. 3. In the dialog box that appears, click the check boxes for all release versions, then click OK. 4. On the C/C++ tab, choose General Category and remove _ATL_MIN_CRT from the preprocessor definitions edit box. Remove calls to CRT functions that require CRT startup code, and replace them with their Win32 equivalents. For example, use lstrcat() instead of strcat(). The /WS:AGGRESSIVE Linker Option When building an application for NT v4, use this linker option with your build. This causes the NT v4 loader to recognize the WS_AGGRESSIVE attribute of your application's image. As a result, the loader will aggressively trim the working set of the parent process when it is not active. This is similar to adding the following statement throughout your source code SetProcessWorkingSetSize(hThisProcess, -1, -1)
This option is recommended for applications that need to minimize their effect on the system's memory pool. But if speed is important, you should first test this configuration in the native environment. The best candidates are background processes, such as services and screen savers, and so on. Option Precedence and the CL Environment Variable Take care when you are specifying compiler options from the command line. Compiler options are processed from left to right; whenever a conflict is detected, the right-most option "wins." Also, the CL environment variable is processed before the command line. Thus, if any conflicts are detected, the command line "wins," since it is processed last.
< previous page
page_160
next page >
< previous page
page_161
next page > Page 161
The /Zm Option This compiler option is used to specify the memory allocation limit. The compiler uses several discrete heaps, each with its own finite limit. The size limit for all these separate heaps is about 105Mb, but if any one heap consumes all its memory, the compiler cannot continue. Furthermore, memory is allocated only when needed; the 105Mb limit is in place just to keep from using too much memory. Although exceeding any of the size limits for the discrete heaps generally occurs only with very large or complex programs, you can use the /Zm compiler option to establish a new heap size. For example /Zm190
causes the new heap size total to change from 105Mb to 190Mb. Missing Type Definition Errors For any typelib dependency needed by the compiler for the typelib it is currently processing, the full path specification to the dependency is written into the typelib header file (project.TLH). This path is written by way of comments; if the typelib also has references to types defined in additional typelibs, then the .TLH file comments will appear as in the following example. // // Cross-referenced type libraries: // // #import "c:\mypath\typelib0.tlb" //
The filename in the comment is the full path for the referenced typelib as it appears in the registry. Should you encounter errors due to missing type definitions, check these comments in the .TLH file. The likely culprits are the following syntax errors. C2143
syntax error : missing 'token1' before 'token2'
C2146
syntax error : missing 'token' before identifier 'identifier'
C2321
syntax error: unexpected 'identifier'
C2433
'number' : illegal scale
C2501
'identifier' : missing decl-specifiers
For dependency comments not supplied by system headers, you will have to provide a #import directive somewhere before the #import for the typelib in order to resolve the errors.
< previous page
page_161
next page >
< previous page
page_162
next page > Page 162
Keywords _emul() and _emulu() Not Documented These keywords aren't defined, but you can use them to efficiently multiply two 32-bit integers. The syntax for these are as follows. _int64 _emul(long, long); unsigned _int64 _emulu(unsigned long, unsigned long); Note that the return values are 64-bit values.
The Linker's /OPT:ICF Option This is a really neat feature that I've put to good use to speed up my builds and reduce the size of the executable. This link option removes identical COMDAT records in order to achieve this result. COMDATs are simply ''packaged functions," meaning that the compiler has optimized individual functions and collected them together in one package. The syntax for the option is /OPT:{REF|NOREF|ICF[,iterations]}
Use the /OPT:REF to remove data and/or functions that are never referenced. To keep the data/functions, use /OPT:NOREF. To remove redundant COMDATs, use /OPT:ICF[,iterations], where iterations specifies the number of times to walk the symbol table searching for duplicates. Debugging Windows API Functions with NT Symbols Loaded If you're running both Windows 95 and NT, you can run into problems setting break points on Windows API functions when NT symbols are loaded. To get around this, you will need to use the decorated name of the function while setting the break point. You can determine the decorated name by specifying that a map file be created during linking; the decorated name for the function will appear in this file.
< previous page
page_162
next page >
page_163
< previous page
next page > Page 163
Resolving Error RC2104 This error reports an undefined keyword or key name, MFT_STRING. If you encounter this error, you can correct it by opening the file MCL\MFC\Include\AfxRes.h
and including the directive #include <winresrc.h>
Compiler Warning (Level 4) C4238 This warning happens when the /Ze option (that is, the Microsoft language extensions) is specified and you try to use a class type as a pointer. The warning message is formatted as follows. nonstandard extension used : class rvalue used as lvalue
An example of this follows. struct X { X(); }; X * pX = &X();
This usage is permitted with the /Ze option in order to maintain compatibility with previous compilers. The language extensions are some Microsoft-specific changes to ANSI C. Some of the features are Support for the _based, __cdecl, _fastcall, _far, __fortran, _huge, _near, __pascal, and _interrupt keywords. Use of casts to produce 1-values. For example, in ANSI C p = (int *)((long *)p + 1);
// increment a pointer
can be written using /Ze as int *p; ((long *)p)++;
< previous page
// pointer to integer // use cast and increment
page_163
next page >
< previous page
page_164
next page > Page 164
Note that the /Za compiler options disables the Microsoft language extensions, so check your compiler directives if you start getting errors on code like this. Compiler Warning (Level 3) C4800 This level 3 warning has the format 'type' : forcing value to bool 'true' or 'false' (performance warning)
which means you're trying to squeeze something big (like an integer) into a boolean variable. The warning can occur when assigning an integer variable's value to a variable of type bool when the integer contains only TRUE and FALSE values. If there's some reason why you can't rewrite your expression, you can always add ! = 0 to it; this actually makes the entire expression type bool automatically. However, you can't do an explicit cast to change the type. bool bVar = false; int iVar = 255; bVar = (bool)iVar;
// boolean variable // integer variable // cast fails!!
Being unable to defeat the warning by using a cast is by design, which might give you some insight into the mindset of the folks who wrote the compiler. Compiler Warning (Level 1) C4804 This is another one of those bool warnings; this time it's with unary operators. The format is '%$L' : unsafe use of type 'bool' in operation
The operators in question include the complement (~) and the negative unary operator ().
< previous page
page_164
next page >
page_165
< previous page
next page > Page 165
Compiler Warning (Level 1) C4806 The boolean variables can sure get you in trouble; this one is a level 1 warning! It's caused by trying to promote, or cast, a bool variable to an integer. For example Bool bVar= false; int iVar= 5; if(bVar > iVar) doSomething();
// bool variable // integer variable // !!trouble!
If you try something like this, the warning states '%$L' : unsafe operation: no value of type 'type' promoted to type 'type' can equal the given constant
Compiler Warning (Level 1) C4807 This level 1 warning with Boolean variables is emitted when trying to compare a 1-bit signed bit field to a variable of type bool. The format is '%$L' : unsafe mix of type 'type' and signed bitfield of type 'type'
The major distinction here is the fact that the bit field is signed. Unsigned bit fields can contain only 0 or 1, while a signed field can contain only 0 or 1. Making a comparison like this is dangerous because you'll often miss the negative value somehow. A 1 is "true", and this may or may not be what you're looking for. Compiler Warning (Level 1) C4808 This warning is generated when you have a switch statement triggered by a variable of type bool, and you've added a case statement for a value that is not one of the five possible values (TRUE, FALSE, 1, 0, default). The message reads case '%d' is not a valid value for switch condition of type 'bool'
< previous page
page_165
next page >
< previous page
page_166
next page > Page 166
Chapter Summary Although compiler bugs and documentation errors are generally the least of your worries, they do occur and must be considered when all else fails. If you suspect a compiler bug, you should contact Microsoft Technical Support and/or post a question to any of the many MFC user groups on the Internet. This could save you lots of time in tracking down something nebulous. Furthermore, don't hesitate to seek out the advice of others in this situation there is no need to reinvent the wheel. You should try duplicating the problem by creating a mini-test project workspace; if it shows up there, you've probably found a compiler bug.
< previous page
page_166
next page >
< previous page
page_167
next page > Page 167
Chapter 9 Common Windows Errors I'd like to start wrapping this book up with a broad-based look at some common errors that can sometimes perplex even the most seasoned Windows developer. These usually involve common-sense issues that are easily overlooked but that are often hard to identify. For the most part, this section will deal with run-time, logic, and resource errors. Bitmapped Buttons A common oversight when using bitmapped buttons concerns improper naming schemes. A button can be in one of four states: 1. Up 2. Down 3. Disabled 4. Has the focus For each bitmapped button you use, you must create four separate bitmaps that reflect these states. The text you place on the button in App Studio
< previous page
page_167
next page >
< previous page
page_168
next page > Page 168
must match what's in the resource file. For each of the preceding four states, you must affix a suffix to the bitmap filename. Use the letters U, D, X, and F, corresponding to the states. Thus, for the OK button, you will need four bitmaps called: 0KU.BMP 0KD.BMP 0KX.BMP 0KF.BMP If you fail to include these in your project and then attempt to show the button in one of these states, you'll throw an assertion. Make sure you create these four horsemen bitmaps when using bitmapped buttons. Radio Button Member Variables You may have noticed that you can't assign a member variable to a radio button in Class Wizard. This has been a problem since version 1.0, but there is a workaround. First, assign all the radio buttons the group property, regardless if they are to be part of a group or not. (Normally, you just want the first one to be part of a group so that the user can Tab to it, as when selecting a comm port baud rate.) Once the group property has been assigned, you can assign the radio buttons member variables. When you're done, go back and remove the group property from the buttons as necessary. It's messy, but it works. Linking with Libraries Failure to link with a specified library is another cause of run-time problems. When using any API that you're not 100 percent familiar with, always check the on-line documentation for the necessary header file(s) and any libraries. Of course, failing to include the header file will give you a compile error, but if you omit the library, your program just won't execute the API. Common examples of these types of errors include using the PlaySound() API (which requires that you link with mmlib.lib).
< previous page
page_168
next page >
< previous page
page_169
next page > Page 169
Coordinate Systems This is one that always gets me. Some APIs use screen coordinates, and others require client coordinates. Again, check the documentation carefully, and use ClientToScreen() and ScreenToClient() APIs to convert as necessary. Also, take note of the parameters used by these and other GDI functions; some parameters are passed by value, while others are passed by reference. Window Handles and Device Contexts Whenever you need a window handle, whether it's for a main window or a child window (like a control), use the CWnd function GetSafeHwnd() API. This will give you an extra layer of protection against an invalid handle in your code. The same goes for a device context outside your view's OnDraw() method. Use the GetSafeHdc() variety for the same reason. Strings and Arrays Strings and arrays are a constant source of problems in C and C++. Why? Well, these are really nothing more than a "starting point" somewhere in memory. Remember, there is no bounds checking, so if you declare something like this char myString[80];
there is absolutely nothing wrong with writing something like this char ch = myString[100];
This will compile and run, but the variable ch will simply contain a byte's worth of data found 20 bytes after the end of myString. So be really careful with these. It's a good idea to always initialize your strings, whether they be character arrays like this or CStrings. Use the memset() API to initialize your character arrays, and always use the sizeof() function. For example memset(myString, 0, sizeof(myString));
// right
instead of memset(myString,0,80);
< previous page
// wrong
page_169
next page >
< previous page
page_170
next page > Page 170
By using sizeof(), you never have to worry about changing your initialization code if you change the length of the string at a later time. For CStrings, the situation is a lot easier: just use the Empty() function CString csMyString; csMyString.Empty();
// declare a CString variable // initialize to NULL
Another common mistake frequently made with CStrings involves the GetBuffer() method. Everytime you use GetBuffer(), you need to call ReleaseBuffer(), as in this example. CString csStr; char *p; csStr.Empty(); csStr += "This is a test"; p = csStr.GetBuffer(csStr.GetLength()); csStr. ReleaseBuffer();
// CString variable // pointer to char string // initialize to all NULL // assign it some value // convert to char string // release the buffer
Trapping WM_HELP There is one small error in Class Wizard when it comes to calling help APIs. At various times and places, you may be asked to trap the WM_HELP message. There's no such message in the list, but there is a WM_HELPINFO. Select this message in response to a WM_HELP message. You should also be aware of the emerging new standard in Help from Microsoft. Although they say they will continue to support the current, WinHelp()-based system (which uses Rich Text Format files), all future products will use HtmlHelp() as the primary API. Microsoft has made available a free Help compiler based on using HTML files. You can download it from their web site http://www.microsoft.com/workshop/author/htmlhelp/ You should be able to create HTML-based Help files straight out of the box. To run the tool, you must have Internet Explorer v3.x installed.
< previous page
page_170
next page >
< previous page
page_171
next page > Page 171
Chapter Summary I hope this book has given you an appreciation of all the things that go into delivering superlative software. Getting out the bugs is critical; deliver a bug-infested product and your credibility sinks. Good luck with your project and debugging efforts.
< previous page
page_171
next page >
< previous page
page_173
next page > Page 173
Appendix A ODBC Error Codes This appendix provides some background material on ODBC error codes, their relationship to SQLError(), and so on. SQLError() returns SQLSTATE values as defined by the X/Open and SQL Access Group SQL CAE draft specification (1991). SQLSTATE values are strings consisting of five characters. The first two characters reflect the class value, while the other three form a subclass value. For example, a class value of 01 is a warning accompanied by a return code of SQL_SUCCESS_WITH_INFO. For all other values, except for class IM, an error is signaled with a return code of SQL_ERROR. The IM class is reserved for warnings and errors about the implementation of ODBC itself a form of self-diagnostics. The following is a list of SQLSTATE values that a driver may return for SQLError(). It includes a brief description and a list of functions from which the state can be returned. Code
Description
Functions
00000
Success
SQLBrowseConnect SQLError
01002
Disconnect error
SQLDisconnect
< previous page
page_173
next page >
< previous page
page_174
next page > Page 174
Code
Description
Functions
01004
Data truncated
SQLBrowseConnect SQLColAttributes SQLDataSources SQLDescribeCol SQLDriverConnect SQLExtendedFetch SQLFetch SQLGetCursorName SQLGetData SQLGetInfo SQLNativeSql
01006
Privilege not revoked
SQLExecDirect SQLExecute
01S00
Invalid connection string attribute
SQLBrowseConnect SQLDriverConnect
07001
Wrong number of parameters
SQLExecDirect SQLExecute
07006
Restricted data type attribute violation
SQLExtendedFetch SQLFetch SQLGetData SQLSetParam
08002
Connection in use
SQLBrowseConnect SQLConnect SQLDriverConnect
08003
Connection not open
SQLAllocStmt SQLDisconnect SQLGetConnectOption SQLGetInfo SQLNativeSql SQLSetConnectOption SQLTransact
08004
Data source rejected connection
SQLBrowseConnect SQLConnect SQLDriverConnect
08007
Connection failure during transaction
SQLTransact
< previous page
page_174
next page >
< previous page
page_175
next page > Page 175
Code
Description
Functions
08S01
Communication link failure
SQLBrowseConnect SQLColumnPrivileges SQLColumns SQLConnect SQLDriverConnect SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch SQLForeignKeys SQLFreeConnect SQLGetData SQLGetTypeInfo SQLParamData SQLPrepare SQLPrimaryKeys SQLProcedureColumns SQLProcedures SQLPutData SQLSetConnectOption SQLSetStmtOption SQLSpecialColumns SQLStatistics SQLTablePrivileges SQLTables
21S01
Insert value list does not match column list
SQLExecDirect SQLPrepare
21S02
Degree of derived table does not match column list
SQLExecDirect SQLPrepare
22001
String data right truncation
SQLExecDirect SQLExecute SQLPutData
22003
Numeric value out of range
SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch SQLGetData SQLGetInfo SQLPutData
< previous page
page_175
next page >
< previous page
page_176
next page > Page 176
Code
Description
Functions
22005
Error in assignment
SQLExecDirect SQLExecute SQLPrepare
22008
Datetime field, overflow
SQLExecDirect SQLExecute SQLPutData
22012
Division by zero
SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch
22026
String data, length mismatch
SQLExecDirect SQLPutData
23000
Integrity constraint violation
SQLExecDirect SQLExecute
24000
Invalid cursor state
SQLColAttributes SQLColumnPrivileges SQLColumns SQLDescribeCol SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch SQLForeignKeys SQLGetData SQLGetTypeInfo SQLPrepare SQLPrimaryKeys SQLProcedureColumns SQLProcedures SQLSetCursorName SQLSetPos SQLSetScrollOptions SQLSpecialColumns SQLStatistics SQLTablePrivileges SQLTables
25000
Invalid transaction state
SQLDisconnect
< previous page
page_176
next page >
< previous page
page_177
next page > Page 177
Code
Description
Functions
28000
Invalid authorization specification
SQLBrowseConnect SQLConnect SQLDriverConnect
34000
Invalid cursor name
SQLExecDirect SQLPrepare SQLSetCursorName
37000
Syntax error or access violation
SQLExecDirect SQLNativeSql SQLPrepare
3C000
Duplicate cursor name
SQLSetCursorName
40001
Serialization failure
SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch
42000
Syntax error or access violation
SQLExecDirect SQLExecute SQLPrepare
70100
Operation aborted
SQLCancel
IM001
Driver does not support this function
Returned from all functions except: SQLAllocConnect SQLAllocEnv SQLDataSources SQLError SQLFreeConnect SQLFreeEnv SQLGetFunctions
IM002
Data source name not found and no default specifie
SQLBrowseConnect SQLConnect SQLDriverConnect
IM003
Driver specified by data source name could not be loaded
SQLBrowseConnect SQLConnect SQLDriverConnect
IM004
Driver's SQLAllocEnv failed
SQLBrowseConnect SQLConnect SQLDriverConnect
< previous page
page_177
next page >
< previous page
page_178
next page > Page 178
Code
Description
Functions
IM005
Driver's SQLAllocConnect failed
SQLBrowseConnect SQLDriverConnect
IM006
Driver's SQLSetConnectOption failed
SQLBrowseConnect SQLConnect SQLDriverConnect
IM007
No data source specified; dialog prohibited
SQLDriverConnect
IM008
Dialog failed
SQLDriverConnect
IM009
Unable to load translation DLL
SQLBrowseConnect SQLConnect SQLDriverConnect SQLSetConnectOption
S0001
Base table or view already exists
SQLExecDirect SQLprepare
S0002
Base table not found
SQLExecDirect SQLprepare
S0011
Index already exists
SQLExecDirect SQLprepare
S0012
Index not found.
SQLExecDirect SQLprepare
S0021
Column already exists
SQLExecDirect SQLExecute SQLprepare
S0022
Column not found
SQLExecDirect SQLprepare
S1000
General error
All ODBC functions except: SQLAllocEnv SQLError
S1001
Memory allocation failure
All ODBC functions except: SQLError SQLFreeConnect SQLFreeEnv
< previous page
page_178
next page >
page_179
< previous page
next page > Page 179
Code
Description
Functions
S1002
Invalid column number
SQLBindCol SQLColAttributes SQLDescribeCol SQLExtendedFetch SQLFetch SQLGetData
S1003
Program type out of range
SQLBindCol SQLGetData SQLSetParam
S1004
SQL data type out of range
SQLGetTypeInfo SQLSetParam
S1008
Operation canceled
All ODBC functions that can be processed asynchronously: SQLColAttributes SQLColumnPrivileges SQLColumns SQLDescribeCol SQLDescribeParam SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch SQLForeignKeys SQLGetData SQLGetTypeInfo SQLMoreResults SQLNumParams SQLNumResultCols SQLParamData SQLPrepare SQLPrimaryKeys SQLProcedureColumns SQLProcedures SQLPutData SQLSetPos SQLSetScrollOptions SQLSpecialColumns SQLStatistics SQLTablePrivileges SQLTables
< previous page
page_179
next page >
< previous page
page_180
next page > Page 180
Code
Description
Functions
S1009
Invalid argument value
SQLAllocConnect SQLAllocStmt SQLBindCol SQLExecDirect SQLForeignKeys SQLGetData SQLGetInfo SQLNativeSql SQLPrepare SQLPutData SQLSetConnectOption SQLSetCursorName SQLSetParam SQLSetPos SQLSetStmtOption
S1010
Function sequence error
SQLBindCol SQLColAttributes SQLColumnPrivileges SQLColumns SQLDescribeCol SQLDescribeParam SQLDisconnect SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch SQLForeignKeys SQLFreeConnect SQLFreeEnv SQLFreeStmt SQLGetConnectOption SQLGetCursorName SQLGetData SQLGetFunctions SQLGetInfo SQLGetStmtOption SQLGetTypeInfo SQLMoreResults SQLNumParams
< previous page
page_180
next page >
< previous page
page_181
next page > Page 181
Code
Description
Functions
S1010
Function sequence error (cont.)
SQLNumResultCols SQLParamData SQLParamOptions SQLPrepare SQLPrimaryKeys SQLProcedureColumns SQLProcedures SQLPutData SQLRowCount SQLSetConnectOption SQLSetCursorName SQLSetParam SQLSetPos SQLSetScrollOptions SQLSetStmtOption SQLSpecialColumns SQLStatistics SQLTablePrivileges SQLTables SQLTransact
S1012
Invalid transaction operation code specified
SQLTransact
S1015
No cursor name available
SQLGetCursorName
S1090
Invalid string or buffer length
SQLBindCol SQLBrowseConnect SQLColAttributes SQLColumnPrivileges SQLColumns SQLConnect SQLDataSources SQLDescribeCol SQLDriverConnect SQLExecDirect SQLExecute SQLForeignKeys SQLGetCursorName SQLGetData
< previous page
page_181
next page >
< previous page
page_182
next page > Page 182
Code
Description
Functions
S1090
Invalid string or buffer length (cont.)
SQLGetInfo SQLNativeSql SQLPrepare SQLPrimaryKeys SQLProcedureColumns SQLProcedures SQLPutData SQLSetCursorName SQLSpecialColumns SQLStatistics SQLTablePrivileges SQLTables
S1091
Descriptor type out of range
SQLColAttributes
S1092
Option type out of range
SQLFreeStmt SQLGetConnectOption SQLGetStmtOption SQLSetConnectOption SQLSetStmtOption
S1093
Invalid parameter number
SQLDescribeParam SQLSetParam
S1094
Invalid scale value
SQLSetParam
S1095
Function type out of range
SQLGetFunctions
S1096
Information type out of range
SQLGetFunctions
S1097
Column type out of range
SQLSpecialColumns
S1098
Scope type out of range
SQLSpecialColumns
S1099
Nullable type out of range
SQLSpecialColumns
S1100
Uniqueness option type out of range
SQLStatistics
S1101
Accuracy option type out of range
SQLStatistics
S1102
Table type out of range
SQLTables
< previous page
page_182
next page >
< previous page
page_183
next page > Page 183
Code
Description
Functions
S1103
Direction option out of range
SQLDataSources
S1106
Fetch type out of range
SQLExtendedFetch
S1107
Row value out of range
SQLExtendedFetch SQLParamOptions SQLSetPos SQLSetScrollOptions
S1108
Concurrency option out of range
SQLSetScrollOptions
S1109
Invalid cursor position; no keyset defined
SQLSetPos
S1110
Invalid driver completion
SQLDriverConnect
S1C00
Driver not capable
SQLBindCol SQLColumnPrivileges SQLColumns SQLExtendedFetch SQLFetch SQLForeignKeys SQLGetConnectOption SQLGetData SQLGetInfo SQLGetStmtOption SQLPrimaryKeys SQLProcedureColumns SQLProcedures SQLSetConnectOption SQLSetParam SQLSetPos SQLSetScrollOptions SQLSetStmtOption SQLSpecialColumns SQLStatistics SQLTablePrivileges SQLTables SQLTransact
S1DE0
No data at execution values pending
SQLParamData SQLPutData
< previous page
page_183
next page >
< previous page
page_184
next page > Page 184
Code
Description
Functions
S1T00
Timeout expired
SQLBrowseConnect SQLColAttributes SQLColumnPrivileges SQLColumnsSQLConnect SQLDescribeCol SQLDescribeParam SQLDriverConnect SQLExecDirect SQLExecute SQLExtendedFetch SQLFetch SQLForeignKeys SQLGetData SQLGetInfo SQLGetTypeInfo SQLMoreResults SQLNumParams SQLNumResultCols SQLParamData SQLPrepare SQLProcedures SQLPutData SQLSetPos SQLStatistics SQLTablePrivileges SQLTables
< previous page
page_184
next page >
< previous page
page_185
next page > Page 185
Appendix B SQLState Values The following is a list of possible SQLState values and their meanings. These are returned in calls to SQLError(). Code
Meaning
00000
Success
01000
General warning
01002
Disconnect error
01004
Data truncated
01006
Privilege not revoked
01S00
Invalid connection string attribute
01S01
Error in row
01S02
Option value changed
01S03
No rows updated or deleted
01S04
More than one row updated or deleted
01S05
Cancel treated as FreeStmt/Close
01S06
Attempt to fetch before the result returned the first rowset
< previous page
page_185
next page >
< previous page
page_186
next page > Page 186
Code
Meaning
07001
Wrong number of parameters
07006
Restricted data type attribute violation
07S01
Invalid use of default parameter
08001
Unable to connect to data source
08002
Connection in use
08003
Connection not open
08004
Data source rejected establishment of connection
08007
Connection failure during transaction
08S01
Communication link failure
21S01
Insert value list does not match column list
21S02
Degree of derived table does not match column list
22001
String data right truncation
22002
Indicator variable required but not supplied
22003
Numeric value out of range
22005
Error in assignment
22008
Datetime field overflow
22012
Division by zero
22026
String data, length mismatch
23000
Integrity constraint violation
24000
Invalid cursor state
25000
Invalid transaction state
28000
Invalid authorization specification
34000
Invalid cursor name
37000
Syntax error or access violation
3C000
Duplicate cursor name
40001
Serialization failure
42000
Syntax error or access violation
70100
Operation aborted
IM001
Driver does not Support this function
IM002
Data source name not found and no default driver specified
IM003
Specified driver could not be loaded
< previous page
page_186
next page >
< previous page
page_187
next page > Page 187
Code
Meaning
IM004
Driver's SQLAllocEnv failed
IM005
Driver's SQLAllocConnect failed
IM006
Driver's SQLSetConnectOption failed
IM007
No data source or driver specified; dialog prohibited
IM008
Dialog failed
IM009
Unable to load translation DLL
IM010
Data source name too long
IM011
Driver name too long
IM012
Driver keyword syntax error
IM013
Trace file error
S0001
Base table or view already exists
S0002
Base table not found
S0011
Index already exists
S0012
Index not found
S0021
Column already exists
S0022
Column not found
S0023
No default for column
S1000
General error
S1001
Memory allocation failure
S1002
Invalid column number
S1003
Program type out of range
S1004
SQL data type out of range
S1008
Operation canceled
S1009
Invalid argument value
S1010
Function sequence error
S1011
Operation invalid at this time
S1012
Invalid transaction operation code specified
S1015
No cursor name available
S1090
Invalid string or buffer length
S1091
Descriptor type out of range
S1092
Option type out of range
< previous page
page_187
next page >
< previous page
page_188
next page > Page 188
Code
Meaning
S1093
Invalid parameter number
S1094
Invalid scale value
S1095
Function type out of range
S1096
Information type out of range
S1097
Column type out of range
S1098
Scope type out of range
S1099
Nullable type out of range
S1100
Uniqueness option type out of range
S1101
Accuracy option type out of range
S1103
Direction option out of range
S1104
Invalid precision value
S1105
Invalid parameter type
S1106
Fetch type out of range
S1107
Row value out of range
S1108
Concurrency option out of range
S1109
Invalid cursor position
S1110
Invalid driver completion
S1111
Invalid bookmark value
S1C00
Driver not capable
S1DE0
No data at execution values pending
S1T00
Timeout expired
< previous page
page_188
next page >
< previous page
page_189
next page > Page 189
Appendix C DDEML Error Codes This appendix lists the value and meaning of the error codes returned from DdeGetLastError(). The return value is the last error value. The following are the possible DDEML error values. Value
Meaning
DMLERR_ADVACKTIMEOUT
A request for a synchronous advice transaction has timed out.
DMLERR_BUSY
The response to the transaction caused the DDE_FBUSY bit to be set.
DMLERR_DATAACKTIMEOUT
A request for a synchronous data transaction has timed out.
DMLERR_DLL_NOT_INITIALIZED
A DDEML function was called without first calling the DdeInitialize function, or an invalid instance identifier was passed to a DDEML function.
< previous page
page_189
next page >
< previous page
page_190
next page > Page 190
Value
Meaning
DMLERR_DLL_USAGE
An application initialized as APPCLASS_MONITOR has attempted to perform a DDE transaction, or an application initialized as APPCMD_CLIENTONLY has attempted to perform server transactions.
DMLERR_EXECACKTIMEOUT
A request for a synchronous execute transaction has timed out.
DMLERR_INVALIDPARAMETER
A parameter failed to be validated by the DDEML. Some of the possible causes are as follows: 1) The application used a data handle initialized with a different item-name handle than that required by the transaction. 2) The application used a data handle that was initialized with a different clipboard data format than that required by the transaction. 3) The application used a client-side conversation handle with a server-side function or vise versa. 4) The application used a freed data handle or string handle. 5) More than one instance of the application used the same object
DMLERR_LOW_MEMORY
A DDEML application has created a prolonged race condition (where the server application outruns the client), causing large amounts of memory to be consumed.
DMLERR_MEMORY_ERROR
A memory allocation failed.
DMLERR_NO_CONV_ESTABLISHED
A client's attempt to establish a conversation has failed.
DMLERR_NOTPROCESSED
A transaction failed.
DMLERR_POKEACKTIMEOUT
A request for a synchronous poke transaction has timed out.
DMLERR_POSTMSG_FAILED
An internal call to the PostMessage function has failed.
< previous page
page_190
next page >
page_191
< previous page
next page > Page 191
Value
Meaning
DMLERR_REENTRANCY
An application instance with a synchronous transaction already in progress attempted to initiate another synchronous transactions, or the DdeEnableCallback function was called from within a DDEML callback function.
DMLERR_SERVER_DIED
A server-side transaction was attempted on a conversation that was terminated by the client, or the server terminated before completing a transaction.
DMLERR_SYS_ERROR
An internal error has occurred in the DDEML.
DMLERR_UNADVACKTIMEOUT
A request to end an advise transaction has timed out.
DMLERR_UNFOUND_QUEUE_ID
An invalid transaction identifier was passed to a DDEML function. Once the application has returned from an XTYP_XACT_COMPLETE callback, the transaction identifier for that callback is no longer valid.
< previous page
page_191
next page >
< previous page
page_193
next page > Page 193
Index Numerics 1NF, normal form rules, 140141, 143 3NF, normal form rules, 140, 142, 144 2NF, normal form rules, 140141, 143 A abort(), exception handling, 61 access violations causes, 4647 exception handling, 158 pages, 35 address space, virtual memory, 1719 AFXAPI AfxFormatStrings(), assertion, 45 AFXAPI AfxMessageBox(), assertion, 4344 AfxCheckMemory(), heap release mode failures, 50 AFX.INI, TRACER, 103104 AfxIsMemoryBlock(), prototype, 75 AfxIsValidAddress(), prototype, 75 AfxIsValidString(), prototype, 75 AfxMessageBox(), error messages, 74 AfxSetAllocHook(), prototype, 72 AfxThrowOleException(), COleException, 68 AfxThrowUserException(), CUserException, 68 afxTraceFlags, AFXWIN.H, 105
AfxWinTerm(), WM_QUIT, 90 allocations, fixed memory, 106108 _amsg_exit(), error reporting, 89 ANSI/ISO, CodeWizard, 126 API BoundsChecker, 124125 Visual Test, 132 applications building support software, 136137 database, 139151 debug v. release versions, 4851 dying, 8990 problems, 8790 user defined entry points, 8889 writing the User's Guide, 137 _argc, referencing, 89 _argn, referencing, 89 arrays alternatives to, 10 sizeof(), 157, 169170 Note: Page references in italic represent illustrations.
< previous page
page_193
next page >
< previous page
page_194
next page > Page 194
ASSERT_VALID, macro, 48 ASSERT dialog box, 38 prototypes, 3746 _ASSERT macro _CrtIsValidHeapPointer(), 5758 library links, 58 _ASSERTE macro _CrtIsValidHeapPointer(), 5758 library links, 58 AssertValid(), prototype, 4748 ATL projects, CRT startup code, 159160 automation, program testing, 130132 B beta testing, technique, 131 bitmapped buttons, bitmapped states, 168169 bool, Visual C++ v5.0 vs. v4.2, 153154 Boolean operations, De Morgan's theorem, 68 Boolean variables, compiler warnings, 164165 BoundsChecker, NuMega Technologies, 124125 Boyce-Codd (BCNF), normal form, 144 break points, setting, 3940, 162 Browse Window, program symbol monitoring, 109111 Browse, dialog box, 110111 .BSC, 109110 BSCMake, .BSC file extensions, 109110
BugCollector Pro, Nesbitt Software, 134135 Build dialog box, CodeWizard, 127 buttons bitmapped, 168169 WS_GROUP window style, 43 C C++, exception handlers, 6163 Call Level Interface, ODBC (Open Database Connectivity), 144 Call Stack debugging toolbar, 78, 84 window, 8485 CArchiveException, MFC exception, 60, 64 casts, using, 73 catch, C++ exception handler, 6263 cb, DDESpy, 114117 cbErrorMsgMax, SQL_SUCCESS_WITH_INFO, 150 CBitmapButton::DrawItem(), assertion, 41 CDaoErrorInfo, structure, 145146 CDaoException GetErrorInfo(), 145146 MFC exception, 60, 6466 CDC::SelectObject(), assertion, 44 CDumpContext, Dump(), 59 CEditView::PaginateTo(), assertion, 43 CException, MFC exception, 60, 64, 69 CFileException, MFC exception, 60, 67 CGdiObject::Attach(), assertion, 44 CGdiObject::FromHandle(), assertion, 44
Checkpoint(), CMemoryState, 7072 CInternetException, MFC exception, 60, 6869 CL environment variable, option precedence, 160 class, application database compatibility, 146 Class Wizard radiobuttons, 168 WM_HELP, 170 Clean command, documentation errors, 159
< previous page
page_194
next page >
< previous page
page_195
next page > Page 195
Clear All Breakpoints, debugging toolbar, 78, 81 ClientToScreen(), screen coordinates, 169 CList objects, vs. arrays, 10 CListBox::CompareItem(), assertion, 42 CListBox::DrawItem(), assertion, 42 CListBox::MeasureItem(), assertion, 42 CMemoryException, MFC exception, 60, 66 CMemoryState Difference(), 7072 memory leak detection, 7072 CMenu::Attach(HMENU()), assertion, 46 CMS (Code Management System), project management, 13 CMS (Code Management Systems), Visual SourceSafe, 128130 CNotSupportedException, MFC exception, 60, 67 CObject::AssertValid(), assertion, 47 CObject::IsKindOf(), assertion, 42 code auto-indenting source, 9394 CodeWizard, 128 comments, 8 converting int to enum, 101 DDEML error, 189191 generation warnings, 96 maintaining reusable, 130 migration issues, 99102 ODBC (Open Database Connectivity) SQLSTATE error, 173184
orphan, 118119 source modules, 3940 SQL debugging, 148 Visual SourceSafe, 128130 CodeWizard, ParaSoft, Inc., 125128 COleDispatchException, MFC exception, 60, 68 COleException, MFC exception, 60, 68 color, workbench, 910 COMDAT records, removing, 162 Comment() pragma, syntax, 97 comments, code, 9 commit, /HEAP, 20 committed page, memory allocation, 1819 compilation, release mode failures, 50 compile-time errors, 34 compiler bug identification, 166 error CXX0017, 82 errors in Visual C++, 9295 pragmas, 9599 compound logic, De Morgan's theorem, 68 coordinates, screen to client, 169 costs, errors in software development, 12 CResourceException, MFC exception, 60, 67 _CRT_ASSERT, reportType, 54 _CRT_BLOCK, leak detection, 53 _CRT_ERROR, reportType, 54 _CRT_WARN, reportType, 54
CRT startup code, ATL projects, 159160 _CrtCheckMemory, memory management, 53 _CRTDBFG_ALLOC_MEM_DF, _CrtSetDbgFlag(), 53 _CRTDBG_CHECK_ALWAYS_DF, _CrtSetDbgFlag(), 53 _CRTDBG_CHECK_CRT_DF, _CrtSetDbgFlag(), 53 _CRTDBG_DELAY_FREE_MEM_DF, _CrtSetDbgFlag(), 53 _CRTDBG_FILE_STDERR, _CrtSetReportFi1e(), 56 _CRTDBG_FILE_STDOUT, _CrtSetReportFile(), 56
< previous page
page_195
next page >
< previous page
page_196
next page > Page 196
_CRTDBG_LEAK_CHECK_DF _CrtSetDbgFlag(), 53, 56 _CRTDBG_MAP_ALLOC, #define, 52 _CRTDBG_MODE_DEBUG _CrtDbgReport(), 55 _CrtSetReportMode(), 55 _CRTDBG_MODE_FILE _CrtDbgReport(), 5455 _CrtSetReportMode(), 55 _CRTDBG_MODE_WINDOW, _CrtDbgReport(), 55 _CRTDBG_MODE_WNDW, _CrtSetReportMode(), 55 _CRTDBG_REPORT_FILE, _CrtSetReportFi1e(), 56 _CRTDBG_REPORT_MODE, _CrtSetReportMode(), 55 _CrtDbgReport(), prototype, 5355 CRTDBH.H, header file, 52, 54 _CrtDumpMemoryLeaks(), prototype, 56 _CrtIsValidHeapPointer(), prototype, 5758 _CrtSetDbgFlag(), prototype, 5253 _CrtSetReportFile(), message output, 54, 56 _CrtSetReportMode() message output, 54 prototype, 5455 Crystal Reports, BugCollector Pro, 134135 CScrollView::SetScrollSizes(), assertion, 45 CStdioFile::Open(), assertion, 46 CString::GetBufferSetLength(), assertion, 4445 CString
Empty(), 170 GetBuffer(), 170 passing objects to ODBC (Open Database Connectivity) functions, 149 ReleaseBuffer(), 170 CTime::CTime(), assertion, 4546 curly braces, mismatch, 93 CUserException, MFC exception, 60, 68 customization exception handling, 61 workbench, 9 CWnd::Create(), assertion, 40 CWnd::DestroyWindow(), assertion, 41 CWnd::PreCreateWindow(), assertion, 41 D DAO (Data Access Objects) database class class compatibility, 146 Microsoft Jet engine, 145 Visual C++ interface, 139, 146147 data, intelligent, 15 data domains, design, 14 data input, software failures, 1415 data size, code migration issues, 100101 data structures, page map, 18 data types code migration issues, 99101 exporting to global scope, 102 SQL (Structured Query Language), 147150 date handler, leap years, 6
dBASE, database class compatibility, 146 DDE (dynamic data exchange) transaction errors, 117118 Visual Test, 132 DdeGetLastError() DDEML error values, 189191 prototype, 117118 DdeInitialize(), DdeGetLastError(), 118 DDEML (Dynamic Data Exchange Management Library) error codes, 189191 protocol, 113
< previous page
page_196
next page >
< previous page
page_197
next page > Page 197
DDESpy DDE (dynamic data exchange) event monitor, 111118 MONCBSTRUCT, 116117 MONERRSTRUCT, 117118 MONHSZSTRUCT, 114115 MONMSGSTRUCT, 115 De Morgan's theorem, logic errors, 68 _DEBUG, flag, 52 debug, vs. release versions, 4851 Debug Run Time Library _CrtDbgReport(), 5355 _CrtDumpMemoryLeaks(), 56 _CrtSetDbgFlag(), 5253 _CrtSetReportFile(), 56 _CrtSetReportMode(), 5455 _CrtlsValidHeapPointer(), 5758 Options dialog box, 83 debuggers, commercial, 123130 debugging, database applications, 139151 DEC Alpha AXP, Visual Test, 132 #define, _CRTDBG_MAP_ALLOC, 52 delete, memory allocation, 25 design bug prevention, 9 database and normalization, 139144 development cycle, 125
recovery path, 23 design errors detection, 3 system analysis, 6 detection, tools, 2 Developer Studio, C++ program development, 48 development, team, 128 development testing, vs. selective testing vs. regression testing, 13 directory, comparison utility, 120121 discardable memory, memory intensive applications, 22 _DllMainCRTStartup, user defined entry point, 8889 DLLs (Dynamic Link Libraries) call chain, 9092 Visual Test, 132 DMLERR_NO_ERR, MONERRSTRUCT, 118 documentation errors, 159 programmer/project, 137138 domains, data, 14 Dump() CDaoErrorInfo(), 146 CDumpContext, 59 dwData1, MONCBSTRUCT, 116117 dwData2, MONCBSTRUCT, 116117 dwFreeType MEM_COMMIT, 34 MEM_RELEASE, 34 dwRet, MONCBSTRUCT, 116
dwsize, releasing memory, 3435 dwTime, DDESpy, 114117 dynamic heaps, memory management, 1921 E Empty(), CString, 72, 170 _emul(), syntax, 162 _emulu(), syntax, 162 enum, converting from int, 101 ErrLock, system error message utility, 120 error codes C1001 fatal error, 9495, 9899 C2079 error message, 102 C2143 syntax error, 161 C2146 syntax error, 161 C2321 syntax error, 161 C2433 syntax error, 161
< previous page
page_197
next page >
< previous page
page_198
next page > Page 198
C2501 syntax error, 161 C4238 compiler warning (Level 4), 163164 C4800 compiler warning (Level 3), 164 C4804 compiler warning (Level 1), 164 C4806 compiler warning (Level 1), 165 C4807 compiler warning (Level 1), 165 C4808 compiler warning (Level 1), 165 CXX0017 compiler error, 82 DDEML error codes, 189190 ODBC error codes, 173184 SQLState values, 185188 errors BoundsChecker, 124125 compiler, 9295, 166 database, 147 DDEML code, 189191 design, 6 documentation, 159 fatal, 9495, 9899 logic, 68 messages, 74, 102 missing type definition, 161 module, 120 reporting, 89 run-time, 36 Shift+Esc key combination, 102 stack, 8687
system, 120 transaction, 117118 typelib syntax, 161 types, 36 typographical, 158 Windows, 167171 events, Windows applications, 2 Excel, database class compatibility, 146 exception handling, 60 access violations, 158 death of an application, 90 resolving exceptions, 6070 exceptions, reporting, 69 executables, debug version vs. release version, 48 Executer, STRESS, 107 ExitInstance(), WM_QUIT, 90 _expand, NULL return value, 154155 expectations, client, 12 F far addresses, code migration issues, 99 File command, DDESpy Output Menu, 112 _findclose(), calling, 154 _findfirst(), _findclose(), 154 _findnext(), _findclose(), 154 fixed memory, uFlags parameter, 22 Fixed Settings, STRESS, 106107 FormatMessage(), GetLastError(), 74 FoxPro, database class compatibility, 146
free page, memory allocation, 18 FreeMemoryDebug(), assertion, 40 fsAction, MONHSZSTRUCT, 114 functions, virtual memory, 2735 G gadgets, data input, 1415 GetBuffer(), CString, 170 GetClassLong(), code migration issues, 100101 GetClassWord(), code migration issues, 100 GetErrorMessage(), CException, 69
< previous page
page_198
next page >
< previous page
page_199
next page > Page 199
GetLastError() prototype, 7374 VirtualFree(), 35 GetProcessHeap(), default heaps, 19 GetSafeHdc(), window handles, 169 GetSafeHwnd() API, window handles, 169 GetWindowLong(), code migration issues, 100101 GetWindowWord, code migration issues, 100 GlobalAlloc(), prototype, 2122 GlobalFree(), memory allocation, 23 GlobalLock(), prototype, 2223 GlobalSize(), function, 22 GlobalUnlock(), prototype, 2223 GMEM_MOVEABLE, flag, 22 guard bytes, heap release mode failures, 4950 guard pages, virtual memory, 2729 H handle types, code migration issues, 99100 handled exceptions, access violations, 158 handles, window, 169 hardware exceptions, detection, 5 hConv, MONCBSTRUCT, 116117 hData, MONCBSTRUCT, 116117 HEAP_GENERATE_EXCEPTIONS HeapAlloc(), 24 HeapCreate(), 24
HEAP_NO_SERIALIZE HeapAlloc(), 24 HeapCreate(), 24 HeapFree(), 25 HeapSize(), 25 HEAP_ZERO_MEMORY, HeapAlloc(), 25 heap management with STRESS.EXE, 2 memory corruption, 126 memory management, 1923 private, 2325 release mode failures, 4950 size modification, 20, 161 /HEAP, syntax, 1920 HeapAlloc(), prototype, 2425 _heapchk(), TRACE, 26 HeapCreate(), prototype, 2324 HeapFree(), prototype, 25 HeapSize() dwMaximumSize, 25 dwsize, 25 prototype, 25 heapstatus, case, 26 Help files Microsoft, 170 support software, 136137 _HFILE, _CrtSetReportFile(), 56 hook functions, memory management, 7273
hsz1, MONCBSTRUCT, 116117 hsz2, MONCBSTRUCT, 116117 hsz, MONHSZSTRUCT, 114 hTask, DDESpy, 114117 Hungarian system, variable naming conventions, 9 hwndTo, MONMSGSTRUCT, 115 hyperbolic tangent function, typographical error, 158 I IDH_CUSTOMER, dummy topic, 137 if statements, logic errors, 6 increment operator (++), ternary conditional, 155156
< previous page
page_199
next page >
< previous page
page_200
next page > Page 200
initialization ambiguous, 126 Data Link Libraries, 9192 InitInstance(), WM_QUIT, 90 input, data, 1415 instruction pointers (IP), register, 85 int, converting to enum, 101 integers, unsigned, 101 intelligent data, hazards, 15 interface, ODBC (Open Database Connectivity), 144 IsBadHugeReadPtr(), access violations, 158 IsBadHugeWritePtr(), access violations, 158 IsBadStringPtr(), access violations, 158 IsBadWritePtr(), access violations, 158 J JustInTimeDebugging, Visual C++, 92 K keywords _emul(), 162 _emulu(), 162 L leaks _CRT_BLOCK, 53 memory, 5658, 7072 leap years, date handler, 6 libraries, linking, 168
link options, /HEAP, 1920 LMEM_MOVEABLE, flag, 22 LocalAlloc(), prototype, 2122 LocalFree(), memory allocation, 23 LocalLock(), prototype, 2223 LocalSize(), function, 22 LocalUnlock(), prototype, 2223 Log Settings, STRESS, 108 logic errors De Morgan's theorem, 68 detection, 3 if statements, 6 Lotus, database class compatibility, 146 lParam, MONMSGSTRUCT, 115 M m_lErrorCode, CDaoErrorInfo, 145 m_lHelpContext, CDaoErrorInfo, 145146 m_pErrorInfo, CDaoException, 145146 m_strDescription, CDaoErrorInfo, 145 m_strError, CDBException, 147 m_strHelpFile, CDaoErrorInfo, 145 m_strSource, CDaoErrorInfo, 145 machine errors, detection, 34 mainCRTStartup, user defined entry point, 8889 Map file, option, 5152 mapping, virtual memory, 18 MEM_COMMIT, dwFreeType, 34
MEM_RELEASE, dwFreeType, 34 MEMORY_BASIC_INFORMATION, prototype, 3334 memory code migration issues, 99 debugging toolbar, 78, 8384 error messages, 89 fixed allocations, 106108 fixed vs. movable, 22 heap corruption problems, 126 STRESS.EXE, 2 window, 84
< previous page
page_200
next page >
< previous page
page_201
next page > Page 201
memory management allocating memory, 1819 _CrtCheckMemory, 53 heaps, 1923 hook functions, 7273 leaks, 5658, 7072 releasing memory, 3435 message, system error, 120 Message() pragma, prototype, 9697 messages errors, 74 TRACE, 104 Windows applications, 2 MFC (Microsoft Foundation Class) assertion errors, 3946 exception handling macros, 60, 6369 MFC User groups, compiler error identification, 166 MFT_STRING, undefined keyword or key name, 163 MH_CLEANUP, fsAction, 114 MH_CREATE, fsAction, 114 MH_DELETE, fsAction, 114 MH_KEEP, fsAction, 114 Microsoft Access database class compatibility, 146 DAO Jet engine, 134135, 145 Help files, 170
Professional Edition of Visual C++, 2 Technical Support, 166 Visual C++, 12 Visual SourceSafe, 128130 Visual Test, 132134 MIPS, Visual Test, 132 module errors, message utility, 120 MONCBSTRUCT, prototype, 116117 MONERRSTRUCT, prototype, 117118 MONHSZSTRRUCT, prototype, 114115 Monitor Menu, DDESpy, 113 MONMSGSTRUCT, prototype, 115 N naming conventions, standardization, 9 near addresses, code migration issues, 99 negatives, De Morgan's theorem, 68 Nesbitt Software, BugCollector Pro, 134135 NetBIOS, Visual Test, 132 new memory allocation, 25 return values, 25 NO_ACCESS flag, PAGE_GUARD flag, 2728 normal forms, rules, 140144 normalization, database design, 139144 notes, programmer, 138 NT BoundsChecker, 124125 Visual Test, 132
NuMega Technologies, BoundsChecker, 124125 O objects, fixed vs. movable, 22 ODBC (Open Database Connectivity) interface, 144, 146147 passing CString objects to functions, 149 SQLSTATE error codes, 173184 Visual C++, 139 OLE, Visual Test, 132 Open(), CFileException, 67 OPT:ICF, syntax, 162 OPT:NOREF, syntax, 162 OPT:REF, syntax, 162 optimization application development, 48 release mode failures, 51 Options formatting screen, 10 specifying compiler, 160
< previous page
page_201
next page >
< previous page
page_202
next page > Page 202
orphan code, identifier, 118119 Output Menu, DDESpy, 112 P Pack() pragma, syntax, 9899 page fault, virtual memory, 18 page map, data structure, 18 pages releasing memory, 35 VirtualAlloc(), 1819 paging file, physical addresses, 1819 Paradox, database class compatibility, 146 ParaSoft, Inc., CodeWizard, 125128 pcbErrorMsg, cbErrorMsgMax, 150 .PCH (precompiled header) file, Clean command, 159 performance, normalization, 142 pfnAllocHook(), prototype, 7273 PlaySound() API, linking libraries, 168 pointers access violations, 158 dangling, 126 instruction (IP), 85 release mode failures, 51 stack (SP), 85 uninitialized variables, 5 validation, 75 #pragma comment(), syntax, 97
#pragma message() programmer notes, 138 syntax, 9697 #pragma pack(), syntax, 9899 #pragma warning(), syntax, 9596 PrepareCtrl(int nIDC), assertion, 4243 prevention, bugs, 2 problems, application, 8790 PROCESS_VM_OPERATION, VirtualFreeEx(), 35 Process Viewer, PVIEW95.EXE, 119120 Profiler, orphan code identifier, 118119 program symbol, monitoring with Browse Window, 109111 program testing, automated, 130132 project building with Visual SourceSafe, 129 documentation, 137138 management with a CMS (Code Management System), 14 Project Settings dialog box, setting stack size, 86 protocol, Dynamic Data Exchange Management Library (DDEML), 113 prototyping, rapid, 1213 push/pop, Pack pragma, 98 PVIEW95.EXE, Process Viewer, 119120 Q QuickWatch Window, Show Variable/Expression, 8182 R radio buttons, Class Wizard, 168 RC2104, MFT_STRING, 163 recovery path, error, 23
Registers, debugging toolbar, 83 regression testing, vs. development testing vs. selective testing, 13 regressions, Visual SourceSafe, 129 release, vs. debug versions, 4951 ReleaseBuffer(), CString, 170 Remote Procedure Calls (RPC), Just-in-time Debugging, 92 ReportError(), CException, 69 reportType _CrtDbgReport(), 54 destinations, 56 reserve, /HEAP, 20
< previous page
page_202
next page >
< previous page
page_203
next page > Page 203
reserved page, memory allocation, 18 resizing heaps, 20 stacks, 8687 resolution, resources, 2 Restart, debugging toolbar, 78 return values, checking, 6970 review, peer, 15 roundoff errors, condition, 34 rules, normal forms, 140142 Run to Cursor, debugging toolbar, 78, 80 run-time errors compiler bugs, 56 detection, 35 S .SBR, 109 SceenToClient(), screen coordinates, 169 scope, structure, 102 selective testing, vs. regression testing vs. development testing, 13 server applications, rebuilding, 159 set_terminate(), exception handling, 61 Set/Clear Breakpoint, debugging toolbar, 78, 81 SetClassLong(), code migration issues, 100 SetClassWord(), code migration issues, 100 SetLastError(), prototype, 73 SetProcessWorkingSetSize(), VirtualLock(), 32
Settings, main menu, 106109 SetWindowLong(), code migration issues, 100 SetWindowWord(), code migration issues, 100 Shift+Esc, WM_CHAR message, 102 Show Source Line, debugging toolbar, 7879 Show Variable/Expression, 78, 81 sizeof(), arrays, 157 software commercial debuggers, 123138 exception detection, 5 support, 135138 writing the User's Guide, 137 software development cost of errors, 11 project leadership, 1112 sources, awareness, 12 SourceSafe, Visual, 128130 specifiers, Warning pragmas, 96 Spy++, Win32 utility, 108109 SQL_ERROR, SQLError(), 150 SQL_INVALID_HANDLE, SQLError(), 150 SQL_NO_DATA_FOUND, SQLError(), 150 SQL_SUCCESS_WITH_INFO, SQLError(), 150 SQL_SUCCESS, SQLError(), 150 SQL (Structured Query Language) Access Group data types, 147 debugging limitations, 148 ODBC (Open Database Connectivity), 144
SQLError() ODBC (Open Database Connectivity) error codes, 173184 prototype, 149150 SQLState values, SQLError(), 185188 stack corruption, causes, 87 stack errors, causes, 8687 stack pointer (SP), register, 85 stack probes, #pragma check_stack, 87 stack unwinding, exception handling, 61 Start/Continue, debugging toolbar, 7879 states, pages, 18 STATUS_GUARD_PAGE, guard page exception, 2729 Step Into, debugging toolbar, 7880 Step Out, debugging toolbar, 78, 80
< previous page
page_203
next page >
< previous page
page_204
next page > Page 204
Step Over, debugging toolbar, 78, 80 Stop, debugging toolbar, 7879 Stop Program, Start Debugger, debugging toolbar, 7879 str, MONHSZSTRUCT, 114 strcpy(), ASSERT, 39 STRESS.EXE memory management, 2, 23 output window, 106 string-handle data, DDESpy Monitor Menu, 113115 strings and arrays, 169170 validation, 75 structure, scope, 102 style, commenting, 9 support software, building, 136137 swapping, process, 18 switch, try blocks, 156157 symbols, break point, 81 system analysis, design errors, 6 system error, message utility, 120 T tangent function, typographical error, 158 Technical Support, compiler errors, 166 terminate(), exception handling, 61 ternary conditional, increment operator (++), 155156 testing assertions, 3739
automated program, 130135 software development, 13 text files, database class compatibility, 146 throw, C++ exception handler, 6263 timing, controlling event, 133 toolbar CodeWizard, 126 debugging, 7884 tools CASE (Computer-Aided Software Engineering), 125126 Debug Run Time Library, 52 Map file, 5152 project, 123138 TRACE dialog box, 49 _heapchk(), 26 Tracer, utility, 103105 Track Menu, DDESpy, 118 try C++ exception handler, 6263 switch statement, 156157 _try/_except, Win32 exception handlers, 64 _try/_fina1ly, Win32 exception handlers, 64 try/catch exception handling, 61 SQL calls, 147 tutorials, support software, 136137 U
uFlags parameter, memory allocation, 2223 Unicode, TRACE macros, 105 unsigned integers, code migration issues, 101 URLMON.DLL, Windows NT3.51, 157 User's Guide, writing, 137
< previous page
page_204
next page >
< previous page
page_205
next page > Page 205
V validation ASSERT_VALID, 48 CObject::AssertVa1id(), 4748 values Comment pragma, 97 SQLState, 185188 variables color-coding, 910 increment operator (++) failure, 155156 initialization, 5 naming conventions, 9 radio button member, 168 VERIFY, routine, 4748 versions, recreating previous, 129 virtual memory address space allocation, 1719 guard pages, 2729 VirtualAlloc() page allocation, 1819 prototype, 2930 VirtualAllocEx() prototype, 31 VitualProtect(), 30 VirtualFree(), prototype, 3435 VirtualFreeEx(), prototype, 35
VirtualLock(), prototype, 32 VirtualProtect(), prototype, 30 VirtualProtectEx(), prototype, 3031 VirtualQuery(), prototype, 3334 VirtualQueryEx(), prototype, 3334 VirtualUnlock(), prototype, 33 Visual C++ BoundsChecker, 124125 CodeWizard, 125126 database applications, 139151 Debugger, 77102 Enterprise Edition, 147 Visual SourceSafe, Microsoft, 128130 Visual Test, Microsoft, 132134 void AFXAPI DDX_Control(), assertion, 42 void AFXAPI DDX_Radio(), assertion, 43 W Warning pragma, syntax, 9596 Watch, debugging toolbar, 78, 8283 wFmt, MONCBSTRUCT, 116 Win32 exception handling extensions, 64 memory management, 1736 Windbg, Visual C++ 5.0, 159 WinDiff, file comparison utility, 120121 window, handles, 169 Windows95, BoundsChecker, 125 Windows NT v3.51, URLMON.DLL, 157
WinHelp, support software, 136137 WinMain(), WM_QUIT, 90 WinMainCRTStartup, entry point defaults, 88 wLastError, MONERRSTRUCT, 117 WM_CHAR, Shift+Esc, 102 WM_HELP, Class Wizard, 170 WM_QUIT, MFC applications, 90 wmainCRTStartup, user defined entry point, 8889 wMsg, MONMSGSTRUCT, 115 workbench adding CodeWizard toolbar, 127 customization, 910 wParam, MONMSGSTRUCT, 115 wReserved, MONCBSTRUCT, 116 WS_GROUP, window style buttons, 43 /WS:AGGRESSIVE, NT v4 applications, 160 wType, MONCBSTRUCT, 116 wWinMainCRTStartup, user defined entry point default, 8889
< previous page
page_205
next page >
< previous page
page_206
next page > Page 206
Z /Ze option, compiler warning, 163164 zero array size, 157 non-integer divide by, 154 virtual address, 1819 /Zm option, memory allocation, 161
< previous page
page_206
next page >
< previous page
page_207 Page 207
What's on the Disk? The Debugging Visual C++ Windows Code Disk accompanies the book and consists of several Visual C++ v5.0 workspace/projects. You should create directories on your machine and copy these projects to your hard drive, then build and run them. Make sure TRACE is enabled, and run the executable(s) in DEBUG mode. Example 1 Demonstrates the #pragma compiler directive, ASSERT, and TRACE. Example 2 Demonstrates the use of memory state checking. Example 3 Demonstrates how to retrieve error messages for GetLastError(). Example 4 Demonstrates compiler bugs in divide by 0, ternary conditional.
< previous page
page_207