UNDOCUMENTED CORNER

The Windows 3.1 Virtual Machine Control Block Part 2

Kelly Zytaruk

Kelly graduated with a Bachelor of Science in Electrical Engineering and Computers from the University of Waterloo in Ontario, Canada. He has spent the last ten years programming in C and assembler on Intel-based machines. Most recently he has worked on peripheral hardware design and virtual device drivers.


Introduction

by Andrew Schulman

For the past few months, this column has focused on the Virtual Machine Manager (VMM) and Virtual Device Drivers (VxDs) in Windows 3.1 Enhanced mode. Windows and DOS programmers pay too little attention to this low-level aspect of Windows. As I've been noting for the past few months, VMM and VxDs will be the basis for most of Microsoft's key additions to Windows 4 and DOS 7, also known as "Chicago." Microsoft is extending the WIN386.EXE file from Windows 3.1 Enhanced mode to create the DOS386.EXE file, which is the basis for Chicago. Indeed, VxDs and VMM (which resides in WIN386.EXE and DOS386.EXE) are the Chicago operating system.

Since the December 1993 column, a number of readers have asked me why I said that "What TSRs were in the mid-eighties, VxDs will be in the mid-nineties." Let me elaborate. First, Microsoft wants all real-mode DOS device drivers and TSRs to be replaced with VxDs. I recently received a form letter from Microsoft's Hardware Vendor Relations group which underlines this: "In future versions of Microsoft Windows operating systems, the preferred device driver will be a protect mode VxD (Virtual Device Driver) because it will be able to offer multi-threading, asynchronous I/O, dynamic loading, and other benefits." So VxDs are much more "Chicago friendly" than real-mode drivers or TSRs. Second, since at its lowest levels the Chicago operating system is really VMM plus a collection of VxDs, clearly this is the place for adventurous programmers to do the sort of "impossible" things that TSRs were once known for.

Before I let Kelly Zytaruk get on with Part 2 of his article on the Windows Virtual Machine Control Block (VMCB), I need to make a number of points about Chicago and clarify some issues from the previous two columns.

To reiterate a point that Kelly made last month, the VMCB structure described here is slightly different from that found in the debug version of Windows 3.1, and totally different from the VMCB in Chicago. For example, in Chicago VMCB+10h is a dword signature 62634D56h ('VMcb'). The new thread-control block has a dword signature 42434854h ('THCB') at offset 0. Why didn't Microsoft put the 'VMcb' signature at offset 0 in the VMCB too? Because there are documented fields there! Offset 10h is the first undocumented location in the VMCB, and hence the first place it is safe to change and put the signature. This shows that, if you rely on obscure undocumented fields in one version of an operating system, all bets are off in the next version!

Last month, I said that in Chicago "offset 0 in the VMCB now appears to hold the initial thread handle." How incredibly stupid! Offset 0 in the VMCB is a documented field (see VMM.INC) and is not changing. I have no idea where I got this bizarre notion about VMCB+0; of course, it's still CB_VM_Status.

Because VMM in Chicago provides a THCB in addition to the old VMCB, I have improved my PROTDUMP utility from last month so that it now shows threads as well as VMs; this update is available electronically (see "Availability," page 3). PROTDUMP uses five new VMM functions to display all the threads for each VM:Get_Cur_Thread_Handle, 10108h;Get_Sys_Thread_Handle, 1010Ah;Get_Initial_Thread_Handle, 1010Dh;Get_VM_Handle_For_Thread, 10111h; andGet_Next_Thread_Handle, 10113h.

Another change to PROTDUMP involves its ability to look at protected-mode selector:offset addresses in other VMs. As discussed last month, this requires the -LDT selector stored at VMCB+114h. However, in the current prerelease of Chicago, the LDT is located at VMCB+5Ch. Again, you see how unreliable undocumented interfaces can sometimes be! I changed PROTDUMP so that, under Windows 4 and higher, it tries to get the LDT from VMCB+5Ch. But this could easily change again, so I also added an LDT command-line switch so you can override this with a different offset.

So much for PROTDUMP. Next, I have some changes to the VXDLIST program from the December 1993 issue. First, there was a bug: The program would fail to call FreeSelector in the (admittedly unlikely) case of finding the string "VMM" that doesn't actually belong to the VMM Device Descriptor Block (DDB).

Second, it turns out that in Chicago there is a VMM function that returns exactly the information that my Get_First_VxD function in the December 1993 issue goes to such trouble to find. This new function, VMM_GetDDBList (1013Fh), simply returns the 32-bit address of the VMM DDB in EAX. To call this function from a Windows or DOS program (rather than from a VxD) would currently require the VxDCall function from my generic VxD; see Example 1.

Thirdly, an anonymous reader sent me a list of new VMM and VxD function numbers for Chicago, and I have added these to the VXDLIST database; a new version of VXDLIST is available electronically. This, in turn, has enabled me to examine DOS386.EXE and describe the new functions mentioned earlier.

Fourth, it turns out that there is an entirely new type of interface in Chicago, called "Win32 services." The new 32-bit kernel (KERNEL32.DLL) and other Win32 DLLs use these services to communicate directly with VxDs via a set of VxDCall functions. For example, the Win32 Console API appears to be implemented by calling down to VCOND (the virtual CON device). VXDLIST will now show which VxDs provide Win32 services, and how many; see Table 1.

I don't currently have names for any of the services, though their purpose can often be figured out easily enough. For example, the Win32 file-system functions in KERNEL32.DLL put values that look like DOS INT 21h function numbers in AH and issue a VxDCall 2A0018h. VxD 2Ah is VWIN32, and Win32 service 18h clearly provides an operating-systems interface that looks a lot like a 32-bit MS-DOS. I would imagine that VWIN32 figures out whether the call really needs to be passed down to DOS or whether it (hopefully) can be handled entirely in "VxD land." If no real-mode DOS device drivers or TSRs have been loaded in CONFIG.SYS or AUTOEXEC.BAT, Windows 4 should be able to remove itself entirely from real-mode DOS, relying on the VMM/VxD operating system. For example, Chicago (and even Windows for Workgroups 3.11) has a VFAT virtual device that provides a 32-bit file system independent of real-mode DOS.

With all this talk of Chicago, I ought to mention that I have not yet signed Microsoft's nondisclosure agreement for its Win32 conference. This is the only reason I can talk about any of this. (It's worth noting, however, that the January 1994 Microsoft Systems Journal has a major article entitled, "Chicago: A First Look at Its Core Architecture," so in a sense, this subject is now wide open.) I doubt I will sign it, either, since it appears to last for three years! Microsoft recently sent the following note to publishers:

IMPORTANT NOTE: All conference attendees must sign a non-disclosure agreement which explicitly prohibits reverse engineering, disassembly, decompilation, etc., of Chicago_. you will not reverse engineer (including to discover the internal architecture or functionality of the software), decompile, or disassemble the software.

This very explicit ban on reverse engineering the prerelease software is especially notable given Microsoft's suit against Stac Electronics, in which Redmond is charging that Stac reverse-engineered MS-DOS to figure out how to make Stacker 3.1 fully compatible with DOS 6.

Turning to Part 2 of Kelly's article, I should first point out that, in addition to finishing up his coverage of the VMCB, Kelly has also uncovered an undocumented VM initialization structure. The WINEXP application Kelly presents this month uses this structure to display ASCII names for each virtual machine.

WINEXP can access this VM initialization structure because the application uses something called the "generic VxD." The generic VxD (VXD.386) enables "normal" Windows and DOS applications to call VMM and VxD services otherwise callable only from a VxD. In other words, the generic VxD is a surrogate, which calls functions on behalf of less-privileged applications not allowed to call those functions on their own. The generic VxD first appeared in Microsoft Systems Journal (February 1993); the article, along with an old version of the code, also appears on the Microsoft Developer Network (MSDN) CD-ROM.

The generic VxD is identical in concept to DEVHLP.SYS, a generic device driver that I wrote several years ago for OS/2, when I still believed in developing for that system ("Opening OS/2's Backdoor," DDJ, October 1990). Whereas OS/2 programs use generic IOCTL to call down to DEVHLP.SYS, Windows programs use INT 2Fh function 1684h to access the generic VxD.

Kelly has modified the generic VxD in several ways; the new version is available electronically, along with his WINEXP program. First, Kelly found an embarrassing bug: The generic VxD uses self-modifying code, yet it is missing the obligatory JMP SHORT $+2 to clear the instruction prefetch queue; this results in erratic behavior, as the processor sometimes ends up executing the old, nonmodified instructions left in the queue.

Next, Kelly added some additional services to the generic VxD. Most important is a new service to get the VM_InfoBlock, an undocumented structure that Kelly has uncovered. For example, this allows PROTDUMP -VM to now display ASCII names for each DOS box. Kelly's code for getting the name from the VM_InfoBlock also works under Chicago.

The generic VxD itself works under Chicago. But the generic VxD may soon become a lot less important. I have just received an incredible article and sample program by Alex Shmidt showing how normal 16-bit Windows programs can use callgates to call 32-bit Ring 0 code, including VMM and VxD functions. With Alex's RINGO.DLL, Windows programs would no longer need the generic VxD in order to call a VMM function such as Get_Cur_VM_Handle (or Get_Cur_Thread_Handle for that matter). The 32-bit Ring 0 code in RINGO.DLL even manages to link itself into the VxD chain (yes, it shows up in the output from VXDLIST). Alex says he named the program RINGO, in honor of both the Beatle and Matt Pietrek's RING0 program from Microsoft Systems Journal (May 1993). Matt's article showed how to use callgates to call 16-bit privileged Ring 0 code; Alex cleverly extends this to calling 32-bit Ring 0 code.

On the other hand, requiring a VxD may not be such an inconvenience under Chicago because VXDLDR provides dynamic VxD loading/unloading capabilities; perhaps VxDs won't have to be installed via an error-prone device= statement in SYSTEM.INI. In addition, it looks as if Chicago will support writing VxDs in C, and perhaps also allow calling VxDs from Win32 applications.

Another interesting upcoming article is Klaus Mueller's piece on instance data. Although instance data will undoubtedly change dramatically in Chicago, many of you have asked for an explanation of it. In the meantime, please continue to send your articles, article ideas, and comments to me on CompuServe at 76320,302.

Last month, I got most of the way through a blow-by-blow description of each field that makes up the Virtual Machine Control Block (VMCB) in Windows 3.1 Enhanced mode. This month, I'll finish describing the fields, and present a VM Explorer application for Windows; this application uses an undocumented structure I call the VM_InfoBlock.

Please remember that these field offsets are only valid for the retail version of Windows 3.1 Enhanced mode. In the debug version of VMM, the fields are generally moved out by eight bytes. For example, while the LDT selector is at VMCB+114h in the retail version, it is at VMCB+11Ch in the debug.

Incidentally, detecting the debug version of VMM is not as easy as you might think! Windows programmers jump to the answer that you call GetSystemMetrics(SM_DEBUG), but that only tells you whether the debug Windows kernel is running; the VMM in WIN386.EXE is totally different from the kernel in (for example) KRNL386.EXE. In fact, you must call a VMM function such as Test_Debug_Installed or Get_VMM_Version.

More Undocumented Fields

Here are the remainder of the undocumented VMCB fields:

0x9C, 0x9E. CB_ForeGround_TS_Priority and CB_BackGround_TS_Priority. Foreground and background time-slice priorities. The time-slice priority as set by the foreground and background values are only a priority in the sense that they determine the fraction of the total CPU time that the VM will receive. When a VM executes in the foreground, its priority is CB_ForeGround_TS_Priority; in the background it is CB_BackGround_TS_Priority.

The total system time-slice priority is the sum of all current priority values. At any point in time the total can change as different VMs become foreground or background processes.

The fraction of time that a VM will get to execute can be calculated by taking the total VM time-slice priority (either foreground or background) and dividing it by the total time-slice priority. This fraction is the amount of time that a VM will get to run. The relationship between total system time-slice priority and the VM time-slice priority determines the priority weighting for execution.

The time-slice priority is not the same as the execution priority. The VM with the highest execution priority is the VM that will run at any given point in time. The time-slice priority is a value that can be used as a decision in determining when to change the execution priority.

0xA0. CB_Weighted_Priority. Weighted priority=(Total of all VM priorities*16)/VM Priority. This priority is inverted: The lower the number, the higher the priority. It is also weighted: It is used as a multiplier to determine the fraction of the total time that this VM is permitted to run.

0xA4. CB_Weighted_Time. Weighted execution time, calculated by taking the time that a VM has been running and multiplying it by CB_Weighted_Priority times a last-action fudge factor. If the VM has released the time slice or it is blocked on a semaphore, the last-action multiplier is a smaller value, giving less weight to the amount of time used. It is a cumulative value based on the previous CB_Weighted_Time+(the difference in time since the last time it was calculated*CB_Weighted_Priority). Each time it is calculated, the current CB_VM_ExecTime is saved in the CB_Last_Weighted_Time field.

This value determines how much CPU time (relative to other VMs) the VM has used, based on the time-slice priorities of the VMs. VMM uses this value to determine whether or not it is time to give another VM its chance to run. The higher the time-slice priority of the VM, the slower this value will increase.

0xA8. CB_Next_Runnable_VM. Handle of next runnable VM. All VMs capable of running are linked together on a list. The first VM in the list is that with the Execution Focus, also called the "foreground VM." If this VM is not running in Exclusive mode, a VM will be linked on this list if it is background executable or if it has a high-priority background. If the Focus VM is running in Exclusive mode, the only other VMs on this list will be those with high background priority.

0xAC. CB_Last_Weighted_VMTime. VM time that the CB_Weighted_Time was last updated. This is subtracted from the CB_Exec_Time and multiplied by the CB_Weighted_Priority to determine the weight of the amount of time the VM has just been executing. The weighted time is added to CB_Weighted_Time to get a total weighted time. This value is compared against that for other VMs to determine if a task switch should occur.

0xB0, 0xB4. CB_DetailedErrorCode and CB_DetailedErrorRefData. Detailed error code and reference data associated with it. These fields are associated with the GetSetDetailedVMError function, documented in the DDK.

The first set of errors (high word=0001) is used when a VM is crashed (VNE_Crashed or VNE_Nuked bit set on VM_Not_Executable). The device which sets the error initially always sets the error with the high bit clear. The system will then optionally set the high bit depending on the result of the attempt to "nicely" crash the VM. This bit allows the system to tell the user whether or not the crash is likely to destabilize the system. The second set of errors (high word=0002) is used when a VM startup fails (VNE_CreateFail, VNE_CrInitFail, or VNE_InitFail bit set on VM_Not_Executable).

0xB8. CB_V86_PgTbl_PhysAddr. Physical address ofCB_V86_PageTable. This is the physical address that matches the linear address at VMCB+18h (see DDJ, January 1994). It is maintained as a physical address so that the Page Directory can be updated.

0xBC. CB_Instance_Table. Linear address of instance-data buffer. Instance Data is common data local to each VM. A perfect example is the DOS current-directory structure (CDS). Each VM must have its own CDS within DOS, but DOS knows nothing about multitasking and VMs. Each VM has an instance-data buffer. The buffer contains the instance data itself; its target location and size are kept elsewhere, in a private VMM data structure not accessible from the VMCB.

0xC0. CB_hMem_VMDataArea. Page handle to VM data area. The VM data area is PageAllocated and consists of the 4K page for the V86 Page table, followed by the VM CB itself, followed by the VxD CB areas described last month. This value is the handle returned by the VMM _PageAllocate function.

0xC4. CB_Int_Table_hMem. Page handle to instance data. This is a page handle as returned by _PageAllocate. VMCB+BCh contains the linear address of the same data. VMM saves the page handle so that instance data pages can be freed when the VM exits.

0xC8. CB_DeviceV86Pages. Device V86 page bitmap. An array of 110h bits representing the first 110h pages (one Mbyte+64K) of memory in a VM. If a bit is set, the corresponding page has been assigned. The VMM Get_Device_V86_Pages_Array function returns the nine dwords stored here.

0xEC. CB_V86PageableArray. V86 pageable array. An array of 100h bits representing the first 100h pages (one Mbyte) of memory in a VM. If a bit is set, the normal lock/unlock behavior for the corresponding page has been disabled. _GetV86PageableArray returns the eight dwords stored here.

0x10C.CB_MMGR_Flag.High memory-area flag. If this flag is set, the A20 high-memory area (HMA) has been enabled. This determines the validity of page accesses and requests above one megabyte.

0x110. CB_MMGR_Pages. Page handle to MMGR allocated pages. When pages are allocated for a VM beyond the one page (4 megabytes) normally used in the DOS box, they are linked through this field. Each page handle has a pointer to the next page handle in the list. So it's a simple matter to determine how many and what pages are owned by a VM. Pages can be allocated either by a VxD for the VM or by a protected-mode program running in the VM.

0x114, 0x118. CB_LDT and CB_hMem_LDT. Local descriptor table (LDT) and page handle to LDT memory. When a protected-mode program is running in a VM, it requires an LDT to provide selectors. The LDT is allocated from the global descriptor table (GDT). CB_LDT is simply the selector (in the GDT) that points to a block of memory that contains the local descriptors. Programs can use this to access protected-mode selector:offset pointers in other VMs. This field is 0 if the VMStat_PM_Exec bit is not set in the CB_VM_Status.

The LDT is PageAllocated, and CB_hMem_LDT is the handle to the page for the LDT; the handle allows the page to be freed when the protected-mode program exits.

0x11C, 0x120. CB_VM_Event_Count and CB_VM_Event_List. Count of VM events on the event list for this VM and linked list of VM events to be serviced by this VM. Before the VMM returns to this VM, VMM will check the VM_Event_Count. If the event count is nonzero, it will call the events linked on this list. The nodes on the list include fields for a call address and reference data to be passed to the called function. Events can be added to this list with Schedule_VM_Event.

0x124. CB_Priority_VM_Event_List. Linked list of priority VM events for this VM. Events can be added with Call_Priority_VM_Event. This service differs from Schedule_VM_Event in that it combines Call_When_VM_Ints_Enabled, Call_When_Not_Critical, and Adjust_Exec_Priority. If all of the required conditions are met, the event is called immediately; otherwise the event is scheduled.

0x128, 0x12C. CB_CallWhenVMIntsEnabled_Count and CB_CallWhenVMIntsEnabled_List. Count of calls and linked list of calls to make when VM interrupts are enabled. When interrupts are enabled within a VM, any events linked on this list will be called. Events are placed on this list if interrupts are disabled at the time of a call to Call_When_VM_Ints_Enabled.

0x130. CB_Next_Timeout_Handle. Handle to a time-out event. Set_VM_Time_Out schedules a time-out to occur after a given period of time has elapsed. The handle to the time-out is linked on this list. The first time-out on the list will be that with the shortest expiration time.

0x134. CB_Prev_Timeout_Handle. Handle to a time-out event. This is possibly a pointer to the previous time-out handle on the list, that is, a pointer to the last time-out.

0x138. CB_First_Timeout. Time in milliseconds until the first time-out will expire.

0x13C. CB_Expiration_Time. Expiration time overrun in milliseconds. If the time-out cannot be serviced immediately when it occurs, this field will maintain a count in milliseconds of the time elapsed since the time-out event actually did occur. Thus, when the time-out is finally serviced, it can tell how long ago the actual time-out happened.

0x140, 0x146, 0x148. CB_IDT_Base_hMem, CB_IDT_Limit and CB_IDT_Base. Page handle to CB_IDT_Base, limit in bytes of IDT for this VM, and linear address of IDT for this VM. Whether executing a protected-mode or V86-mode program in a VM, the underlying VMM system is still running in protected mode. All interrupts are processed through an interrupt-descriptor table (IDT). If the application is a V86-mode program, the VMM "catches" the interrupt and reflects it into real mode, if it chooses to, through the use of a real-mode interrupt table at address 0000:0000 in the real-mode VM address space. This field will then point to the system IDT.

If the application is a protected-mode program, the IDT will be a copy of the system IDT modified specifically for this VM, with this field pointing to the copy. The page handle allows the IDT to be locked, unlocked, and freed when the protected-mode application finishes executing.

0x14C. CB_Exception_Handlers. An array of 32 DPMI exception handlers; each entry is six bytes, identical to the CX:EDX return value from DPMI INT 31h function 0202h. Installing an exception handler with either DPMI function 0203h or the ToolHelp InterruptRegister function changes this array.

0x20C. CB_V86_CallBack_List. Handle to V86 callback list. When a protected-mode program running in a VM allocates a V86 callback (DPMI INT 31h function 0303h), a callback structure is defined and linked to this handle. V86 callbacks are a limited resource; this field allows them to be freed when a VM exits.

The VM Explorer

As Figure 1 shows, the VM Explorer (WINEXP) neatly displays each field in the VMCB. Even though WINEXP is a Windows application, and therefore running inside the System VM (VM #1), it can display the contents of any VM.

More than this, WINEXP provides a constantly updated real-time display. It is educational to run a windowed DOS box over WINEXP while WINEXP is displaying the DOS box's VMCB. For example, if you run DOS programs that use DPMI, such as VXDLIST or PROTDUMP, WINEXP will show fields change in the VMCB as the DOS program enters and exits protected mode.

Normally, a VM Control Block is accessed from a VxD, which works with 32-bit "flat" linear addresses ranging from 0 to 4GB. WINEXP is a normal 16-bit Windows application using selector:offset pairs. To gain access to the VMCBs and their associated fields, WINEXP uses Andrew Schulman's generic VxD. I've made several enhancements to the original.

First, in Figure 1 WINEXP knows the ASCII name of each DOS box, such as "COMMAND" or "MS-DOS Prompt". This name does not come from the VMCB. While the name "System VM" is supplied by WINEXP itself, the DOS-box names are not. Where do these come from?

When a new VM is created, VMM passes a Create_VM control message to each VxD in the system. The DDK says that this message passes the VM handle of the new VM to each VxD, permitting the VxD to perform any VM-specific initialization. What the DDK doesn't say is that, passed along with the message, there is also an undocumented pointer: ESI points to a VM_InfoBlock structure created from the PIF settings (see DDJ, July 1993) for the VM. The VM_InfoBlock is shown in Figure 2.

The field we are interested in here is AppName, which comes directly from the "Window Title" PIF setting (if the window title later changes, AppName is not updated). WINEXP uses this field to provide a name for each DOS box.

I've modified the generic VxD to handle the Create_VM message and save the entire VM_InfoBlock in the VMCB, reserving space for it with the Allocate_Device_CB_Area service. I have added a new VxD_Get_InfoBlock service to the generic VxD to return this information. As Figure 2 shows, besides AppName, the VM_InfoBlock includes other interesting information such as the window handle (hWnd) for each DOS box. Note that this information is read-only; you cannot modify the DOS box's state by changing this structure.

WINEXP keeps its display constantly updated by creating a Windows timer that goes off four times a second. The WM_TIMER handler then calls a function that updates the window. Unfortunately, the WINEXP code is far too long to print here. However, the large number of C files that comprise WINEXP are available electronically. Much of the code is not directly related to VMs, and is devoted to displays strings at a given line number in a window, highlighting the line, scrolling, and so on.

As a smaller example, Listing One (page 129) shows an extremely simple Windows program (it uses Borland's EasyWin or any similar stdio facility for Windows) that shows how to access the VMCB and VM_InfoBlock.

Besides displaying a list of VMs and the contents of an individual VMCB, WINEXP also has an option to display time-slice information. This includes the scheduler list and various scheduler values for each VM. One thing to keep in mind while viewing the time-slice info: To display the data, the Windows program (more accurately, the System VM) is the active VM. Even if the focus is on another VM, VMEXP will always show the System VM as the current VM. This is not a bug. Think about it!

Example 1: Calling the VMM_GetDDBList function.

#define VMM_GetDDBList  0x1013FL
DWORD Get_First_VxD(void) {
    VxDParams p;
    // should check if Windows 4+
    p.CallNum = VMM_GetDDBList;
    VxDCall(&p);
    return p.OutEAX;
    }

Table 1: VxDs provided by Win32 services.

VxD       Number of Win32 Services
----------------------------------
VMM       29
VWIN32    39
VCOMM     21
VCOND     41

Figure 1: WINEXP, the Windows VM explorer.

Figure 2: The VM_InfoBlock. A pointer to this structure appears in ESI during a Create_V message.

#pragma pack(1)

typedef struct { DWORD lin; WORD sel; } SELLIN; // use lin; ignore sel

typedef struct  {
    DWORD   SGVMI_Flags;            /* initial SHELL_GetVMInfo flags */
    DWORD   PIF_Bits;
    SELLIN  Comspec;                /* COMSPEC string */
    SELLIN  CmdLine;                /* command line */
    SELLIN  CurDrive;               /* current drive string */
    WORD    MaxAllocPages, MinAllocPages;
    WORD    ForeGroundPrio, BackGroundPrio;
    WORD    MaxEMS_Kbytes, MinEMS_Kbytes;
    WORD    MaxXMS_Kbytes, MinXMS_Kbytes;
    WORD    hWnd;                   /* Window handle for WinOldAp */
    WORD    Offset2C;               /* ??? */
    char    AppName[32];            /* Name from PIF "Window Title" */
} VM_InfoBlock;

[LISTING ONE]


// VM.C -- Display information about Windows Virtual Machines
// bcc -W -2 vm.c (Borland EasyWin) or cl -Mq -G2 vm.c (Microsoft QuickWin)
// Andrew Schulman, December 1993

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <dos.h>
#include "windows.h"

#define VXD_VxDCall         1
#define VxD_Get_InfoBlock   7
#define Generic_Dev_ID      0x28c0

#define Get_Next_VM_Handle  0x01003BL
#define Get_Sys_VM_Handle   0x010003L

typedef void far *FP;

// from ddk vmm.inc
typedef struct {
    DWORD CB_VM_Status, CB_High_Linear, CB_Client_Pointer, CB_VMID;
    // etc.: we don't need any undocumented VM_CB fields for this
    } VM_CB;    // Virtual Machine Control Block

#pragma pack(1)

///// VM_InfoBlock: see Figure 2 /////

typedef struct {
    DWORD CallNum, Reserved1;
    DWORD InEAX, InEBX, InECX, InEDX, InEBP, InESI, InEDI;
    DWORD Reserved2, Reserved3;
    DWORD OutEAX, OutEBX, OutECX, OutEDX, OutEBP, OutESI, OutEDI;
    WORD  OutFS, OutGS, OutEFLAGS;
    } VxDParams;

static FP API = (FP) 0;
void show_vm(DWORD vm, VM_CB far *vm_cb);
VM_InfoBlock far *get_vm_info(DWORD vm);
WORD GetVMInfoBlockOffset(void);
void InitVxDAPI(void);
FP GetVxDAPI(WORD vxd_id);
FP map_linear(DWORD lin_addr, DWORD num_bytes);
void free_mapped_linear(FP fp);
DWORD GetSysVMHandle(void), GetNextVMHandle(DWORD vm);
BOOL VxDCall(VxDParams far *fp);

void fail(char *s) { puts(s); exit(1); }

main()
{
    VM_CB far *vm_cb;
    DWORD sys_vm = GetSysVMHandle();
    DWORD vm = sys_vm;

    while (vm_cb = (VM_CB far *) map_linear(vm, sizeof(VM_CB)+0x2000))
    {
        show_vm(vm, vm_cb);
        free_mapped_linear(vm_cb);
        if ((vm = GetNextVMHandle(vm)) == sys_vm)
            break;  // GetNextVMHandle makes look like circular list
    }
    return 0;
}
void show_vm(DWORD vm, VM_CB far *vm_cb)
{
    VM_InfoBlock far *info;
    printf("#%lu\tVMCB=%08lX   high_lin=%08lX",
        vm_cb->CB_VMID, vm, vm_cb->CB_High_Linear);
    // could show undoc information here too
    if (info = get_vm_info(vm))
    {
        char buf[33];
        _fstrncpy(buf, info->AppName, 32);
        buf[33] = '\0';
        printf("   %04Xh   \"%s\"", info->hWnd, buf);
        free_mapped_linear(info);
    }
    printf("\n");
}
VM_InfoBlock far *get_vm_info(DWORD vm)
{
    DWORD ofs = (DWORD) GetVMInfoBlockOffset();
    if (! ofs)
        return (VM_InfoBlock far *) 0;  // call not supported
    ofs += vm;  // add in handle to VMCB
    return (VM_InfoBlock far *) map_linear(ofs, sizeof(VM_InfoBlock));
    // caller must call free_mapped_linear
}
WORD GetVMInfoBlockOffset(void)
{
    WORD ofs = 0;
    InitVxDAPI();
    _asm mov ax, VxD_Get_InfoBlock
    _asm call dword ptr [API]
    _asm jc done
    _asm mov ofs, bx
done:
    return ofs;
}
void InitVxDAPI(void)
{
    if (! API)  // one-time initialization
        if (! (API = GetVxDAPI(Generic_Dev_ID)))
            fail("This program requires device=VXD.386");
}
FP GetVxDAPI(WORD vxd_id)
{
    _asm push di
    _asm mov ax, 1684h
    _asm mov bx, vxd_id
    _asm xor di, di
    _asm mov es, di
    _asm int 2fh
    _asm mov ax, di
    _asm mov dx, es
    _asm pop di
    // returns in DX:AX
}
FP map_linear(DWORD lin_addr, DWORD num_bytes)
{
    WORD sel;
    _asm mov sel, ds
    if ((sel = AllocSelector(sel)) == 0)
        return (FP) 0;
    SetSelectorBase(sel, lin_addr);
    SetSelectorLimit(sel, num_bytes - 1);
    return MAKELP(sel, 0);
}

void free_mapped_linear(FP fp) { FreeSelector(FP_SEG(fp)); }
DWORD GetSysVMHandle(void)
{
    VxDParams p;
    p.CallNum = Get_Sys_VM_Handle;
    return (VxDCall(&p)) ? p.OutEBX : 0;
}
DWORD GetNextVMHandle(DWORD vm)
{
    VxDParams p;
    p.CallNum = Get_Next_VM_Handle;
    p.InEBX = vm;
    return (VxDCall(&p)) ? p.OutEBX : 0;
}
BOOL VxDCall(VxDParams far *fp)
{
    InitVxDAPI();
    _asm les bx, dword ptr fp
    _asm mov ax, VXD_VxDCall
    _asm call dword ptr [API]
    _asm jc error
    return TRUE;
error:
    return FALSE;
}

Copyright © 1994, Dr. Dobb's Journal