Introduction
Have you ever wondered about the contents of Microsoft's import
library file? This article gives a brief description of .lib file structure and provides source to create import library given the name of export module
(.dll
; .sys
; .exe
files) and list of its exported functions (functions can be cherry-picked, so it's not necessary to provide full list).
Two Words About Calling Conventions and Name Decoration
On x86, we can use the following calling conventions for C function
:
__cdecl
__stdcall
__fastcall
__vectorcall
On x64, we can use the following calling conventions for C function
(__cdecl
, __stdcall
, __fastcall
are ignored by compiler):
- default (function names are not decorated)
__vectorcall
See this link for details about name decoration.
Import Library Structure
Import library is an archive, it starts with arch signature
, it is an 8-byte string
, namely:
!<arch>\n
After the signature, we have a bunch of files:
Each file starts with fixed-length header, following the arbitrary-length body (header contains length of the body):
Now let's consider symbols
"hosted" by import
library. Assuming that export module
in question is BOOTVID.DLL, we will have three "predefined" symbols
:
__IMPORT_DESCRIPTOR_BOOTVID
__NULL_IMPORT_DESCRIPTOR
<7Fh> BOOTVID_NULL_THUNK_DATA
// string
starts with 0x7F
For each imported function, we will have two additional symbols. Let's consider this C function:
void __stdcall VidDisplayString(char *pString);
In x86 version of import
library, we will have:
_VidDisplayString@4
// decorated function name __imp__VidDisplayString@4
// the same string
prefixed by __imp_
In x64 version of import
library, we will have:
VidDisplayString
__imp_VidDisplayString
// the same string
prefixed by __imp_
Now let's describe each file contents. First two files in archive contain all symbol names along with offsets to other files. Then we have three files dedicated to three "predefined" symbols. And finally, we have files dedicated to functions (one file for each function).
File 1 contains number of symbols field, symbol names and their corresponding offsets (offsets to another files from the beginning of archive). So File 3 is dedicated to __IMPORT_DESCRIPTOR_BOOTVID
, File 4 is dedicated to __NULL_IMPORT_DESCRIPTOR
, and so on. Note that only one file is dedicated to each function, despite the fact that function introduces two symbols.
File 2 contains offset table, symbol names and their corresponding indexes (indexes into offset table). It is essentially the same information given in a different form. Note that indexing starts from 1
.
Files 3, 4, 5 are dedicated to __IMPORT_DESCRIPTOR_BOOTVID
, __NULL_IMPORT_DESCRIPTOR
, <7F> BOOTVID_NULL_THUNK_DATA
. I won't picture them in detail, they are essentially the same for all import
libraries. Instead, I want to focus on File 6
and picture it in detail. Let's see the structure that represents file header:
struct FILE_HEADER {
char Part1[16];
char Id[24];
char Part3[8];
char BodyLength[10];
char Part5[2];
};
Id
field holds random number generated by time
function (in ASCII form), BodyLength
field holds the length of the following file body (in ASCII form). After file header, we have symbol descriptor:
struct SYMBOL_DESCRIPTOR {
WORD a;
WORD b;
WORD c;
WORD Architecture;
DWORD Id;
DWORD Length;
union
{
WORD Hint;
WORD Ordinal;
WORD Value;
}
WORD Type;
};
Architecture
field contains 0x14C
for x86 and 0x8664
for x64. Id
field is the same random number represented in binary form. Hint
/ Ordinal
field contains function hint / ordinal, Type field specifies import
type (you will find description further). Length
field contains the summary length of two following string
s (including their null
-characters):
Import / Export Scenarios
We will consider three possible scenarios:
- We compile
export module
and export
function without module definition file.
Input:
Source:
__declspec(dllexport) void __cdecl function1() {}
__declspec(dllexport) void __stdcall function2() {}
__declspec(dllexport) void __fastcall function3() {}
__declspec(dllexport) void __vectorcall function4() {}
Output for x86:
Import library (SYMBOL_DESCRIPTOR
):
Name = _function1, Hint = 0, Type = 8
Name = _function2@0, Hint = 1, Type = 4
Name = @function3@0, Hint = 2, Type = 4
Name = function4@@0, Hint = 3, Type = 4
Import library stores Hint (starts from 0
, represents index into AddressOfNames
array).
Export module
's export (AddressOfNames
, AddressOfNameOrdinals
):
Name = _function1, Ordinal = index into AddressOfFunctions array
Name = _function2@0, Ordinal = index into AddressOfFunctions array
Name = @function3@0, Ordinal = index into AddressOfFunctions array
Name = function4@@0, Ordinal = index into AddressOfFunctions array
Despite the name Ordinal
, AddressOfNameOrdinals
stores Index
(starts from 0
, represents index into AddressOfFunctions
array).
Import module
's import (IMAGE_IMPORT_BY_NAME
):
Name = _function1, Hint = 0
Name = _function2@0, Hint = 1
Name = @function3@0, Hint = 2
Name = function4@@0, Hint = 3
Output for x64:
Import library (SYMBOL_DESCRIPTOR
):
Name = function1, Hint = 0, Type = 4
Name = function2, Hint = 1, Type = 4
Name = function3, Hint = 2, Type = 4
Name = function4@@0, Hint = 3, Type = 4
Import library stores Hint (starts from 0
, represents index into AddressOfNames
array).
Export module
's export (AddressOfNames
, AddressOfNameOrdinals
):
Name = function1, Ordinal = index into AddressOfFunctions array
Name = function2, Ordinal = index into AddressOfFunctions array
Name = function3, Ordinal = index into AddressOfFunctions array
Name = function4@@0, Ordinal = index into AddressOfFunctions array
Despite the name Ordinal
, AddressOfNameOrdinals
stores Index
(starts from 0
, represents index into AddressOfFunctions
array).
Import module
's import (IMAGE_IMPORT_BY_NAME
):
Name = function1, Hint = 0
Name = function2, Hint = 1
Name = function3, Hint = 2
Name = function4@@0, Hint = 3
- We compile some DLL and export function using module definition file (we specify function name).
Input:
Source:
void __cdecl function1() {}
void __stdcall function2() {}
void __fastcall function3() {}
void __vectorcall function4() {}
Def file:
EXPORTS
function1
function2
function3
function4
Output for x86:
Import library (SYMBOL_DESCRIPTOR
):
Name = _function1, Hint = 0, Type = 8
Name = _function2@0, Hint = 1, Type = 0xC
Name = @function3@0, Hint = 2, Type = 0xC
Name = function4@@0, Hint = 3, Type = 0xC
Import library stores Hint (starts from 0
, represents index into AddressOfNames
array).
Export module
's export (AddressOfNames
, AddressOfNameOrdinals
):
Name = function1, Ordinal = index into AddressOfFunctions array
Name = function2, Ordinal = index into AddressOfFunctions array
Name = function3, Ordinal = index into AddressOfFunctions array
Name = function4, Ordinal = index into AddressOfFunctions array
Despite the name Ordinal
, AddressOfNameOrdinals
stores Index
(starts from 0
, represents index into AddressOfFunctions
array).
Import module
's import (IMAGE_IMPORT_BY_NAME
):
Name = function1, Hint = 0
Name = function2, Hint = 1
Name = function3, Hint = 2
Name = function4, Hint = 3
Output for x64:
Import library (SYMBOL_DESCRIPTOR
):
Name = function1, Hint = 0, Type = 4
Name = function2, Hint = 1, Type = 4
Name = function3, Hint = 2, Type = 4
Name = function4@@0, Hint = 3, Type = 0xC
Import library stores Hint (starts from 0
, represents index into AddressOfNames
array).
Export module
's export (AddressOfNames
, AddressOfNameOrdinals
):
Name = function1, Ordinal = index into AddressOfFunctions array
Name = function2, Ordinal = index into AddressOfFunctions array
Name = function3, Ordinal = index into AddressOfFunctions array
Name = function4, Ordinal = index into AddressOfFunctions array
Despite the name Ordinal
, AddressOfNameOrdinals
stores Index
(starts from 0
, represents index into AddressOfFunctions
array).
Import module
's import (IMAGE_IMPORT_BY_NAME
):
Name = function1, Hint = 0
Name = function2, Hint = 1
Name = function3, Hint = 2
Name = function4, Hint = 3
- We compile some DLL and
export
function using module definition file (we specify function name and ordinal).
Input:
Source:
void __cdecl function1() {}
void __stdcall function2() {}
void __fastcall function3() {}
void __vectorcall function4() {}
Def file:
EXPORTS
function1 @1
function2 @2
function3 @3
function4 @4
Output for x86:
Import library (SYMBOL_DESCRIPTOR
):
Name = _function1, Ordinal = 1, Type = 0
Name = _function2@0, Ordinal = 2, Type = 0
Name = @function3@0, Ordinal = 3, Type = 0
Name = function4@@0, Ordinal = 4, Type = 0
Import library stores Ordinal
(starts from 1
, represents alternative function name).
Export module
's export (AddressOfNames
, AddressOfNameOrdinals
):
Name = function1, Ordinal = index into AddressOfFunctions array
Name = function2, Ordinal = index into AddressOfFunctions array
Name = function3, Ordinal = index into AddressOfFunctions array
Name = function4, Ordinal = index into AddressOfFunctions array
Despite the name Ordinal
, AddressOfNameOrdinals
stores Index
(starts from 0
, represents index into AddressOfFunctions
array). If we would use NONAME
directive for some function, it would not have corresponding entries in AddressOfNames
and AddressOfNameOrdinals
arrays.
Import module
's import (now we have Ordinal
instead of IMAGE_IMPORT_BY_NAME
):
Ordinal = 1
Ordinal = 2
Ordinal = 3
Ordinal = 4
Output for x64:
Import
library (SYMBOL_DESCRIPTOR
):
Name = function1, Ordinal = 1, Type = 0
Name = function2, Ordinal = 2, Type = 0
Name = function3, Ordinal = 3, Type = 0
Name = function4@@0, Ordinal = 4, Type = 0
Import
library stores Ordinal
(starts from 1
, represents alternative function name).
Export module
's export (AddressOfNames
, AddressOfNameOrdinals
):
Name = function1, Ordinal = index into AddressOfFunctions array
Name = function2, Ordinal = index into AddressOfFunctions array
Name = function3, Ordinal = index into AddressOfFunctions array
Name = function4, Ordinal = index into AddressOfFunctions array
Despite the name Ordinal
, AddressOfNameOrdinals
stores Index
(starts from 0
, represents index
into AddressOfFunctions
array). If we would use NONAME
directive for some function, it would not have corresponding entries in AddressOfNames
and AddressOfNameOrdinals
arrays.
Import module
's import (now we have Ordinal
instead of IMAGE_IMPORT_BY_NAME
):
Ordinal = 1
Ordinal = 2
Ordinal = 3
Ordinal = 4
Import by Name
When we import function by Name
, we obtain its address in the following way:
if (strcmp(FunctionName, AddressOfNames[Hint]))
{
for (int i = 0; i < NumberOfNames; ++i)
{
if (!strcmp(FunctionName, AddressOfNames[i]))
{
Hint = i
break
}
}
}
Index = AddressOfNameOrdinals[Hint]
Address = AddressOfFunctions[Index]
Import by Ordinal
When we import function by Ordinal
, we obtain its address in the following way:
Address = AddressOfFunctions[Ordinal - Base]
Two Words About Another def File Directives
INTERNALNAME
allows you to use another name ("internal
") in your export module
's code. Consider this example:
Source:
void __stdcall internal_name() {}
Def file:
EXPORTS
external_name = internal_name
From import/export mechanism point of view, it is equivalent to:
Source:
void __stdcall external_name() {}
Def file:
EXPORTS
external_name
PRIVATE
allows you to build import
library without some symbol ("partial
" import
library).
Using dumpbin to Get Export Information
We can use dumpbin
tool to get information about export module
's exports (for which we do not have source). Possible dumpbin
outputs:
Here we see decorated function name, hint and ordinal.
The same, except function name is not decorated.
Here we see ordinal only. Since module does not store function name, hint also does not exist.
Note that export will always have ordinal, however export will not necessarily have name and hint.
Since calling convention is not stored, we can determine it by examining function's code by disassembler. On x86, it is usually __stdcall
or __fastcall
, on x64, it is usually default
calling convention.
Using the Code
For each imported function, we need to specify:
- Name
- Value
- Size of argument list in bytes
- Calling convention
- Import type
Import
types:
IMPORT_BY_SPECIFIED_NAME
IMPORT_BY_DECORATED_NAME
IMPORT_BY_ORDINAL
IMPORT_BY_SPECIFIED_NAME
:
Import module
will hold undecorated function name and Hint
that is given by Value
parameter.
Export module
must store undecorated function name.
IMPORT_BY_DECORATED_NAME
:
Import module
will hold decorated function name and Hint
that is given by Value
parameter.
Export module
must store decorated function name.
IMPORT_BY_ORDINAL
:
Import module
will hold Ordinal
that is given by Value
parameter.
Export module
does not need to store function name.
Size of argument list in bytes
parameter is needed to perform function name decoration.
The code to generate import library:
int main(int argc, char* argv[])
{
char *pName;
SYMBOL_INFO *pSymbolList;
pName = "BOOTVID";
pSymbolList = CreateSymbolList(pName);
g_InfoAll.bX64 = FALSE;
AddFunction(pSymbolList, "VidDisplayString", 4, 4,
CALLING_CONVENTION_STDCALL, IMPORT_BY_SPECIFIED_NAME);
WriteImportLibrary(pName, ".dll", pSymbolList);
DestroySymbolList(pSymbolList);
return 0;
}
Here, we specify architecture (x86
), export module
name (BOOTVID
), export module
extension (.dll) and function list.
In our import module
's source, we would declare the function in the following way:
__declspec(dllimport) void __stdcall VidDisplayString(char *pString);
Afterword
We have not considered variables, DATA
and CONSTANT
directives, importing using .def file, purpose of __imp_
, and C++ names. I will update the article later to cover all these topics. Thank you for reading.