Live DLL Reloading

Replace DLLs at runtime for faster iteration with minimal effort

Posted on Friday, February 4, 2022

Live DLL Reloading

Iteration time is very important for development and debugging. Usually, in order to see the effects of a change in code we need to close the process, compile a new version, start the process again and then reach a specific part or scenario in the program - this entire flow can be very slow. DLL live reloading can decrease iteration times by replacing an old DLL with a new one during runtime without even closing the process so that we can observe the changes almost immediatly. Moreover, you can easily integrate DLL live reloading with an existing project without changing a single line. While visual studio can compile and load changes it isn’t available in many build configurations.

The code is available at:
https://github.com/a10nw01f/DLLReplacer

The Main Issue with Unloading DLLs

There are many great resources about live dll reloading:
https://ourmachinery.com/post/dll-hot-reloading-in-theory-and-practice/
https://github.com/fungos/cr/
https://nlguillemot.wordpress.com/2018/02/16/simple-live-c-reloading-in-visual-studio/
https://github.com/nlguillemot/live_reload_test
https://gian-sass.com/rapid-native-game-development-with-live-code-reloading/
https://github.com/RandyGaul/C-Hotloading

but most of them have a main drawback - they unload the old DLL. Initially this may seem like a good idea since it releases the DLL file for writing and frees some of the memory, however it also means that any pointer which pointed to any static data or code (including vtables) is now invalid and accessing it will probably crash your program or worse.

A Different Approach

A better approach in my opinion is to keep the old DLL in memory and set a hook between the old functions and the new ones so we wouldn’t have the dangling pointers problem anymore. It does mean that we will have two or more versions of the DLL in memory, but since the additional memory usage is usually relatively small in comparison to the available system memory you probably wouldn’t run out of memory beacuse of this technique.

Hooks

Iterating over all the exports of a DLL and setting up the hooks can be easily done by using the Detours library. (link)
Not all of the exports are functions so an easy and dirty solution is to check if it’s address resides in executable memory

bool IsFunction(void* address)
{
    constexpr uint64_t mask = PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
    MEMORY_BASIC_INFORMATION buffer;
    auto size = VirtualQuery(address, &buffer, sizeof(buffer));
    return size && (buffer.Protect & mask);
}

Regex Filtering

If we don’t want to hook all the functions we can test if the symbol matches a regex before setting up the hook

ForEachExport(new_dll, [&](const char* name, void* address)
{
    if (IsFunction(address) && std::regex_match(name, regex))
    {
        if (auto old_address = GetProcAddress(old_dll, name); old_address)
        {
            DetourAttach(&(PVOID&)old_address, address);
        }
    }
});

Config File

For simplicity reasons the name of the dll file and the regex are read from dll_replacer_config.txt. For example if we want to replace all the functions in ExampleDLL.dll then the config file should look like this

ExampleDll
.*

DLL File Names

Once a DLL is loaded the file is locked for writing so if we try to compile the same DLL after the changes we will get an error. Even though the file is locked for writing it can still be renamed which will allow us to compile a new version. For fun and diversity the renaming code is written in python and can be executed by running rename_old.bat

import uuid
import os

with open("dll_replacer_config.txt") as file:
    name = file.readline().strip()

new_dll = name + ".dll"
old_dll = name + "_old.dll"

if os.path.exists(old_dll):
    tmp_name = name + "_" + str(uuid.uuid4()) + "_old.dll"
    os.rename(old_dll , tmp_name)

os.rename(new_dll, old_dll)

Later if we want to delete all the old versions we can run delete_old.bat

del *_old.dll

DLL Renaming and Loading Behaviour

A few notes about DLL names and the LoadLibrary function:

  1. When a DLL is loaded with LoadLibrary(“originalFileName.dll”) any subsequent call to LoadLibrary(“originalFileName.dll”) will return a handle to the same DLL even if the file has been renamed and a different file is now called “originalFileName.dll”
  2. If we rename a loaded DLL to “newFileName.dll” and call LoadLibrary(“newFileName.dll”) it will return a handle to the original DLL only while the file name is still “newFileName.dll”

Every time we rename and compile a new DLL the file names would be something like ExampleDLL.dll and ExampleDLL_old.dll so in order to always get a handle to the correct DLL we can rename it to an unique temporary name, load it with the new name and rename it back.

HMODULE LoadLibraryWithRename(const char* name)
{
    auto tmp_name = GetNewGUID() + "_tmp.dll";
    rename(name, tmp_name.c_str());
    auto library = LoadLibraryA(tmp_name.c_str());
    rename(tmp_name.c_str(), name);

    return library;
}

Intergaration with Existing Project

While DLLReplacer can be used as a single header library or as a DLL and both approaches can be easily integrated into an existing project we can borrow a trick from the previous blog post and use DLL injection. Running DLLInjector.exe processname.exe DLLReplacer.dll will replace a DLL in your process by using DLL injection and without the need to modify anything in your project.

Typical Workflow

  1. Run your program
  2. Modify the code in a DLL
  3. Run rename_old.bat
  4. Compile the modified DLL (detach the debugger if needed)
  5. Run replace_dll.bat
  6. Repeat steps 2-5 in order to live reload more new changes

Limitations

  1. Any changes to the memory layout can’t be live reloaded for example adding/removing data members (or virtual functions since it will change the layout of the virtual table). Having a mechanism to serialize and deserialize all objects can be used to solve this problem, but it isn’t in the scope of this library.
  2. If an exported function’s symbol has changed, for example by renaming it or changing it’s signature, it wouldn’t be hooked.
  3. All of the static data including static data members and static variables will be loaded multiple times which might cause problems. This can sometimes be avoided by using the regex symbol filtering.
  4. Having too many hooks can hurt performance.

Possible Future Improvements

A direction that could be worth investigating is parsing the PDB file and hooking all the functions instead of only the exported ones since it will enable replacing non exported functions. Another possible improvement is to hook all previous versions of a DLL. This can increase performance by always having only one hook jump instead of many at the cost of slightly slower load times since all previous versions of each function needs to be hooked. This is already implemented in the DLLReloader class but not as an injectable DLL.

Conclusion

While there are still some limitations having fast iterations with almost no integration effort has already saved me a lot of time and was definitely worth it for under 200 lines of cpp and a very short python script. I hope it will be helpful or educational to you too.