COFF: Support creating DLLs.
authorRui Ueyama <ruiu@google.com>
Wed, 17 Jun 2015 00:16:33 +0000 (00:16 +0000)
committerRui Ueyama <ruiu@google.com>
Wed, 17 Jun 2015 00:16:33 +0000 (00:16 +0000)
DLL files are in the same format as executables but they have export tables.
The format of the export table is described in PE/COFF spec section 5.3.

A new class, EdataContents, takes care of creating chunks for export tables.
What we need to do is to parse command line flags for dllexports, and then
instantiate the class to create chunks. For the writer, export table chunks
are opaque data -- it just add chunks to .edata section.

llvm-svn: 239869

lld/COFF/Config.h
lld/COFF/DLL.cpp
lld/COFF/DLL.h
lld/COFF/Driver.cpp
lld/COFF/Driver.h
lld/COFF/DriverUtils.cpp
lld/COFF/SymbolTable.cpp
lld/COFF/Writer.cpp
lld/COFF/Writer.h
lld/test/COFF/Inputs/export.yaml [new file with mode: 0644]
lld/test/COFF/export.test [new file with mode: 0644]

index 9fe5777..28aba45 100644 (file)
@@ -22,9 +22,21 @@ namespace coff {
 
 using llvm::COFF::WindowsSubsystem;
 using llvm::StringRef;
+class Defined;
 
-class Configuration {
-public:
+// Represents an /export option.
+struct Export {
+  StringRef Name;
+  StringRef ExtName;
+  Defined *Sym = nullptr;
+  uint16_t Ordinal = 0;
+  bool Noname = false;
+  bool Data = false;
+  bool Private = false;
+};
+
+// Global configuration.
+struct Configuration {
   llvm::COFF::MachineTypes MachineType = llvm::COFF::IMAGE_FILE_MACHINE_AMD64;
   bool Verbose = false;
   WindowsSubsystem Subsystem = llvm::COFF::IMAGE_SUBSYSTEM_UNKNOWN;
@@ -39,6 +51,10 @@ public:
   std::set<StringRef> NoDefaultLibs;
   bool NoDefaultLibAll = false;
 
+  // True if we are creating a DLL.
+  bool DLL = false;
+  std::vector<Export> Exports;
+
   // Used by /failifmismatch option.
   std::map<StringRef, StringRef> MustMatch;
 
index 024d190..288558b 100644 (file)
@@ -23,6 +23,7 @@
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Object/COFF.h"
 #include "llvm/Support/Endian.h"
+#include "llvm/Support/Path.h"
 
 using namespace llvm;
 using namespace llvm::object;
@@ -31,11 +32,12 @@ using namespace llvm::COFF;
 using llvm::RoundUpToAlignment;
 
 static const size_t LookupChunkSize = sizeof(uint64_t);
-static const size_t DirectoryChunkSize = sizeof(ImportDirectoryTableEntry);
 
 namespace lld {
 namespace coff {
 
+// Import table
+
 // A chunk for the import descriptor table.
 class HintNameChunk : public Chunk {
 public:
@@ -88,10 +90,10 @@ public:
 };
 
 // A chunk for the import descriptor table.
-class DirectoryChunk : public Chunk {
+class ImportDirectoryChunk : public Chunk {
 public:
-  explicit DirectoryChunk(Chunk *N) : DLLName(N) {}
-  size_t getSize() const override { return DirectoryChunkSize; }
+  explicit ImportDirectoryChunk(Chunk *N) : DLLName(N) {}
+  size_t getSize() const override { return sizeof(ImportDirectoryTableEntry); }
 
   void writeTo(uint8_t *Buf) override {
     auto *E = (coff_import_directory_table_entry *)(Buf + FileOff);
@@ -118,7 +120,7 @@ private:
 };
 
 uint64_t IdataContents::getDirSize() {
-  return Dirs.size() * DirectoryChunkSize;
+  return Dirs.size() * sizeof(ImportDirectoryTableEntry);
 }
 
 uint64_t IdataContents::getIATSize() {
@@ -192,13 +194,120 @@ void IdataContents::create() {
     // Create the import table header.
     if (!DLLNames.count(Name))
       DLLNames[Name] = make_unique<StringChunk>(Name);
-    auto Dir = make_unique<DirectoryChunk>(DLLNames[Name].get());
+    auto Dir = make_unique<ImportDirectoryChunk>(DLLNames[Name].get());
     Dir->LookupTab = Lookups[Base].get();
     Dir->AddressTab = Addresses[Base].get();
     Dirs.push_back(std::move(Dir));
   }
   // Add null terminator.
-  Dirs.push_back(make_unique<NullChunk>(DirectoryChunkSize));
+  Dirs.push_back(make_unique<NullChunk>(sizeof(ImportDirectoryTableEntry)));
+}
+
+// Export table
+// Read Microsoft PE/COFF spec 5.3 for details.
+
+// A chunk for the export descriptor table.
+class ExportDirectoryChunk : public Chunk {
+public:
+  ExportDirectoryChunk(int I, int J, Chunk *D, Chunk *A, Chunk *N, Chunk *O)
+      : MaxOrdinal(I), NameTabSize(J), DLLName(D), AddressTab(A), NameTab(N),
+        OrdinalTab(O) {}
+
+  size_t getSize() const override {
+    return sizeof(export_directory_table_entry);
+  }
+
+  void writeTo(uint8_t *Buf) override {
+    auto *E = (export_directory_table_entry *)(Buf + FileOff);
+    E->NameRVA = DLLName->getRVA();
+    E->OrdinalBase = 0;
+    E->AddressTableEntries = MaxOrdinal + 1;
+    E->NumberOfNamePointers = NameTabSize;
+    E->ExportAddressTableRVA = AddressTab->getRVA();
+    E->NamePointerRVA = NameTab->getRVA();
+    E->OrdinalTableRVA = OrdinalTab->getRVA();
+  }
+
+  uint16_t MaxOrdinal;
+  uint16_t NameTabSize;
+  Chunk *DLLName;
+  Chunk *AddressTab;
+  Chunk *NameTab;
+  Chunk *OrdinalTab;
+};
+
+class AddressTableChunk : public Chunk {
+public:
+  explicit AddressTableChunk(size_t MaxOrdinal) : Size(MaxOrdinal + 1) {}
+  size_t getSize() const override { return Size * 4; }
+
+  void writeTo(uint8_t *Buf) override {
+    for (Export &E : Config->Exports)
+      write32le(Buf + FileOff + E.Ordinal * 4, E.Sym->getRVA());
+  }
+
+private:
+  size_t Size;
+};
+
+class NamePointersChunk : public Chunk {
+public:
+  explicit NamePointersChunk(std::vector<Chunk *> &V) : Chunks(V) {}
+  size_t getSize() const override { return Chunks.size() * 4; }
+
+  void writeTo(uint8_t *Buf) override {
+    uint8_t *P = Buf + FileOff;
+    for (Chunk *C : Chunks) {
+      write32le(P, C->getRVA());
+      P += 4;
+    }
+  }
+
+private:
+  std::vector<Chunk *> Chunks;
+};
+
+class ExportOrdinalChunk : public Chunk {
+public:
+  explicit ExportOrdinalChunk(size_t I) : Size(I) {}
+  size_t getSize() const override { return Size * 2; }
+
+  void writeTo(uint8_t *Buf) override {
+    uint8_t *P = Buf + FileOff;
+    for (Export &E : Config->Exports) {
+      if (E.Noname)
+        continue;
+      write16le(P, E.Ordinal);
+      P += 2;
+    }
+  }
+
+private:
+  size_t Size;
+};
+
+EdataContents::EdataContents() {
+  uint16_t MaxOrdinal = 0;
+  for (Export &E : Config->Exports)
+    MaxOrdinal = std::max(MaxOrdinal, E.Ordinal);
+
+  auto *DLLName = new StringChunk(sys::path::filename(Config->OutputFile));
+  auto *AddressTab = new AddressTableChunk(MaxOrdinal);
+  std::vector<Chunk *> Names;
+  for (Export &E : Config->Exports)
+    if (!E.Noname)
+      Names.push_back(new StringChunk(E.ExtName));
+  auto *NameTab = new NamePointersChunk(Names);
+  auto *OrdinalTab = new ExportOrdinalChunk(Names.size());
+  auto *Dir = new ExportDirectoryChunk(MaxOrdinal, Names.size(), DLLName,
+                                       AddressTab, NameTab, OrdinalTab);
+  Chunks.push_back(std::unique_ptr<Chunk>(Dir));
+  Chunks.push_back(std::unique_ptr<Chunk>(DLLName));
+  Chunks.push_back(std::unique_ptr<Chunk>(AddressTab));
+  Chunks.push_back(std::unique_ptr<Chunk>(NameTab));
+  Chunks.push_back(std::unique_ptr<Chunk>(OrdinalTab));
+  for (Chunk *C : Names)
+    Chunks.push_back(std::unique_ptr<Chunk>(C));
 }
 
 } // namespace coff
index abaf4b4..94995da 100644 (file)
@@ -17,7 +17,7 @@ namespace lld {
 namespace coff {
 
 // Windows-specific.
-// IdataContents creates all chunks for the .idata section.
+// IdataContents creates all chunks for the DLL import table.
 // You are supposed to call add() to add symbols and then
 // call getChunks() to get a list of chunks.
 class IdataContents {
@@ -41,6 +41,14 @@ private:
   std::map<StringRef, std::unique_ptr<Chunk>> DLLNames;
 };
 
+// Windows-specific.
+// EdataContents creates all chunks for the DLL export table.
+class EdataContents {
+public:
+  EdataContents();
+  std::vector<std::unique_ptr<Chunk>> Chunks;
+};
+
 } // namespace coff
 } // namespace lld
 
index b739ca9..610effd 100644 (file)
@@ -91,6 +91,14 @@ LinkerDriver::parseDirectives(StringRef S,
     return EC;
   std::unique_ptr<llvm::opt::InputArgList> Args = std::move(ArgsOrErr.get());
 
+  // Handle /export
+  for (auto *Arg : Args->filtered(OPT_export)) {
+    ErrorOr<Export> E = parseExport(Arg->getValue());
+    if (auto EC = E.getError())
+      return EC;
+    Config->Exports.push_back(E.get());
+  }
+
   // Handle /failifmismatch
   if (auto EC = checkFailIfMismatch(Args.get()))
     return EC;
@@ -180,6 +188,17 @@ std::vector<StringRef> LinkerDriver::getSearchPaths() {
   return Ret;
 }
 
+static WindowsSubsystem inferSubsystem() {
+  if (Config->DLL)
+    return IMAGE_SUBSYSTEM_WINDOWS_GUI;
+  return StringSwitch<WindowsSubsystem>(Config->EntryName)
+      .Case("mainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_CUI)
+      .Case("wmainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_CUI)
+      .Case("WinMainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_GUI)
+      .Case("wWinMainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_GUI)
+      .Default(IMAGE_SUBSYSTEM_UNKNOWN);
+}
+
 bool LinkerDriver::link(int Argc, const char *Argv[]) {
   // Needed for LTO.
   llvm::InitializeAllTargetInfos();
@@ -221,6 +240,10 @@ bool LinkerDriver::link(int Argc, const char *Argv[]) {
   if (Args->hasArg(OPT_verbose))
     Config->Verbose = true;
 
+  // Handle /dll
+  if (Args->hasArg(OPT_dll))
+    Config->DLL = true;
+
   // Handle /entry
   if (auto *Arg = Args->getLastArg(OPT_entry))
     Config->EntryName = Arg->getValue();
@@ -318,6 +341,14 @@ bool LinkerDriver::link(int Argc, const char *Argv[]) {
     }
   }
 
+  // Handle /export
+  for (auto *Arg : Args->filtered(OPT_export)) {
+    ErrorOr<Export> E = parseExport(Arg->getValue());
+    if (E.getError())
+      return false;
+    Config->Exports.push_back(E.get());
+  }
+
   // Handle /failifmismatch
   if (auto EC = checkFailIfMismatch(Args.get())) {
     llvm::errs() << "/failifmismatch: " << EC.message() << "\n";
@@ -414,6 +445,15 @@ bool LinkerDriver::link(int Argc, const char *Argv[]) {
     }
   }
 
+  // Windows specific -- Make sure we resolve all dllexported symbols.
+  // (We don't cache the size here because Symtab.resolve() may add
+  // new entries to Config->Exports.)
+  for (size_t I = 0; I < Config->Exports.size(); ++I) {
+    StringRef Sym = Config->Exports[I].Name;
+    Symtab.addUndefined(Sym);
+    Config->GCRoots.insert(Sym);
+  }
+
   // Windows specific -- If entry point name is not given, we need to
   // infer that from user-defined entry name. The symbol table takes
   // care of details.
@@ -441,19 +481,21 @@ bool LinkerDriver::link(int Argc, const char *Argv[]) {
   // Windows specific -- if no /subsystem is given, we need to infer
   // that from entry point name.
   if (Config->Subsystem == IMAGE_SUBSYSTEM_UNKNOWN) {
-    Config->Subsystem =
-      StringSwitch<WindowsSubsystem>(Config->EntryName)
-          .Case("mainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_CUI)
-          .Case("wmainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_CUI)
-          .Case("WinMainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_GUI)
-          .Case("wWinMainCRTStartup", IMAGE_SUBSYSTEM_WINDOWS_GUI)
-          .Default(IMAGE_SUBSYSTEM_UNKNOWN);
+    Config->Subsystem = inferSubsystem();
     if (Config->Subsystem == IMAGE_SUBSYSTEM_UNKNOWN) {
       llvm::errs() << "subsystem must be defined\n";
       return false;
     }
   }
 
+  // Windows specific -- fix up dllexported symbols.
+  if (!Config->Exports.empty()) {
+    for (Export &E : Config->Exports)
+      E.Sym = Symtab.find(E.Name);
+    if (fixupExports())
+      return false;
+  }
+
   // Write the result.
   Writer Out(&Symtab);
   if (auto EC = Out.write(Config->OutputFile)) {
index 136ecd2..88357fb 100644 (file)
@@ -10,6 +10,7 @@
 #ifndef LLD_COFF_DRIVER_H
 #define LLD_COFF_DRIVER_H
 
+#include "Config.h"
 #include "lld/Core/LLVM.h"
 #include "llvm/ADT/Optional.h"
 #include "llvm/ADT/StringRef.h"
@@ -115,6 +116,10 @@ std::error_code parseVersion(StringRef Arg, uint32_t *Major, uint32_t *Minor);
 std::error_code parseSubsystem(StringRef Arg, WindowsSubsystem *Sys,
                                uint32_t *Major, uint32_t *Minor);
 
+// Used for dllexported symbols.
+ErrorOr<Export> parseExport(StringRef Arg);
+std::error_code fixupExports();
+
 // Parses a string in the form of "key=value" and check
 // if value matches previous values for the key.
 // This feature used in the directive section to reject
index 9f65b45..662e582 100644 (file)
@@ -114,6 +114,99 @@ std::error_code parseSubsystem(StringRef Arg, WindowsSubsystem *Sys,
   return std::error_code();
 }
 
+// Parse a string in the form of
+// "<name>[=<internalname>][,@ordinal[,NONAME]][,DATA][,PRIVATE]".
+// Used for parsing /export arguments.
+ErrorOr<Export> parseExport(StringRef Arg) {
+  Export E;
+  StringRef Rest;
+  std::tie(E.Name, Rest) = Arg.split(",");
+  if (E.Name.empty())
+    goto err;
+  if (E.Name.find('=') != StringRef::npos) {
+    std::tie(E.ExtName, E.Name) = E.Name.split("=");
+    if (E.Name.empty())
+      goto err;
+  } else {
+    E.ExtName = E.Name;
+  }
+
+  while (!Rest.empty()) {
+    StringRef Tok;
+    std::tie(Tok, Rest) = Rest.split(",");
+    if (Tok.equals_lower("noname")) {
+      if (E.Ordinal == 0)
+        goto err;
+      E.Noname = true;
+      continue;
+    }
+    if (Tok.equals_lower("data")) {
+      E.Data = true;
+      continue;
+    }
+    if (Tok.equals_lower("private")) {
+      E.Private = true;
+      continue;
+    }
+    if (Tok.startswith("@")) {
+      int32_t Ord;
+      if (Tok.substr(1).getAsInteger(0, Ord))
+        goto err;
+      if (Ord <= 0 || 65535 < Ord)
+        goto err;
+      E.Ordinal = Ord;
+      continue;
+    }
+    goto err;
+  }
+  return E;
+
+err:
+  llvm::errs() << "invalid /export: " << Arg << "\n";
+  return make_error_code(LLDError::InvalidOption);
+}
+
+// Performs error checking on all /export arguments.
+// It also sets ordinals.
+std::error_code fixupExports() {
+  // Symbol ordinals must be unique.
+  std::set<uint16_t> Ords;
+  for (Export &E : Config->Exports) {
+    if (E.Ordinal == 0)
+      continue;
+    if (!Ords.insert(E.Ordinal).second) {
+      llvm::errs() << "duplicate export ordinal: " << E.Name << "\n";
+      return make_error_code(LLDError::InvalidOption);
+    }
+  }
+
+  // Uniquefy by name.
+  std::set<StringRef> Names;
+  std::vector<Export> V;
+  for (Export &E : Config->Exports) {
+    if (!Names.insert(E.Name).second) {
+      llvm::errs() << "warning: duplicate /export option: " << E.Name << "\n";
+      continue;
+    }
+    V.push_back(E);
+  }
+  Config->Exports = std::move(V);
+
+  // Sort by name.
+  std::sort(
+      Config->Exports.begin(), Config->Exports.end(),
+      [](const Export &A, const Export &B) { return A.ExtName < B.ExtName; });
+
+  // Assign unique ordinals if default (= 0).
+  uint16_t Max = 0;
+  for (Export &E : Config->Exports)
+    Max = std::max(Max, E.Ordinal);
+  for (Export &E : Config->Exports)
+    if (E.Ordinal == 0)
+      E.Ordinal = ++Max;
+  return std::error_code();
+}
+
 // Parses a string in the form of "key=value" and check
 // if value matches previous values for the same key.
 std::error_code checkFailIfMismatch(llvm::opt::InputArgList *Args) {
index 856d431..cf1be7d 100644 (file)
@@ -189,6 +189,14 @@ Defined *SymbolTable::find(StringRef Name) {
 
 // Windows specific -- Link default entry point name.
 ErrorOr<StringRef> SymbolTable::findDefaultEntry() {
+  // If it's DLL, the rule is easy.
+  if (Config->DLL) {
+    StringRef Sym = "_DllMainCRTStartup";
+    if (auto EC = resolve(new (Alloc) Undefined(Sym)))
+      return EC;
+    return Sym;
+  }
+
   // User-defined main functions and their corresponding entry points.
   static const char *Entries[][2] = {
       {"main", "mainCRTStartup"},
index 4e23677..a6cdb94 100644 (file)
@@ -42,6 +42,7 @@ std::error_code Writer::write(StringRef OutputPath) {
   markLive();
   createSections();
   createImportTables();
+  createExportTable();
   if (Config->Relocatable)
     createSection(".reloc");
   assignAddresses();
@@ -174,6 +175,15 @@ void Writer::createImportTables() {
     Sec->addChunk(C);
 }
 
+void Writer::createExportTable() {
+  if (Config->Exports.empty())
+    return;
+  Edata.reset(new EdataContents());
+  OutputSection *Sec = createSection(".edata");
+  for (std::unique_ptr<Chunk> &C : Edata->Chunks)
+    Sec->addChunk(C.get());
+}
+
 // The Windows loader doesn't seem to like empty sections,
 // so we remove them if any.
 void Writer::removeEmptySections() {
@@ -243,6 +253,8 @@ void Writer::writeHeader() {
   COFF->NumberOfSections = OutputSections.size();
   COFF->Characteristics = IMAGE_FILE_EXECUTABLE_IMAGE;
   COFF->Characteristics |= IMAGE_FILE_LARGE_ADDRESS_AWARE;
+  if (Config->DLL)
+    COFF->Characteristics |= IMAGE_FILE_DLL;
   if (!Config->Relocatable)
     COFF->Characteristics |= IMAGE_FILE_RELOCS_STRIPPED;
   COFF->SizeOfOptionalHeader =
@@ -292,6 +304,10 @@ void Writer::writeHeader() {
   // Write data directory
   auto *DataDirectory = reinterpret_cast<data_directory *>(Buf);
   Buf += sizeof(*DataDirectory) * NumberfOfDataDirectory;
+  if (OutputSection *Sec = findSection(".edata")) {
+    DataDirectory[EXPORT_TABLE].RelativeVirtualAddress = Sec->getRVA();
+    DataDirectory[EXPORT_TABLE].Size = Sec->getVirtualSize();
+  }
   if (Idata) {
     DataDirectory[IMPORT_TABLE].RelativeVirtualAddress = Idata->getDirRVA();
     DataDirectory[IMPORT_TABLE].Size = Idata->getDirSize();
@@ -397,6 +413,7 @@ OutputSection *Writer::createSection(StringRef Name) {
                        .Case(".bss", BSS | R | W)
                        .Case(".data", DATA | R | W)
                        .Case(".didat", DATA | R)
+                       .Case(".edata", DATA | R)
                        .Case(".idata", DATA | R)
                        .Case(".rdata", DATA | R)
                        .Case(".reloc", DATA | DISCARDABLE | R)
index 1e4a9b9..78fc953 100644 (file)
@@ -78,6 +78,7 @@ private:
   void markLive();
   void createSections();
   void createImportTables();
+  void createExportTable();
   void assignAddresses();
   void removeEmptySections();
   std::error_code openFile(StringRef OutputPath);
@@ -99,6 +100,7 @@ private:
   llvm::SpecificBumpPtrAllocator<BaserelChunk> BAlloc;
   std::vector<OutputSection *> OutputSections;
   std::unique_ptr<IdataContents> Idata;
+  std::unique_ptr<EdataContents> Edata;
 
   uint64_t FileSize;
   uint64_t SizeOfImage;
diff --git a/lld/test/COFF/Inputs/export.yaml b/lld/test/COFF/Inputs/export.yaml
new file mode 100644 (file)
index 0000000..10a049e
--- /dev/null
@@ -0,0 +1,51 @@
+---
+header:
+  Machine:         IMAGE_FILE_MACHINE_AMD64
+  Characteristics: []
+sections:
+  - Name:            .text
+    Characteristics: [ IMAGE_SCN_CNT_CODE, IMAGE_SCN_MEM_EXECUTE, IMAGE_SCN_MEM_READ ]
+    Alignment:       4
+    SectionData:     B800000000506800000000680000000050E80000000050E800000000
+  - Name:            .drectve
+    Characteristics: [ IMAGE_SCN_LNK_INFO, IMAGE_SCN_LNK_REMOVE ]
+    Alignment:       2147483648
+    SectionData:     2f6578706f72743a6578706f7274666e3300  # /export:exportfn3
+symbols:
+  - Name:            .text
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_STATIC
+    SectionDefinition:
+      Length:          28
+      NumberOfRelocations: 4
+      NumberOfLinenumbers: 0
+      CheckSum:        0
+      Number:          0
+  - Name:            _DllMainCRTStartup
+    Value:           0
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_EXTERNAL
+  - Name:            exportfn1
+    Value:           8
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_EXTERNAL
+  - Name:            exportfn2
+    Value:           16
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_EXTERNAL
+  - Name:            exportfn3
+    Value:           16
+    SectionNumber:   1
+    SimpleType:      IMAGE_SYM_TYPE_NULL
+    ComplexType:     IMAGE_SYM_DTYPE_NULL
+    StorageClass:    IMAGE_SYM_CLASS_EXTERNAL
+...
diff --git a/lld/test/COFF/export.test b/lld/test/COFF/export.test
new file mode 100644 (file)
index 0000000..f42c744
--- /dev/null
@@ -0,0 +1,51 @@
+# RUN: yaml2obj < %p/Inputs/export.yaml > %t.obj
+#
+# RUN: lld -flavor link2 /out:%t.dll /dll %t.obj /export:exportfn1 /export:exportfn2
+# RUN: llvm-objdump -p %t.dll | FileCheck -check-prefix=CHECK1 %s
+
+CHECK1:      Export Table:
+CHECK1:      DLL name: export.test.tmp.dll
+CHECK1:      Ordinal      RVA  Name
+CHECK1-NEXT:       0        0
+CHECK1-NEXT:       1   0x1008  exportfn1
+CHECK1-NEXT:       2   0x1010  exportfn2
+
+# RUN: lld -flavor link2 /out:%t.dll /dll %t.obj /export:exportfn1,@5 /export:exportfn2
+# RUN: llvm-objdump -p %t.dll | FileCheck -check-prefix=CHECK2 %s
+
+CHECK2:      Export Table:
+CHECK2:      DLL name: export.test.tmp.dll
+CHECK2:      Ordinal      RVA  Name
+CHECK2-NEXT:       0        0
+CHECK2-NEXT:       1        0
+CHECK2-NEXT:       2        0
+CHECK2-NEXT:       3        0
+CHECK2-NEXT:       4        0
+CHECK2-NEXT:       5   0x1008  exportfn1
+CHECK2-NEXT:       6   0x1010  exportfn2
+CHECK2-NEXT:       7   0x1010  exportfn3
+
+# RUN: lld -flavor link2 /out:%t.dll /dll %t.obj /export:exportfn1,@5,noname /export:exportfn2
+# RUN: llvm-objdump -p %t.dll | FileCheck -check-prefix=CHECK3 %s
+
+CHECK3:      Export Table:
+CHECK3:      DLL name: export.test.tmp.dll
+CHECK3:      Ordinal      RVA  Name
+CHECK3-NEXT:       0        0
+CHECK3-NEXT:       1        0
+CHECK3-NEXT:       2        0
+CHECK3-NEXT:       3        0
+CHECK3-NEXT:       4        0
+CHECK3-NEXT:       5   0x1008
+CHECK3-NEXT:       6   0x1010  exportfn2
+
+# RUN: lld -flavor link2 /out:%t.dll /dll %t.obj /export:f1=exportfn1 /export:f2@4=exportfn2
+# RUN: llvm-objdump -p %t.dll | FileCheck -check-prefix=CHECK4 %s
+
+CHECK4:      Export Table:
+CHECK4:      DLL name: export.test.tmp.dll
+CHECK4:      Ordinal      RVA  Name
+CHECK4-NEXT:       0        0
+CHECK4-NEXT:       1   0x1010  exportfn3
+CHECK4-NEXT:       2   0x1008  f1
+CHECK4-NEXT:       3   0x1010  f2