diff --git a/lld/test/wasm/command-exports-no-tors.s b/lld/test/wasm/command-exports-no-tors.s new file mode 100644 --- /dev/null +++ b/lld/test/wasm/command-exports-no-tors.s @@ -0,0 +1,62 @@ +# RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s +# RUN: wasm-ld --no-entry %t.o -o %t.wasm +# RUN: obj2yaml %t.wasm | FileCheck %s + +# Like command-exports.s, but with no ctors or dtors, so there should be no +# __wasm_call_ctors, __cxa_atexit, or wrappers. + + .section .text.foo_i32,"",@ + .globl foo_i32 + .type foo_i32,@function +foo_i32: + .functype foo_i32 (i32, i32) -> (i32) + local.get 0 + local.get 1 + i32.add + end_function +.Lfunc_end0: + .size foo_i32, .Lfunc_end0-foo_i32 + + .section .text.foo_f64,"",@ + .globl foo_f64 + .type foo_f64,@function +foo_f64: + .functype foo_f64 (f64, f64) -> (f64) + local.get 0 + local.get 1 + f64.add + end_function +.Lfunc_end1: + .size foo_f64, .Lfunc_end1-foo_f64 + + .export_name foo_i32, foo_i32 + .export_name foo_f64, foo_f64 + +# CHECK: - Type: EXPORT +# CHECK-NEXT: Exports: +# CHECK-NEXT: - Name: memory +# CHECK-NEXT: Kind: MEMORY +# CHECK-NEXT: Index: 0 +# CHECK-NEXT: - Name: foo_i32 +# CHECK-NEXT: Kind: FUNCTION +# CHECK-NEXT: Index: 0 +# CHECK-NEXT: - Name: foo_f64 +# CHECK-NEXT: Kind: FUNCTION +# CHECK-NEXT: Index: 1 + +# CHECK: - Type: CODE + +# CHECK: - Index: 0 +# CHECK-NEXT: Locals: [] +# CHECK-NEXT: Body: 200020016A0B +# CHECK-NEXT: - Index: 1 +# CHECK-NEXT: Locals: [] +# CHECK-NEXT: Body: 20002001A00B + +# CHECK: - Type: CUSTOM +# CHECK-NEXT: Name: name +# CHECK-NEXT: FunctionNames: +# CHECK-NEXT: - Index: 0 +# CHECK-NEXT: Name: foo_i32 +# CHECK-NEXT: - Index: 1 +# CHECK-NEXT: Name: foo_f64 diff --git a/lld/test/wasm/command-exports.s b/lld/test/wasm/command-exports.s new file mode 100644 --- /dev/null +++ b/lld/test/wasm/command-exports.s @@ -0,0 +1,141 @@ +# RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s +# RUN: wasm-ld --no-entry %t.o -o %t.wasm +# RUN: obj2yaml %t.wasm | FileCheck %s + +# This test defines a command with two exported functions, as well as a static +# constructor and a static destructor. Check that the exports, constructor, and +# destructor are all set up properly. + + .section .text.foo_i32,"",@ + .globl foo_i32 + .type foo_i32,@function +foo_i32: + .functype foo_i32 (i32, i32) -> (i32) + local.get 0 + local.get 1 + i32.add + end_function +.Lfunc_end0: + .size foo_i32, .Lfunc_end0-foo_i32 + + .section .text.foo_f64,"",@ + .globl foo_f64 + .type foo_f64,@function +foo_f64: + .functype foo_f64 (f64, f64) -> (f64) + local.get 0 + local.get 1 + f64.add + end_function +.Lfunc_end1: + .size foo_f64, .Lfunc_end1-foo_f64 + + .section .text.some_ctor,"",@ + .globl some_ctor + .type some_ctor,@function +some_ctor: + .functype some_ctor () -> () + end_function +.Lfunc_end2: + .size some_ctor, .Lfunc_end2-some_ctor + + .section .text.some_dtor,"",@ + .globl some_dtor + .type some_dtor,@function +some_dtor: + .functype some_dtor () -> () + end_function +.Lfunc_end3: + .size some_dtor, .Lfunc_end3-some_dtor + + .section .text.__cxa_atexit,"",@ + .hidden __cxa_atexit + .globl __cxa_atexit + .type __cxa_atexit,@function +__cxa_atexit: + .functype __cxa_atexit (i32, i32, i32) -> (i32) + i32.const 0 + end_function +.Lfunc_end4: + .size __cxa_atexit, .Lfunc_end4-__cxa_atexit + + .section .text..Lcall_dtors.1,"",@ + .type .Lcall_dtors.1,@function +.Lcall_dtors.1: + .functype .Lcall_dtors.1 (i32) -> () + call some_dtor + end_function +.Lfunc_end5: + .size .Lcall_dtors.1, .Lfunc_end5-.Lcall_dtors.1 + + .section .text..Lregister_call_dtors.1,"",@ + .type .Lregister_call_dtors.1,@function +.Lregister_call_dtors.1: + .functype .Lregister_call_dtors.1 () -> () + block + i32.const .Lcall_dtors.1 + i32.const 0 + i32.const __dso_handle + call __cxa_atexit + i32.eqz + br_if 0 + unreachable +.LBB6_2: + end_block + end_function +.Lfunc_end6: + .size .Lregister_call_dtors.1, .Lfunc_end6-.Lregister_call_dtors.1 + + .hidden __dso_handle + .section .init_array.1,"",@ + .p2align 2 + .int32 some_ctor + .int32 .Lregister_call_dtors.1 + .weak __dso_handle + .export_name foo_i32, foo_i32 + .export_name foo_f64, foo_f64 + +# CHECK: - Type: EXPORT +# CHECK-NEXT: Exports: +# CHECK-NEXT: - Name: memory +# CHECK-NEXT: Kind: MEMORY +# CHECK-NEXT: Index: 0 +# CHECK-NEXT: - Name: foo_i32 +# CHECK-NEXT: Kind: FUNCTION +# CHECK-NEXT: Index: 8 +# CHECK-NEXT: - Name: foo_f64 +# CHECK-NEXT: Kind: FUNCTION +# CHECK-NEXT: Index: 9 + +# CHECK: - Type: CODE + +# CHECK: - Index: 8 +# CHECK-NEXT: Locals: [] +# CHECK-NEXT: Body: 10002000200110010B +# CHECK-NEXT: - Index: 9 +# CHECK-NEXT: Locals: [] +# CHECK-NEXT: Body: 10002000200110020B + +# CHECK: - Type: CUSTOM +# CHECK-NEXT: Name: name +# CHECK-NEXT: FunctionNames: +# CHECK-NEXT: - Index: 0 +# CHECK-NEXT: Name: __wasm_call_ctors +# CHECK-NEXT: - Index: 1 +# CHECK-NEXT: Name: foo_i32 +# CHECK-NEXT: - Index: 2 +# CHECK-NEXT: Name: foo_f64 +# CHECK-NEXT: - Index: 3 +# CHECK-NEXT: Name: some_ctor +# CHECK-NEXT: - Index: 4 +# CHECK-NEXT: Name: some_dtor +# CHECK-NEXT: - Index: 5 +# CHECK-NEXT: Name: __cxa_atexit +# CHECK-NEXT: - Index: 6 +# CHECK-NEXT: Name: .Lcall_dtors.1 +# CHECK-NEXT: - Index: 7 +# CHECK-NEXT: Name: .Lregister_call_dtors.1 +# CHECK-NEXT: - Index: 8 +# CHECK-NEXT: Name: foo_i32.command_export +# CHECK-NEXT: - Index: 9 +# CHECK-NEXT: Name: foo_f64.command_export diff --git a/lld/test/wasm/init-fini-gc.ll b/lld/test/wasm/init-fini-gc.ll deleted file mode 100644 --- a/lld/test/wasm/init-fini-gc.ll +++ /dev/null @@ -1,48 +0,0 @@ -; RUN: llc -filetype=obj -o %t.o %s -; RUN: wasm-ld %t.o -o %t.wasm -; RUN: obj2yaml %t.wasm | FileCheck %s - -; RUN: wasm-ld %t.o -o %t.wasm -; RUN: obj2yaml %t.wasm | FileCheck %s - -; RUN: wasm-ld --export=__wasm_call_ctors %t.o -o %t.export.wasm -; RUN: obj2yaml %t.export.wasm | FileCheck %s -check-prefix=EXPORT - -; Test that the __wasm_call_ctor function if not referenced - -target triple = "wasm32-unknown-unknown" - -define hidden void @_start() { -entry: - ret void -} - -define hidden void @func1() { -entry: - ret void -} - -define hidden void @func2() { -entry: - ret void -} - -define i32 @__cxa_atexit(i32 %func, i32 %arg, i32 %dso_handle) { - ret i32 0 -} - -@llvm.global_ctors = appending global [1 x { i32, void ()*, i8* }] [ - { i32, void ()*, i8* } { i32 1, void ()* @func1, i8* null } -] - -@llvm.global_dtors = appending global [1 x { i32, void ()*, i8* }] [ - { i32, void ()*, i8* } { i32 1, void ()* @func2, i8* null } -] - -; CHECK-NOT: __cxa_atexit -; CHECK-NOT: __wasm_call_ctors - -; EXPORT: __wasm_call_ctors -; EXPORT: func1 -; EXPORT: func2 -; EXPORT: __cxa_atexit diff --git a/lld/test/wasm/init-fini-no-gc.ll b/lld/test/wasm/init-fini-no-gc.ll new file mode 100644 --- /dev/null +++ b/lld/test/wasm/init-fini-no-gc.ll @@ -0,0 +1,85 @@ +; RUN: llc -filetype=obj -o %t.o %s +; RUN: wasm-ld %t.o -o %t.wasm +; RUN: obj2yaml %t.wasm | FileCheck %s + +; RUN: wasm-ld --export=__wasm_call_ctors %t.o -o %t.export.wasm +; RUN: obj2yaml %t.export.wasm | FileCheck %s -check-prefix=EXPORT + +; Test that we emit wrappers and call __wasm_call_ctor when not referenced. + +target triple = "wasm32-unknown-unknown" + +define hidden void @_start() { +entry: + ret void +} + +define hidden void @func1() { +entry: + ret void +} + +define hidden void @func2() { +entry: + ret void +} + +define hidden i32 @__cxa_atexit(i32 %func, i32 %arg, i32 %dso_handle) { + ret i32 0 +} + +@llvm.global_ctors = appending global [1 x { i32, void ()*, i8* }] [ + { i32, void ()*, i8* } { i32 1, void ()* @func1, i8* null } +] + +@llvm.global_dtors = appending global [1 x { i32, void ()*, i8* }] [ + { i32, void ()*, i8* } { i32 1, void ()* @func2, i8* null } +] + +; Check that we have exactly the needed exports: `memory` because that's +; currently on by default, and `_start`, because that's the default entrypoint. + +; CHECK: - Type: EXPORT +; CHECK-NEXT: Exports: +; CHECK-NEXT: - Name: memory +; CHECK-NEXT: Kind: MEMORY +; CHECK-NEXT: Index: 0 +; CHECK-NEXT: - Name: _start +; CHECK-NEXT: Kind: FUNCTION +; CHECK-NEXT: Index: 7 + +; Check the body of `_start`'s command-export wrapper. + +; CHECK: - Type: CODE + +; CHECK: - Index: 7 +; CHECK-NEXT: Locals: [] +; CHECK-NEXT: Body: 100010010B + +; Check the symbol table to ensure all the functions are here, and that +; index 7 above refers to the function we think it does. + +; CHECK: - Type: CUSTOM +; CHECK-NEXT: Name: name +; CHECK-NEXT: FunctionNames: +; CHECK-NEXT: - Index: 0 +; CHECK-NEXT: Name: __wasm_call_ctors +; CHECK-NEXT: - Index: 1 +; CHECK-NEXT: Name: _start +; CHECK-NEXT: - Index: 2 +; CHECK-NEXT: Name: func1 +; CHECK-NEXT: - Index: 3 +; CHECK-NEXT: Name: func2 +; CHECK-NEXT: - Index: 4 +; CHECK-NEXT: Name: __cxa_atexit +; CHECK-NEXT: - Index: 5 +; CHECK-NEXT: Name: .Lcall_dtors.1 +; CHECK-NEXT: - Index: 6 +; CHECK-NEXT: Name: .Lregister_call_dtors.1 +; CHECK-NEXT: - Index: 7 +; CHECK-NEXT: Name: _start.command_export + +; EXPORT: __wasm_call_ctors +; EXPORT: func1 +; EXPORT: func2 +; EXPORT: __cxa_atexit diff --git a/lld/wasm/Driver.cpp b/lld/wasm/Driver.cpp --- a/lld/wasm/Driver.cpp +++ b/lld/wasm/Driver.cpp @@ -567,7 +567,6 @@ make(nullSignature, "__wasm_apply_relocs")); } - if (config->isPic) { WasmSym::stackPointer = createUndefinedGlobal("__stack_pointer", config->is64.getValueOr(false) @@ -834,6 +833,29 @@ config->entry); } + // If the user code defines a `__wasm_call_dtors` function, remember it so + // that we can call it from the command export wrappers. Unlike + // `__wasm_call_ctors` which we synthesize, `__wasm_call_dtors` is defined + // by libc/etc., because destructors are registered dynamically with + // `__cxa_atexit` and friends. + if (!config->relocatable && !config->shared && + !WasmSym::callCtors->isUsedInRegularObj && + WasmSym::callCtors->getName() != config->entry && + !config->exportedSymbols.count(WasmSym::callCtors->getName())) { + if (Symbol *callDtors = handleUndefined("__wasm_call_dtors")) { + if (auto *callDtorsFunc = dyn_cast(callDtors)) { + if (callDtorsFunc->signature && + (!callDtorsFunc->signature->Params.empty() || + !callDtorsFunc->signature->Returns.empty())) { + error("__wasm_call_dtors must have no argument or return values"); + } + WasmSym::callDtors = callDtorsFunc; + } else { + error("__wasm_call_dtors must be a function"); + } + } + } + createOptionalSymbols(); if (errorCount()) diff --git a/lld/wasm/InputChunks.h b/lld/wasm/InputChunks.h --- a/lld/wasm/InputChunks.h +++ b/lld/wasm/InputChunks.h @@ -120,7 +120,10 @@ class InputFunction : public InputChunk { public: InputFunction(const WasmSignature &s, const WasmFunction *func, ObjFile *f) - : InputChunk(f, InputChunk::Function), signature(s), function(func) {} + : InputChunk(f, InputChunk::Function), signature(s), function(func), + exportName(func && func->ExportName.hasValue() + ? (*func->ExportName).str() + : llvm::Optional()) {} static bool classof(const InputChunk *c) { return c->kind() == InputChunk::Function || @@ -131,8 +134,10 @@ StringRef getName() const override { return function->SymbolName; } StringRef getDebugName() const override { return function->DebugName; } llvm::Optional getExportName() const { - return function ? function->ExportName : llvm::Optional(); + return exportName.hasValue() ? llvm::Optional(*exportName) + : llvm::Optional(); } + void setExportName(std::string exportName) { this->exportName = exportName; } uint32_t getComdat() const override { return function->Comdat; } uint32_t getFunctionInputOffset() const { return getInputSectionOffset(); } uint32_t getFunctionCodeOffset() const { return function->CodeOffset; } @@ -170,6 +175,7 @@ } const WasmFunction *function; + llvm::Optional exportName; llvm::Optional functionIndex; llvm::Optional tableIndex; uint32_t compressedFuncSize = 0; diff --git a/lld/wasm/MarkLive.cpp b/lld/wasm/MarkLive.cpp --- a/lld/wasm/MarkLive.cpp +++ b/lld/wasm/MarkLive.cpp @@ -44,6 +44,7 @@ void enqueue(Symbol *sym); void markSymbol(Symbol *sym); void mark(); + bool isCallCtorsLive(); // A list of chunks to visit. SmallVector queue; @@ -58,22 +59,6 @@ sym->markLive(); if (InputChunk *chunk = sym->getChunk()) queue.push_back(chunk); - - // The ctor functions are all referenced by the synthetic callCtors - // function. However, this function does not contain relocations so we - // have to manually mark the ctors as live if callCtors itself is live. - if (sym == WasmSym::callCtors) { - if (config->isPic) - enqueue(WasmSym::applyRelocs); - for (const ObjFile *obj : symtab->objectFiles) { - const WasmLinkingData &l = obj->getWasmObj()->linkingData(); - for (const WasmInitFunc &f : l.InitFunctions) { - auto* initSym = obj->getFunctionSymbol(f.Symbol); - if (!initSym->isDiscarded()) - enqueue(initSym); - } - } - } } void MarkLive::run() { @@ -86,16 +71,29 @@ if (sym->isNoStrip() || sym->isExported()) enqueue(sym); - // For relocatable output, we need to preserve all the ctor functions - if (config->relocatable) { - for (const ObjFile *obj : symtab->objectFiles) { - const WasmLinkingData &l = obj->getWasmObj()->linkingData(); - for (const WasmInitFunc &f : l.InitFunctions) - enqueue(obj->getFunctionSymbol(f.Symbol)); + // If we'll be calling the user's `__wasm_call_dtors` function, mark it live. + if (Symbol *callDtors = WasmSym::callDtors) + enqueue(callDtors); + + // The ctor functions are all referenced by the synthetic callCtors + // function. However, this function does not contain relocations so we + // have to manually mark the ctors as live. + for (const ObjFile *obj : symtab->objectFiles) { + const WasmLinkingData &l = obj->getWasmObj()->linkingData(); + for (const WasmInitFunc &f : l.InitFunctions) { + auto *initSym = obj->getFunctionSymbol(f.Symbol); + if (!initSym->isDiscarded()) + enqueue(initSym); } } + // In Emscripten-style PIC, `__wasm_call_ctors` calls `__wasm_apply_relocs`. if (config->isPic) + enqueue(WasmSym::applyRelocs); + + // If we have any non-discarded init functions, mark `__wasm_call_ctors` as + // live so that we assign it an index and call it. + if (isCallCtorsLive()) enqueue(WasmSym::callCtors); if (config->sharedMemory && !config->shared) @@ -169,5 +167,27 @@ } } +bool MarkLive::isCallCtorsLive() { + // In a reloctable link, we don't call `__wasm_call_ctors`. + if (config->relocatable) + return false; + + // In Emscripten-style PIC, we call `__wasm_call_ctors` which calls + // `__wasm_apply_relocs`. + if (config->isPic) + return true; + + // If there are any init functions, mark `__wasm_call_ctors` live so that + // it can call them. + for (const ObjFile *file : symtab->objectFiles) { + const WasmLinkingData &l = file->getWasmObj()->linkingData(); + for (const WasmInitFunc &f : l.InitFunctions) + if (!file->getFunctionSymbol(f.Symbol)->isDiscarded()) + return true; + } + + return false; +} + } // namespace wasm } // namespace lld diff --git a/lld/wasm/Symbols.h b/lld/wasm/Symbols.h --- a/lld/wasm/Symbols.h +++ b/lld/wasm/Symbols.h @@ -470,6 +470,10 @@ // Function that directly calls all ctors in priority order. static DefinedFunction *callCtors; + // __wasm_call_dtors + // Function that calls the libc/etc. cleanup function. + static DefinedFunction *callDtors; + // __wasm_apply_relocs // Function that applies relocations to data segment post-instantiation. static DefinedFunction *applyRelocs; diff --git a/lld/wasm/Symbols.cpp b/lld/wasm/Symbols.cpp --- a/lld/wasm/Symbols.cpp +++ b/lld/wasm/Symbols.cpp @@ -66,6 +66,7 @@ namespace wasm { DefinedFunction *WasmSym::callCtors; +DefinedFunction *WasmSym::callDtors; DefinedFunction *WasmSym::initMemory; DefinedFunction *WasmSym::applyRelocs; DefinedFunction *WasmSym::initTLS; diff --git a/lld/wasm/Writer.cpp b/lld/wasm/Writer.cpp --- a/lld/wasm/Writer.cpp +++ b/lld/wasm/Writer.cpp @@ -61,6 +61,8 @@ void createApplyRelocationsFunction(); void createCallCtorsFunction(); void createInitTLSFunction(); + void createCommandExportWrappers(); + void createCommandExportWrapper(uint32_t functionIndex, DefinedFunction *f); void assignIndexes(); void populateSymtab(); @@ -94,6 +96,9 @@ std::vector initFunctions; llvm::StringMap> customSectionMapping; + // Stable storage for command export wrapper function name strings. + std::list commandExportWrapperNames; + // Elements that are used to construct the final output std::string header; std::vector outputSections; @@ -627,6 +632,53 @@ out.typeSec->registerType(e->signature); } +// In a command-style link, create a wrapper for each exported symbol +// which calls the constructors and destructors. +void Writer::createCommandExportWrappers() { + // This logic doesn't currently support Emscripten-style PIC mode. + assert(!config->isPic); + + // If there are no ctors and there's no libc `__wasm_call_dtors` to + // call, don't wrap the exports. + if (initFunctions.empty() && WasmSym::callDtors == NULL) + return; + + std::vector toWrap; + + for (Symbol *sym : symtab->getSymbols()) + if (sym->isExported()) + if (auto *f = dyn_cast(sym)) + toWrap.push_back(f); + + for (auto *f : toWrap) { + auto funcNameStr = (f->getName() + ".command_export").str(); + commandExportWrapperNames.push_back(funcNameStr); + const std::string &funcName = commandExportWrapperNames.back(); + + auto func = make(*f->getSignature(), funcName); + if (f->function->getExportName().hasValue()) + func->setExportName(f->function->getExportName()->str()); + else + func->setExportName(f->getName().str()); + + DefinedFunction *def = + symtab->addSyntheticFunction(funcName, f->flags, func); + def->markLive(); + + def->flags |= WASM_SYMBOL_EXPORTED; + def->flags &= ~WASM_SYMBOL_VISIBILITY_HIDDEN; + def->forceExport = f->forceExport; + + f->flags |= WASM_SYMBOL_VISIBILITY_HIDDEN; + f->flags &= ~WASM_SYMBOL_EXPORTED; + f->forceExport = false; + + out.functionSec->addFunction(func); + + createCommandExportWrapper(f->getFunctionIndex(), def); + } +} + static void scanRelocations() { for (ObjFile *file : symtab->objectFiles) { LLVM_DEBUG(dbgs() << "scanRelocations: " << file->getName() << "\n"); @@ -907,7 +959,10 @@ // Create synthetic "__wasm_call_ctors" function based on ctor functions // in input object. void Writer::createCallCtorsFunction() { - if (!WasmSym::callCtors->isLive()) + // If __wasm_call_ctors isn't referenced, there aren't any ctors, and we + // aren't calling `__wasm_apply_relocs` for Emscripten-style PIC, don't + // define the `__wasm_call_ctors` function. + if (!WasmSym::callCtors->isLive() && initFunctions.empty() && !config->isPic) return; // First write the body's contents to a string. @@ -936,6 +991,46 @@ createFunction(WasmSym::callCtors, bodyContent); } +// Create a wrapper around a function export which calls the +// static constructors and destructors. +void Writer::createCommandExportWrapper(uint32_t functionIndex, + DefinedFunction *f) { + // First write the body's contents to a string. + std::string bodyContent; + { + raw_string_ostream os(bodyContent); + writeUleb128(os, 0, "num locals"); + + // If we have any ctors, or we're calling `__wasm_apply_relocs` for + // Emscripten-style PIC, call `__wasm_call_ctors` which performs those + // calls. + if (!initFunctions.empty() || config->isPic) { + writeU8(os, WASM_OPCODE_CALL, "CALL"); + writeUleb128(os, WasmSym::callCtors->getFunctionIndex(), + "function index"); + } + + // Call the user's code, leaving any return values on the operand stack. + for (size_t i = 0; i < f->signature->Params.size(); ++i) { + writeU8(os, WASM_OPCODE_LOCAL_GET, "local.get"); + writeUleb128(os, i, "local index"); + } + writeU8(os, WASM_OPCODE_CALL, "CALL"); + writeUleb128(os, functionIndex, "function index"); + + // Call the function that calls the destructors. + if (DefinedFunction *callDtors = WasmSym::callDtors) { + writeU8(os, WASM_OPCODE_CALL, "CALL"); + writeUleb128(os, callDtors->getFunctionIndex(), "function index"); + } + + // End the function, returning the return values from the user's code. + writeU8(os, WASM_OPCODE_END, "END"); + } + + createFunction(f, bodyContent); +} + void Writer::createInitTLSFunction() { if (!WasmSym::initTLS->isLive()) return; @@ -1073,6 +1168,18 @@ if (config->isPic) createApplyRelocationsFunction(); createCallCtorsFunction(); + + // Create export wrappers for commands if needed. + // + // If the input contains a call to `__wasm_call_ctors`, either in one of + // the input objects or an explicit export from the command-line, we + // assume ctors and dtors are taken care of already. + if (!config->relocatable && !config->isPic && + !WasmSym::callCtors->isUsedInRegularObj && + !WasmSym::callCtors->isExported()) { + log("-- createCommandExportWrappers"); + createCommandExportWrappers(); + } } if (!config->relocatable && config->sharedMemory && !config->shared)