Introduction
There are two ways that one can use a DLL: implicit (or static) and explicit (or dynamic). For the former, an import library (.lib file) will be required. Normally this .lib file is shipped along with the DLL, however, this may not be the case for some third-party DLLs. So to use such a DLL implicitly, we have to create its .lib file.
If you search the web on this topic, you will get Microsoft's KB page http://support.microsoft.com/kb/131313/en-us and a previous CodeProject article libfromdll.aspx. The Microsoft's page contains a lot of useful information, but it lacks sample source to illustrate the procedure, and the latter page is to a large extent off the topic. This article will provide both the underlying theory and a detailed tutorial together with a sample source package. So I expect my article to be very useful.
The Main Principle
To tell the truth, there is no hat magic for generating a lib. We will absolutely need the names or ordinals of the exported functions. In addition to these, we may need a header file that contains the argument descriptions, type of return value, calling convention, etc. (the so-called "prototype" or "signature" information) of the exported functions to properly use them. The former information can be obtained directly from the DLL itself, while the latter needs to be provided by the vendor/author of the DLL.
In my understanding, the lib file does not itself contain the full signature information; it is only that the signature of the function may affect its name mangling, which is directly reflected in the lib. If extern
"C" and __cdecl
are used for the exported functions, then the signature will not affect the name mangling, and in such cases, I believe it possible to create a lib without the header file. However, a lib is useless without a header file to ensure safe use of the DLL. So it makes little (if not no) sense to talk about generating a lib without a header file.
We will use Microsoft Visual C++ (MSVC6 in my case) to make a lib. Recall that MSVC will generate a DLL and a lib from the c/CPP source and a .def (module definition) file. The .def file is not absolutely necessary if you work only with the __cdecl
calling convention, but to work with __stdcall
, I found it indispensable. So, roughly, we use the following major steps:
- Write a .def file based on the information from the DLL, using the
DUMPBIN
tool. - Write the source.
- Let MSVC build the DLL and lib. And we are done.
But wait, how do we ensure that the generated lib can be used as the lib of the original DLL? In fact, the above-mentioned source is just meant as a "trivial wrapper" of the original DLL, that is, for each exported function in the original DLL, in the wrapper there is a corresponding exported function, which does nothing but jump to the entry of the original function. So the trivial wrapper has the same external behavior as the original DLL, and thus the lib we built can be used as if it were the lib of the original DLL.
The Sample DLL
To illustrate our procedure, I have included a sample DLL, which is pretended to be without a lib (although actually it is not). The source of the sample DLL is contained in DllSample.zip for download, but it is not important and can be disregarded.
The sample DLL (DllSample
) exports two functions:
BOOL __cdecl NumberList(int begin, int end);
void __stdcall LetterList(int begin, int end, BOOL capital);
We deliberately use different calling conventions and return types in order to illustrate our method in a manner as general as possible. When NumberList
is invoked, it displays a message box listing the numbers from begin to end. When LetterList
is invoked, it displays a message box listing the letters between the (begin+1)-th and the (end+1)-th; the BOOL argument capital controls whether the letters are capital.
Writing the DEF File
MSVC comes with a DUMPBIN
tool to dump the contents of Windows binary files. Now if DllSample.dll is in the current directory, type "dumpbin /exports dllsample.dll", then the exported functions will be displayed as follows:
ordinal hint RVA name
2 0 000010A0 LetterList
1 00001000 [NONAME]
This reveals that LetterList
is exported both by name and by ordinal, and NumberList
(this name is not included in the DLL binary, but the header file will include its information) is exported only by ordinal. By copying the output of DUMPBIN
to the clipboard, pasting it to a text file, and modifying it appropriately according to the format of a def file, we can arrive at the following def file:
LIBRARY "DllSample"
EXPORTS
NumberList @1 NONAME
LetterList @2
Writing the Source
This major step consists further of several steps.
Writing the header file for caller's use
A header file to be included at the DLL's caller side should contain the prototype of the exported functions. The vendor/author of the original DLL will usually have provided a raw header, but it may be in another language or developed in another platform, for example, in C++ Builder, Delphi, or VB, so we often need to edit it so that it can successfully compile under MSVC. In our example, the core part of the header DllSample.h should be:
extern "C" __declspec(dllimport) BOOL __cdecl NumberList(int begin, int end);
extern "C" __declspec(dllimport) void __stdcall LetterList
(int begin, int end, BOOL capital);
The inclusion of extern "C" is to instruct the compiler to use C-style name mangling, to allow programs written in C to use our lib. The inclusion of __declspec(dllimport) is optional, but it will help boost the performance slightly. For detailed discussion of this, see Matt Pietrek's article http://msdn.microsoft.com/en-us/magazine/cc301805.aspx.
Renaming DllSample.dll to __DllSample.dll
The target files of the MakeLib
project are not MakeLib.dll and MakeLib.lib, but instead are DllSample.dll and DllSample.lib. To avoid name collision, the original DLL, i.e., the DllSample.dll generated in the DllSample
project, is to be renamed to __DllSample.dll.
Writing the Data Part of MakeLib
The DllUtil.zip contains three projects: DynCall
, MakeLib
, and UseLib
. DynCall
is to illustrate the dynamic call of the DLL; MakeLib
, as we said, is to generate a wrapper DLL and a lib; and UseLib
is to demonstrate that the lib generated in MakeLib
can be used the same as the unavailable lib of the original DLL. The inclusion of project DynCall
in our workspace is because DynCall
and MakeLib
share a large portion of source code, and DynCall
is useful in its own right.
The data part of DynCall
and MakeLib
are very similar, but they also have important differences. In both sources, there are many pointers to the imported functions, but in DynCall
they must be declared as global, whereas in MakeLib
they must be defined as static, since they need not be externally linked. Roughly, the core source snippet of MakeLib.cpp relevant to function pointers is as follows:
#define FUNCDEF_NumberList(NumberList) \
BOOL __cdecl NumberList(int begin, int end)
#define FUNCDEF_LetterList(LetterList) \
void __stdcall LetterList(int begin, int end, BOOL capital)
#define STATDEF_IMPORT_FUNC(funcname) \
typedef FUNCDEF_##funcname(IMP_##funcname); \
static IMP_##funcname *imp_##funcname;
STATDEF_IMPORT_FUNC(NumberList)
STATDEF_IMPORT_FUNC(LetterList)
This expands to the following:
typedef BOOL __cdecl IMP_NumberList(int begin, int end);
static IMP_NumberList *imp_NumberList;
typedef void __stdcall IMP_LetterList(int begin, int end, BOOL capital);
static IMP_LetterList *imp_LetterList;
Then it is easy to see that the imp_xxxx's are static
pointers to imported functions, which are to be assigned values when the wrapper DLL is loaded.
At this point, the reader may wonder why I used so many "weird" macros. Macros are not really useful if you only have a handful of functions, but a commercial DLL may well export hundreds of functions, and in such cases using a suitably designed set of macros can save us a lot of typing work. I myself have a direct experience on this.
Writing the Code Part of MakeLib
About half of MakeLib
code implements the functionality of loading the original __DllSample.dll, getting the addresses of the exported functions, and storing them in the function pointers for dynamic call. The code for this functionality is the same as in ImportDllSample.cpp, the main source for DynCall
, and it should be easy to read.
Now let us look at the implementation of exported wrapper functions. As said, the wrapper is called trivial because whenever an exported wrapper function is called, it simply jumps to the entry point of the corresponding original function in __DllSample.dll. But how do we achieve this? Please see the following code snippet excerpted from MakeLib.cpp:
#define NAKED_WRAPPER(funcname) \
extern "C" __declspec(naked) FUNCDEF_##funcname(funcname) \
{ \
__asm jmp imp_##funcname \
}
NAKED_WRAPPER(NumberList)
NAKED_WRAPPER(LetterList)
Again, this expands to the following:
extern "C" __declspec(naked) BOOL __cdecl NumberList(int begin, int end)
{
__asm jmp imp_NumberList
}
extern "C" __declspec(naked)
void __stdcall LetterList(int begin, int end, BOOL capital)
{
__asm jmp imp_LetterList
}
The key to the above code is the keyword __declspec(naked)
. There is a detailed explanation of it in MSDN.
Verifying the Correctness
Now we launch UseLib.exe to test the correctness. The calling chain now is UseLib
-> (new) DllSample.dll -> (original) DllSample.dll. So if you wish to check that DllSample.lib can be used directly as the lib of the original DllSample.dll. You need to rename __DllSample.dll back to DllSample.dll, and then run UseLib.exe.
Conclusion
We can now see that the so-created DllSample.lib can work perfectly as the import library of the original DllSample.dll. To sum up, the above-described procedure for import library creation has two merits:
- MakeLib.cpp has a very generic skeleton, and the use of macros helps to keep typing minimal.
- The wrapper DLL can sometimes be used for investigation or hacking purposes. The implementation of the wrapper functions can easily be modified to be "non-trivial", to allow us to do some hacking before jumping to the original function. However, if you wish to do the hacking after calling the original function, then the naked implementation is probably no longer suitable.