August 1998
Download Aug98hood.exe (28KB)
Matt Pietrek does advanced research for the NuMega Labs of Compuware Corporation, and is the author of several books. His Web site at http://www.tiac.net/users/mpietrek has a FAQ page and information on previous columns and articles. |
I love running 32-bit programs on my fire-breathing
Windows NT® barn burner. Conversely, I hate running
16-bit programs. Nonetheless, because of the astute foresight of my bank, I'm stuck running a cranky 16-bit program if I want to bank online. Personally, I think that anyone still running Windows® 3.1 is probably not going to try online banking.
In any event, judging from my email, I'm not alone in my predicament. I receive a number of requests from people who want to know about the inner workings of 16-bit applications running under Windows NT. With this in mind, I'll devote this month's column to describing the Windows NT support for monitoring 16-bit programs. I'll start with a brief overview of how Windows NT runs programs for 16-bit Windows and MS-DOS®. Under Windows NT 3.5 and earlier, every MS-DOS and 16-bit Windows-based program ran in its own virtual machine (VM). To these programs, each VM appeared as a relatively complete Windows 3.1-based system, down to having its own Local Descriptor Table (LDT). The activities of each 16-bit program were completely separate from those of other 16-bit programs. As a result, a 16-bit program could blow up with no ill effect on any other program. Starting with version 3.51, Windows NT acquired the ability to run multiple 16-bit programs in the same VM. These programs shared the same address space and LDT, much like Windows 3.1. The good thing about this capability is that once a 16-bit program loaded, subsequent 16-bit programs would load faster and use less memory by using the already created VM. The downside to sharing VMs is that if one 16-bit program choked, the entire VM would go away (again, much like Windows 3.1). Luckily, the capability of running a badly behaving application in its own VM is still present. In the Run dialog, the Run in Separate Memory Space checkbox lets you specify that a separate VM should be used for the selected program, rather than running it in the communal system VM. The focal point of VM capability in Windows NT is NTVDM.EXE from the SYSTEM32 directory. Each running instance of NTVDM.EXE constitutes a separate VM. NTVDM.EXE is a Win32® program. However, it uses its separate address space to create a VM that runs alongside regular Win32 processes. In other words, programs for MS-DOS and 16-bit Windows are like children belonging to the adult NTVDM process. NTVDM is just another adult Win32 process, with essentially the same rights and privileges as any other Win32-based program. This architecture even has its own name, Windows On Windows (WOW). To briefly digress from Windows NT, Windows 95 has quite a different method for mixing MS-DOS, 16-bit Windows, and Win32 apps. Each MS-DOS-based program has its own VM. However, 16-bit Windows-based applications share the same VM and LDT, and have no address space protection from one another. Under Windows 95, when a Win32 process has the CPU, that process can see the memory of the VM containing all the 16-bit programs. Likewise, 16-bit code running in the context of a 32-bit process (via a thunk) can see the 32-bit code and data of that process. At least the memory of other 32-bit programs isn't visible. This looseness of the Windows 95 address space is required for the flexibility of Windows 95 thunking, as well as to reduce excess thread switching and memory copying. This concludes the five-cent tour of MS-DOS and 16-bit Windows-based programs running under Windows NT and Windows 95. Now, it's on to the facilities that Windows NT has for snooping around in this environment. The word you want to remember here is VDMDBG (that is, VDM Debug). Nearly all of the VDM debugging support is in a DLL named VDMDBG.DLL, which resides in the SYSTEM32 directory. If you've never heard of VDMDBG.DLL, don't feel bad. Microsoft doesn't mention it in any of the online documentation that I searched. Nonetheless, VDMDBG.H and VDMDBG.LIB come with Visual C++® and the Platform SDK (at least as of the January 1998 edition). In the Platform SDK, there's a .HLP file from September 1994 in the \MSSDK\DOC\MISC directory. Alas, I couldn't find VDMDBG.HLP on my Visual Studio 97 CDs. Using VDMDBG.DLL, you could write a 32-bit debugger program that runs on Windows NT and debugs 16-bit programs. However, VDMDBG.DLL isn't solely for debugger writers. Programmers often wonder how the Windows NT Task Manager (TASKMGR.EXE) is able to display the running 16-bit Windows tasks in the Processes tab. The answer is VDMDBG.DLL, which TASKMGR.EXE links to implicitly. If you look closely, the 16-bit Windows tasks you see in the Task Manager are always subordinate to an NTVDM process instance.
The VDMDBG API
Experimenting with VDMDBG.DLL
|
Figure 2 VDMDBGDemo |
As each Win32 debug event comes in, the event is passed off to the HandleDebugEvent function. This function either emits basic information about the event, or passes the event off to a more specialized function such as HandleExceptionDebugEvent. If the event is passed off, the receiving function may close the process, thread, and file handles that the debugger process receives automatically as an added bonus.
Let's look at the HandleExceptionDebugEvent function since that's where all of the information about 16-bit Windows-based events eventually winds up. The first thing the code does is check if the exception number is STATUS_ VDM_EVENT. If it is, the event is passed to VDMProcessException, which the online help says is necessary to call for STATUS_VDM_EVENT exceptions. Next, the code uses the W1 macro from VDMDBG.H to break out what type of 16-bit event occurred. A big switch statement further cracks open the remaining event fields to show more information about the particular event. When examining the events, it's important to realize that you won't see 16-bit Windows events that occurred prior to attaching to the NTVDM session. Looking at the switch statement that cracks apart 16-bit Windows events, you'll see that full processing of some 16-bit Windows events requires reading structures from the debuggee's memory (that is, from the NTVDM address space). If the exception isn't a STATUS_VDM_EVENT, it's a regular exception that any Win32 process could see. The HandleExceptionDebugEvent code just displays some rudimentary information for it. An example of such an exception is the DebugBreak (INT 3) that all debuggee processes generate when debugging begins. If you look throughout VDMDBGDemoDbgLoop.CPP, you'll see that information is added to the listbox via the _lbPrintf function at the bottom of the source file. _lbPrintf first acts like sprintf, but then calls strdup for the resultant string. Last, _lbPrintf posts the user-defined message (WM_ LB_ADDITEM) to the main dialog procedure, with the LPARAM, pointing to the just-allocated string buffer. In _lbPrintf, why didn't I just call SendDlgItemMessage with LB_ADDSTRING? Consider this: I'm not dealing with just a single thread here. Two threads then? Nope, I'm not so fortunate. At a minimum, there are three threads involved: the main VDMDBGDemo thread that drives the display dialog, the debug loop thread, and all the threads in the NTVDM process. Why is NTVDM involved in this? When WaitForDebugEvent returns, the debuggee process (in this case NTVDM) is completely frozen until you call ContinueDebugEvent. Since SendMessage and SendDlgItemMessage are quite sensitive to thread synchronization issues, I decided to avoid the possibility of a deadlock altogether. By posting rather than sending the WM_LB_ADDITEM messages, the messages are queued until it's safe to add items to the listbox. Another reason why I went this route is that the user may select multiple NTVDM sessions to monitor, compounding the potential for deadlock scenarios even further. Sprinkled throughout NTVDMDbgDemoDbgLoop.CPP are references to PSAPI.DLL and the GetModuleFileNameExW function. What's that for? One of the notifications that a debugger gets is for module loads. Unfortunately, it receives an HMODULE but no module name. Within your own process, you could call GetModuleHandle to translate the HMODULE to a meaningful file name. However, since I'm a debugger, the HMODULE is relative to another process. With a thorough understanding of Portable Executable files, you could craft some code to read the module name from the debuggee process. Being lazy, and not wanting to reinvent the wheel, I used the GetModuleFileNameExW function in PSAPI.DLL instead. I described this API in my August 1996 column. Since not everybody may have PSAPI.DLL on their system, I used LoadLibrary and GetProcAddress to avoid linking implicitly to PSAPI.DLL.
Some Observations
Wrap-up
Have a question about programming in Windows? Send it to Matt at mpietrek@tiac.com. From the August 1998 issue of Microsoft Systems Journal.
|