Windows XP
|
Christophe Nasarre |
This article assumes you're familiar with Win32 |
Level of Difficulty 1 2 3 |
Download the code for this article: Debug.exe (583KB) |
SUMMARY DLL conflict problems can be tough to solve, but a large number of tools are available to help. There are also many Windows APIs that you can use to build custom debugging tools. Three such tools are discussed here and provided as samples. DllSpy lists all the DLLs loaded in the system and the processes that are using them. ProcessSpy enumerates the running processes and exposes the DLLs they are using, and ProcessXP displays the list of concurrent running sessions for Windows XP. |
LL Hell is nothing new. When you rely upon DLLs from external sources, you can get all kinds of problems, like missing entry points or incompatible versions of the library. .NET allows side-by-side execution of components, alleviating these versioning problems, but if you're not yet deploying to .NET, what do you do? Well, you can trace DLL dependencies using a variety of tools. But when you use standard tools to trace this information, you might not get all the information you're after. And many tools don't provide functionality you need, like automatic logging, trace analysis, console-only operation, and scriptability.
I'll take a look at some of the available tools for examining running processes, and describe three I've developed: DllSpy, ProcessSpy, and ProcessXP, which can be used in your own debugging and development efforts.
Depends.exe is one tool that comes with Visual C++. Perhaps the simplest tool, it lists the DLLs that an application or another DLL needs. (Look at https://www.dependencywalker.com for updated versions.) If you need the full path of the DLLs and executables, simply right-click on the DLL name in the list or the tree and select Full Path from the context menu, as shown in Figure 1.
While tools like Depends.exe are fine for static dependencies (those constructed during the link process), except with profiling in newer versions, they are useless if you are using COM objects instantiated by the runtime or dynamically loading a DLL to call a particular function via LoadLibrary and GetProcAddress. You don't know when or from which folder this type of DLL will be loaded.
One way to identify dynamically loaded DLLs is to find out which DLL is loaded by each process. The SysInternals Web site, at https://www.sysinternals.com, provides LISTDLLS.EXE, a console-mode tool, and a GUI tool called Process Explorer that does this and much more (see Figure 2).
In addition to listing the DLLs used by a process, Process Explorer lets you know which kernel objects are consumed. Since version 3.11, Process Explorer has allowed you to easily detect the new or unused objects between two snapshots.
Occasionally, a DLL is loaded for a short period of time and then released before you can detect it using Process Explorer. When this happens, you need another kind of tool, which I'll discuss in my next article.
To navigate through processes and DLLs, you first need to know which processes are using each loaded DLL. You can use my sample program, DllSpy, to discover them (see Figure 3). In DllSpy an upper pane lists every loaded DLL and a lower pane enumerates the processes that are using the selected DLL.
My ProcessSpy tool exposes the opposite relationship between processes and DLLs (see Figure 4). It enumerates the running processes in the upper pane; the lower pane lists each DLL that is used by a selected process, focusing on the real load address rather than the preferred load address, and static rather than dynamic dependencies.
These tools are available in the code download (available at the link at the top of this article). If these don't meet your needs but you want something similar, you'll need to know how to get information about and enumerate running processes. Then you can list their modules, whether statically or dynamically loaded.
Getting the List of Running Processes
When you need to enumerate Win32® running processes, the three methods shown in Figure 5 are available. I won't discuss TOOLHELP32 since MSDN® provides a lot of sample code that uses its functions. Performance counters provide much more information than the processes list. They're invaluable if you need to fetch information from remote computers. If you always wanted to get a process list from another machine, you have your solution!
The process status API (PSAPI) is a useful tool that's available in the Microsoft Platform SDK. The CProcessList class in my sample file, Process.cpp, wraps PSAPI to get the process list. Once Refresh has been called, a process description can be retrieved from a process ID and easily enumerated using GetFirst and GetNext (see Figure 6). Refresh is implemented using EnumProcesses (found in PSAPI), as shown in Figure 7.
If you are still supporting 16-bit code for Windows®, such as the applications listed under ntvdm.exe by the TaskManager, enumerating processes is more involved. The Knowledge Base articles referenced at the beginning of this article can help you, as well as two Under the Hood columns by Matt Pietrek in the August 1998 and September 1998 issues of MSJ.
Once you have a list of the running processes, you'll need to get as much information as possible from each of them, based on their IDs returned by EnumProcesses, to build a useful tool. Using a process handle obtained by calling OpenProcess with PROCESS_QUERY_INFORMATION | PROCESS_VM_READ as a parameter, the AttachProcess method (in the CProcessList class in Process.cpp) creates the description shown in Figure 8.
A few details about AttachProcess require explanation. First, to avoid being statically linked with OS-specific DLLs such as PSAPI or NTDLL, it is worth writing classes that wrap each function needed from these DLLs. (See Wrappers.h and Wrappers.cpp in the sample code for details.) For example, you only need to define a CPSAPIWrapper object and call its GetModuleFileNameEx method; you don't need to link with the PSAPI library. In addition, you should call its IsValid method to check that these DLLs are usable on the system you are running. This allows your code to run on any Windows platform without raising a system error such as undefined links. But check the Windows version or the result from IsValid before using a particular feature. (See DllSpyApp::InitInstance in DllSpy.cpp for an example.)
Notice that the GetModuleFileNameEx function from PSAPI returns strange file names such as "\SystemRoot\ System32\ smss.exe" or "\??\C:\WINNT\system32\winlogon.exe." The TranslateFilename function (in Helpers.cpp) gives you file names that are easier to read. More on this later.
When it's time to find the main window of a process, EnumWindows executes the callback as a parameter for each top-level window. In the callback function, GetWindowThreadProcessId retrieves the ID of the process that creates the corresponding window; if this window is visible, I stop the enumeration (see the GetMainWindow implementation). Note that GetWindowText can be used to get the title of a window from a different process.
Conversely, to get the name of the file name corresponding to the process that created a particular window, you'll run into problems using GetWindowModuleFileName. This function always returns the path name of the current running process.
The procedure for getting the main window of a process (for which you can use the GetWindowThreadProcessId API) is described in Jeff Prosise's Wicked Code column in the August 1999 issue of MSJ. You know how to get the full path name from a process ID using PSAPI. Taking the full path name and using GetWindowThreadProcessID will get you the file name of a process that created a particular window.
In AttachProcess, the call to OpenProcess that is needed to get most of the process information may return with an access denied error. In that case, I used the method presented by Keith Brown in his August 1999 Security Briefs column in MSJ for getting a process handle with high-level rights. (See the GetProcessHandleWithEnoughRights function in Helpers.cpp in the code for implementation details.)
One example where this behavior occurs is when a process is run as a scheduled task under another user account. Even the Windows Task Manager can't terminate such a process; it merely displays the dialog box shown in Figure 9.
Figure 9Can't Terminate Process
If you double-click on a process in my sample application ProcessSpy, it will be terminated. You should take a look at SlayProcess in the same project. This helper function uses GetProcessHandleWithEnoughRights to obtain a process handle, but with PROCESS_TERMINATE as an access right instead of PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, as is used by AttachProcess.
Finally, GetProcessOwner finds the user account (in \\Domain\User format) under which the process is running. It does this by using GetTokenInformation with TokenUser as a parameter and then LookupAccountSid to transform the returned user SID into a human-readable domain and user. Sometimes, OpenProcessToken fails with an access denied error for some process such as System. Even PULIST.EXE from the Windows 2000 Resource Kit fails to display the owning user for the same processes. Only ProcessExplorer succeeds in finding the owner of such "secured" applications. You'll see later in this article how the Windows Terminal Services API can be used to get the owner of these processes under Windows XP.
Getting the Command Line for a Process
One method in the list in Figure 8, GetCmdLine, returns the command line of the process. Actually, it doesn't exactly return the command line, but rather the parameters received by the process at startup. Let's take an example. If you install TweakUI from the Microsoft® Power Toys (https://www.microsoft.com/ntworkstation/downloads), you get an additional Command Prompt Here entry in the context menu when you right-click on any folder in the Windows Explorer. And as if by magic, a command prompt appears with the folder as the current working directory.
But how do you know what parameter is used when cmd.exe is called? You could use TLIST.EXE from the Microsoft Debugging Tools (see https://www.microsoft.com/ddk/debugging/) to find out. This is a console-mode program that lists the running processes and information such as the command line, if called with a process ID as a parameter, as shown in Figure 10 in the case of a C: root.
The third line in Figure 10 shows the parameters /k cd "C:\" used by the shell extension to call cmd.exe. If you specify the parameter /k, then cmd.exe carries out the specified command but it does not exit. This doesn't work if you are building your own tool because this invokes an application, whereas you need to call functions from an API.
The source code for the TLIST tool is available on the MSDN Web site at "TList: Task List Application Sample". Unfortunately, with that code you can only retrieve the ID, name, and main window for a process.
So you have three options for getting the command line of a process. The first is brutal: dig into TLIST at the assembly level. The result is the function GetProcessCmdLine in Process.cpp, which navigates inside the process address space to find its command line. A pointer to the command line (in Unicode) is stored in a memory block pointed to by a field of the Process Environment Block (PEB), a kernel data structure that I'll discuss in my next article.
The second solution is to browse the Web and find someone who has already solved the problem! GetCommandLine tells you the command line, but only for the calling process. The best way around this is to execute this call within another process context using CreateRemoteThread, which Felix Kasza explains at https://www.mvps.org/win32/processes/remthread.html.
The last option is code reuse. Or, to be more accurate, output reuse. You could catch the output from TLIST so you only have to parse it in order to get the command line. I'll explain this method more fully in my next article.
Windows XP has a new feature called Fast User Switching, which allows several users to be logged on at the same time on the same machine. Processes launched by one user can still run while another user is logged in. The magic behind this feature lies in the Windows Terminal Services (WTS) APIs. If you need a broad explanation of WTS, you should take a look at "Introducing the Terminal Services APIs for Windows NT Server and Windows 2000" in the October 1999 issue of MSJ.
Windows XP creates an instance of a WTS session for each logged-on user. A running process is always associated with such a session. The Windows XP TaskManager allows you to list processes either for all sessions or for only your own session using the "Show processes for all users" checkbox (see Figure 11).
Figure 11Listing Processes
If you need to learn the session ID under which a process is attached, call the ProcessIdToSessionId API exported by kernel32.dll. When given a process ID, it returns the corresponding session ID. It is interesting to note that this API is not exported by wtsapi32.dll, which implements all other Windows Terminal Services APIs, but by kernel32.dll. In fact, even when Windows Terminal Services are not enabled, the session ID is stored by Windows 2000 and Windows XP in the PEB.
Note that Windows NT® neither stores the session ID in its PEB nor exports ProcessIdToSessionId in kernel32.dll. When you call ProcessIdToSessionId on Windows 2000 without WTS enabled, you get 0 as session ID for all processes.
In addition to allowing you to list the open sessions, WTS has an API to enumerate running processes that is different from those in PSAPI and TOOLHELP32. I have written a class named CWTSWrapper to wrap the functions related to processes and sessions in WTS in order to avoid static linking with wtsapi32.dll. (See \Common\wrappers.cpp for implementation details.) Using CWTSWrapper, it is easy to build a console mode application such as ProcessXP (see Figure 12) that enumerates open sessions with the corresponding logged-on user and the running processes.
As you can see in Figure 12, three sessions are open on the MACHINE computer. The first session has an ID of 0, is active (since this is the one where ProcessXP is running), and serves the logged-on user called Administrator. The second one has 1 as its ID, is disconnected, and serves the Standard user who has started Notepad and Freecell by hand. Finally, Player has opened the session 2 to launch WordPad, but it's now disconnected.
The source code that uses the CWTSWrapper class to generate this output can be found in the download for this article. Like the Registry, the WTS allows you to grab information from another machine. That's why WTS enumeration APIs take a server handle as the first parameter. WTS_CURRENT_SERVER_HANDLE is used for the current machine. The second parameter is reserved and should be set to 0. The third is the expected version and should be 1. The last two parameters contain the meaningful information on return. The last one is the count of sessions or processes, while the second to last points to an array of structures describing either a session or a process. Since the array has been allocated by WTS, you must remember to release it using WTSFreeMemory.
A session is described by a WTS_SESSION_INFO structure:
|
|