DLL Main on Windows Vs. __attribute__((constructor)) entry points on Linux
Asked Answered
C

2

9

Consider code

EXE:

int main ()
{

    printf("Executable Main, loading library\n");
#ifdef HAVE_WINDOWS
    HMODULE lib = LoadLibraryA ("testdll.dll"); 
#elif defined(HAVE_LINUX)
    void * lib  = dlopen("testdll.so", RTLD_LAZY);  
#endif 

    if (lib) {
        printf("Executable Main, Freeing library\n");
    #ifdef HAVE_WINDOWS
        FreeLibrary (lib); 
    #elif defined(HAVE_LINUX)
        dlclose(lib);   
    #endif 
    }
    printf("Executable Main, exiting\n");
    return 0;
}

DLL

struct Moo
{
    Moo() { printf("DLL Moo, constructor\n"); }
    ~Moo() { printf("DLL Moo, destructor\n"); }
};

Moo m;

#ifdef HAVE_WINDOWS
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        printf("DllMain, DLL_PROCESS_ATTACH\n");
        break;
    case DLL_THREAD_ATTACH:
        printf("DllMain, DLL_THREAD_ATTACH\n");
        break;
    case DLL_THREAD_DETACH:
        printf("DllMain, DLL_THREAD_DETACH\n");
        break;
    case DLL_PROCESS_DETACH:
        printf("DllMain, DLL_PROCESS_DETACH\n");
        break;
    default:
        printf("DllMain, ????\n");
        break;
    }
    return TRUE;
}
#else
CP_BEGIN_EXTERN_C
__attribute__((constructor))
/**
 * initializer of the dylib.
 */
static void Initializer(int argc, char** argv, char** envp)
{
    printf("DllInitializer\n");
}

__attribute__((destructor))
/** 
 * It is called when dylib is being unloaded.
 * 
 */
static void Finalizer()
{
    printf("DllFinalizer\n");
}

CP_END_EXTERN_C
#endif

The output differs:

On windows

Executable Main, loading library
DLL Moo, constructor
DllMain, DLL_PROCESS_ATTACH
Executable Main, Freeing library
DllMain, DLL_PROCESS_DETACH
DLL Moo, destructor
Executable Main, exiting

Linux

Executable Main, loading library
DllInitializer
DLL Moo, constructor
Executable Main, Freeing library
DllFinalizer
DLL Moo, destructor
Executable Main, exiting

On windows, Moo constructor is called before DLLMain and whereas on linux it is called after Initializer defined using attribute((constructor)).

WHY?

Coxa answered 31/3, 2014 at 14:14 Comment(1)
Why should they be the same? They are different implementations and we are talking about very platform specific aspects, not covered by the standard. I would be surprised if they were the same.Helio
C
8

Moo constructor isn't called before DllMain, it is called from DllMain. To be precise, its called from the real DllMain, the function Windows calls first. This real DllMain calls C++ constructors and then calls your C++ DllMain. The reason for this real DllMain is exactly to initialize constructors, something which wasn't needed in C before

Linux (GCC/ELF) doesn't have this concept at all; it only has constructors. Your manual ctor and the C++ ctor for Moo are treated the same.

Contra answered 31/3, 2014 at 17:6 Comment(15)
Documentation of this: msdn.microsoft.com/en-us/library/988ye33t.aspx "Included in the C/C++ run-time library code is the DLL entry-point function called _DllMainCRTStartup. The _DllMainCRTStartup function does several things, including calling _CRT_INIT, which initializes the C/C++ run-time library and invokes C++ constructors on static, non-local variables." and "In addition to initializing the C run-time library, _DllMainCRTStartup calls a function called DllMain."Passed
Some more inside about this stuff: https://mcmap.net/q/454967/-loading-a-dll-from-a-dll/…Helio
@BenVoigt: That's the Microsoft Visual Studio documentation, which is indeed documenting the "C++ DllMain". From the OS side (with kernel colored glasses, to quote Raymond Chen) DllMain is the entrypoint of a DLL. That "real DllMain" has to obey all the OS rules, such as not calling functions which may cause another DLL to be loaded. Which transitively means that global ctors in DLLs can't do a lot.Contra
@MSalters: There is no "real DllMain" from the OS perspective. DllMain is not an exported symbol. There IS an entry-point address in the PE header, and that entry-point points to _DllMainCRTStartup if you use the Microsoft toolchain (without using LINK.EXE's /ENTRY switch). The OS documentation unfortunately has C-colored glasses, and uses the name DllMain pretty much everywhere except the page I linked.Passed
I have previously looked in the Windows documentation and literally could not find any documentation of the signature needed for the direct entry-point, for either EXEs or DLLs. All it talks about, ever, is the user entry-point WinMain, wWinMain, main, wmain, or DllMain. The conclusion I've come to is that for an EXE, take no arguments (use GetCommandLineW()) and never return (use ExitProcess()), thus obviating the signature match issue. For DLLs, use the documented signature for user DllMain and __stdcall convention.Passed
@BenVoigt: That's why Raymond Chen uses that term "kernel colored glasses". As far as the kernel is concerned, there's no such thing as a CRT or _DllMainCRTStartup. There's a documented DllMain entry point, with a signature, and that signature is how the entrypoint is called.Contra
@MSalters: It's just the entry-point. The DllMain name means nothing, and is not used to call the entry-point. Rather a particular offset in the PE header contains the address (relative offset) of the entry-point function, and that's all the kernel cares about. And the entry point signature isn't documented anywhere as such. (The PE structure is, but that seems to be the end). Only the user function called by the CRT's entry-point implementation is documented, even in the OS documentation.Passed
What you called "documented DllMain entry point" is documentation for the CRT-invoked user function, as made clear by "DllMain is a placeholder for the library-defined function name. You must specify the actual name you use when you build your DLL. For more information, see the documentation included with your development tools." That documentation seems not to be an accurate description of the true entry-point.Passed
Or maybe they document it right for DllMain (where the user DllMain and true entry-point use the same signature) and only WinMain/wWinMain/main/wmain are different from the true EXE entry-point?Passed
@Ben: The "naming of unnamed functions" is a recurring theme in the OS documentation. [WindowProc](msdn.microsoft.com/en-us/library/windows/desktop/…) is another classic example, and again you'd better stick to the signature even though the name is a placeholder. The documentation for WinMain specifically states that it's the user-provided entry point in many frameworks, i.e. not a kernel thing. GetExitCodeProcess` makes it clear that at least the return value is required; the "falling out of main" better be implemented by the compiler.Contra
@MSalters: The GetExitCodeProcess doc indicates things about the library-called user main (why oh why do they insist on using CRT-colored glasses?). It makes no statement about the return value from the actual entry-point, and in fact I think that correctly written (EXE) entry points don't return, they call ExitProcess.Passed
In fact I left a community contribution on the GetExitCodeProcess page years ago after looking at the CRT implementation. "The exit code always comes from either ExitProcess or TerminateProcess. The others are just special cases of calling ExitProcess (handy to know what value is passed of course): The C Runtime Library calls ExitProcess when your main/WinMain function returns. The default unhandled exception filter calls ExitProcess when an exception is unhandled."Passed
@Ben: have you read blogs.msdn.com/b/oldnewthing/archive/2010/08/27/10054832.aspx ?Contra
@MSalters: Yes I have, but it never answers the title question. None of the resolutions actually include "returning" (some comments do mention the RET instruction, but never get to "this behavior is contractual", only observed behavior). Although that's another option I hadn't considered... that the EXE actual entry-point should conform to the signature for a thread procedure.Passed
In between, is there any way i can simulate the same behavior on Linux as on Linux such that all global and static constructor are called before my initializer code executes.Coxa
M
0

There is a way:

StartupCleanup.cpp:

// Redefine the same StartupCleanup class as it is in DllMain.cpp
// Definition of constructor and destructor must stay in DllMain.cpp
// And including here any headers which may define normal static or global constructors/destructors is strictly forbidden!
struct StartupAndCleanup
{
    /**/  StartupAndCleanup();
    /**/ ~StartupAndCleanup();
};

// It ensures this instance is the first to be constructed *BEFORE* any normal static or global constructors calls
// and the last to be destructed *AFTER* all normal destructors calls.
// The key to do so is using #pragma init_seg(lib), but that key applies for all the static and global constructors/destructors in the same .obj file! 
#pragma warning(push)
#pragma warning(disable:4073)
#pragma init_seg(lib)
#pragma warning(pop)

// this function is just to keep linker from discarding startupAndCleanup.obj when linking to an executable or dll
void needStartupAndCleanup()
{
}

static StartupAndCleanup startupAndCleanup;

DllMain.cpp:

...
// Definition of this class should be the same as in StartupAndCleanup.cpp!
struct StartupAndCleanup
{
    /**/  StartupAndCleanup();
    /**/ ~StartupAndCleanup();
};

StartupAndCleanup::StartupAndCleanup()
{
    // Do your initialization here !
}

StartupAndCleanup::~StartupAndCleanup()
{
    // Do your termination here !
}

You DllMain must be just an empty shell and do your usual safe initialization and termination in those constructor and destructor like for Linux.

NOTE: Beware! you cannot create/delete threads inside static or global constructors/destructors if you plan to synchronize them. That's all folks !

EDIT: you also need to call needStartupAndCleanup() in a function you know it is linked, otherwise the object file StartupCleanup.obj will be discarded and those global constructor/destructor as well.

Maroc answered 20/7, 2015 at 16:56 Comment(3)
What is the alternative on GCC/Clang? for #pragma init_seg(lib).Coxa
If I understand correctly StartupAndCleanup cror is running before all other static objects are initialized, and StartupAndCleanup dtor is running after all other static objects are destructed. Which is the opposite of how DllMain works, and of what is usually needed.Prospectus
Well sorry, I posted it 4 years ago, so I may not recall well. That class is for two things: 1) allowing to order the construction and destruction of singletons as you want them. 2) the main class is sure to construct first and destruct last so if another singleton not defined in that class is referring a singleton defined in that class, it will be okay. Same for destruction. Now it doesn't change anything for DllMain regarding how it handles ALL static objects : we are just setting the priority order of static objects. So I don't understand what you mean by your last statements.Maroc

© 2022 - 2024 — McMap. All rights reserved.