CodeGuru content and product recommendations are
editorially independent. We may make money when you click on links
to our partners.
Learn More
Introduction
This article was an afterthought. There was a question asked in the CodeGuru forums dealing with a way to determine whether a given DLL/EXE is built as managed or not, and, in addition, to know the .NET framework the DLL is reliant on. All this programatically, of course. Given a DLL, I could make a guess whether it is a COM DLL or not (by querying for DllRegisterServer, DllGetClassObject, and such exports). This was much more intriguing because, unlike other kinds of FLLs/EXEs, the managed code had to run within the .NET runtime environment, and so, the loader has to know well in advance that it is a managed component and do the necessary tasks to prepare the managed code to execute. This article is a byproduct of the quest for an answer to that question and several other questions that came up in the process.
The question itself can be broken up into two parts:
- How do we know, given a file, that it is a .NET managed component?
- Given a .NET managed component, how do you know what other assemblies (indirectly the framework version) it is dependent on?
Let me deal with each of these questions in that order.
Details
This is how it all started. The first thing I attempted was to look at what the dependent DLLs were. I fired up a sample managed DLL in depends.exe (dependency walker). It’s just one import from one DLL _CorDllMain from MSCOREE.dll for DLLs and _CorExeMain from MSCOREE.dll for EXEs. What about all the code for the classes the assembly exposes? If you are familiar with .NET, you will know that all this is put into the assembly as metadata and the runtime uses this metadata for the actual action. So, no exports whatsoever, no imports whatsoever. Surely, the loader is doing something. The DLL has all the information, but it is not exposed in a traditional way (exported functions). There is a level of indirection and there should be some way to get to the metadata.
The next thing that came to my mind is Microsoft’s PE file format. Having played with it sometime before and having seen the loads of information in there, I decided to take a peek again at the PE structures to see whether there was anything that could take me in the right direction. (Pretty much all the basic PE file format structures are “document”ed in WinNT.H.) I opened this file, and sure enough; there was some information that could possibly be it. It was called the CLR 2.0 header structure. A little research on the Internet gave me the confidence that I was headed the right direction.
So, basically this is it. There is a PE structure like the one below; it has headers followed by sections of data. The PE Headers has NT Headers (IMAGE_NT_HEADERS in WinNT.h) that encapsulate optional headers (IMAGE_OPTIONAL_HEADER in WinNT.h). The Optional Header has an array of IMAGE_DATA_DIRECTORY entries called DataDirectory. The IMAGE_DATA_DIRECTORY entry actually points to the actual location of the data within the PE file (called module when loaded). The entry you are interested in is at index IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR. This entry is what points to the .NET information. If this entry exists and the VirtualAddress field points to a valid area within one of the Sections following, it means the component is a managed component.
The following image shows the basic structure of how the metadata can be reached.
All this can be translated into the following code that given a path to the file. It will return TRUE if it is a managed component and FALSE otherwise.
BOOL IsManaged(LPTSTR lpszImageName)
{
BOOL bIsManaged = FALSE;
TCHAR szPath[MAX_PATH];
HANDLE hFile = CreateFile(lpszImageName, GENERIC_READ,
FILE_SHARE_READ,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,NULL);
if(INVALID_HANDLE_VALUE == hFile)
{
GetWindowsDirectory(szPath,MAX_PATH);
_tcscat(szPath,_T(“\\”));
_tcscat(szPath,lpszImageName);
hFile = CreateFile(szPath, GENERIC_READ, FILE_SHARE_READ,
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,
NULL);
}
if(INVALID_HANDLE_VALUE == hFile)
{
GetSystemDirectory(szPath,MAX_PATH);
_tcscat(szPath,_T(“\\”));
_tcscat(szPath,lpszImageName);
hFile = CreateFile(szPath, GENERIC_READ, FILE_SHARE_READ,
NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,
NULL);
}
if(INVALID_HANDLE_VALUE != hFile)
{
HANDLE hOpenFileMapping = CreateFileMapping(hFile,NULL,
PAGE_READONLY,0,
0,NULL);
if(hOpenFileMapping)
{
BYTE* lpBaseAddress = NULL;
lpBaseAddress = (BYTE*)MapViewOfFile(hOpenFileMapping,
FILE_MAP_READ,0,0,0);
if(lpBaseAddress)
{
IMAGE_DOS_HEADER* pDOSHeader =
(IMAGE_DOS_HEADER*)lpBaseAddress;
IMAGE_NT_HEADERS* pNTHeaders =
(IMAGE_NT_HEADERS*)((BYTE*)pDOSHeader +
pDOSHeader->e_lfanew);
IMAGE_SECTION_HEADER* pSectionHeader =
(IMAGE_SECTION_HEADER*)((BYTE*)pNTHeaders +
sizeof(IMAGE_NT_HEADERS));
if(pNTHeaders->Signature == IMAGE_NT_SIGNATURE)
{
DWORD dwNETHeaderTableLocation =
pNTHeaders->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].
VirtualAddress;
if(dwNETHeaderTableLocation)
{
IMAGE_COR20_HEADER* pNETHeader =
(IMAGE_COR20_HEADER*)((BYTE*)pDOSHeader +
GetActualAddressFromRVA(pSectionHeader,
pNTHeaders,dwNETHeaderTableLocation));
if(pNETHeader)
{
bIsManaged = TRUE;
}
}
}
else
{
cout << “Not PE file\r\n”;
}
UnmapViewOfFile(lpBaseAddress);
}
CloseHandle(hOpenFileMapping);
}
CloseHandle(hFile);
}
return bIsManaged;
}
That answers the first question.