windowsdlllinkerreverse-engineeringportable-executable

Indirect jumps for DLL function calls


Address fixup for calls to DLL functions is a multistage process: the linker directs the call instruction to an indirect jump instruction, and the indirect jump instruction to a word of memory in the import table in the .rdata section where the Windows program loader will place the address of the function when the DLL is loaded at runtime.

The indirect jump instruction must be generated by the linker because the compiler doesn't know the function will turn out to be in a DLL. Program file size is minimized by generating only one indirect jump instruction for each function, no matter how many places it's called from.

Given that, the obvious way to do it is to gather all the indirect jump instructions at the end of the text section, after all the compiler-generated code in all the object files, and that does seem to be what happens when I try a simple test case with the Microsoft linker /nodefaultlib switch (which generates a small enough executable that I can understand the full disassembly).

When I link a small program in the normal way with the C standard library, the resulting executable is large enough that I can't follow all of the disassembly, but as far as I can see, the indirect jump instructions seem to be scattered throughout the code in small groups of maybe three at a time.

Is there a reason for this that I'm missing?


Solution

  • The indirect jump instruction must be generated by the linker because the compiler doesn't know the function will turn out to be in a DLL.

    Actually, this is not always the case. If you mark the function with __declspec(dllimport), the compiler does know it will be a DLL import and in that case it can generate an indirect call:

    ; HMODULE = LoadLibrary("mylib");
    push  offset $SG66630
    call  [__imp__LoadLibraryA@4]
    

    (__imp__LoadLibraryA@4 is the pointer to the import in the IAT)

    If you do not use dllimport then the compiler generates a relative function call:

    push  offset $SG66630
    call  _LoadLibraryA@4
    

    And in such case the linker has to generate a jump stub:

    LoadLibraryA    proc near
                    jmp     [__imp__LoadLibraryA@4]
    LoadLibraryA    endp
    

    And, in fact, it does group such jump stubs together (though possibly by compile unit and/or imported DLL, not 100% sure here).

    Note: in the past, the linker did not explicitly generate jump stubs but took them from the import libraries. They contained complete object files both the stubs and the structures necessary for generating the PE import directory. See this article for how it all worked: https://web.archive.org/web/20070924090618/https://www.microsoft.com/msj/0498/hood0498.aspx

    These days the import libraries have only the API and DLL names and the linker knows how to generate the necessary code and metadata for importing them.