How to change ordinary EXE file into a DLL v1.3
by Tomasz Lis, Lebork 2008-2010
2.2. Thinking about size of the export section
2.3. Creating the export section
2.4. Setting the section as Export Directory
3. Filling the Export Directories
4. Creating the new entry point function
After I changed EXE file into DLL, many people asked me how I did it. Writing responses on many emails with similar questions becomes boring quite fast, so I've decided to put, what I remember, here.
Let's start with what you need:
Just do not smile when reading the "motivation" point; if you're not highly skilled in binary formats, it will take you weeks to solve all the problems you'll encounter.
Please note that this document will only give you directions; it's nothing like "EXE2DLL for Dummies"; you will really need all the knowledge in these points. If you're not sure about your skills, you will fail.
This tutorial is only for professionals. Use it at your own risk.
The EXE file you wish to change will probably be a Portable Executable (PE) file. Download a reference of PE file format - you will need to understand the "Exports Directory" structure.
You will also need some tool for changing the header. My choice was "CFF Explorer", but you may use other tool if you prefer. There are many EXE header editors available; some are commercial, others free. I'm sure you'll find something for you.
We're now starting the easier part of turning EXE into DLL.
In the next point, you will create "Export Section" in your new DLL file. You will have to know its size. But how to pick a size for export section?
Every exported function will consume 4 bytes for name offset, 2 bytes for ordinal number and strlen(name)+1 bytes for the function name. You probably don't know yet how many functions will be in the dll, or how long the names are. If this is the case, you will have to estimate how much space you need.
Then let's assume typical function name length is 31 bytes. At first such name may seem too long, but remember that most compilers puts arguments in the funtion name, for example "_AIL_set_sample_loop_block@12" (taken from Miles Sound System API). Now let's assume every function is approximately 450 bytes long. This means if our DLL has 1MB, then it contains:
1048576/450 = 2330 functions
And for every function, we should allocate:
31+1+2+4 = 38 bytes
So the total size of the new section should be not smaller than:
2330*38 = 88540 bytes
I would recommend to round the section size up to an easy hexadecimal number. For example the value 88540 is 0x159DC as hex, so it's better to round it to 0x20000. This way we shouldn't have problems with free space for exports. On the other hand if you do not intend to export all functions from the DLL, but only a part of them, you can also select smaller size - it's your decision.
Now as we know how much space to allocate, let's get to work.
We've created new section in the DLL file, but it is not recognized as an "Export Directory" yet.
Now the file header is ready. It was the easier part of the job. Good luck with the next section!
Now you will have to fill the Export Directory. I recommed to first create one entry; then you may extend it when everything works.
With use hexadecimal editor and the PE file format reference, you should be able make the export section by hand: put there function RVAs, ordinals and names. Remember that entries must be in alphabetical order.
If you're planning to update the Export Table incrementally, it's good idea to write a tool which automatically rewrites the section. I wrote such tool in C, and it is published somewhere around, with source. It is named "PE/DLL Rebuilder of Export Section" (PeRESec).
PeRESec is designed to use with a disassembler called "IDA Pro". It requires function names and addresses to be listed in .MAP file - this file can be easily generated with IDA. More details are available in the PeRESec's readme file.
If you've made everything properly, you should be able to run a program with the new DLL attached; but when you'll load the DLL, it will just start, like an EXE file.
This is because we haven't changed the entry point function inside DLL.
How to do this? I just can't tell exactly. It is different for every program. Learn what differs EXE file entry point function from the one in DLL, then get a disassembler and do it!
Basically, you have to change the EXE entry point function:
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow
);
into DLL entry point function:
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved
);
You have to prevent running the main program function from the entry point function, but keep all the libraries initiated. If the input flag (fdwReason) says that DLL is being detached, the libraries cleanup code should be executed.
So, here's how the typical EXE entry point looks (simplified view):
int WINAPI WinStart(...)
{
check_os_version();
initialize_static_linked_libraries();
analyze_command_line_params(...);
create_global_and_static_objects();
actual_main(...);
cleanup_variables();
uninitialize_libraries();
}
And, here's what we want to make from it:
int WINAPI DllMain(..., reason, ...)
{
// Perform actions based on the reason for calling.
switch( reason )
{
case 1: // DLL_PROCESS_ATTACH
// Initialize once for each new process.
check_os_version();
initialize_static_linked_libraries();
analyze_command_line_params(...);
create_global_and_static_objects();
break;
case 0: // DLL_PROCESS_DETACH
// Perform any necessary cleanup.
cleanup_variables();
uninitialize_libraries();
break;
}
return 1; // Successful exit.
}
We are not calling actual_main() inside the DllMain, because we want to call it outside - in our executable file:
#define DLLIMPORT __declspec(dllimport)
DLLIMPORT int actual_main(int argc, char *argv[]);
int main(int argc, char *argv[])
{
return actual_main(argc, argv);
}
Note: To disable the main function call, you can turn it into series of NOP commands. But remember that only relative offsets can be changed to anything - non-relative pointer addresses should be pointed in relocation table, so their value may be shifted when loading the DLL. In order to change them you will have to remove some entries from the relocation table.
One importand difference between EXE and DLL which is very hard to overcome are relocations. When EXE is loaded into memory, it can be stored on any address; but when a DLL is loaded, some addresses are already taken by the main program and previously loaded modules. Relocation Table is used to help in changing address at which DLL is loaded. But EXE file do not have the relocation table, and therefore loading it into different address may cause invalid memory references.
Relocations are a big problem which could make the program closed into DLL, or at least a part of this program, to work incorrectly. How to deal with this? I don't really know; I didn't touched relocations when making KeeperFX - I just had luck, because complete relocation table was there.
If the DLL which you've created works fine, but at certain point suddenly hangs, then the reason may be lack of relocation table, which leads to operations on incorrect memory address. Even if you see that the relocation table is inside exe, it still doesn't necessarly mean that all needed entries are there.
Games are large projects, which usually use code of various libraries supplied by different companies. It is possible that some of these files will lack relocation entries.
To fix the problem, you could try to re-create missing entries, but that could be very time-consuming. You could also try to influence the project so that DLLs base address is free when it is being loaded - if there is enough place in base location, OS will not relocate it, but load into base address. This way relocations may not exist and everything will work anyway.
You should also remember about relocations when modifying entry point function, or making any other modification inside DLL. You may be forced to remove or modify some relocation entries when modifying assembly opcodes inside DLL - if you wish to make different use of a place where pointer was stored, then you will have to remove an entry. And if you wish to use non-relative address somewhere, you have to create new relocation entry for it.
Well, that's all. If you know what you're doing, you will succeed.