Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LLD][COFF] Make unresolved symbol search behavior compliant with MSVC link.exe #85290

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

aganea
Copy link
Member

@aganea aganea commented Mar 14, 2024

Before this PR, unresolved symbols were always resolved depending on the order of inputs on the command line. The first input (.LIB or .OBJ) to provide a symbol would win. Subsequent inputs to provide the same symbol would either error out, or in case of archives (LIBs) would be ignored.

The MSVC documentation however suggests a subtle difference for resolving unresolved symbols, once an archive OBJ is brought in by another symbol. As per: https://learn.microsoft.com/en-us/cpp/build/reference/link-input-files?view=msvc-170

Object files on the command line are processed in the order they appear on the command line. Libraries are searched in command line order as well, with the following caveat: Symbols that are unresolved when bringing in an object file from a library are searched for in that library first, and then the following libraries from the command line and /DEFAULTLIB (Specify default library) directives, and then to any libraries at the beginning of the command line.

(Thanks to Grant Richins from the MSVC linker team who pointed me to that paragraph)

This PR aligns the LLD behavior with MSVC. The approach I've taken is to keep (in the symbol table) all the duplicated lazy symbols exposed by input archives. More specifically, the first symbol for a given symbol name is pushed in the symbol table as usual, and any subsequent (lazy) symbols with the same name will be linked to that first symbol in a (intrusive) linked list. When an archive OBJ is pulled in through another symbol, any unresolved symbols for that OBJ are resolved by searching through the linked list, starting at the OBJ's specific archive position. For example:

> lld-link /nodefaultlib main.obj a.lib b.lib c.lib /entry:main

In this case, main.obj pulls on a symbol inside b.lib, which pulls on b1.obj in that archive. b1.obj then requires another unresolved symbol f, and starts searching at b.lib, then c.lib, then a.lib. Previously, we always pulled the symbol from the first archive exposing it, a.lib in this case.

In practical terms, the unresolved symbol searching remains O(1) time, unless N archives are exposing the same symbol name, which makes the worst case O(N) time for that given symbol.

This fixes a long-standing bug we were seeing from both Rust and C++, see: #82050

Additional note: I also had to fix the behavior of command-line archives introduced in 7dc5e7a, to be symmetric with regular LIB files. Therefore OBJs enclosed in -start-lib/-end-lib are now lazily read, to postpone resolving of their symbols to after the entire command-line was processed.

@llvmbot
Copy link
Collaborator

llvmbot commented Mar 14, 2024

@llvm/pr-subscribers-llvm-support
@llvm/pr-subscribers-lld

@llvm/pr-subscribers-platform-windows

Author: Alexandre Ganea (aganea)

Changes

Before this PR, unresolved symbols were always resolved depending on the order of inputs on the command line. The first input to provide a symbol would win. Subsequent inputs to provide the same symbol would either error out, or in case of archives (LIBs) would be ignored.

The MSVC documentation however suggests a subtle difference for resolving unresolved symbols, once an archive OBJ is brought in by another symbol. As per: https://learn.microsoft.com/en-us/cpp/build/reference/link-input-files?view=msvc-170

> Object files on the command line are processed in the order they appear on the command line. Libraries are searched in command line order as well, with the following caveat: Symbols that are unresolved when bringing in an object file from a library are searched for in that library first, and then the following libraries from the command line and /DEFAULTLIB (Specify default library) directives, and then to any libraries at the beginning of the command line.

This PR aligns the LLD behavior with MSVC. The approach I've taken is to keep (in the symbol table) all the duplicated lazy symbols exposed by input archives. More specifically, the first symbol for a given symbol name is pushed in the symbol table as usual, and any subsequent (lazy) symbols with the same name will be linked to that first symbol in a (intrusive) linked list. When an archive OBJ is pulled in through another symbol, any unresolved symbols for that OBJ are resolved by searching through the linked list, starting at the OBJ's specific archive position. For example:

> lld-link /nodefaultlib main.obj a.lib b.lib c.lib /entry:main

In this case, main.obj pulls on a symbol inside b.lib, which pulls on b1.obj which is in that archive. b1.obj then requires the yet another unresolved symbol f, and starts searching at b.lib, then c.lib, then a.lib. Previously, we always pulled the symbol from the first archive exposing it, a.lib in this case.

In practical terms, the unresolved symbol searching remains O(1) time, unless N archives are exposing the same symbol name, which makes the worst case O(N) time for that given symbol.

This fixes a long-standing bug we were seeing from both Rust and C++, see: #82050


Patch is 26.71 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/85290.diff

9 Files Affected:

  • (modified) lld/COFF/Driver.cpp (+40-13)
  • (modified) lld/COFF/Driver.h (+15-6)
  • (modified) lld/COFF/InputFiles.cpp (+8-5)
  • (modified) lld/COFF/InputFiles.h (+19-7)
  • (modified) lld/COFF/SymbolTable.cpp (+101-9)
  • (modified) lld/COFF/Symbols.h (+11)
  • (modified) lld/test/COFF/duplicate-imp-func.s (+4-2)
  • (added) lld/test/COFF/lib-searching-behavior.s (+67)
  • (modified) llvm/include/llvm/Support/Allocator.h (+27-1)
diff --git a/lld/COFF/Driver.cpp b/lld/COFF/Driver.cpp
index 22ee2f133be98a..38e0392a876307 100644
--- a/lld/COFF/Driver.cpp
+++ b/lld/COFF/Driver.cpp
@@ -187,7 +187,8 @@ MemoryBufferRef LinkerDriver::takeBuffer(std::unique_ptr<MemoryBuffer> mb) {
 }
 
 void LinkerDriver::addBuffer(std::unique_ptr<MemoryBuffer> mb,
-                             bool wholeArchive, bool lazy) {
+                             bool wholeArchive, bool lazy,
+                             ArchiveFile *parent) {
   StringRef filename = mb->getBufferIdentifier();
 
   MemoryBufferRef mbref = takeBuffer(std::move(mb));
@@ -213,11 +214,11 @@ void LinkerDriver::addBuffer(std::unique_ptr<MemoryBuffer> mb,
     ctx.symtab.addFile(make<ArchiveFile>(ctx, mbref));
     break;
   case file_magic::bitcode:
-    ctx.symtab.addFile(make<BitcodeFile>(ctx, mbref, "", 0, lazy));
+    ctx.symtab.addFile(make<BitcodeFile>(ctx, mbref, "", 0, lazy, parent));
     break;
   case file_magic::coff_object:
   case file_magic::coff_import_library:
-    ctx.symtab.addFile(make<ObjFile>(ctx, mbref, lazy));
+    ctx.symtab.addFile(make<ObjFile>(ctx, mbref, lazy, parent));
     break;
   case file_magic::pdb:
     ctx.symtab.addFile(make<PDBInputFile>(ctx, mbref));
@@ -242,7 +243,9 @@ void LinkerDriver::addBuffer(std::unique_ptr<MemoryBuffer> mb,
   }
 }
 
-void LinkerDriver::enqueuePath(StringRef path, bool wholeArchive, bool lazy) {
+void LinkerDriver::enqueuePath(
+    StringRef path, bool wholeArchive, bool lazy,
+    std::optional<std::shared_future<ArchiveFile *>> parent) {
   auto future = std::make_shared<std::future<MBErrPair>>(
       createFutureForFile(std::string(path)));
   std::string pathStr = std::string(path);
@@ -281,13 +284,15 @@ void LinkerDriver::enqueuePath(StringRef path, bool wholeArchive, bool lazy) {
       else
         error(msg + "; did you mean '" + nearest + "'");
     } else
-      ctx.driver.addBuffer(std::move(mb), wholeArchive, lazy);
+      ctx.driver.addBuffer(std::move(mb), wholeArchive, lazy,
+                           parent ? parent->get() : nullptr);
   });
 }
 
 void LinkerDriver::addArchiveBuffer(MemoryBufferRef mb, StringRef symName,
                                     StringRef parentName,
-                                    uint64_t offsetInArchive) {
+                                    uint64_t offsetInArchive,
+                                    ArchiveFile *parent) {
   file_magic magic = identify_magic(mb.getBuffer());
   if (magic == file_magic::coff_import_library) {
     InputFile *imp = make<ImportFile>(ctx, mb);
@@ -298,10 +303,10 @@ void LinkerDriver::addArchiveBuffer(MemoryBufferRef mb, StringRef symName,
 
   InputFile *obj;
   if (magic == file_magic::coff_object) {
-    obj = make<ObjFile>(ctx, mb);
+    obj = make<ObjFile>(ctx, mb, /*lazy=*/false, parent);
   } else if (magic == file_magic::bitcode) {
-    obj =
-        make<BitcodeFile>(ctx, mb, parentName, offsetInArchive, /*lazy=*/false);
+    obj = make<BitcodeFile>(ctx, mb, parentName, offsetInArchive,
+                            /*lazy=*/false, parent);
   } else if (magic == file_magic::coff_cl_gl_object) {
     error(mb.getBufferIdentifier() +
           ": is not a native COFF file. Recompile without /GL?");
@@ -318,7 +323,8 @@ void LinkerDriver::addArchiveBuffer(MemoryBufferRef mb, StringRef symName,
 
 void LinkerDriver::enqueueArchiveMember(const Archive::Child &c,
                                         const Archive::Symbol &sym,
-                                        StringRef parentName) {
+                                        StringRef parentName,
+                                        ArchiveFile *parent) {
 
   auto reportBufferError = [=](Error &&e, StringRef childName) {
     fatal("could not get the buffer for the member defining symbol " +
@@ -335,7 +341,7 @@ void LinkerDriver::enqueueArchiveMember(const Archive::Child &c,
     enqueueTask([=]() {
       llvm::TimeTraceScope timeScope("Archive: ", mb.getBufferIdentifier());
       ctx.driver.addArchiveBuffer(mb, toCOFFString(ctx, sym), parentName,
-                                  offsetInArchive);
+                                  offsetInArchive, parent);
     });
     return;
   }
@@ -356,7 +362,15 @@ void LinkerDriver::enqueueArchiveMember(const Archive::Child &c,
     // used as the buffer identifier.
     ctx.driver.addArchiveBuffer(takeBuffer(std::move(mbOrErr.first)),
                                 toCOFFString(ctx, sym), "",
-                                /*OffsetInArchive=*/0);
+                                /*OffsetInArchive=*/0, parent);
+  });
+}
+
+void LinkerDriver::enqueueLazyFile(InputFile *file) {
+  enqueueTask([=]() {
+    // Once it has been enqued, it cannot be lazy anymore.
+    file->lazy = false;
+    ctx.symtab.addFile(file);
   });
 }
 
@@ -2111,17 +2125,30 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
   {
     llvm::TimeTraceScope timeScope2("Parse & queue inputs");
     bool inLib = false;
+    std::optional<std::shared_future<ArchiveFile *>> inLibArchive;
     for (auto *arg : args) {
       switch (arg->getOption().getID()) {
       case OPT_end_lib:
         if (!inLib)
           error("stray " + arg->getSpelling());
         inLib = false;
+        inLibArchive = std::nullopt;
         break;
       case OPT_start_lib:
         if (inLib)
           error("nested " + arg->getSpelling());
         inLib = true;
+        // In is important to create a fake archive here so that we remember its
+        // placement on the command-line. This will be later needed to resolve
+        // symbols in the archive order required by the MSVC specification.
+        {
+          auto a = std::make_shared<std::promise<ArchiveFile *>>();
+          inLibArchive = a->get_future().share();
+          enqueueTask([=] {
+            a->set_value(
+                make<ArchiveFile>(ctx, MemoryBufferRef({}, "<cmdline-lib>")));
+          });
+        }
         break;
       case OPT_wholearchive_file:
         if (std::optional<StringRef> path = findFileIfNew(arg->getValue()))
@@ -2129,7 +2156,7 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
         break;
       case OPT_INPUT:
         if (std::optional<StringRef> path = findFileIfNew(arg->getValue()))
-          enqueuePath(*path, isWholeArchive(*path), inLib);
+          enqueuePath(*path, isWholeArchive(*path), inLib, inLibArchive);
         break;
       default:
         // Ignore other options.
diff --git a/lld/COFF/Driver.h b/lld/COFF/Driver.h
index fa54de05befb58..da3c41e1bca734 100644
--- a/lld/COFF/Driver.h
+++ b/lld/COFF/Driver.h
@@ -22,6 +22,7 @@
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/TarWriter.h"
 #include "llvm/WindowsDriver/MSVCPaths.h"
+#include <future>
 #include <memory>
 #include <optional>
 #include <set>
@@ -91,13 +92,20 @@ class LinkerDriver {
 
   // Used by ArchiveFile to enqueue members.
   void enqueueArchiveMember(const Archive::Child &c, const Archive::Symbol &sym,
-                            StringRef parentName);
+                            StringRef parentName,
+                            ArchiveFile *parent = nullptr);
 
-  void enqueuePDB(StringRef Path) { enqueuePath(Path, false, false); }
+  void enqueuePDB(StringRef Path) {
+    enqueuePath(Path, false, false, /*parent=*/std::nullopt);
+  }
 
   MemoryBufferRef takeBuffer(std::unique_ptr<MemoryBuffer> mb);
 
-  void enqueuePath(StringRef path, bool wholeArchive, bool lazy);
+  void enqueuePath(
+      StringRef path, bool wholeArchive, bool lazy,
+      std::optional<std::shared_future<ArchiveFile *>> parent = std::nullopt);
+
+  void enqueueLazyFile(InputFile *file);
 
   std::unique_ptr<llvm::TarWriter> tar; // for /linkrepro
 
@@ -182,10 +190,11 @@ class LinkerDriver {
   StringRef findDefaultEntry();
   WindowsSubsystem inferSubsystem();
 
-  void addBuffer(std::unique_ptr<MemoryBuffer> mb, bool wholeArchive,
-                 bool lazy);
+  void addBuffer(std::unique_ptr<MemoryBuffer> mb, bool wholeArchive, bool lazy,
+                 ArchiveFile *parent = nullptr);
   void addArchiveBuffer(MemoryBufferRef mbref, StringRef symName,
-                        StringRef parentName, uint64_t offsetInArchive);
+                        StringRef parentName, uint64_t offsetInArchive,
+                        ArchiveFile *parent = nullptr);
 
   void enqueueTask(std::function<void()> task);
   bool run();
diff --git a/lld/COFF/InputFiles.cpp b/lld/COFF/InputFiles.cpp
index 037fae45242c6f..42cdd1cf3b6c2c 100644
--- a/lld/COFF/InputFiles.cpp
+++ b/lld/COFF/InputFiles.cpp
@@ -94,9 +94,12 @@ static bool ignoredSymbolName(StringRef name) {
 }
 
 ArchiveFile::ArchiveFile(COFFLinkerContext &ctx, MemoryBufferRef m)
-    : InputFile(ctx, ArchiveKind, m) {}
+    : InputFile(ctx, ArchiveKind, m, /*lazy=*/true) {
+  static unsigned Order = 0;
+  CmdLineIndex = Order++;
+}
 
-void ArchiveFile::parse() {
+void ArchiveFile::parseLazy() {
   // Parse a MemoryBufferRef as an archive file.
   file = CHECK(Archive::create(mb), this);
 
@@ -115,7 +118,7 @@ void ArchiveFile::addMember(const Archive::Symbol &sym) {
   if (!seen.insert(c.getChildOffset()).second)
     return;
 
-  ctx.driver.enqueueArchiveMember(c, sym, getName());
+  ctx.driver.enqueueArchiveMember(c, sym, getName(), this);
 }
 
 std::vector<MemoryBufferRef> lld::coff::getArchiveMembers(Archive *file) {
@@ -1000,8 +1003,8 @@ void ImportFile::parse() {
 
 BitcodeFile::BitcodeFile(COFFLinkerContext &ctx, MemoryBufferRef mb,
                          StringRef archiveName, uint64_t offsetInArchive,
-                         bool lazy)
-    : InputFile(ctx, BitcodeKind, mb, lazy) {
+                         bool lazy, ArchiveFile *parent)
+    : InputFile(ctx, BitcodeKind, mb, lazy), parent(parent) {
   std::string path = mb.getBufferIdentifier().str();
   if (ctx.config.thinLTOIndexOnly)
     path = replaceThinLTOSuffix(mb.getBufferIdentifier(),
diff --git a/lld/COFF/InputFiles.h b/lld/COFF/InputFiles.h
index 3b55cd791bfda2..7070f51fdf78ac 100644
--- a/lld/COFF/InputFiles.h
+++ b/lld/COFF/InputFiles.h
@@ -66,7 +66,6 @@ class InputFile {
   enum Kind {
     ArchiveKind,
     ObjectKind,
-    LazyObjectKind,
     PDBKind,
     ImportKind,
     BitcodeKind,
@@ -105,7 +104,7 @@ class InputFile {
 
 public:
   // True if this is a lazy ObjFile or BitcodeFile.
-  bool lazy = false;
+  bool lazy;
 };
 
 // .lib or .a file.
@@ -113,23 +112,30 @@ class ArchiveFile : public InputFile {
 public:
   explicit ArchiveFile(COFFLinkerContext &ctx, MemoryBufferRef m);
   static bool classof(const InputFile *f) { return f->kind() == ArchiveKind; }
-  void parse() override;
+  void parse() override{};
+  void parseLazy();
 
   // Enqueues an archive member load for the given symbol. If we've already
   // enqueued a load for the same archive member, this function does nothing,
   // which ensures that we don't load the same member more than once.
   void addMember(const Archive::Symbol &sym);
 
-private:
   std::unique_ptr<Archive> file;
+
+  // The order this archive was seen on the cmd-line. This is later needed for
+  // resolving undefined symbols in archive OBJs.
+  uint32_t CmdLineIndex;
+
+private:
   llvm::DenseSet<uint64_t> seen;
 };
 
 // .obj or .o file. This may be a member of an archive file.
 class ObjFile : public InputFile {
 public:
-  explicit ObjFile(COFFLinkerContext &ctx, MemoryBufferRef m, bool lazy = false)
-      : InputFile(ctx, ObjectKind, m, lazy) {}
+  explicit ObjFile(COFFLinkerContext &ctx, MemoryBufferRef m, bool lazy = false,
+                   ArchiveFile *parent = nullptr)
+      : InputFile(ctx, ObjectKind, m, lazy), parent(parent) {}
   static bool classof(const InputFile *f) { return f->kind() == ObjectKind; }
   void parse() override;
   void parseLazy();
@@ -182,6 +188,9 @@ class ObjFile : public InputFile {
   // True if this file was compiled with /guard:ehcont.
   bool hasGuardEHCont() { return feat00Flags & 0x4000; }
 
+  // Whether this Obj buffer is part of an archive.
+  ArchiveFile *parent;
+
   // Pointer to the PDB module descriptor builder. Various debug info records
   // will reference object files by "module index", which is here. Things like
   // source files and section contributions are also recorded here. Will be null
@@ -369,7 +378,7 @@ class BitcodeFile : public InputFile {
 public:
   explicit BitcodeFile(COFFLinkerContext &ctx, MemoryBufferRef mb,
                        StringRef archiveName, uint64_t offsetInArchive,
-                       bool lazy);
+                       bool lazy = false, ArchiveFile *parent = nullptr);
   ~BitcodeFile();
   static bool classof(const InputFile *f) { return f->kind() == BitcodeKind; }
   ArrayRef<Symbol *> getSymbols() { return symbols; }
@@ -377,6 +386,9 @@ class BitcodeFile : public InputFile {
   void parseLazy();
   std::unique_ptr<llvm::lto::InputFile> obj;
 
+  // Whether this bitcode buffer is part of an archive.
+  ArchiveFile *parent;
+
 private:
   void parse() override;
 
diff --git a/lld/COFF/SymbolTable.cpp b/lld/COFF/SymbolTable.cpp
index 44aa506d2c35da..f570e8c211f43d 100644
--- a/lld/COFF/SymbolTable.cpp
+++ b/lld/COFF/SymbolTable.cpp
@@ -54,8 +54,10 @@ void SymbolTable::addFile(InputFile *file) {
   if (file->lazy) {
     if (auto *f = dyn_cast<BitcodeFile>(file))
       f->parseLazy();
-    else
-      cast<ObjFile>(file)->parseLazy();
+    else if (auto *o = dyn_cast<ObjFile>(file))
+      o->parseLazy();
+    else if (auto *a = dyn_cast<ArchiveFile>(file))
+      a->parseLazy();
   } else {
     file->parse();
     if (auto *f = dyn_cast<ObjFile>(file)) {
@@ -102,7 +104,7 @@ static void forceLazy(Symbol *s) {
   }
   case Symbol::Kind::LazyObjectKind: {
     InputFile *file = cast<LazyObject>(s)->file;
-    file->ctx.symtab.addFile(file);
+    file->ctx.driver.enqueueLazyFile(file);
     break;
   }
   case Symbol::Kind::LazyDLLSymbolKind: {
@@ -562,6 +564,57 @@ std::pair<Symbol *, bool> SymbolTable::insert(StringRef name, InputFile *file) {
   return result;
 }
 
+static LazyIntrusiveNode *lazyNode(Symbol *s) {
+  if (auto *sym = dyn_cast<LazyArchive>(s))
+    return &sym->node;
+  if (auto *sym = dyn_cast<LazyObject>(s))
+    return &sym->node;
+  return nullptr;
+}
+
+static ArchiveFile *lazyParent(InputFile *f) {
+  if (!f)
+    return nullptr;
+  if (auto *obj = dyn_cast<ObjFile>(f))
+    return obj->parent;
+  if (auto *obj = dyn_cast<BitcodeFile>(f))
+    return obj->parent;
+  return nullptr;
+}
+
+static ArchiveFile *lazyArchive(Symbol *s) {
+  if (auto *sym = dyn_cast<LazyArchive>(s))
+    return sym->file;
+  if (auto *sym = dyn_cast<LazyObject>(s))
+    return lazyParent(sym->file);
+  return nullptr;
+}
+
+// The search behavior for undefined symbols is different when the OBJ
+// was pulled from an archive (LIB). This is documented here:
+// https://learn.microsoft.com/en-us/cpp/build/reference/link-input-files?view=msvc-170
+// "Object files on the command line are processed in the order they
+// appear on the command line. Libraries are searched in command line
+// order as well, with the following caveat: Symbols that are unresolved
+// when bringing in an object file from a library are searched for in
+// that library first, and then the following libraries from the command
+// line and /DEFAULTLIB (Specify default library) directives, and then
+// to any libraries at the beginning of the command line."
+static Symbol *searchArchiveSymbol(Symbol *s, ArchiveFile *pivot) {
+  auto &Alloc = getSpecificAllocSingleton<SymbolUnion>().Allocator;
+  Symbol *curr = s;
+  for (;;) {
+    if (lazyArchive(curr)->CmdLineIndex >= pivot->CmdLineIndex)
+      return curr;
+    uint32_t next = lazyNode(curr)->next;
+    if (!next)
+      break;
+    curr = reinterpret_cast<LazyArchive *>(
+        Alloc.fromAlignedIndex<SymbolUnion>(next));
+  }
+  return s;
+}
+
 Symbol *SymbolTable::addUndefined(StringRef name, InputFile *f,
                                   bool isWeakAlias) {
   auto [s, wasInserted] = insert(name, f);
@@ -569,11 +622,43 @@ Symbol *SymbolTable::addUndefined(StringRef name, InputFile *f,
     replaceSymbol<Undefined>(s, name);
     return s;
   }
-  if (s->isLazy())
+  if (s->isLazy()) {
+    if (ArchiveFile *parent = lazyParent(f)) {
+      Symbol *selected = searchArchiveSymbol(s, parent);
+      forceLazy(selected);
+      // Now that we have selected a symbol, we don't need the linked list of
+      // `LazyArchive`s anymore. Collapse to the selected symbol.
+      memcpy(s, selected, sizeof(SymbolUnion));
+      return s;
+    }
     forceLazy(s);
+  }
   return s;
 }
 
+// This creates a linked list of archives where a specific symbol was seen.
+// We later walk that list if a undefined symbol needs to be resolved from an
+// archive OBJ.
+template <typename T, typename... ArgT>
+static void chainLazy(LazyIntrusiveNode *front, ArgT &&...arg) {
+  // Chain with symbols defined in other archives
+  Symbol *newSym = reinterpret_cast<Symbol *>(make<SymbolUnion>());
+  newSym->canInline = true;
+  replaceSymbol<T>(newSym, std::forward<ArgT>(arg)...);
+
+  auto &Alloc = getSpecificAllocSingleton<SymbolUnion>().Allocator;
+  uint32_t index = Alloc.identifyKnownAlignedObject<SymbolUnion>(newSym);
+
+  if (!front->next)
+    front->next = index;
+  if (front->last) {
+    Symbol *last = reinterpret_cast<Symbol *>(
+        Alloc.fromAlignedIndex<SymbolUnion>(front->last));
+    lazyNode(last)->next = index;
+  }
+  front->last = index;
+}
+
 void SymbolTable::addLazyArchive(ArchiveFile *f, const Archive::Symbol &sym) {
   StringRef name = sym.getName();
   auto [s, wasInserted] = insert(name);
@@ -581,6 +666,10 @@ void SymbolTable::addLazyArchive(ArchiveFile *f, const Archive::Symbol &sym) {
     replaceSymbol<LazyArchive>(s, f, sym);
     return;
   }
+  if (auto *n = lazyNode(s)) {
+    chainLazy<LazyArchive>(n, f, sym);
+    return;
+  }
   auto *u = dyn_cast<Undefined>(s);
   if (!u || u->weakAlias || s->pendingArchiveLoad)
     return;
@@ -588,19 +677,22 @@ void SymbolTable::addLazyArchive(ArchiveFile *f, const Archive::Symbol &sym) {
   f->addMember(sym);
 }
 
-void SymbolTable::addLazyObject(InputFile *f, StringRef n) {
+void SymbolTable::addLazyObject(InputFile *f, StringRef name) {
   assert(f->lazy);
-  auto [s, wasInserted] = insert(n, f);
+  auto [s, wasInserted] = insert(name, f);
   if (wasInserted) {
-    replaceSymbol<LazyObject>(s, f, n);
+    replaceSymbol<LazyObject>(s, f, name);
+    return;
+  }
+  if (auto *n = lazyNode(s)) {
+    chainLazy<LazyObject>(n, f, name);
     return;
   }
   auto *u = dyn_cast<Undefined>(s);
   if (!u || u->weakAlias || s->pendingArchiveLoad)
     return;
   s->pendingArchiveLoad = true;
-  f->lazy = false;
-  addFile(f);
+  f->ctx.driver.enqueueLazyFile(f);
 }
 
 void SymbolTable::addLazyDLLSymbol(DLLFile *f, DLLFile::Symbol *sym,
diff --git a/lld/COFF/Symbols.h b/lld/COFF/Symbols.h
index ca69fb2d052706..1577406c8626cf 100644
--- a/lld/COFF/Symbols.h
+++ b/lld/COFF/Symbols.h
@@ -286,6 +286,15 @@ class DefinedSynthetic : public Defined {
   uint32_t offset;
 };
 
+// Keep track of symbols with the same name exposed by archives. This is
+// required to later resolve unresolved symbols in the same order as required
+// by the MSVC spec. These are indexes in the specific bump allocator for
+// SymbolUnion.
+struct LazyIntrusiveNode {
+  uint32_t next = 0;
+  uint32_t last = 0;
+};
+
 // This class represents a symbol defined in an archive file. It is
 // created from an archive file header, and it knows how to load an
 // object file from an archive to replace itself with a defined
@@ -302,6 +311,7 @@ class LazyArchive : public Symbol {
 
   ArchiveFile *file;
   const Archive::Symbol sym;
+  LazyIntrusiveNode node;
 };
 
 class LazyObject : public Symbol {
@@ -309,6 +319,7 @@ class LazyObject : public Symbol {
   LazyObject(InputFile *f, StringRef n) : Symbol(LazyObjectKind, n), file(f) {}
   static bool classof(const Symbol *s) { return s->kind() == LazyObjectKind; }
   InputFile *file;
+  LazyIntrusiveNode node;
 };
 
 // MinGW only.
diff --git a/lld/test/COFF/duplicate-imp-func.s b/lld/test/COFF/duplicate-imp-func.s
index fc0cf1ef6ae051..631c714c951f77 100644
--- a/lld/test/COFF/duplicate-imp-func.s
+++ b/lld/test/COFF/duplicate-imp-func.s
@@ -28,8 +28,10 @@
 # Once the import library member from %t.lib.dll.a gets loaded, libfunc
 # and __imp_libfunc already are defined.
 
-# Just check that this fails...
[truncated]

// placement on the command-line. This will be later needed to resolve
// symbols in the archive order required by the MSVC specification.
{
auto a = std::make_shared<std::promise<ArchiveFile *>>();
Copy link
Member Author

@aganea aganea Mar 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review note: this is made a shared_ptr<promise> simply because lambdas cannot capture non-CopyConstructible object instances, because of https://timsong-cpp.github.io/cppwp/n4659/func.wrap.func.con#7

@aganea aganea changed the title [LLD][COFF] Make unresolved symbol search behavior compliant with MSVC link.exe WIP: [LLD][COFF] Make unresolved symbol search behavior compliant with MSVC link.exe Mar 15, 2024
lld/test/COFF/duplicate-imp-func.s Outdated Show resolved Hide resolved
lld/test/COFF/lib-searching-behavior.s Outdated Show resolved Hide resolved
lld/COFF/Driver.cpp Outdated Show resolved Hide resolved
lld/COFF/Driver.h Outdated Show resolved Hide resolved
lld/COFF/SymbolTable.cpp Outdated Show resolved Hide resolved
lld/COFF/SymbolTable.cpp Outdated Show resolved Hide resolved
lld/COFF/SymbolTable.cpp Outdated Show resolved Hide resolved
lld/COFF/SymbolTable.cpp Outdated Show resolved Hide resolved
lld/COFF/SymbolTable.cpp Outdated Show resolved Hide resolved
lld/COFF/SymbolTable.cpp Outdated Show resolved Hide resolved
@aganea aganea changed the title WIP: [LLD][COFF] Make unresolved symbol search behavior compliant with MSVC link.exe [LLD][COFF] Make unresolved symbol search behavior compliant with MSVC link.exe Apr 8, 2024
Copy link

github-actions bot commented Apr 8, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@aganea
Copy link
Member Author

aganea commented Apr 8, 2024

This is ready for review. This has been tested with a two-stage LLVM toolchain, then on a Unreal Engine-based project.

I've also added support for handling /DEFAULTLIB (explicitly on the command line, or implicitly pulled from a OBJ file) in the same way as described by the MSVC document.

@aganea
Copy link
Member Author

aganea commented Jun 7, 2024

Gentle ping! Is anyone able to review this please? I can jump on a call if a more interactive review is desired.

@mstorsjo
Copy link
Member

mstorsjo commented Jun 7, 2024

This is something I'd like @rnk to have a look at.

@aganea
Copy link
Member Author

aganea commented Jul 1, 2024

Ping.

@mstorsjo
Copy link
Member

@rnk - do you have time to have a look at this one?

@honkstar1
Copy link

Ping again, we also really want this fix in order to unblock our progress to support ld-lld for linking. Thanks

@YanzheL
Copy link

YanzheL commented Sep 21, 2024

Ping again, we also need this to support using lld-link for building Unreal Engine 5 with ClangCL toolchain

@honkstar1
Copy link

fyi, I binary patched the checked in xinput import lib in the unreal engine depot and removed the export of winmain.. this is a workaround until we get this fix submitted.

@yuzhy8701
Copy link

Another Ping @rnk.

@aganea I think this PR would also fix #109707? If so, could you add a testcase for start-lib/end-lib behavior?

Copy link
Contributor

@cjacek cjacek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I encountered those ordering differences during some recent experiments with MSVC in the context of ARM64EC. It’s interesting that they make a real difference. I have a few questions/suggestions.

Comment on lines +511 to +515

// If we are running off the low-priority task list, execute and drain the
// high priority task list before going any further. This is to ensure symbols
// provided by /DEFAULTLIB archives are linked properly in the symbol table.
executeFirstQueue(firstTaskQueue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m wondering if we could simplify this by having run() handle it directly without the need for additional assistance. For example, the attached patch also passes the tests.

ArchiveFile::ArchiveFile(COFFLinkerContext &ctx, MemoryBufferRef m)
: InputFile(ctx, ArchiveKind, m) {}
: InputFile(ctx, ArchiveKind, m, /*lazy=*/true) {
static unsigned Order = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be a member of COFFLinkerContext.

Comment on lines +293 to +296
struct LazyIntrusiveNode {
uint32_t next = 0;
uint32_t last = 0;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume it uses indices instead of pointers to avoid enlarging SymbolUnion. If that’s the case, perhaps we could avoid storing last. To maintain O(1) insertion, we could always insert the element just after the front element. The order would then be almost reversed: (first), (last), (second to last), (third to last), and so on. Lookup would need to compensate by returning either the front or the last matching element. With the saved space, we could store next as a pointer and eliminate the need for fromAlignedIndex.

Copy link
Collaborator

@rnk rnk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't finished reviewing this, but I wanted to say I read the problem statement and the reproducer and I think I understand the failure mode from the Rust issue where some unrelated exported function from an import library was preferred over the symbol defined local to a static library.

I think my main consideration is, how will this behavior interact with any future attempt to parallelize symbol resolution? I'm still dreaming of this, but maybe everyone else has moved on. The critical requirement is that we can define some kind of global symbol priority comparator, and the need to "prefer" library local symbols seems like it would complicate that.

Anway, I'll have to revisit this tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants