[originally published 8/4/2017]
I’m sure I have some of the details wrong… but I thought I’d write this down. I’ve been asked this several times this week. Hadn’t come up in the 10 years before that. Anyway here it is:
You might think the loader takes care of your initializers, that’d be wrong. The loader gives no shits, it calls main, then it’s your problem. Actually it doesn’t call what you think is main, it calls something that’s probably got a name like __main or some such. The “real” main.
So the runtime needs to take care of this stuff for you, the runtime does it something like this:
- every object file can have some initializers that must be run
- if there are any, there is a method generated in the object file that does the job
- it has a name like $global$foo_cpp_$initializers$or$something$of$that$ilk
So to do the job, that method is called by the runtime. Great, so now you may ask how does the runtime know to call it? The answer is… it sort of doesn’t…
The runtime has three magic sections in the .data section, let’s call the three sections magic_1 magic_2 and magic_3. If I ever find a system that actually uses those names that would be kinda cool…
Now these are just named sections that are in order in the .data section (actually on many systems they’re inside of .rdata or something like that, because they’re read-only). Named sections are a linker thing. The loader doesn’t know or care about them. But the linker lets you specify that some data is to go in a certain section. With this much you just need the following setup:
- magic_1 has nothing in it, but a symbol points to it.
- magic_3 has nothing in it, but a symbol points to it.
- every object file with global initializers puts a pointer to its initialization method in magic_2
The linker does its job, after which the following will be true:
- all the initializer function pointers are in magic_2
- the symbol at the start of the empty magic_1 section is the first initializer
- the symbol at the start of the empty magic_3 section is now the address of the last initializer +1 pointer.
The runtime can now invoke every pointer in magic_2 by going from the known symbol in magic_1 to the known symbol in magic_3
Note, the linker contributes to magic_2 in an unpredictable order, probably the order that the object files got resolved; this could vary, especially in the presence of libraries, but the initializers will be in a fixed order for any given linker output.
Also there is no law that says the objects should be processed in the order they appear on the command line, in fact there are several passes and the passes are almost certainly not in command line order because whatever order you did the objects in during pass 1 that is the LAST thing you should do in pass 2 because if you do that, for sure you will miss the disk cache on every object file. Probably you should do them backwards or something.
Whatever strategy the linker uses as it encounters the objects they will contribute to magic_2. When this is all done the initializer function pointers will be sitting there on a silver platter. So once that is all done you have a section that has exactly the pointers you need to call. So the runtime does exactly that.
Then it does whatever else it needs to do and eventually calls your main.
Thread local storage is considerably more complicated than this but it actually ends up using not dissimilar linker methods to gather the thread local variables. Raymond Chen wrote an excellent article on it if you’re interested.