Support/jitlink coff seh - #5
Conversation
Tested by (with assertions enabled): test/ExecutionEngine/Orc/trivial-return-zero.ll
…AT .pdata$* sections
|
Sorry for the delayed reply -- I'm looking at this now. |
Fix llvm#201615. Fix the issue that non atomic operations race in waiting queue, which causes missed futex wakeup signals. Confirmed by TSAN: ``` ================== WARNING: ThreadSanitizer: data race (pid=388518) Write of size 4 at 0x7ffd21cf98e4 by thread T23: #0 __llvm_libc_23_0_0_git::RawRwLock::notify_pending_threads() ./libc/src/__support/threads/raw_rwlock.h:443:44 #1 __llvm_libc_23_0_0_git::RawRwLock::unlock() ./libc/src/__support/threads/raw_rwlock.h:520:5 #2 randomized_thread_operation(SharedData*) ./libc/test/integration/src/__support/threads/tsan_full_rwlock.cpp:104:18 #3 thread_runner(void*) ./libc/test/integration/src/__support/threads/tsan_full_rwlock.cpp:148:5 Previous atomic read of size 4 at 0x7ffd21cf98e4 by thread T4: #0 __llvm_libc_23_0_0_git::cpp::Atomic<unsigned int>::load(...) ./libc/src/__support/CPP/atomic.h:115:5 #1 __llvm_libc_23_0_0_git::Futex::wait(...) ./libc/src/__support/threads/linux/futex_utils.h:43:17 #2 __llvm_libc_23_0_0_git::cpp::expected<int, int> __llvm_libc_23_0_0_git::rwlock::WaitingQueue::wait<Role::Reader>(...) ./libc/src/__support/threads/raw_rwlock.h:101:35 #3 __llvm_libc_23_0_0_git::rwlock::LockResult __llvm_libc_23_0_0_git::RawRwLock::lock_slow<Role::Reader>(...) ./libc/src/__support/threads/raw_rwlock.h:402:34 #4 __llvm_libc_23_0_0_git::RawRwLock::read_lock(...) ./libc/src/__support/threads/raw_rwlock.h:485:12 #5 randomized_thread_operation(SharedData*) ./libc/test/integration/src/__support/threads/tsan_full_rwlock.cpp:79:16 swiftlang#6 thread_runner(void*) ./libc/test/integration/src/__support/threads/tsan_full_rwlock.cpp:148:5 Thread T23 (tid=388553, running) created by main thread at: #0 pthread_create ... #1 main ./libc/test/integration/src/__support/threads/tsan_full_rwlock.cpp:166:5 Thread T4 (tid=388533, running) created by main thread at: #0 pthread_create ... #1 main ./libc/test/integration/src/__support/threads/tsan_full_rwlock.cpp:166:5 SUMMARY: ThreadSanitizer: data race ./libc/src/__support/threads/raw_rwlock.h:443:44 in __llvm_libc_23_0_0_git::RawRwLock::notify_pending_threads() ================== ``` AI wrote the detection script. Manually fixed.
lhames
left a comment
There was a problem hiding this comment.
More review coming, but I have to crash out for the night.
|
|
||
| // Create GOT entries and PLT stubs in G for calls to external | ||
| // symbols. Returns Error::success() unconditionally. | ||
| Error buildTables_COFF_x86_64(LinkGraph &G) { |
There was a problem hiding this comment.
You may want to use llvm#203906 rather than this.
If we decide to keep both (a case could be made for that) then both should be declared in COFF_x86_64.h, and the comments should clearly call out the default, the alternative, and the difference in their behaviours.
There was a problem hiding this comment.
Adopted llvm#203906. It replaces the ORC layer DLLImportDefinitionGenerator filter I had in LLJIT.cpp. The IAT synthesis pass now runs at the JITLink level, which is cleaner.
Both passes are needed though. They handle different codegen patterns:
synthesizeIATEntries_COFF_x86_64handles__imp_Xreferences (indirect calls/loads through named IAT slots), which appear when code uses__declspec(dllimport).buildTables_COFF_x86_64handles directcallq fooreferences (REL32 relocations to undefined externals), which is what mingw emits by default for libc/runtime functions without explicit dllimport annotations. These need PLT stubs when the target is beyond ±2GB.
Mingw objects typically have both patterns in the same module. Direct calls to things like puts or __gxx_personality_seh0, and __imp_ references for explicitly imported symbols. coff-plt-stubs.ll tests the direct-call case, coff-dllimport-filter.ll tests the __imp_ case.
Moved both declarations to COFF_x86_64.h with comments distinguishing their roles.
| break; | ||
| case Triple::x86_64: | ||
| UseJITLink = !TT.isOSBinFormatCOFF(); | ||
| UseJITLink = true; |
There was a problem hiding this comment.
I like this, but it will impact other clients: we should expect to have to revert and reapply a few times before this sticks, and we should definitely call it out in the release notes next release.
| class COFFImageBaseResolution_x86_64 { | ||
| public: | ||
| // Resolves __ImageBase to the lowest allocated section address in G | ||
| Error operator()(LinkGraph &G) { | ||
| GetImageBaseSymbol GetImageBase; | ||
|
|
||
| auto ImageBase = GetImageBase(G); | ||
| if (ImageBase) { | ||
| orc::ExecutorAddr Base(~uint64_t(0)); | ||
| for (auto &Sec : G.sections()) { | ||
| if (Sec.empty()) | ||
| continue; | ||
| SectionRange SR(Sec); | ||
| Base = std::min(Base, SR.getStart()); | ||
| } | ||
| assert(ImageBase && "__ImageBase symbol must be defined"); | ||
| ImageBase->getAddressable().setAddress(Base); | ||
| } | ||
| return Error::success(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
I've been looking at how Windows handles __ImageBase and I think I've convinced myself that we should handle this via the memory manager (and maybe a custom materialisation unit), rather than a pass.
E.g. the memory manager could define (and resolve) __ImageBase in each object as it passes through the allocate method. @jaredwy -- what do you think?
There was a problem hiding this comment.
My thought was a postallocation pass. That way we can ensure that its in the right place and maybe +-2gb of relevant sections. But I guess same idea, just different approach, yours does give us a bit more control over the way things layout I suppose?
There was a problem hiding this comment.
Eventually we should use the memory manager: objects in the same JITDylib should agree on __ImageBase, and the memory manager is the system that's in a position to decide what address it is.
That said this should be easy to change later: I think it's fine if we land with a PostAllocationPass.
There was a problem hiding this comment.
Updated to use PostAllocationPass
|
HI @lhames, thanks for taking the time to comment on the code. I haven't had a chance to go through your feedback in detail yet, but I'm hoping to do so soon. In the meantime, I've pushed two follow up commits that recently came out of running jank's full compilation pipeline against the SEH patches:
Note: the first commit in the series (296a6d1) applies MSYS2 distro patches (host triple detection, zstd, pthread) needed to build and test under MSYS2/CLANG64. These are not part of the SEH changes and won't be included in any upstream submission. |
Adds a default COFF/x86_64 JITLink pass that synthesizes `__imp_` Import Address Table (IAT) entries for dllimport references. This allows COFF objects using dllimport to be JIT-linked without a hand-built import library or a special generator. On COFF, `__declspec(dllimport)` codegen emits indirect accesses through a named `__imp_X` symbol (`callq *__imp_bar(%rip)`; `movq __imp_g(%rip)` for data), with `__imp_X` left undefined. JITLink had no handling for this. The new pass — the COFF counterpart of the ELF/Mach-O GOT builder — defines each undefined external `__imp_X` over an 8-byte slot holding the address of `X`, and leaves `X` as an ordinary external to be resolved normally (import library, dynamic-library search generator, etc.). Both the call and data-access forms then resolve indirectly through the slot. Rather than the `GOTTableManager` pattern (anonymous entry + edge redirection), the pass defines the *named* `__imp_X` symbol over the slot. ELF GOT references are nameless edge kinds, so that builder must create an anonymous entry and redirect edges; COFF references `__imp_X` by name, so defining it is simpler — no edge rewriting, no orphaned-external cleanup, sharing is automatic, and the call/data-access forms are handled identically. x86_64 only (runs in the COFF/x86_64 backend's default pass pipeline). New lit test `COFF_dllimport_iat.s`: assembles an object referencing `__imp_bar` (call) and `__imp_foo` (data load), supplies `foo`/`bar` via `-abs`, links with `-noexec`, and uses `jitlink-check` to verify each `__imp_` slot holds the target's address and that the references resolve through the slot. Partly implements github issue: llvm#190122 In the comment section of the github issue there is this comment llvm#190122 (comment) This PR implements point 2 Synthesis IAT entries.
|
Hi @lhames, I've addressed all the review comments and replied inline. Could you take another look when you get a chance? New commits addressing the feedback: 14. Migrate COFF edge kinds to generic 15. Adopt Thanks (cc @jeaye) |
lhames
left a comment
There was a problem hiding this comment.
Partial review comments. I've run out of time for more tonight.
From what I've read so far this looks good, but it would be great to break it up before it lands. Can you split out PRs for some of the parts that I flagged? (You'll need other reviewers on the Clang and CMake changes, but I can review the switch to generic JITLInk edge kinds).
| class COFFImageBaseResolution_x86_64 { | ||
| public: | ||
| // Resolves __ImageBase to the lowest allocated section address in G | ||
| Error operator()(LinkGraph &G) { | ||
| GetImageBaseSymbol GetImageBase; | ||
|
|
||
| auto ImageBase = GetImageBase(G); | ||
| if (ImageBase) { | ||
| orc::ExecutorAddr Base(~uint64_t(0)); | ||
| for (auto &Sec : G.sections()) { | ||
| if (Sec.empty()) | ||
| continue; | ||
| SectionRange SR(Sec); | ||
| Base = std::min(Base, SR.getStart()); | ||
| } | ||
| assert(ImageBase && "__ImageBase symbol must be defined"); | ||
| ImageBase->getAddressable().setAddress(Base); | ||
| } | ||
| return Error::success(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Eventually we should use the memory manager: objects in the same JITDylib should agree on __ImageBase, and the memory manager is the system that's in a position to decide what address it is.
That said this should be easy to change later: I think it's fine if we land with a PostAllocationPass.
| ; Test that __ImageBase resolves to the correct base address so that | ||
| ; image-relative (ADDR32NB) relocations in .pdata/.xdata produce valid | ||
| ; 32-bit offsets. Without the fix, __ImageBase is zero and the offsets | ||
| ; overflow, causing a link failure. | ||
| ; | ||
| ; The test compiles a non-leaf function (one that calls another) with the | ||
| ; uwtable attribute. Non-leaf functions require unwind info, so the compiler | ||
| ; emits .pdata/.xdata with ADDR32NB relocations that reference __ImageBase. | ||
| ; | ||
| ; We use llvm-jitlink -noexec to test linking in isolation — verifying that | ||
| ; __ImageBase resolves correctly and all ADDR32NB edges fit in 32 bits. | ||
| ; | ||
| ; REQUIRES: system-windows && host-unwind-supports-jit | ||
| ; RUN: llc -mtriple=x86_64-w64-windows-gnu -filetype=obj -o %t.obj %s | ||
| ; RUN: llvm-jitlink -noexec -entry entry %t.obj | ||
|
|
||
| define i32 @helper() #0 { | ||
| ret i32 42 | ||
| } | ||
|
|
||
| define i32 @entry() #0 { | ||
| %val = call i32 @helper() | ||
| ret i32 0 | ||
| } | ||
|
|
||
| attributes #0 = { nounwind uwtable } |
There was a problem hiding this comment.
This should probably be a yaml2obj test so that we test ADDR32NB directly, rather than indirectly through IR compilation.
There was a problem hiding this comment.
Replaced with an assembly test (COFF_imagebase_addr32nb.s) that hand-crafts .pdata with @IMGREL relocations and uses jitlink-check to verify the fixup values directly. Same approach as COFF_dllimport_iat.s (disclaimer: written in collaboration with AI)
| /// Create stubs in G for external references: PLT stubs for calls (PCRel32) | ||
| /// and ADDR32NB stubs for image-relative pointers (Pointer32NB). Always succeeds. | ||
| Error buildTables_COFF_x86_64(LinkGraph &G); | ||
|
|
||
| /// Synthesize IAT entries for undefined __imp_X externals in G. Always succeeds. | ||
| Error synthesizeIATEntries_COFF_x86_64(LinkGraph &G); | ||
|
|
There was a problem hiding this comment.
Looks good, but we should try to share the logic here with COFFAutoImportGenerator.cpp if possible (CC @mkovacevic99, who wrote that class).
There was a problem hiding this comment.
What did you have in mind to merge here? The only possibility I see here is to create a shared function that will create and IAT pointer slot and call it instead of CreatePointer(*G, Sec, &Target, 0)/createAnonymousPointer(G, IATSec, Target).
This pattern is also called inside DLLImportDefinitionGenerator (I have added a FIXME comment for COFFAutoImportGenerator)
|
|
||
| /// A 32-bit section-relative relocation. | ||
| /// | ||
| /// The value written is the target's offset from the start of its | ||
| /// containing section. | ||
| /// | ||
| /// Fixup expression: | ||
| /// Fixup <- Target + Addend : uint32 | ||
| /// | ||
| SecRel32, | ||
|
|
||
| /// A 16-bit section index relocation. | ||
| /// | ||
| /// The value written is the 1-based index of the section containing the | ||
| /// target symbol. | ||
| /// | ||
| /// Fixup expression: | ||
| /// Fixup <- Target + Addend : uint16 | ||
| /// | ||
| SectionIdx16, | ||
|
|
There was a problem hiding this comment.
What are these used for? Will sections need to have a consistent index across object files?
I note that SectionIdx16 gets mapped below back to Pointer16. If that's happening on all paths, is there any point to having a separate SectionIdx16 value?
There was a problem hiding this comment.
Both support debugging info. SecRel32 additionally supports static TLS (PE Format spec).
You're right about SectionIdx16, the index is baked into an absolute symbol at build time, so the lowering is a no-op. Removed it and emitting Pointer16 directly now.
| enum EdgeKind_coff_x86_64 : Edge::Kind { | ||
| PCRel32 = x86_64::FirstPlatformRelocation, | ||
| Pointer32NB, | ||
| Pointer64, | ||
| SectionIdx16, | ||
| SecRel32, | ||
| }; |
There was a problem hiding this comment.
It's fantastic to see this updated to use the generic edge kinds. Are you able to break that out into its own PR? That could land independently of the rest of this.
| return nullptr; | ||
| }; | ||
|
|
||
| Section &IATSec = G.createSection("$__IAT", orc::MemProt::Read); |
There was a problem hiding this comment.
We should name the IAT section in JITLink/COFF.h, then use the named constant in this and COFFAutoImportGenerator.cpp.
There was a problem hiding this comment.
Added COFFIATSectionName in COFF.h. COFFAutoImportGenerator doesn't exist in this tree yet but the constant is ready for it.
| Error buildTables_COFF_x86_64(LinkGraph &G) { | ||
| LLVM_DEBUG(dbgs() << "Visiting edges in graph:\n"); | ||
|
|
||
| x86_64::GOTTableManager GOT(G); |
There was a problem hiding this comment.
The GOT and IAT on Windows are functionally the same, right? We should make sure that they end up being the same section with shared entries.
There was a problem hiding this comment.
They are functionally the same (both 8 byte pointer slots). I've tested sharing a section and it works. However I'm not sure what shared entries would look like in practice: GOTTableManager creates anonymous entries while synthesizeIATEntries defines named __imp_X symbols over its slots. @mkovacevic99 @lhames any guidance on what the unified approach should look like?
There was a problem hiding this comment.
Sorry for the late reply from my side.
I think that it's possible for GOT and IAT to share one GOTTableManager, and that would mean IAT synthesis would need to do something like this:
Symbol &Entry = GOT.getEntryForTarget(G, *Target);
G.makeDefined(*Imp, Entry.getBlock(), 0, G.getPointerSize(),
Linkage::Strong, Scope::Local, /*IsLive=*/true);
but that would mean that the slot will end up with two symbols (which could be solved by letting entry creation take a name directly) and it also means that the two passes can't stay fully independent, which is not something we want I guess. Maybe Lang has a better idea
|
Hi @lhames, Pushed further commits addressing the review feedback: 16. Revert MSYS2 distro patches ( 17. Move 18. Add release note for JITLink default on COFF x86_64 ( 19. Replace IR imagebase test with assembly jitlink-check test ( 20. Remove 21. Define I've replied to the inline comments. Will split into standalone PRs once the review is complete, if you don't mind. |
This PR promotes the
WindowsEasyEHPluginto a library level plugin and fixes the prerequisite JITLink COFF bugs needed to make C++ exceptions work end to end through LLJIT on msys2/mingw.The fixes were developed iteratively using jank as the test driver. Each iteration: run jank's test suite, hit a crash or link error, write a minimal lit test that reproduces the failure, diagnose the root cause, implement the fix, confirm the lit test passes, then move on to the next failure.
Tests were written with AI assistance and verified with the fail before/pass after principle. Mentioning for transparency, they could benefit from an expert eye. Feedback welcome. I leave it to you whether you'd like to keep them.
The implementation code includes comments at the API level and inside functions where the reasoning isn't obvious from the code alone.
Changes:
Enable JITLink for COFF x86_64 (
2019be527937) - Previously LLJIT explicitly excluded COFF (UseJITLink = !TT.isOSBinFormatCOFF()), falling back to RTDyld. Test:coff-imagebase-resolution.ll(basic JITLink linking).Resolve
__ImageBasebefore ADDR32NB lowering (8fcef754b66c) -__ImageBasewas left at address 0, so.pdata/.xdataimage-relative offsets were computed as absolute addresses overflowing 32 bits and crashing the unwinder. Test:coff-imagebase-resolution.ll.Add GOT/PLT stubs for external calls (
5bb976340d29) - External DLL calls use 32bit PC relative branches that can't reach targets beyond 2GB. Without stubs, calls to DLL functions (e.g.puts) crash with out of range relocations. Test:coff-plt-stubs.ll.Add ADDR32NB stubs for image relative external references (
f9f8a22050de) -.xdatareferences the personality function via a 32bit image relative offset, but externals in DLLs are too far away. An executable stub near JIT'd code bridges the gap. Test:coff-addr32nb-stubs.ll.Fix COMDAT section-definition symbol registration (
59c2bca9d561) - The first symbol in a COMDAT pair wasn't registered in the graph symbol table, so.pdatarelocations referencing it by index failed with "Could not find symbol at given index." Test:coff-comdat-relocation.ll.Set OverrideObjectFlags for JITLink on COFF (
f226c46abf37) - COFF has no hidden visibility, all externals becomeExported. Without the override, hidden visibility IR symbols (e.g.__lljit_run_atexits) trigger a "Resolving symbol with incorrect flags" assertion. Test:trivial-return-zero.ll(existing test now passes).Add SEHFrameRegistrationPlugin (
8327fd89a8a3) — PromotesWindowsEasyEHPlugininto a library level plugin, wired intosetUpGenericLLVMIRPlatformso all LLJIT users get SEH registration automatically. Test:coff-seh-registration.ll.Add filtered DLLImportDefinitionGenerator (
905aef3d2517) - Only forwards__imp_prefixed symbols. Without the filter, the generator intercepts personality function lookups and creates conflicting null stubs, crashing the unwinder. Test:coff-dllimport-filter.ll.Extend SEHFrameKeepAlivePass for COMDAT
.pdata$*(4198c1650b54) - The pass only matched the exact name.pdata, so COMDAT sections like.pdata$comdat_fnwere dead-stripped and their unwind info lost. Test:coff-comdat-pdata-keepalive.ll.Remove dead
findDefinedSymbolByNamefrom GetImageBaseSymbol (1b574b035c62) -__ImageBaseis always external or absolute, never defined. The scan over all defined symbols was unnecessary dead code. Test:coff-imagebase-lookup-perf.ll.Add end-to-end integration tests (
13c2f438225e) - C++ throw/catch through multiple JIT'd frames (with and without frame pointers), exercising the full SEH pipeline. Tests:throw-catch-mingw-seh.ll,throw-catch-mingw-seh-no-fp.ll.Happy to answer any follow up questions regarding any of the fixes.
Thanks