When are global objects constructed and destructed by Visual C++?, redux


Today we're going to make some clarifications to this table, which came from an earlier article:

When does it run? Constructor Destructor
Global object in EXE C runtime startup code C runtime DLL hired lackey
Global object in DLL C runtime DLL_PROCESS_ATTACH prior to Dll­Main C runtime DLL_PROCESS_DETACH after Dll­Main returns

It turns out that the upper right corner of the diagram actually splits into two cases. The table lists what happens if the process terminates by calling Exit­Process. The thing that makes termination with Exit­Process interesting is that the first (and only) time the C runtime library learns about it is when the C runtime library itself receives its DLL_PROCESS_DETACH notification, and we saw last time that by the time this notification arrives, it could very well already be too late.

The escape here is to exit the program not by calling Exit­Process but rather by calling the C runtime exit function. When you do that, the C runtime gets control (by virtue of the fact that you explicitly called it), so it can run down your executable's global objects right away, before calling the operating system's Exit­Process function. That way, the global objects are run down while all of the dependent DLLs are still in memory.

Let's update our table:

When does it run? Constructor Destructor
Ends with exit() Ends with ExitProcess()
Global object in EXE C runtime startup code Prior to ExitProcess C runtime DLL hired lackey
Global object in DLL C runtime DLL_PROCESS_ATTACH prior to Dll­Main C runtime DLL_PROCESS_DETACH after Dll­Main returns

The C and C++ language standards say nothing about what happens if you exit a process by calling some operating system low-level process termination function. Which makes sense, because the C and C++ language standards deal with the standard, not with operating system-specific stuff. I believe that more recent versions of the C runtime library take advantage of this and say, "You know what? If you exit the process by calling Exit­Process, then I'm simply not going to destruct anything. Serves you right for invoking behavior not covered by the standard." In those cases, the upper right corner changes from "C runtime DLL hired lackey" to "never".

Comments (30)
  1. camhusmj38 says:

    Returning from main / WinMain also calls exit and then shuts the CRT down. This is probably the easiest way to get there if you can coordinate between all your threads etc.

    1. Antonio Rodríguez says:

      Also, this follows the pattern of having a single exit point in each function (i.e., not using intermediate returns), which makes code more readable and understandable, and easier to debug.

      1. smf says:

        If you don't have exceptions (i.e. you're using plain C) then having to return back to main() on any non-trivial program is a pain.

        However what you probably should do is call an application specific function that logs and exits, so you can break point it.

      2. GregM says:

        "Also, this follows the pattern of having a single exit point in each function (i.e., not using intermediate returns), which makes code more readable and understandable, and easier to debug."

        Except that in a language like C++, this is often considered an anti-pattern, and often does the exact opposite. It makes the code harder to read, understand and debug. Proper use of RAII eliminates the need for cleanup code at the end of the function, and deeply nested conditionals that it spawns. In any case, that's probably getting too far off-topic for this post.

  2. ZLB says:

    Doesn't this highlight a deficiency in the C runtime spec?

    Ostensibly, the C runtime shouldn’t really be aware of the concept of a process. That’s an OS thing, so it seems correct that to exit a Windows process, you should call a Windows API.

    Shouldn’t the C Runtime have a ‘call_global_dtors()’ function?

    Then the ExitProcess goes from:

    ExitWindows() -> UnloadLibs() -> DestructGlobals() -> Kaboom() (optional) -> Shutdown C Runtime()

    To:

    ExitWindows() ->DestructGlobals() ->UnloadLibraries() ->Shutdown C Runtime()

    1. mark says:

      That's a layering violation. The OS should not know about the C runtime.

      1. It also assumes that everybody in the process uses the same runtime, and that no runtime depends on another runtime.

      2. Karellen says:

        The kernel certainly shouldn't know about the C runtime, but I'd argue that it would be reasonable for the low level userspace wrappers to the kernel, which has a high probability of being mostly written in C with a tiny bit of ASM for register twiddling and the actual int 80h/syscall instruction, for them to be aware of the C runtime. So having the userspace wrapper for ExitProcess() do C runtime bookkeeping before passing control to the kernel's ExitProcess() is not necessarily a (large) laying violation.

        Alternatively, providing hooks for a process to set arbitrary pre-ExitProcess() callbacks, similar to C's atexit(3), would allow the C runtime to set a hook at initialisation to do cleanup before it receives the DLL_PROCESS_DETACH, with no layering violation.

        1. Darran Rowe says:

          Of course, there is always the easy way, make the process clean-up occur before the call to ExitProcess. This then stops the need to change the behaviour of functions that have been in windows forever. This is what happens normally if you just return from main, or call exit. In fact, you have to go out of your way and call outside of the CRT/VCRuntime layer to actually get things to go wrong. (I mention the VCRuntime here because it took over the process initialisation/uninitialisation in VC2015.)
          It is also easy to forget that this whole thing is a major special case used to highlight how process exit can go bad. Normal processes on Windows should be calling exit or returning from the entrypoint in order to exit the process.

        2. Voo says:

          There is just no good reason for such a violation of good design principles.

          As is usually the case this just highlights a bad design: If instead of calling the lore level win32 API you called the c runtime function you wouldn't have the problem in the first case. That's not only a simple but also clean fix.

          1. Karellen says:

            You're assuming my application is written in C, which it might not be. Why would my F# program, which in this particular run has used a COM component that happens to have been written in C and therefore loaded a C runtime behind my back, call a C runtime function to exit? It's not a C program, it's a Windows program, and the ExitProcess() is the relevant language-agnostic Windows API call that the program *should* use to exit.

        3. Darran Rowe says:

          Re: the reply to voo:
          If you are calling a Win32 API function instead of using the facilities supplied by the language to exit, then your design is even more questionable.
          ExitProcess just shouldn't be used as a general purpose way of exiting your program, instead you should use the ways provided by the runtime of the language that you are using to exit. In fact, I would say that if you are tempted to use ExitProcess, even if it means pInvoking or some other native call method of calling it, then you are unfamiliar with the language and are struggling to adapt to what you don't know.
          But anyway, ExitProcess as a function was only designed to do one thing, take the process as it is and then just exit it. From the documented behaviour, ExitProcess makes the assumption that the process has already been cleaned up by the current program. After all, the first thing it does is terminates, with extreme prejudice, all running threads except the thread that called ExitProcess. Immediately you should realise that if you have a program that is even slightly multithreaded, then that means abandoned synchronisation objects, deadlocks and fun, oh my.
          So no, ExitProcess is the Win32 function used to exit the process, it isn't meant to cleanly exit, or clean-up the runtime or anything like that. As such, the runtime itself should be what calls ExitProcess, a developer should only call ExitProcess if you are in an unhosted process, so you came in via the raw entry point, and you do that only after you completely cleaned up after yourself.

          1. Karellen says:

            I'm coming around to your way of thinking, but...

            I've generally viewed the Microsoft's APIs as being "on top of", and recommended over, whatever API is usually provided by a language. So, you should generally use OpenFile()/ReadFile()/CloseFile() over fopen()/fread()/fclose(). Or CString over std::string. Or *Alloc() over malloc(). I'm sure there are plenty of other examples out there.

            Generally, if the Windows API provides an alternative way of performing a task than the "native" way for your particular language, then code using those APIs will integrate better with the rest of the Windows APIs that your other code is using, and be less painful in the long run.

            Maybe this is an exception to the rule, but I don't like those. I've found that a lot of the practice of "good programming" is figuring out good habits and sticking to them as much as possible. Exceptions, like this one appears to be, are additional cognitive load that I could do without. :-/

          2. GregM says:

            "I’m coming around to your way of thinking, but… I’ve generally viewed the Microsoft’s APIs as being “on top of”, and recommended over, whatever API is usually provided by a language. So, you should generally use OpenFile()/ReadFile()/CloseFile() over fopen()/fread()/fclose(). Or CString over std::string. Or *Alloc() over malloc(). I’m sure there are plenty of other examples out there."

            It depends on whether you're willing to be tied to the platform, or if you're looking to be portable.

            "Generally, if the Windows API provides an alternative way of performing a task than the “native” way for your particular language, then code using those APIs will integrate better with the rest of the Windows APIs that your other code is using, and be less painful in the long run."

            As long as "the long run" is only on Windows, and as long as "your other code" is also stuff that only runs on Windows. If you're using portable third-party libraries, then you likely want to stick with portable stuff.

          3. Karellen says:

            If you're looking to write portable code, you won't be calling any Win32 functions at all, including ExitProcess(), so that point is kind of moot in this thread.

          4. GregM says:

            "If you’re looking to write portable code, you won’t be calling any Win32 functions at all, including ExitProcess(), so that point is kind of moot in this thread."

            Your post sounded like a general philosophy, not "I've already decided that I'm writing non-portable Win32 code, so in that case, which is better?"

          5. Karellen says:

            Yeah, sorry, reading it back now, I see what I wrote down was not quite as clear as the thoughts were in my head!

            My bad.

    2. Tanveer Badar says:

      It is an implementation detail and certainly how VC++ implements it. I am not sure if gcc follows the same pattern.

    3. Darran Rowe says:

      The CRT has to be aware of the concept of a process, it has exit and abort, C++ has unexpected and terminate for exception handling. So to an extent it has to be aware of the concept of processes.
      The problem here is that the global object creation and destruction happens behind the scenes, it happens in the CRT/VCRuntime code that executes before and after the main function is called. By calling ExitProcess, you are reaching out beyond this and basically going straight to destroying the process without cleaning up the contents first. The current documentation for ExitProcess tells you exactly what it does, and it basically terminates all threads, unloads all DLLs and then terminates the calling thread and process. So basically, it allows the DLLs to clean up, but not the container process which should go first.
      But you are right in that it is an OS thing, and there is a call to ExitProcess involved normally. If you just return from the main function, then the library calls exit, which calls the clean-up code and then ExitProcess. If you call exit yourself, then it calls the clean-up code and then ExitProcess. If you return and for some reason the library doesn't call exit, it returns to the Windows process creation code, which is guaranteed to call ExitProcess.
      For "Shouldn’t the C Runtime have a ‘call_global_dtors()’ function?", well, it does. The problem is, calling ExitProcess caused it to come at the wrong time. After you call ExitProcess, it basically goes onto the DLL clean-up, and the clean-up order is implementation defined. This means that the order that DLLs are unloaded can't be known in advance, and it so happened that GDI was unloaded before MSVCRT. Of course, there could be some call in ExitProcess that could do the global clean-up in the process, but what is the point when ExitProcess should be the very last call that a process makes anyway.
      My thought is that this problem is a holdover from the very early days of Windows when the rules for using the CRT was very different.

      1. smf says:

        Some systems have hooks that you can use to be told when your application exited, so you can do all your cleanup.

        But as windows closes files, sockets and releases memory anyway, there isn't much of an argument for it.

  3. Yukkuri says:

    What exactly is happening in these destructors that matters if the process is about to exit?

    1. Yuri Khan says:

      For example, releasing a resource which is physically located on a different machine and accessed and locked over the network.

    2. Darran Rowe says:

      The post yesterday, it crashed while cleaning up CBitmap. So what it was trying to recover was GDI handles.
      The thing to remember is that unlike kernel handles, GDI handles aren't automatically recovered when a process exits or dies. The GDI memory does get recovered, but it can take a while, so any memory not cleaned up properly is leaked for a while. So in this case, it is used to clean up a resource which wasn't automatically deleted and would stay allocated outside of the process bounds.

      1. Yukkuri says:

        Hmmm, I see, ty

      2. That would explain why TerminateProcess didn't get mentioned yet!

  4. Richard says:

    I have a great deal of fun working on a project where the original programmer decided that every class with a single instance should be declared in it's implementation file and extern'd in it's header file.

  5. Adrina says:

    The msdn documentation has historically lacked a lot of information about the interaction between MS OS and MS implementation of C/C++. This interaction is very important to be able to understand, to be able to understand how any developed application work. Sometimes the standards cannot be exactly implemented, and sometimes they can be implemented in different ambiguous ways and a developer can then only guess how it actually is implemented and what side effects exists.

    Application specific cleanup logic should of course not rely on any particular OS or CRT hooks. Since WEP it has always been "best effort", which in practical situations are totally useless to implement because there's no guarantees for any cleanup code ever executing anyway. Both protocols and persistence logic in general of course needs to take abrupt shutdowns into consideration. If the power cord is yanked files should not been kept locked on remote servers forever.

  6. Neil says:

    I assume this table is for the dynamic CRT. Has the static CRT ever been aware of calls to ExitProcess?

    1. Darran Rowe says:

      Well, the only reason why the DLL version of the CRT detected this was the call to the DLL entrypoint with the DLL_PROCESS_DETACH.
      The static CRT itself would only become aware of it if the executable itself did the clean-up. A direct call to ExitProcess in an executable that was statically linked to the runtime would most likely leak any global handles from things like the GDI, or leak reference counts from out of process COM servers.
      In the case of a statically linked executable, the call to ExitProcess would call into the loader and just terminate the process, only executing code from the DLL entry points. The global destructors would never get called because they are part of the executable clean-up code that would never get called. So while the dynamic version had a chance and crashed doing it, the static version would just do nothing and let everything leak.

  7. Joshua says:

    I've ran analysis on use of a lackey to run destructors, etc. While the lacky does work for direct code execution, trying to use it with a further layer of indirection is doomed to failure because this can set up cross-dependencies, that is the lackey cleanup routine calls into code that has already had its dependencies removed, and because it was statically linked, assumes that is not possible. Oops.

Comments are closed.

Skip to main content