How DLL Imports Work

When you make a function call into a function that exists in a DLL, what happens, exactly? How does the function call happen, and what work goes on behind the scenes to make it so? I want to cover some of this information so you can have a more clear picture of the way shared libraries work on Windows. I am not going to cover how exporting a function works, but instead focus on how importing a function works as that’s a bit more involved.

Let’s start with a simple Win32 program:

#include <Windows.h>

int main( void ) {
  LPWSTR cmdLine = ::GetCommandLineW();
  return 0;
}

What I am attempting to explain in this post is how the call to GetCommandLineW works at runtime. It’s a bit different than calling a function your application defines, though. When you write a function in your application, the compiler is able to compile the function definition, and the linker is able to place the resulting compiled machine code somewhere in the executable. This means that the compiler knows everything it needs to be able to call the function, and will emit machine code that executes a call instruction that jumps to the function definition’s location in memory. However, for imported functions, the compiler doesn’t know at compile time where that function will live in memory. So it can’t just emit a call instruction straight away! For instance, with address space layout randomization, that function’s address could be somewhere different in memory every time the application is run. (Note, this is true of your own functions! But the compiler emits a call instruction relative to the base address of the executable, and that relative address is fixed up at runtime by the loader as part of the relocation fixups. This can’t happen for shared libraries though, since there’s no indication of where the library will be loaded relative to the base address of the executable.)

So the compiler gins a call up for you, and that makes the linker happy. Strange, isn’t it? What kind of call could the compiler possibly gin up that would work? In order to answer that question, we have to dig a bit deeper.

When you create an application which uses methods in shared libraries, the linker needs to have some knowledge about those functions. This means that the functions need to be declared somewhere (even though they are defined somewhere else), and there needs to be information about what libraries the functions are exported from. This generally happens by way of a stub library — the stub library doesn’t contain any of the actual executable code. Instead, it only contains the bare minimum needed for the linker to be able to create the appropriate structures so that the library can be loaded at runtime. This is where the fun begins!

When you create a Win32 project, you typically will see some additional dependencies listed in the linker section. These dependencies are for things like Kernel32, User32, et al. Those are not static libraries (which contain complete compiled binary data), but stub import libraries. Alternatively, you may tell the linker about a library to link against by using the #pragma comment(lib) directive. Either approach results in the same outcome — the linker knows about a stub library to import functions from. The stub library includes information such as the DLL to locate (which explains why you cannot rename a DLL and have its functions located when the application launches), as well as the functions exported. You can see these exports by running the dumpbin /export command on a library such as User32.lib, or your own DLL’s stub library.

When the linker determines there is an imported function from a library, it adds the function to an internal list of libraries, and the functions imported from those libraries. This list is then used to populate two tables in the resulting executable file — the Import Directory Table, and the Import Address Table. These two tables are very similar in structure, but serve different purposes. The Import Directory Table is used by the loader to perform loading operations, and the Import Address Table is used by the executable itself. The IDT contains an entry for every DLL that needs to be loaded, and it contains a pointer to a lookup table, as well as the IAT for that DLL. The lookup table is what’s used by the loader, and it is used to fill out actual address information in the IAT. This is important! Remember that function the compiler gins up for the imported library functions? The Import Address Table is what ties this together.

The compiler and linker don’t know where the DLL will be loaded at runtime. But they do know where the Import Address Table will live, since that will be in a location relative to the base address of the executable. So the IAT is used for that ginned up function call!

The compiler automatically creates a data pointer for you named __imp__ExportedFunctionName. In our sample code, this data would be named __imp__GetCommandLineW@0 (the @0 is part of the name mangling of the original function, in case you were wondering. It means the function takes no parameters.). This “imp” data is located within the IAT itself, at the exact spot the loader will dump in the true address of the function GetCommandLineW when it is imported from Kernel32.dll. The code generated to call the imported function does not actually call this “imp” data as though it were a function. Instead, it uses it as a memory location to be dereferenced during a call instruction. So the code emitted for our example will look something like this (in x86):

call dword ptr [__imp__GetCommandLineW@0]

So the memory location __imp__GetCommandLineW@0 is dereferenced (getting the true address of the function), and then the call jumps to that dereferenced memory. In this fashion, there is only a single jump required for the imported function call, and it winds up in the right place regardless of where the library is loaded at.

So recapping a bit: There is an Import Directory Table that will have a single entry in it for our example: Kernel32.dll is the library, and that library has only a single function entry: GetCommandLineW@0. There is an Import Address Table that will have an entry in it, initially empty, for the memory location that GetCommandLineW@0 resides in at load time. The loader is responsible for filling in this value as the library is loaded. The compiler generates an “imp” data pointer that only occupies 4/8 bytes (you can think of it as a global void *, essentially), and the linker places that data in the same place as the Import Address Table’s slot for GetCommandLineW@0. When the imported function is called, the compiler emits a call instruction that dereferences the “imp”‘s memory location, which is presumed to have been filled out by the loader properly as the executable is loaded.

Pretty crazy, but also quite efficient! It doesn’t require an extra level of jumping (such as a trampoline would), which keeps the performance reasonable. And it only requires a single level of indirection to perform the function call, which isn’t too bad. Most of the complexities are offloaded onto the linker (locating the “imp” data in the proper place, filling out the IDT and IAT) and the loader (processing the IDT to fill out the IAT). But since you only link the application once, and only load the application once, this is the perfect place for that complexity to live.

That’s the long and short about how an imported function is loaded and called by your executable. I doubt this will be of any practical use to you in your day to day coding. But it may come in handy when doing disassembly debugging. Just today I was scratching my head, wondering how a “function” named __imp__foobar was sneaking into my application, and why the disassembly for that function looked like complete junk. It looked like junk because it wasn’t truly machine code — just other memory locations filled out by the loader!

This entry was posted in Win32 and tagged , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published.