Maintaining Win32 Export Ordinals
Introduction:
Whenever you build a dll or any file that exports something, an import library(.lib) is generated. In turn, whenever you link against this dll, you use that import library to link against the desired symbols. When a linker links a dll, it assigns unique names and numbers to the exported functions. The names are called decorated names(and contain information about the type of return value, parameters and class names for c++) and the numbers are called ordinals. These ordinals are normally not fixed and can vary between builds of the same dll. It can happen that your application links to ordinal number 2 that has the decorated name func1@@YAHXZ but on a subsequent builds/updates of the library, that same ordinal has the decorated name func2@@YAHXZ. The application that uses your library, in absence of an up-to-date .lib file cannot re-link itself to the real func1@@YAHXZ.
Let’s look at an example:
Say your client uses an application made by you called A.exe that depends on a library made by you called B.dll. B.dll is a treasure trove of APIs that the client wants to build his own application C.exe – to that end the client needs B.dll and B.lib(the import library). This can be part of an SDK that you sell to the client. Now say you added some features to A.exe and B.dll, and you ship an updated version of A.exe and B.dll, but do not ship the updated SDK – therefore the client doesn’t get the import library B.lib. What happens if the ordinals in B.dll do not match against the ordinals in B.lib anymore? the client’s application C.exe will no longer work(or have catastrophic consequences).
Let’s fire up dependency walker and look at how the export section looks in a library:
So from this we see we have 3 exported functions named func1,func2,func3 each having a unique ordinal starting from 1 to 3.
Let’s see what happens if we add another function, called “anotherfunc” to the library:
As you can see, we now have a new function exported that has taken the ordinal previously used by func1. Even though our library has maintained backwards compatibility by virtue of retaining the old interface, simply adding a new function breaks the old ordinal assignments, which in turn means that the client cannot use the newly updated .dll with the old .lib you shipped him with the SDK.
What can we do?
Luckily, MSVC can take an input Module Definition File into the linker process via the /DEF parameter(Linker > Input > Module Definition File). This is normally used to tell the linker what symbols to export, but it can also be used to ensure we always have the same symbols with the same ordinals. The process should go like this:
In order to create a module definition file from an existing binary, we can use dumpbin /exports to dump the exports and then create a moddef file from that:
:: %1 is path, %2 is libname
@ECHO OFF
echo "dumping exports...\n"
dumpbin /exports %1/%2.dll > exports.temp
echo LIBRARY %2.dll > %2.def
echo EXPORTS >> %2.def
echo "writing module definition file...\n"
for /f "skip=19 tokens=1,4" %%a in (exports.temp) do (
if "%%a" == "Summary" goto done
echo %%b @%%a >> %2.def
)
:done
rm exports.temp
echo "done!\n"
This is a batch file that takes the path to the library.dll as a first parameter and the library name(no ext.) as the second and produces a .def file. I’ve configured a visual studio post-build-step to run this script after every build:
And this is the final result using the new module definition file:
Once you create this file and add it into the linker input it will ensure that whatever new functions you add to the library, it will never steal an ordinal from the ones that came before it. Needless to say, you shouldn’t remove old(deprecated) functions, as the module definition file is essentially trying to specify an export table – and if that function no longer exists then the build will fail.
There is one more caveat: if you use __declspec(dllexport) to mark your functions then a warning will be thrown for each of them also present in the module definition: LNK4197. Feel free to disable it by adding /IGNORE:4197 to your linker command line arguments.