/v show version information
/t show time & date information
/p show full path
/l show link time & date information
If you run DEPENDS with just the name of an executable file, you’ll get a list of all DLLs used by the executable, along with the EXE’s name. For example, running DEPENDS on MSDEV.EXE from Visual C++¨ gives the output shown in Figure 2. Of the 19 executables that are required to run MSDEV.EXE (18 DLLs and one EXE), only four of the DLLs are referenced directly: MSVCSHL.DLL, MFC40.DLL, MSVCRT40.DLL, and KERNEL32.DLL. The remaining DLLs are indirectly imported; that is, they’re imported by one of the four DLLs used by MSDEV. Alternatively, the DLLs may be another level away and are imported by one of the DLLs imported by MSDEV’s four DLLs. The DEPENDS program uses recursion to show you all of an executable’s dependencies, much like the Win32 loader does when it loads the program. I ’ll have more to say on this later.
Figure 2 DEPENDS Output for MSDEV.EXE
LZ32.dll
VERSION.dll
comdlg32.dll
COMCTL32.dll
SHELL32.dll
WINMM.dll
ole32.dll
RPCRT4.dll
ADVAPI32.dll
USER32.dll
GDI32.dll
MFC40.DLL
MSVCIRT.dll
ntdll.dll
KERNEL32.dll
MSVCRT.dll
MSVCRT40.dll
MSVCSHL.dll
MSDEV.EXE
The four command-line switches to DEPENDS let you tailor the output to your liking. You can use either – or / as the switch character, and the options are not case-sensitive (/V is equivalent to –v).
The /v switch causes DEPENDS to emit any version information that it finds in an executable file. Figure 3 shows an example of the /v switch used on CLOCK.EXE.
The /p switch tells the program to append the complete path to each EXE or DLL in the dependency list. For example, this command line
DEPENDS /p c:\WINNT\SYSTEM32\CLOCK.EXE
generates this:
COMCTL32.dll (c:\WINNT\system32\COMCTL32.dll)
SHELL32.dll (c:\WINNT\system32\SHELL32.dll)
RPCRT4.dll (c:\WINNT\system32\RPCRT4.dll)
ADVAPI32.dll (c:\WINNT\system32\ADVAPI32.dll)
GDI32.dll (c:\WINNT\system32\GDI32.dll)
USER32.dll (c:\WINNT\system32\USER32.dll)
KERNEL32.dll (c:\WINNT\system32\KERNEL32.dll)
ntdll.dll (c:\WINNT\system32\ntdll.dll)
comdlg32.dll (c:\WINNT\system32\comdlg32.dll)
clock.exe (c:\WINNT\system32\clock.exe)
This capability lets you see exactly which DLLs are being used by a program. If you suspect a DLL mismatch (for instance, a DLL in multiple directories in the path), the /p switch can be helpful in tracking down the problem.
Figure 3 CLOCK.EXE Version Information
COMCTL32.dll
CompanyName Microsoft Corporation
FileDescription Custom Controls Library
FileVersion 4.70
InternalName COMMCTRL
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename COMMCTRL.DLL
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.70
SHELL32.dll
CompanyName Microsoft Corporation
FileDescription Windows Shell Common Dll
FileVersion 4.00
InternalName SHELL32
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename SHELL32.DLL
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
RPCRT4.dll
CompanyName Microsoft Corporation
FileDescription Remote Procedure Call Runtime
FileVersion 4.00
InternalName rpcrt4.dll
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename rpcrt4.dll
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
ADVAPI32.dll
CompanyName Microsoft Corporation
FileDescription Advanced Windows 32 Base API
FileVersion 4.00
InternalName advapi32.dll
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename advapi32.dll
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
GDI32.dll
CompanyName Microsoft Corporation
FileDescription GDI Client DLL
FileVersion 4.00
InternalName gdi32
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename gdi32
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
USER32.dll
CompanyName Microsoft Corporation
FileDescription Windows NT USER API Client DLL
FileVersion 4.00
InternalName user32
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename user32
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
KERNEL32.dll
CompanyName Microsoft Corporation
FileDescription Windows NT BASE API Client DLL
FileVersion 4.00
InternalName kernel32
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename kernel32
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
ntdll.dll
CompanyName Microsoft Corporation
FileDescription NT Layer DLL
FileVersion 4.00
InternalName ntdll.dll
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename ntdll.dll
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
comdlg32.dll
CompanyName Microsoft Corporation
FileDescription Common Dialogs DLL
FileVersion 4.00
InternalName comdlg32
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename comdlg32.dll
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
clock.exe
CompanyName Microsoft Corporation
FileDescription Clock Applet
FileVersion 4.00
InternalName clock
LegalCopyright Copyright (C) Microsoft Corp. 1981-1996
OriginalFilename CLOCK.EXE
ProductName Microsoft(R) Windows NT(TM) Operating System
ProductVersion 4.00
The remaining two command-line switches emit the time and date of each EXE or DLL in the dependency list. Using /t forces DEPENDS to emit the date and time of the file as recorded by the file system. This time and date information is what you’ll see in the Explorer or by doing a DIR from the command line.
The other date and time information that DEPENDS can show is when the executable was created. This information is stored in the PE header and doesn’t change even if you explicitly modify the traditional date and time by using tools like TOUCH. To see this information, use DEPENDS with the /l switch.
How I obtained this information is an interesting programming story, which I’ll come back to later. As a side note, I was quite surprised when I ran DEPENDS on some Windows NT 4.0 executables. It seems that USER32.DLL, KERNEL32.DLL, and NTDLL.DLL were all created on different days, and those dates were about two weeks before the formal release date of 08/09/96 that the Windows Explorer shows. Give it a try and see for yourself!
The MODULE_DEPENDENCY_LIST Class
The heart of the DEPENDS code is the MODULE_DEPENDENCY_LIST class, implemented in DependencyList.h and DependencyList.cpp (see Figure 4). The constructor for this class takes one argument, the name of the executable to be searched for dependencies. When the constructor returns, the dependency list has been generated. There are querying methods to retrieve information from the list.
Figure 4 MODULE_DEPENDENCY_LIST Class
DependencyList.h
//==========================================
// Matt Pietrek
// Microsoft Systems Journal, Feb 1997
// FILE: DependencyList.h
//==========================================
#ifndef __DEPLIST_H__
#define __DEPLIST_H__
#ifndef __MODULEFILEINFO_H__
#include "modulefileinfo.h"
#endif
enum errModuleDependencyList { errMDL_NO_ERROR,
errMDL_FILE_NOT_FOUND,
errMDL_NOT_PE_FILE,
errMDL_GENERAL_FAILURE };
//
// The MODULE_DEPENDENCY_LIST class creates a linked list of MODULE_FILE_INFO
// structures. In theory, this list will represent every executable file
// loaded by the Win32 loader when the executable is loaded. The class creates
// the list by starting with the file passed to the constructor, and recursing
// through all the import tables.
//
class MODULE_DEPENDENCY_LIST
{
public:
MODULE_DEPENDENCY_LIST( PSTR pszFileName );
~MODULE_DEPENDENCY_LIST( );
BOOL IsValid( void ){ return (BOOL)(m_errorType = = errMDL_NO_ERROR); }
errModuleDependencyList GetErrorType( void ){ return m_errorType; }
PSTR GetErrorString( void );
PMODULE_FILE_INFO GetNextModule( PMODULE_FILE_INFO p );
PMODULE_FILE_INFO LookupModule( PSTR pszFileName, BOOL fFullName );
unsigned GetNumberOfModules( void ){ return m_cModules; }
protected:
unsigned m_cModules; // Number of modules in list
PMODULE_FILE_INFO m_pList; // Pointer to head of linked list
// Recursively adds modules to the list
errModuleDependencyList AddModule( PSTR pszFullName );
errModuleDependencyList m_errorType; // Error type
};
#endif
DependencyList.cpp
//==========================================
// Matt Pietrek
// Microsoft Systems Journal, Feb 1997
// FILE: DependencyList.cpp
//==========================================
#include #include #pragma hdrstop
#include "peexe.h"
#include "dependencylist.h"
MODULE_DEPENDENCY_LIST::MODULE_DEPENDENCY_LIST( PSTR pszFileName )
{
m_errorType = errMDL_GENERAL_FAILURE;
m_cModules = 0;
m_pList = 0;
// Make a copy of the path that we can modify to get just the path portion
PSTR pszJustPath = strdup( pszFileName );
if ( !pszJustPath )
return;
BOOL fHasPath = FALSE;
PSTR pszEnd = strrchr( pszJustPath, '\\' );
if ( pszEnd )
{
*pszEnd = 0; /// Strip off the filename
fHasPath = TRUE;
}
//
// If a path was part of the input filename, save the current directory,
// then switch to the new directory.
//
char szOriginalPath[MAX_PATH];
if ( fHasPath )
{
// This doesn't take into account "App_Paths"!
GetCurrentDirectory(MAX_PATH, szOriginalPath); // Save original dir
SetCurrentDirectory( pszJustPath ); // Switch to app's dir
}
//
// recursively build the module list
//
m_errorType = AddModule( pszFileName );
if ( fHasPath ) // Set things back to the way they were
SetCurrentDirectory( szOriginalPath );
free( pszJustPath ); // Free the copy of the path that we allocated
}
MODULE_DEPENDENCY_LIST::~MODULE_DEPENDENCY_LIST( )
{
PMODULE_FILE_INFO pTemp;
// Delete each MODULE_FILE_INFO structures in the regular linked list
pTemp = m_pList;
while ( pTemp )
{
pTemp = m_pList->m_pNext;
// Before we delete the module, delete each MODULE_FILE_INFO
// structures in the not found list
PMODULE_FILE_INFO pNotFound = m_pList->m_pNotFoundNext;
while ( pNotFound )
{
pNotFound = m_pList->m_pNotFoundNext->m_pNotFoundNext;
delete m_pList->m_pNotFoundNext;
m_pList->m_pNotFoundNext = pNotFound;
}
// Now it's OK to delete the module
delete m_pList;
m_pList = pTemp;
m_cModules--;
}
m_pList = 0;
}
PMODULE_FILE_INFO MODULE_DEPENDENCY_LIST::GetNextModule( PMODULE_FILE_INFO p )
{
// Returns the next module in the linked list of MODULE_FILE_INFO's
return p ? p->m_pNext : m_pList;
}
// Given the name of a file, find the MODULE_FILE_INFO structure that
// represents it. The fFullName parameter specifies whether the full path
// names or just the base file names will be compared.
PMODULE_FILE_INFO MODULE_DEPENDENCY_LIST::LookupModule( PSTR pszFileName,
BOOL fFullName )
{
PMODULE_FILE_INFO p = m_pList; // Start at the list head
while ( p ) // While there's still entries in the list...
{
PSTR pszCompName = fFullName ? p->m_szFullName : p->m_szBaseName;
if ( 0 = = lstrcmpi( pszFileName, pszCompName ) )
return p;
p = p->m_pNext;
}
return 0;
}
PSTR MODULE_DEPENDENCY_LIST::GetErrorString( void )
{
switch ( m_errorType )
{
case errMDL_NO_ERROR: return "No error";
case errMDL_FILE_NOT_FOUND: return "File not found";
case errMDL_NOT_PE_FILE: return "Not a PE file";
case errMDL_GENERAL_FAILURE:return "General failure";
default: return "";}
}
// Adds a modules to the MODULE_FILE_INFO list. If the module imports other
// modules, this routine recurses to add them, and check their imports.
errModuleDependencyList
MODULE_DEPENDENCY_LIST::AddModule( PSTR pszFileName )
{
PE_EXE peFile( pszFileName ); // Get easy access to the executable
if ( FALSE = = peFile.IsValid() ) // A valid PE file???
return (errModuleDependencyList)peFile.GetErrorType();
PMODULE_FILE_INFO pNew = new MODULE_FILE_INFO( pszFileName );
if ( !pNew )
return errMDL_GENERAL_FAILURE;
pNew->m_pNext = m_pList;
m_pList = pNew;
m_cModules++;
//
// Now see if this module imports any other modules. If so, we need
// to recurse and add them as well.
//
if (0 = = peFile.GetDataDirectoryEntrySize( IMAGE_DIRECTORY_ENTRY_IMPORT ))
return errMDL_NO_ERROR;
// Make a pointer to the imports table
PIMAGE_IMPORT_DESCRIPTOR pImportDir;
pImportDir = (PIMAGE_IMPORT_DESCRIPTOR)
peFile.GetDataDirectoryEntryPointer(IMAGE_DIRECTORY_ENTRY_IMPORT);
if ( !pImportDir )
return errMDL_NO_ERROR;
// While there are still non-null IMAGE_IMPORT_DESCRIPTORs...
while ( pImportDir->Name )
{
// Get a pointer to the imported module's base name
PSTR pszBaseName;
pszBaseName = (PSTR)peFile.GetReadablePointerFromRVA(pImportDir->Name);
if ( !pszBaseName )
break;
// Check to see if it's already in our list. Don't add again if so.
if ( 0 = = LookupModule( pszBaseName, FALSE ) )
{
// Search path supposedly has the same searching algorithm as
// the the Win32 loader...
char szPath[MAX_PATH];
PSTR pszDontCare;
if ( SearchPath(0, pszBaseName, 0, MAX_PATH, szPath, &pszDontCare))
AddModule( szPath );
else
pNew->AddNotFoundModule( pszBaseName );
}
pImportDir++; // Advance to next imported module
}
return errMDL_NO_ERROR;
}
What if there’s an error and a dependency list isn’t generated? For instance, what if a nonexistent file name is passed to the constructor? The IsValid method returns a BOOL indicating if a dependency list was successfully created. If there was a problem, you can ascertain the reason via the GetErrorType method, which returns an enum indicating the cause of the problem. The possible problems are a file that doesn’t exist, a file that’s not a Win32 PE file, and "general." The last is a catchall for problems such as memory allocation failures. You can also get a descriptive string for the problem by calling the GetErrorString method.
If the dependency list was created successfully, there are two methods for finding out about a particular module in the list. The LookupModule method takes either the base file name or the full path to a module and returns information about the module, if found. The GetNextModule method is for iterating through each module in succession. To start the enumeration, pass in zero as the parameter. All subsequent calls should pass a pointer to the information returned by the previous call to GetNextModule.
The most interesting code in MODULE_DEPENDENCY_LIST occurs during the constructor call. This code takes the file name parameter and prepares to add it as the first entry in the dependency list. Next, the constructor saves the current directory away and switches to the directory where the file is located. This mimics the behavior of the operating system, which treats the executable’s directory as implicitly part of the search path. After creating the entire dependency list, the constructor switches the current directory back to its original value. If you decide to use the MODULE_DEPENDENCY_LIST code in your own programs, be aware that this directory switching makes the class thread-unsafe. Remember, the current working directory is effectively global data for a program.
The workhorse of the MODULE_DEPENDENCY_LIST class is the private AddModule method, invoked from the class’s constructor. AddModule takes a file name as a parameter and adds the file’s information to the dependency list. AddModule next scans through the file’s import table and looks for other files that aren’t already in the dependency list. If AddModule finds such a module, it calls itself again, this time with the name of the imported module. This recursiveness is similar to what the Win32 loader does when it verifies that all required modules are present before starting a process. By the time the first call to AddModule returns, the entire dependency tree has been recursively searched and built.
Another way that the AddModule method imitates the system’s behavior is in how it finds the complete path to imported DLLs. In the import table of a module, only the base name of the imported DLL appears (for example, "ONION32.DLL"). The system takes that base file name and searches the path for a file with that name. I didn’t want to write my own path-searching code and, luckily, I didn’t have to; the Win32 SearchPath API does exactly what I need.
With the AddModule method behind me, let’s now return to the subject of extracting information about the dependency list. Both the LookupModule and GetNextModule methods of the MODULE_DEPENDENCY_LIST class return a pointer to a class known as MODULE_FILE_INFO. A MODULE_FILE_INFO class describes exactly one module in the dependency list, and is implemented in ModuleFileInfo.H and ModuleFileInfo.CPP. The primary public methods are GetBaseName and GetFullName, which return the base file name and full path to the module, respectively.
One slick new addition to the MODULE_DEPENDENCY_LIST code (relative to the version of this code from my Liposuction article) is the "not found" list. Each MODULE_FILE_INFO class contains a list of imported modules that the MODULE_DEPENDENCY_LIST::AddModule method was unable to locate. To enumerate this list, use the GetNextNotFoundModule method, which returns a pointer to a MODULE_FILE_INFO describing the unlocatable module. To start enumerating the unfound modules, pass in a NULL pointer. In subsequent calls, pass the previously returned MODULE_FILE_INFO pointer. I’ll demonstrate this method later on.
The PE_EXE Class
While much of the action of DEPENDS occurs in MODULE_DEPENDENCY_LIST, this class relies heavily on the underlying PE_EXE class shown in Figure 5. The PE_EXE class is itself derived from the EXE_FILE class (see Figure 6), which is derived from the MEMORY_MAPPED_FILE class. Figure 7 shows the hierarchy. Let’s start at the lowest level, and look at each successive class briefly.
Figure 5 PE_EXE Class
PeExe.h
//==========================================
// Matt Pietrek
// Microsoft Systems Journal, Feb 1997
// FILE: PeExe.h
//==========================================
#ifndef __PEEXE_H__
#define __PEEXE_H__
#ifndef __EXEFILE_H__
#include "exefile.h"
#endif
class PE_EXE : public EXE_FILE
{
public:
PE_EXE( PSTR pszFileName );
~PE_EXE( ){ }
IsValid() { return m_pNtHdr ? TRUE : FALSE; }
// For those who want at the data directly
PIMAGE_NT_HEADERS GetIMAGE_NT_HEADERS( void ) { return m_pNtHdr; }
// IMAGE_FILE_HEADER fields
WORD GetMachine( void )
{ return m_pNtHdr->FileHeader.Machine; }
WORD GetNumberOfSections( void )
{ return m_pNtHdr->FileHeader.NumberOfSections; }
DWORD GetTimeDateStamp(void)
{ return m_pNtHdr->FileHeader.TimeDateStamp; }
DWORD GetCharacteristics( void )
{ return m_pNtHdr->FileHeader.Characteristics; }
// IMAGE_OPTIONAL_HEADER fields
DWORD GetSizeOfCode( void )
{ return m_pNtHdr->OptionalHeader.SizeOfCode; }
DWORD GetSizeOfInitializedData( void )
{ return m_pNtHdr->OptionalHeader.SizeOfInitializedData; }
DWORD GetSizeOfUninitializedData( void )
{ return m_pNtHdr->OptionalHeader.SizeOfUninitializedData; }
DWORD GetAddressOfEntryPoint( void )
{ return m_pNtHdr->OptionalHeader.AddressOfEntryPoint; }
DWORD GetBaseOfCode( void )
{ return m_pNtHdr->OptionalHeader.BaseOfCode; }
DWORD GetBaseOfData( void )
{ return m_pNtHdr->OptionalHeader.BaseOfData; }
DWORD GetImageBase( void )
{ return m_pNtHdr->OptionalHeader.ImageBase; }
DWORD GetSectionAlignment( void )
{ return m_pNtHdr->OptionalHeader.SectionAlignment; }
DWORD GetFileAlignment( void )
{ return m_pNtHdr->OptionalHeader.FileAlignment; }
WORD GetMajorOperatingSystemVersion( void )
{ return m_pNtHdr->OptionalHeader.MajorOperatingSystemVersion; }
WORD GetMinorOperatingSystemVersion( void )
{ return m_pNtHdr->OptionalHeader.MinorOperatingSystemVersion; }
WORD GetMajorImageVersion( void )
{ return m_pNtHdr->OptionalHeader.MajorImageVersion; }
WORD GetMinorImageVersion( void )
{ return m_pNtHdr->OptionalHeader.MinorImageVersion; }
WORD GetMajorSubsystemVersion( void )
{ return m_pNtHdr->OptionalHeader.MajorSubsystemVersion; }
WORD GetMinorSubsystemVersion( void )
{ return m_pNtHdr->OptionalHeader.MinorSubsystemVersion; }
// DWORD GetWin32VersionValue( void )
// { return m_pNtHdr->OptionalHeader.Win32VersionValue; }
DWORD GetSizeOfImage( void )
{ return m_pNtHdr->OptionalHeader.SizeOfImage; }
DWORD GetSizeOfHeaders( void )
{ return m_pNtHdr->OptionalHeader.SizeOfHeaders; }
WORD GetSubsystem( void )
{ return m_pNtHdr->OptionalHeader.Subsystem; }
DWORD GetSizeOfStackReserve( void )
{ return m_pNtHdr->OptionalHeader.SizeOfStackReserve; }
DWORD GetSizeOfStackCommit( void )
{ return m_pNtHdr->OptionalHeader.SizeOfStackCommit; }
DWORD GetSizeOfHeapReserve( void )
{ return m_pNtHdr->OptionalHeader.SizeOfHeapReserve; }
DWORD GetSizeOfHeapCommit( void )
{ return m_pNtHdr->OptionalHeader.SizeOfHeapCommit; }
DWORD GetDataDirectoryEntryRVA( DWORD id );
PVOID GetDataDirectoryEntryPointer( DWORD id );
DWORD GetDataDirectoryEntrySize( DWORD id );
PVOID GetReadablePointerFromRVA( DWORD rva );
protected:
DWORD RVAToFileOffset( DWORD rva );
PIMAGE_NT_HEADERS m_pNtHdr;
};
#endif
PeExe.cpp
//==========================================
// Matt Pietrek
// Microsoft Systems Journal, Feb 1997
// FILE: PeExe.cpp
//==========================================
#include #include #pragma hdrstop
#include "peexe.h"
PE_EXE::PE_EXE( PSTR pszFileName ) : EXE_FILE( pszFileName )
{
m_pNtHdr = 0;
if ( FALSE = = EXE_FILE::IsValid() )
return;
// It's an EXE, but is it a *PE* file??? If not, set code and bail
if ( GetExeType() != exeType_PE )
{
m_errorType = errEXE_FILE_INVALID_FORMAT;
return;
}
m_pNtHdr = MakePtr(PIMAGE_NT_HEADERS,GetBase(),GetSecondaryHeaderOffset());
}
DWORD PE_EXE::GetDataDirectoryEntryRVA( DWORD id )
{
// Given a IMAGE_DIRECTORY_ENTRY_XXX value (see WINNT.H), retrive the
// RVA stored in the corresponding slot
if ( id >= IMAGE_NUMBEROF_DIRECTORY_ENTRIES )
return (DWORD)-1;
return m_pNtHdr->OptionalHeader.DataDirectory[id].VirtualAddress;
}
PVOID PE_EXE::GetDataDirectoryEntryPointer( DWORD id )
{
// Given a IMAGE_DIRECTORY_ENTRY_XXX value (see WINNT.H), return a pointer
// to memory that corresponds to the RVA in the specified slot.
if ( id >= IMAGE_NUMBEROF_DIRECTORY_ENTRIES )
return (PVOID)-1;
DWORD va = m_pNtHdr->OptionalHeader.DataDirectory[id].VirtualAddress;
if ( !va ) // Return 0 if the RVA is 0
return 0;
return GetReadablePointerFromRVA( va );
}
DWORD PE_EXE::GetDataDirectoryEntrySize( DWORD id )
{
// Given a IMAGE_DIRECTORY_ENTRY_XXX value (see WINNT.H), retrive the
// size value stored in the corresponding slot
if ( id >= IMAGE_NUMBEROF_DIRECTORY_ENTRIES )
return (DWORD)-1;
return m_pNtHdr->OptionalHeader.DataDirectory[id].Size;
}
PVOID PE_EXE::GetReadablePointerFromRVA( DWORD rva )
{
// Given an RVA, translate it into a pointer within our linear memory
// mapping for the executable.
DWORD fileOffset = RVAToFileOffset( rva );
if ( (DWORD)-1 = = fileOffset )
return 0;
return MakePtr( PVOID, GetBase(), fileOffset );
}
DWORD PE_EXE::RVAToFileOffset( DWORD rva )
{
// Given an RVA, figure out which section encompasses it. Next, using
// the PointerToRawData field for the found section, return an actual
// file offset that corresponds to the RVA
PIMAGE_SECTION_HEADER pSectHdr = IMAGE_FIRST_SECTION( m_pNtHdr );
for ( unsigned i = 0; i < GetNumberOfSections(); i++, pSectHdr++ )
{
DWORD cbMaxOnDisk
= min( pSectHdr->Misc.VirtualSize, pSectHdr->SizeOfRawData );
DWORD startSectRVA = pSectHdr->VirtualAddress;
DWORD endSectRVA = startSectRVA + cbMaxOnDisk;
if ( (rva >= startSectRVA) && (rva < endSectRVA) )
return pSectHdr->PointerToRawData + (rva - startSectRVA);
}
return (DWORD)-1; // RVA not found in the section table... Ooops!
}
Figure 6 EXE_FILE Class
ExeFile.h
//==========================================
// Matt Pietrek
// Microsoft Systems Journal, Feb 1997
// FILE: ExeFile.h
//==========================================
#ifndef __EXEFILE_H__
#define __EXEFILE_H__
#ifndef __MEMMAPFL_H__
#include "memorymappedfile.h"
#endif
// MakePtr is a macro that allows you to easily add to values (including
// pointers) together without dealing with C’s pointer arithmetic. It
// essentially treats the last two parameters as DWORDs. The first
// parameter is used to typecast the result to the appropriate pointer type.
#define MakePtr( cast, ptr, addValue ) (cast)( (DWORD)(ptr) + (DWORD)(addValue))
enum EXE_TYPE { exeType_Invalid, exeType_DOS, exeType_NE, exeType_VXD,
exeType_LX, exeType_PE };
enum errEXE_FILE { errEXE_FILE_NO_ERROR,
errEXE_FILE_FILE_NOT_FOUND,
errEXE_FILE_INVALID_FORMAT };
class EXE_FILE : public MEMORY_MAPPED_FILE
{
public:
EXE_FILE( PSTR pszFileName );
~EXE_FILE( ){ ; }
BOOL IsValid( void ){ return errMMF_NoError == m_errorType; }
errEXE_FILE GetErrorType( void ){ return m_errorType; }
DWORD GetSecondaryHeaderOffset( void ){ return m_secondaryHeaderOffset; }
EXE_TYPE GetExeType( void ){ return m_exeType; }
PSTR GetFileTypeDescription( void );
protected:
errEXE_FILE m_errorType;
private:
LONG m_secondaryHeaderOffset;
EXE_TYPE m_exeType;
};
#endif
ExeFile.cpp
//==========================================
// Matt Pietrek
// Microsoft Systems Journal, Feb 1997
// FILE: ExeFile.cpp
//==========================================
#include #pragma hdrstop
#include "ExeFile.h"
EXE_FILE::EXE_FILE( PSTR pszFileName ) : MEMORY_MAPPED_FILE( pszFileName )
{
m_errorType = errEXE_FILE_FILE_NOT_FOUND;
m_secondaryHeaderOffset = -1; // A bogus value to catch bugs
m_exeType = exeType_Invalid;
if ( FALSE == MEMORY_MAPPED_FILE::IsValid() )
return; // m_errorType already set to errEXE_FILE_FILE_NOT_FOUND
// If we get here, the file exists, and was mapped. We’re still not
// sure that it’s a valid EXE though
m_errorType = errEXE_FILE_INVALID_FORMAT;
if ( GetFileSize() < sizeof(IMAGE_DOS_HEADER) )
return;
PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)GetBase();
if ( IMAGE_DOS_SIGNATURE != pDosHdr->e_magic )
return;
// If we get here, it’s at least a DOS ‘MZ’ file
m_errorType = errEXE_FILE_NO_ERROR;
if ( pDosHdr->e_lfarlc < 0x40 ) // Theoretically, this field must be >=
{ // 0x40 for it to be a non-DOS executable
m_exeType = exeType_DOS;
return;
}
// Sanity check. Make sure the "new header" offset isn’t past the end
// of the file
if ( pDosHdr->e_lfanew > (LONG)GetFileSize() )
return;
// Make a pointer to the secondary header
m_secondaryHeaderOffset = pDosHdr->e_lfanew;
PWORD pSecondHdr = MakePtr( PWORD, GetBase(), m_secondaryHeaderOffset );
// Decide what type of EXE, based on the start of the secondary header
switch ( *pSecondHdr )
{
case IMAGE_OS2_SIGNATURE: m_exeType = exeType_NE; break;
case IMAGE_VXD_SIGNATURE: m_exeType = exeType_VXD; break;
case 0x4558: m_exeType = exeType_LX; break; // OS/2 2.X
}
if ( *(PDWORD)pSecondHdr == IMAGE_NT_SIGNATURE )
m_exeType = exeType_PE;
}
PSTR EXE_FILE::GetFileTypeDescription( void )
{
// Returns a static string that describes what type this file is
switch ( m_exeType )
{
case exeType_DOS: return "DOS";
case exeType_NE: return "NE";
case exeType_VXD: return "VXD";
case exeType_LX: return "LX";
case exeType_PE: return "PE";
default: return "Invalid";
}
}
The base class for the hierarchy is the MEMORY_MAPPED_FILE class. It simply provides a wrapper around the APIs necessary to use memory-mapped files: CreateFile, CreateFileMapping, and MapViewOfView. The destructor for the class automatically undoes everything to clean up properly.
Figure 7 Class Hierarchy
After the MEMORY_MAPPED_FILE constructor returns, you can check that everything went OK by calling the IsValid method. For more detailed information in the event of an error, call the GetErrorType method. If everything succeeded, the GetBase method returns a pointer to the beginning of the mapped region.
Up a level in the hierarchy is the EXE_FILE class, which is derived from the MEMORY_MAPPED_FILE class. This is because an EXE_FILE is just a special case of a regular file. The EXE_FILE constructor also takes a file name as its only parameter, and passes the file name on to the MEMORY_MAPPED_FILE constructor. The guts of the EXE_FILE constructor check to make sure that the file is (at a minimum) an MS-DOS¨ MZ executable. Code using the EXE_FILE class can use the IsValid function to ensure that the specified file really is an executable.
If the file begins with an MS-DOS MZ executable, the executable may be just an MS-DOS stub for a newer type of executable. The file might really be a 16-bit Windows executable (NE), a Win32 executable (PE), an OS/2 executable (LX), or a VxD (LE). The EXE_FILE constructor examines the file and tries to determine what type of executable it is. The EXE_FILE::GetExeType method returns an enum indicating the kind of executable it is. All of the more modern executables contain a secondary header, so the EXE_FILE class also has the GetSecondaryHeaderOffset method, which does just what its name implies.
Finally, the PE_EXE class derives from the EXE_FILE class. The PE_EXE constructor also takes a file name as the only parameter, and passes it down the chain to the EXE_FILE constructor. The PE_EXE class has specific knowledge about the IMAGE_NT_HEADERS and related structures defined in WINNT.H. After creating the class, call the IsValid method to make sure that everything went OK before using the other methods. PE_EXE doesn’t define its own GetErrorType method. Rather, the same error codes returned by the base class EXE_FILE::GetErrorType method apply.
Once a valid PE_EXE exists, there are several different ways of accessing the data in the file. The GetIMAGE_NT_HEADERS method returns a pointer to the IMAGE_NT_HEADERS structure in the memory-mapped file, and you’re free to pick through it however you want. For simpler access to the data, the PE_EXE class provides wrapper methods that return the values of individual fields in the PE header (for example, the GetAddressOfEntryPoint method). The class also provides easy access to information in the PE file’s DataDirectory via the GetDataDirectoryEntryXXX methods. Finally, the GetReadablePointerFromRVA method takes a Relative Virtual Address (RVA) as input, and returns a pointer to the corresponding location in the underlying memory-mapped file.
In my Liposuction code, I went a step further and derived a PE_EXE2 class from the PE_EXE class. I don’t need anything so fancy here. The PE_EXE class provides quick and easy access to information in a PE file with a minimum of overhead. I suspect that I’ll be using the PE_EXE class in future projects because it’s so handy.
Who’s Got the Time?
My initial goals for DEPENDS were to just spit out the dependency list and then elaborate it with the ability to print out the full path to the module. Next on the list was to add date and time information. That’s when I ran into trouble with Win32. As I mentioned earlier, there are at least four different ways that a time can be stored under Win32, and I ended up working with all four.
The first type of time that I encountered was FILETIME, which is a 64-bit structure returned by the GetFileTime function. Looking up the FILETIME structure in the SDK documentation, you’ll come across this definition: "The FILETIME structure is a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601." I don’t know about you, but I find that friends and relatives get testy when I specify the time in 100-nanosecond intervals. Luckily, the second Win32 time format comes to the rescue. This time format is a structure known as a SYSTEMTIME that has fields for the year, month, day, hour, second, and millisecond. There’s even a Win32 API, FileTimeToSystemTime, that does the conversion for you. Of course, if you’re big into the whole Julian, Gregorian, leap year, leap century thing, you could do the conversion yourself.
Once I had coded up my calls to GetFileTime and FileTimeToSystemTime, I fired up the program and promptly discovered that all the times were off by several hours. Ooops! Remember when you installed Windows NT or Windows® 95 and you had to tell it where you live? There’s a reason for that. It turns out that, under Win32, file times are specified in Coordinated Universal Time (UTC). Using UTC allows for the operating system to account for the fact that while it’s 7PM in Greenwich, England, it’s only 2PM in Nashua, New Hampshire.
Making every programmer responsible for checking time zones and compensating accordingly would be a bad thing. That’s why Win32 has the FileTimeToLocalTime API. To make my file dates and times appear correct, I had to first call FileTimeToLocalFileTime before calling FileTimeToSystemTime.
The third format for representing times in Win32 is the old MS-DOS way. In this format, the date and time are stored in separate WORDs. Because there are only 16 bits to play with, the year is stored relative to 1980. Likewise, the lowest time resolution is two seconds. If you choose to work in this time format, the FileTimeToDosDateTime API will be of interest. Why bring up this archaic time format? Silly me; when I started work on DEPENDS, I didn’t immediately realize that the SYSTEMTIME format was what I should be using. The early versions of DEPENDS converted FILETIMEs to MS-DOS dates and times until I realized the error of my ways.
The fourth time format under Win32 is one you won’t see in any of the API documentation. In Win32 executables, there’s a DWORD in the IMAGE_FILE_HEADER portion of the PE header. This DWORD is called the TimeDateStamp, and represents the number of seconds since midnight on January 1, 1970, in Greenwich, England. The TimeDateStamp is set by the linker, and is actually used in other parts of a PE file.
At this point, I need to confess a small boo-boo. In my article, "Peering Inside the PE: A Tour of the Win32 Portable Executable File Format," (MSJ March 1994), as well as in my book, Windows 95 System Programming Secrets, I described the TimeDateStamp field as being the number of seconds since 4PM on December 31, 1969. I obtained this particular time by setting the TimeDateStamp on a file to the value zero and then running DUMPBIN on the file. What I didn’t take into account was that DUMPBIN was adjusted for my time zone (which pretty obviously wasn’t Greenwich Mean Time). So here’s another one for all you errata collectors out there.
This TimeDateStamp field can be quite useful. For example, while you can change the file’s date and time in the file system, the TimeDateStamp remains unaffected. Therefore, if you really want to know when an executable was created, the TimeDateStamp field is more accurate (assuming the linker set it properly). The only tricky part is fig- uring out how to get the number of seconds since 1970 into a format that the general population cares to work with.
After pondering this problem for a while, I came across the following trick. Both FILETIME and TimeDateStamp are values relative to some point in time. If I can somehow express a TimeDateStamp in terms of a FILETIME, I can then use the various Win32 time APIs to do whatever I desire. To start with, I need to know how January 1, 1970, is expressed as a 64-bit FILETIME. Next, I need to convert the TimeDateStamp (expressed in seconds) into 100-nanosecond units. Finally, add the two values together to make a FILETIME containing the desired time.
To convert January 1, 1970, into a FILETIME, I work backwards. First, I create a SYSTEMTIME structure and fill in the fields corresponding to January 1, 1970. Next, I pass this structure to the SystemTimeToFileTime API and print out the resulting 64-bit FILETIME value. You can see this value (0x0x019DB1DED53E8000) in use in the DEPENDS code. Converting seconds to 100-nanosecond units is easy. Just multiply by 10 million. Of course, the result could overflow a 32-bit DWORD, so I made sure to cast one of the multiplicands to a 64-bit integer (an __int64 in Visual C++). Of course, if you want to take the easy way out, you could just use the ctime function from the C runtime library.
The DEPENDS Code
The main code for Depends.exe is in Depends.cpp (see Figure 8). The main function first invokes the ProcessCommandLine function to parse the command-line arguments, including the name of the file to process. Next, function main declares a MODULE_DEPENDENCY_LIST class instance. The rest of function main is a while loop that iterates through every MODULE_FILE_INFO class in the dependency list. Each MODULE_FILE_INFO instance is passed to the DisplayFileInformation function, which emits the requested information about the file. Before continuing on to the next module, the while loop also uses the MODULE_FILE_INFO::GetNextNotFoundModule method to print out any imported modules that weren’t located. By doing this, DEPENDS makes it easy to track down exactly who’s referencing some DLL that the system isn’t finding.
Figure 8 Depends.cpp
//==========================================
// Matt Pietrek
// Microsoft Systems Journal, Feb 1997
// FILE: Depends.cpp
//==========================================
#include #include #include "DependencyList.h"
#include "PeExe.h"
//============================== Variables ===============================
char g_szHelpSyntax[] =
"DEPENDS - Matt Pietrek, 1997, for MSJ\n"
"Syntax: DEPENDS [args] \n"" /v show version information\n"
" /t show time & date information\n"
" /p show full path\n"
" /l show link time & date information\n\n";
char * g_pszPrimaryFile = 0;
BOOL g_fShowDateTime = FALSE;
BOOL g_fShowLinkDateTime = FALSE;
BOOL g_fShowVersion = FALSE;
BOOL g_fShowFullPath = FALSE;
//============================== Prototypes ===============================
void DisplayFileInformation( PMODULE_FILE_INFO pModInfo );
void ShowVersionInfo( PSTR pszFileName );
BOOL TimeDateStampToFileTime( DWORD timeDateStamp, LPFILETIME pFileTime );
BOOL GetFileDateAsString( LPFILETIME pFt, char * pszDate, unsigned cbIn );
BOOL GetFileTimeAsString( LPFILETIME pFt, char * pszTime, unsigned cbIn,
BOOL fSeconds );
//=================================== Code ================================
BOOL ProcessCommandLine( int argc, char * argv[] )
{
BOOL fSawFileName = FALSE;
if ( argc < 2 )
return FALSE;
for ( int i = 1; i < argc; i++ )
{
PSTR pArg = argv[i];
if ( (*pArg = = '/') || (*pArg = = '-') ) // Is it a switch char?
{
pArg++; // Point past switch char
if ( 0 = = lstrcmpi( pArg, "v" ) )
g_fShowVersion = TRUE;
else if ( 0 = = lstrcmpi( pArg, "t" ) )
g_fShowDateTime = TRUE;
else if ( 0 = = lstrcmpi( pArg, "l" ) )
g_fShowLinkDateTime = TRUE;
else if ( 0 = = lstrcmpi( pArg, "p" ) )
g_fShowFullPath = TRUE;
else
{
printf( "Unrecognized option: \"%s\"\n", pArg );
return FALSE;
}
}
else
{
if ( fSawFileName )
return FALSE;
g_pszPrimaryFile = pArg;
fSawFileName = TRUE;
}
}
return fSawFileName;
}
int main( int argc, char * argv[] )
{
if ( !ProcessCommandLine( argc, argv ) )
{
printf( g_szHelpSyntax );
return 1;
}
MODULE_DEPENDENCY_LIST depends( g_pszPrimaryFile );
if ( !depends.IsValid() )
{
printf( "Error: %s %s\n", g_pszPrimaryFile, depends.GetErrorString() );
return 1;
}
PMODULE_FILE_INFO pModInfo = 0;
while ( pModInfo = depends.GetNextModule( pModInfo ) )
{
DisplayFileInformation( pModInfo );
PMODULE_FILE_INFO pNotFound = 0;
while ( pNotFound = pModInfo->GetNextNotFoundModule(pNotFound) )
{
printf( " Not found: %s\n", pNotFound->GetBaseName() );
}
}
return 0;
}
void DisplayFileInformation( PMODULE_FILE_INFO pModInfo )
{
printf( "%-14s", pModInfo->GetBaseName() );
PSTR pszFullName = pModInfo->GetFullName();
if ( g_fShowDateTime )
{
HFILE hFile = _lopen( pszFullName, OF_READ );
if ( HFILE_ERROR != hFile )
{
FILETIME ft;
if ( GetFileTime( (HANDLE)hFile, 0, 0, &ft ) )
{
char szFileDate[32] = { 0 };
char szFileTime[32] = { 0 };
GetFileDateAsString(&ft, szFileDate, sizeof(szFileDate) );
GetFileTimeAsString(&ft, szFileTime, sizeof(szFileTime),
TRUE);
printf( "%s %s ", szFileDate, szFileTime );
}
_lclose( hFile );
}
}
if ( g_fShowLinkDateTime )
{
FILETIME ft;
char szFileDate[32] = { 0 };
char szFileTime[32] = { 0 };
PE_EXE exe( pszFullName );
TimeDateStampToFileTime( exe.GetTimeDateStamp(), &ft );
GetFileDateAsString(&ft, szFileDate, sizeof(szFileDate) );
GetFileTimeAsString(&ft, szFileTime, sizeof(szFileTime),
TRUE);
printf( "%s %s ", szFileDate, szFileTime );
}
if ( g_fShowFullPath )
printf( "(%s)", pszFullName );
printf( "\n" );
if ( g_fShowVersion )
ShowVersionInfo( pszFullName );
}
void ShowVersionInfo( PSTR pszFileName )
{
DWORD cbVerInfo, dummy;
// How big is the version info?
cbVerInfo = GetFileVersionInfoSize( pszFileName, &dummy );
if ( !cbVerInfo )
return;
// Allocate space to hold the info
PBYTE pVerInfo = new BYTE[cbVerInfo];
if ( !pVerInfo )
return;
_try
{
if ( !GetFileVersionInfo(pszFileName, 0, cbVerInfo, pVerInfo) )
_leave;
char * predefResStrings[] =
{
"CompanyName",
"FileDescription",
"FileVersion",
"InternalName",
"LegalCopyright",
"OriginalFilename",
"ProductName",
"ProductVersion",
0
};
for ( unsigned i=0; predefResStrings[i]; i++ )
{
char szQueryStr[ 0x100 ];
char szQueryStr2[0x100 ];
// Format the string with the 1200 codepage (Unicode)
wsprintf( szQueryStr, "\\StringFileInfo\\%04X%04X\\%s",
GetUserDefaultLangID(), 1200,
predefResStrings[i] );
// Format the string with the 1252 codepage (Windows Multilingual)
wsprintf( szQueryStr2, "\\StringFileInfo\\%04X%04X\\%s",
GetUserDefaultLangID(), 1252,
predefResStrings[i] );
// We may want to format a string with the "0000" codepage
PSTR pszVerRetVal;
UINT cbReturn;
BOOL fFound;
// Try first with the 1252 codepage
fFound = VerQueryValue( pVerInfo, szQueryStr,
(LPVOID *)&pszVerRetVal, &cbReturn );
if ( !fFound )
{
// Hmm... 1252 wasn't found. Try the 1200 codepage
fFound = VerQueryValue( pVerInfo, szQueryStr2,
(LPVOID *)&pszVerRetVal, &cbReturn );
}
if ( fFound )
printf( " %s %s\n", predefResStrings[i], pszVerRetVal );
}
}
_finally
{
delete []pVerInfo;
}
}
// Convert a TimeDateStamp (i.e., # of seconds since 1/1/1970) into a FILETIME
BOOL TimeDateStampToFileTime( DWORD timeDateStamp, LPFILETIME pFileTime )
{
__int64 t1970 = 0x019DB1DED53E8000; // Magic... GMT... Don't ask....
__int64 timeStampIn100nsIncr = (__int64)timeDateStamp * 10000000;
__int64 finalValue = t1970 + timeStampIn100nsIncr;
memcpy( pFileTime, &finalValue, sizeof( finalValue ) );
return TRUE;
}
BOOL GetFileDateAsString( LPFILETIME pFt, char * pszDate, unsigned cbIn )
{
FILETIME ftLocal;
SYSTEMTIME st;
if ( !FileTimeToLocalFileTime( pFt, &ftLocal ) )
return FALSE;
if ( !FileTimeToSystemTime( &ftLocal, &st ) )
return FALSE;
char szTemp[12];
wsprintf( szTemp, "%02u/%02u/%04u",
st.wMonth, st.wDay, st.wYear );
lstrcpyn( pszDate, szTemp, cbIn );
return TRUE;
}
BOOL GetFileTimeAsString( LPFILETIME pFt, char * pszTime, unsigned cbIn,
BOOL fSeconds )
{
FILETIME ftLocal;
SYSTEMTIME st;
if ( !FileTimeToLocalFileTime( pFt, &ftLocal ) )
return FALSE;
if ( !FileTimeToSystemTime( &ftLocal, &st ) )
return FALSE;
char szTemp[12];
if ( fSeconds ) // Want seconds???
{
wsprintf( szTemp, "%02u:%02u:%02u", st.wHour, st.wMinute,
st.wSecond );
}
else // No thanks.. Just hours and minutes
{
wsprintf( szTemp, "%02u:%02u", st.wHour, st.wMinute );
}
lstrcpyn( pszTime, szTemp, cbIn );
return TRUE;
}
The DisplayFileInformation function, at a minimum, displays the base file name from the MODULE_FILE_INFO passed to it. The remainder of the function’s output depends on the command-line switches. If /t is specified, the function uses GetFileTime and a pair of helper functions to display the file system’s date and time for the file. Next, if /l is specified, the function creates a temporary instance of the PE_EXE class in order to retrieve the TimeDateStamp. The TimeDateStamp is then passed to a helper function, TimeDateStampToFileTime, and the returned FILETIME information is displayed
The last bit of code in the DisplayFileInformation function is for the /v switch. If set, the file name is passed to the ShowVersionInfo function. ShowVersionInfo uses several of the version APIs: GetFileVersionInfoSize, GetFileVersionInfo, and VerQueryValue. After allocating space for the version information for a file and reading it in, the code uses VerQueryValue to look for the Win32 predefined version strings such as "CompanyName," "FileDescription," and so on. In writing this code, I found that even Microsoft is inconsistent in their use of a code page for the version strings. Most version resources use code page 1252 (Unicode), but a few use code page 1200 (Windows Multilingual). My code checks for both. In testing, I found that some executables used even other code pages. If you’re looking to improve the DEPENDS code, this function is fertile ground.
When I was all done, I figured that it was a perfect candidate for TINYCRT, which appeared in my October 1996 column. TINYCRT is a minimal replacement runtime library for the standard C++ RTL. Using TINYCRT (the Visual C++ version is called LIBCTINY.LIB) is as simple as including it in the linker’s list of libraries. By using LIBCTINY.LIB, I was able to cut Depends.exe from 25KB down to 9KB. I’ve included LIBCTINY.LIB in the downloadable sources so that you can rebuild DEPENDS if necessary.
To obtain complete source code listings, see Editor's page.
Have a question about programming in Windows? Send it to Matt at 71774.362@compuserve.com.
This article is reproduced from Microsoft Systems Journal. Copyright © 1997 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.
To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S. and Canada, or (303) 678-0439 in all other countries. For other inquiries, call (415) 905-2200.