This is an archive of the discontinued LLVM Phabricator instance.

[LLD][COFF] Fix ordering of CRT global initializers in COMDAT sections
ClosedPublic

Authored by aganea on Oct 1 2018, 2:22 PM.

Details

Summary

(patch by Benoit Rousseau)

This patch fixes a bug where the global variable initializers were sometimes not invoked in the correct order when it involved a C++ template instantiation.

The compiler generates an array of function pointers to the global variable initializers in the .CRT$XCU and .CRT$XIU sub-sections, and then the CRT calls those functions one by one to initialize the global variables. The sections that contain the initializer for a C++ template instantiation have the COMDAT bit set because the linker needs to do some additional work to delete the duplicate definitions.

The initial bug was that the linker puts the COMDAT sections at the end of the non-COMDAT sections, even though they are intended to be called in the same order that they appear in their OBJ.

We had the bug in the Microsoft Cloud SDK. Looking at the disassembly, the code of the library looks a bit like this.

#include <vector>

class State
{
public:
    static std::vector<State*> states;
    State(const char* name)
    {
        s_states.push_back(this);
        // ...
    }

    //
    // ...
    //
};

std::vector<State*> State::states();
State idle("Idle");
State connection_pending("Connection pending");
State connected("Connected");
// ...

Because of the bug, State::states was initialized after the globals idle, connected pushed themselves onto it, which caused State::states to drop all those states. And later on the code accesses them with a std::vector::operator[] and it crashes.

The fix simply sorts the .CRT by section number, after the COMDAT sections have been resolved. I expect the other sections to be independent of the order they appear in the executable (although there could be caching effects in the resulting executable if code that is related is separated by a wide gap in the executable’s code pages)

Diff Detail

Repository
rL LLVM

Event Timeline

aganea created this revision.Oct 1 2018, 2:22 PM
aganea added a comment.Oct 1 2018, 2:26 PM

Please note that this issue occurs only when using MSVC cl.exe. Compiling with clang-cl.exe does not expose the issue.

rnk added a comment.Oct 1 2018, 2:30 PM

Please note that this issue occurs only when using MSVC cl.exe. Compiling with clang-cl.exe does not expose the issue.

I was going to say, I think clang generates a single initializer function to avoid relying on the linker's chosen section order.

That's crummy, though. I guess we could limit it to the CRT section, and say anyone relying on section order corresponding to link line order in other sections is out of luck.

ruiu added a comment.Oct 1 2018, 2:30 PM

Wow, I was surprised that we still have this kind of silly bugs... Thank you for find this out.

COFF/Writer.cpp
748 ↗(On Diff #167832)

Maybe you should mention that C++ guarantees that global variables in a single compilation unit are initialized from top to bottom.

1594 ↗(On Diff #167832)

I don't think you need this guard -- you can still sort an empty or a singleton list.

1614 ↗(On Diff #167832)

I don't think you need a parallel sort, because the number of sections in a .CRT shouldn't be that large.

test/COFF/crt-dyn-initializer-order.test
1 ↗(On Diff #167832)

Remove an unnecessary blank line.

aganea added a comment.Oct 1 2018, 2:35 PM
In D52749#1251704, @rnk wrote:

Please note that this issue occurs only when using MSVC cl.exe. Compiling with clang-cl.exe does not expose the issue.

I was going to say, I think clang generates a single initializer function to avoid relying on the linker's chosen section order.

That's crummy, though. I guess we could limit it to the CRT section, and say anyone relying on section order corresponding to link line order in other sections is out of luck.

You're right indeed, that is the source of the bug. MSVC cl.exe does this:

- Name:            '.CRT$XCU'
  Characteristics: [ IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_LNK_COMDAT, IMAGE_SCN_MEM_READ ]
  Alignment:       8
  SectionData:     '0000000000000000'
  Relocations:     
    - VirtualAddress:  0
      SymbolName:      '??__E?Instances@?$TemplatedObject@$0A@@@2V?$vector@PEAU?$TemplatedObject@$0A@@@V?$allocator@PEAU?$TemplatedObject@$0A@@@@std@@@std@@A@@YAXXZ'
      Type:            IMAGE_REL_AMD64_ADDR64
- Name:            '.CRT$XCU'
  Characteristics: [ IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_READ ]
  Alignment:       8
  SectionData:     '0000000000000000'
  Relocations:     
    - VirtualAddress:  0
      SymbolName:      '??__Eidle@@YAXXZ'
      Type:            IMAGE_REL_AMD64_ADDR64

While clang-cl does this:

- Name:            '.CRT$XCU'
  Characteristics: [ IMAGE_SCN_CNT_INITIALIZED_DATA, IMAGE_SCN_MEM_READ ]
  Alignment:       8
  SectionData:     '0000000000000000'
  Relocations:     
    - VirtualAddress:  0
      SymbolName:      _GLOBAL__sub_I_Source1.cpp
      Type:            IMAGE_REL_AMD64_ADDR64
mstorsjo added inline comments.Oct 1 2018, 2:37 PM
COFF/Writer.cpp
1605 ↗(On Diff #167832)

Won't this override the internal ordering based on the section name suffixes like .CRT$ABC?

mstorsjo added inline comments.Oct 1 2018, 2:39 PM
COFF/Writer.cpp
1605 ↗(On Diff #167832)

Or what actually does getBufferIdentifier() return - the name of the object file? In that case I'd say this is wrong. If it returns the section name it might be ok...

aganea added inline comments.Oct 2 2018, 9:09 AM
COFF/Writer.cpp
1605 ↗(On Diff #167832)

@mstorsjo getBufferIdentifier() indeed returns the file name. However at this point, chunks were already grouped by specific section. Meaning that this function will sort only, say, .CRT$XCU chunks at once. Internal TU order is guaranteed by the test below (return SA->SectionNumber < SB->SectionNumber;)

The only side-effect I could see is that this change makes deterministic initialization of global initializers (which in this case, means the TUs will be called in lexicographic order, based on their relative/absolute path and filename). The C++ std says the behavior is "indefinite", but this makes it "definite", at least for LLD. What is your stance on this? @ruiu @zturner @pcc @rnk

aganea added inline comments.Oct 2 2018, 9:15 AM
COFF/Writer.cpp
1605 ↗(On Diff #167832)

In an ideal world, it'd be nice if this behavior could be randomized, with a command-line flag. That could trap some (global initialization) issues earlier in the target program, by successive "link"-"run program" runs.

rnk added inline comments.Oct 2 2018, 9:31 AM
COFF/Chunks.cpp
32–33 ↗(On Diff #167832)

You should be able to recover the section index with Obj->getCOFFObj()->getSectionID(Header) later. The index should be redundant with the coff_section*, which is a pointer into the section table.

COFF/Writer.cpp
1605 ↗(On Diff #167832)

You know, this might actually come back and bite us at some point. I agree that the linker should not provide guarantees about the order in which unrelated TUs initialize, but I am aware of a codebase that goes out of its way to maintain a particular ordering of source files in the build system to ensure that some files are initialized first. This codebase has used -fno-init-array on Linux for years to avoid dealing with the reversal of initialization order that happened when GCC moved from .ctors to .init_array.

I think, at the very least, Windows doesn't have anything like crt0.o, which is supposed to come first on the link line. The way that LLD and link.exe sort section names alphabetically makes that unnecessary.

I'm actually surprised that LLD doesn't number each object file so that we can just sort them by input order. It seems like that might be useful elsewhere.

All that said, I'd be fine with reordering input initializers alphabetically and seeing what happens. If anyone is relying on this behavior, they can use #pragma init_seg to mark the initializers that need to run first, and their code base will be less fragile as a result.

mstorsjo added inline comments.Oct 2 2018, 10:51 AM
COFF/Writer.cpp
1605 ↗(On Diff #167832)

Ah, sorry, I missed that this was called at that stage. Then I have no objections.

A testcase that shows that ordering of different .CRT$FOO sections (with different FOO part) from different object files is preserved just for clarity, unless we already have that. But maybe there's no need...

Btw, I'd prefer a more precise commit subject - this one makes it sound like it has been completely broken so far, while it's only broken for one specific case.

aganea retitled this revision from [LLD][COFF] Fix CRT global initializers to [LLD][COFF] Fix ordering of CRT global initializers in COMDAT sections.Oct 2 2018, 12:26 PM
aganea updated this revision to Diff 168008.Oct 2 2018, 1:26 PM
aganea marked 11 inline comments as done.

Addressed all comments. Added more /verbose logging for sections, which also prints the CRT initializers. Extended the test, as requested by @mstorsjo.

Also added tests to show that arbitrary OBJ ordering on the command-line does not affect the layout of CRT section(s). This is a breaking change from the previous behavior of LLD. Please let me know if we should go ahead with that, or if I should retain the previous TUs ordering (which was previously dependent on both the OBJs order on the command-line, and the contents of said OBJs).

COFF/Chunks.cpp
32–33 ↗(On Diff #167832)

I've removed SectionNumber and added instead SectionChunk::getSectionNumber() for that purpose.

COFF/Writer.cpp
1605 ↗(On Diff #167832)

Extended the test to illustrate that non-COMDAT .CRT$XXX sections from different OBJs retain their internal ordering.

This is a breaking change from the previous behavior of LLD. Please let me know if we should go ahead with that, or if I should retain the previous TUs ordering (which was previously dependent on both the OBJs order on the command-line, and the contents of said OBJs).

My 2 cents is that I'd prefer keeping the overall order from objs on the command line if the rest of this is implementable while keeping that.

aganea updated this revision to Diff 168129.Oct 3 2018, 10:20 AM

Updated diff. The order of the CRT initializers is now the same as before; with the exception of COMDAT sections which now appear in the same order as declared in the .cpp file.

Please let me know if any other changes are required. Thanks!

With those minor remarks, this patch is ok with me, but I'll let @rnk or @ruiu do the actual approval.

COFF/Writer.cpp
736 ↗(On Diff #168129)

Hmm, this log line might be useful, but might also add quite a significant amount of spam to the verbose log. At least with mingw targets, all comdats have a separate subsection name like .text$symbolname.

745 ↗(On Diff #168129)

I'd rather have this comment above the function itself. Being this verbose (which in itself is great!) inline in a function makes you lose focus on the overall structure.

aganea updated this revision to Diff 168167.Oct 3 2018, 1:52 PM
aganea marked 2 inline comments as done.

Updated as per Martin's comments.

COFF/Writer.cpp
736 ↗(On Diff #168129)

I moved it down in the .CRT part.

mstorsjo added inline comments.Oct 3 2018, 1:59 PM
COFF/Writer.cpp
745 ↗(On Diff #168129)

I don't see this one addressed even though you checked it as done. I meant moving the large comment to be above the function definition, not here by the function call, to keep the overall structure of createSections as readable as possible.

aganea updated this revision to Diff 168170.Oct 3 2018, 2:07 PM
aganea added inline comments.
COFF/Writer.cpp
745 ↗(On Diff #168129)

Ah sorry about that - please see the updated diff.

This version seems ok to me, but I'd like someone else's ack/approval as well.

rnk accepted this revision.Oct 3 2018, 2:33 PM

lgtm, thanks

This revision is now accepted and ready to land.Oct 3 2018, 2:33 PM
aganea added a comment.Oct 3 2018, 2:34 PM

Thank you all!

This revision was automatically updated to reflect the committed changes.