Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
|
Now it's time to add a little stress to your life. Believe it or not, stress can be a good thing. Unfortunately, stressing Win32 applications is much harder these days than it used to be. In the days of 16-bit Windows systems, we could run our programs through their paces with STRESS.EXE, a neat program that comes with the SDK. STRESS.EXE allowed you to torment your application in all sorts of nasty ways, such as eating up disk space, gobbling up the graphics device interface (GDI) heap, and using up file handles. It even had a cool icon: an elephant walking a tightrope.
To stress your Win32 applications, you can hook into the DCRT library's allocation system and control whether allocations succeed or fail. The MemStress extension gives you a means to stress your C and C++ memory allocation. (I'll leave it up to you to write the disk-eating code.) To make MemStress easy to use, I wrote a Windows Forms front end that lets you specify under exactly what conditions you'd like your allocation to fail.
The MemStress extension lets you force allocation failures based on various criteria: all allocations, on every n allocation, after x bytes are allocated, on requests over y bytes, on all allocations out of a source file, and on a specific line in a source file. In addition, you can have the MemStress extension prompt you with a message box on each allocation asking whether you want this particular allocation to fail, and you can also set the DCRT library flags you'd like in effect for your program. The MemStressDemo program is a sample MFC program (that comes with this book's sample files) that allows you to experiment with setting different options from the MemStress user interface (UI) and see the results. It also serves as the unit test for MemStress itself.
Using the MemStress extension is relatively simple. In your code, include
After you've compiled your program, start the MemStress UI, click the Add Program button, and type the same name that you specified in the MEMSTRESSINIT macro. After you've selected the failure options you want, click the Save Program Settings button to save the settings into MEMSTRESS.INI. Now you can run your program and see how it behaves when it fails memory allocations.
You'll probably want to be very selective about using the MemStress extension. For example, if you specify that you want all allocations over 100 bytes to fail and you have the MEMSTRESSINIT macro in your MFC application's InitInstance function, you'll likely take down MFC because it will be unable to initialize. You'll have the best results with the MemStress extension if you limit its use to key areas in your code so that you can test them in isolation.
Most of the implementation of MemStress is in the reading and processing of the MEMSTRESS.INI file, in which all the settings for individual programs are stored. From the DCRT library perspective, the important function is the call to _CrtSetAllocHook during the MemStress initialization because this call sets the hook function, AllocationHook, as the allocation hook. If the allocation hook function returns TRUE, the allocation request is allowed to continue. By returning FALSE, the allocation hook can have the DCRT library fail the allocation request. The allocation hook has only one hard requirement from the DCRT library: if the type of block, as specified by the nBlockUse parameter, is marked as a _CRT_BLOCK, the hook function must return TRUE to allow the allocation to take place.
The allocation hook gets called on every type of allocation function. The different types, as specified in the first parameter to the hook, are _HOOK_ALLOC, _HOOK_REALLOC, and _HOOK_FREE. In my AllocationHook function, if the type is _HOOK_FREE, I skip all the code that determines whether the memory request should pass or fail. For _HOOK_ALLOC and _HOOK_REALLOC types, my AllocationHook function performs a series of if statements to determine whether any of the failure conditions are met. If a failure condition is met, I return FALSE.
Interesting Stress Problems
Everything worked great in MemStress on my initial console sample program, and I was feeling fine. As I finished off the MFC-based MemStressDemo program, I noticed a compelling problem. If I elected to have MemStress ask me with a message box whether I wanted allocations to fail, I would hear a series of beeps and MemStressDemo would stop working. I was able to duplicate the problem every time— and to duplicate the serious stress the problem was causing me because I didn't see what the issue could be.
After a few runs, I finally got a message box to show up. Instead of being in the center of the screen, the message box was down in the lower right-hand corner. When message boxes start moving off the lower right-hand corner of the screen, you can be pretty sure that you're stuck in a situation in which calling the MessageBox API function somehow became reentrant. I suspected that my allocation hook was getting called in the middle of my call to MessageBox. I confirmed my hypothesis by setting a breakpoint on the first instruction of AllocationHook just before I stepped over the MessageBox call. Sure enough, the debugger stopped on the breakpoint.
I looked up the stack and saw that a direct call to the MessageBox API function was somehow going through MFC code. As I poked through the code looking at what was happening, I saw that I was in the _AfxActivationWndProc function on a line that was calling CWnd::FromHandle, which causes memory allocations so that MFC can create a CObject. I was a little stumped about how I got in there, but a comment in the code indicated that _AfxActivationWndProc is used to handle activation and make gray dialog boxes. MFC uses a computer-based training (CBT) hook to catch window creations in the process space. When a new window is created—in my case, a simple message box—MFC subclasses the window with its own window procedure.
When I understood all the issues, my personal stress level really shot up because I wasn't sure how to handle the situation. Because the reentrancy was on the same thread, I couldn't use a synchronization object such as a semaphore because that would have deadlocked the thread. After contemplating the problem a bit, I concluded that what I needed was a recursion flag that told me when AllocationHook was being reentered—but it had to be on a per-thread basis. I already had a critical section protecting against multiple-thread reentrancy in AllocationHook.
When I stated the problem this way, I realized that all I needed was a variable in thread local storage that I would access at the beginning of AllocationHook. If the value was greater than 0, AllocationHook was being reentered as part of the MessageBox processing and I just needed to bail out of the function. I implemented a quick dynamic thread local storage solution and my anxiety dropped considerably because everything started working as planned.
I thought I had everything rolling at this point, but I noticed my second stress problem. When testing the code to fail on a specific source and line, the source filename was coming in as NULL and the source line was 0. Because I was using MFC for the MemStressDemo program, I thought it would have done the right thing and properly used the CRTDBG.H versions of the allocation functions to pass in the source and line. Alas, that wasn't the case.
I figured that all I needed to do was to drop in a define of _CRTDBG_MAP_ALLOC at the top of
|