Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Enumerating processes on NT/Win9x

0.00/5 (No votes)
9 Sep 2002 1  
A simple class encapsulating tlhelp32 and PSAPI

Sample running on Win2k

Motivation

Many programmers write software that require the set of running processes to be enumerated. Unfortunately there is no standard method of doing this. On Windows 95 and Windows 98 there are the ToolHelp API functions (found in Kernel32.dll). For some reason the Microsoft NT team didn't like the ToolHelp functions and decided not to add them to Windows NT. Instead they provided their own set of Process Status functions, the PSAPI, and added them to an external module (residing in psapi.dll). Finally, on Windows 2000, the developers chose to provide both methods of enumeration.

What this is

CEnumProcess is a simple class for enumerating running processes using either PSAPI or ToolHelp. The preferred method is decided on runtime. It contains two classes: CEnumProcess::CProcessEntry and CEnumProcess::CModuleEntry for storing results.

How it works

When creating an instance of the class, it attempts to load the appropriate modules and find PSAPI/ToolHelp related functions. Based on which set of functions it finds the class sets the enumeration method to the most appropriate. For example, this is the code for finding the PSAPI related functions:

// Try to load psapi.dll	

PSAPI = ::LoadLibrary(TEXT("PSAPI"));
if (PSAPI)  
{
     // Find PSAPI functions

     FEnumProcesses = (PFEnumProcesses)::GetProcAddress(PSAPI, 
                                                      TEXT("EnumProcesses"));
     FEnumProcessModules = (PFEnumProcessModules)::GetProcAddress(PSAPI, 
                                                      TEXT("EnumProcessModules"));
#ifdef UNICODE
     FGetModuleFileNameEx = (PFGetModuleFileNameEx)::GetProcAddress(PSAPI, 
                                                      TEXT("GetModuleFileNameExW"));
#else
     FGetModuleFileNameEx = (PFGetModuleFileNameEx)::GetProcAddress(PSAPI, 
                                                      TEXT("GetModuleFileNameExA"));
#endif
}

Usage

There are seven public functions in the class:
  • int GetAvailableMethods()
  • int GetSuggestedMethod()
  • int SetMethod(int method)
  • BOOL GetProcessNext(CProcessEntry *pEntry)
  • BOOL GetProcessFirst(CProcessEntry* pEntry)
  • BOOL GetModuleNext(DWORD dwPID, CModuleEntry* pEntry)
  • BOOL GetModuleFirst(DWORD dwPID, CModuleEntry* pEntry)
The enumerationmethod related functions sets/returns one of the values found in the namespace ENUM_METHOD below:
namespace ENUM_METHOD 
{const int NONE    = 0x0;
 const int PSAPI   = 0x1;
 const int TOOLHELP= 0x2;
 const int PROC16  = 0x4;
} 

ENUM_METHOD::NONE is used for the unlikely event that no method can be found, like if an NT-user has deleted psapi.dll. Under Windows 2000, GetAvailableMethods returns ENUM_METHOD::PSAPI + ENUM_METHOD::TOOLHELP. The suggested method will be to use the ToolHelp API. If 16-bit processes should be enumerated ENUM_METHOD::PROC16 should be added to the enumerationmethod. This is usually done by default.

More interesting are the GetProcess/GetModule functions. They take a pointer to a CProcessEntry/CModuleEntry as input, and returns TRUE/FALSE depending on success or failure in enumeration. The useful members of the classes are as follows:

CProcessEntry

LPTSTR lpFilename;  // name of file

DWORD  dwPID;	    // ID of the process	

WORD   hTask16;	    // If this is a 16-bit process, 

                    // return task handle here, otherwise 0

The member hTask16 is only used on NT and Win2k. If hTask16 is anything other than 0 on these operating systems, this is a 16-bit process. In that case lpFilename will be the path of the 16-bit process, and dwPID will be the identifier of the NTVDM currently run under (see "A WORD on 16-bit processes").

Note that if using ToolHelp under Win2k lpFileName will not be the full path of the file. Try using GetModuleFirst to get the first module loaded (the .exe itself), and then retrive the full path from CModuleEntry.

CModuleEntry

LPTSTR lpFilename;    // path of file

PVOID pLoadBase;      // Loading address

PVOID pPreferredBase; // Address specified in fileheader

All this means you can write code like this, and it will work under both Win9x and NT.

CEnumProcess enumeration;
CEnumProcess::CProcessEntry entry;

for (BOOL OK = enumeration.GetProcessFirst(&entry); OK; 
     OK = enumeration.GetProcessNext(&entry) )
     {
         // Do something useful

         TRACE("PID = %X, process = %s\n", entry.dwPID, entry.lpFilename);
     }

Why the preferred base is important

I have found that many modules does not load at their preferred baseaddress. If a module does not load at its preferred base, the application using it will require more memory and take a performance hit when initializing. This is because a module can only be mapped to its .dll on disk if the baseaddress is equal to the loadaddress. This was the reason for including the preferred base in the moduleentry in the first place. Usually is it due to lazy programmers, who do not rebase their modules from the the default address (0x10000000, or Visual Basic 0x11000000). It could also be hard to predict where a module can find some place to load. The function for finding the preferred base is found below:

PVOID CEnumProcess::GetModulePreferredBase(DWORD dwPID, PVOID pModBase)
{if (ENUM_METHOD::NONE == m_method) return NULL; 
 HANDLE hProc = OpenProcess(PROCESS_VM_READ, FALSE, dwPID);
 if (hProc)
 {IMAGE_DOS_HEADER idh = {0};
  IMAGE_NT_HEADERS inh = {0};
  //Read DOS header

  ReadProcessMemory(hProc, pModBase, &idh, sizeof(idh), NULL);

  if (IMAGE_DOS_SIGNATURE == idh.e_magic) // DOS header OK?

      // Read NT headers at offset e_lfanew 

      ReadProcessMemory(hProc, (PBYTE)pModBase + idh.e_lfanew,
                        &inh, sizeof(inh), NULL);

  CloseHandle(hProc); 
    
  if (IMAGE_NT_SIGNATURE == inh.Signature) //NT signature OK?

   // Get the preferred base...

   return (PVOID) inh.OptionalHeader.ImageBase; 
 }
 return NULL; //didn't find anything useful..

}
For those new to the PE format, these are standard headers of an executable file. When an executable is loaded into memory, the entire file is mapped - even the headers. A lot of useful information can be found here, like the size of the mapped file.

A WORD on 16-bit processes

Under Windows 95 and Windows 98 ToolHelp API treats 16-bit applications just like any other process. This is not the case under NT. Instead CEnumProcess will return the name of the NT virtual DOS machine (NTVDM) it is currently run under. This means that if you run the testapplication and find some processnames like "ntvdm.exe", you have found a 16-bit process. If ENUM_METHOD::PROC16 is currently set, the next entries returned by GetNextProcess will be the 16-bit processes that currently run in this virtual machine. This is done by using the VDMDBG API - a set of functions for debugging 16-bit applications. VDMDBG allows enumeration of modules, but only if the process is acting as a debugger. This require that the 16-bit debugger WOWDEB.EXE task runs in the VM currently debugged. Attaching as a debugger is not a very good idea, though. There is no way of unattaching and if the debugger process terminates so will the debugee. For that reason enumeration of 16-bit modules is not included in the class.

Known bugs/limitations

Some processes has a sequrity attribute that prevents reading its memory. This means that modules can not be enumerated. If using PSAPI, not even the filename can be retrieved.

Contact

Send suggestions, improvements and comments to me.

History

10 Sep 2002 - updated downloads

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here