diff --git a/.gitattributes b/.gitattributes index 1a49d8b44d..fb13ab9b04 100644 --- a/.gitattributes +++ b/.gitattributes @@ -51,6 +51,7 @@ coverage.xml.in tab-indent *.make tab-indent .hooks-config tab-indent +*.obj -text *.pfx -text *.png -text *.png.in -text diff --git a/Source/bindexplib.cxx b/Source/bindexplib.cxx index 4d5e86a63f..27715a5d9b 100644 --- a/Source/bindexplib.cxx +++ b/Source/bindexplib.cxx @@ -67,14 +67,16 @@ #include #include // IWYU pragma: keep #include +#include #include +#include +#include #include #ifdef _WIN32 # include # include "cmsys/Encoding.hxx" -# include "cmsys/String.h" #endif #include "cmsys/FStream.hxx" @@ -84,6 +86,133 @@ #include "cmStringAlgorithms.h" #include "cmSystemTools.h" +namespace { + +enum class Arch +{ + Generic, + I386, + ARM64EC, +}; + +/* + * The two backends below, the COFF symbol table walker used on Windows + * hosts and the nm reader used everywhere else, must select the same + * symbols, so every rule that depends only on the symbol name is shared. + */ + +bool SymbolIsFromManagedCode(std::string const& symbol) +{ + return symbol == "__t2m" || symbol == "__m2mep" || symbol == "__mep" || + symbol.find("$$F") != std::string::npos || + symbol.find("$$J") != std::string::npos; +} + +bool SymbolIsOperatorExternC(std::string const& symbol) +{ + return symbol.find_first_not_of("=<>+-*/%,?|~!^&[]()") == std::string::npos; +} + +/* + * The "(vector) deleting destructor" must not be exported: + * "??_G" scalar deleting dtor + * "??_E" vector deleting dtor + */ +bool SymbolIsDeletingDestructor(std::string const& symbol) +{ + return cmHasLiteralPrefix(symbol, "??_G") || + cmHasLiteralPrefix(symbol, "??_E"); +} + +bool SymbolIsVftable(std::string const& symbol) +{ + return cmHasLiteralPrefix(symbol, "??_7"); +} + +bool SymbolIsArm64ECThunk(std::string const& symbol) +{ + return symbol.find("$ientry_thunk") != std::string::npos || + symbol.find("$entry_thunk") != std::string::npos || + symbol.find("$iexit_thunk") != std::string::npos || + symbol.find("$exit_thunk") != std::string::npos; +} + +/* + * Strip the decorations that must not appear in the generated .def file. + */ +void NormalizeSymbolName(std::string& symbol, Arch arch) +{ + // clear out any leading spaces + symbol.erase(0, symbol.find_first_not_of(" \t\n\v\f\r")); + // if it starts with _ and has an @ then it is a __cdecl + // so remove the @ stuff for the export + if (!symbol.empty() && symbol[0] == '_') { + std::string::size_type posAt = symbol.find('@'); + if (posAt != std::string::npos) { + symbol.erase(posAt); + } + } + // For i386 builds we need to remove _ + if (arch == Arch::I386 && !symbol.empty() && symbol[0] == '_') { + symbol.erase(0, 1); + } +} + +/* + * Symbols that are never exported, whatever section they are defined in. + */ +bool SymbolIsExcluded(std::string const& symbol, Arch arch) +{ + // The original code had a check for + // symbol.find("real@") == std::string::npos) + // but this disallows member functions with the name "real". + return symbol.empty() || SymbolIsDeletingDestructor(symbol) || + // Skip symbols containing a dot, that are from managed code, + // or that are C++ operators incorrectly declared extern "C". + symbol.find('.') != std::string::npos || SymbolIsFromManagedCode(symbol) || + SymbolIsOperatorExternC(symbol) || + // Skip arm64ec thunk symbols. + (arch == Arch::ARM64EC && SymbolIsArm64ECThunk(symbol)); +} + +/* + * Read the machine type out of a COFF object file header so that the nm + * backend can apply the same architecture-specific rules as the COFF + * symbol table walker. Inputs that are not COFF object files, such as + * LLVM bitcode, are reported as generic. + */ +Arch GetObjectArch(std::string const& filename) +{ + cmsys::ifstream fin(filename.c_str(), std::ios::in | std::ios::binary); + if (!fin) { + return Arch::Generic; + } + unsigned char header[8] = { 0 }; + fin.read(reinterpret_cast(header), sizeof(header)); + if (!fin) { + return Arch::Generic; + } + auto readWord = [&header](size_t i) -> unsigned int { + return static_cast(header[i]) | + (static_cast(header[i + 1]) << 8); + }; + unsigned int machine = readWord(0); + if (machine == 0x0000 && readWord(2) == 0xffff) { + // In a /bigobj file the machine type follows Sig1, Sig2 and Version. + machine = readWord(6); + } + switch (machine) { + case 0x014c: // IMAGE_FILE_MACHINE_I386 + return Arch::I386; + case 0xa641: // IMAGE_FILE_MACHINE_ARM64EC + return Arch::ARM64EC; + default: + return Arch::Generic; + } +} + +} // namespace + #ifdef _WIN32 # ifndef IMAGE_FILE_MACHINE_ARM # define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian @@ -145,13 +274,6 @@ typedef struct _cmIMAGE_SYMBOL_EX } cmIMAGE_SYMBOL_EX; typedef cmIMAGE_SYMBOL_EX UNALIGNED* cmPIMAGE_SYMBOL_EX; -enum class Arch -{ - Generic, - I386, - ARM64EC, -}; - PIMAGE_SECTION_HEADER GetSectionHeaderOffset( PIMAGE_FILE_HEADER pImageFileHeader) { @@ -266,63 +388,24 @@ public: symbol = stringTable + pSymbolTable->N.Name.Long; } - // clear out any leading spaces - while (cmsysString_isspace(symbol[0])) - symbol.erase(0, 1); - // if it starts with _ and has an @ then it is a __cdecl - // so remove the @ stuff for the export - if (symbol[0] == '_') { - std::string::size_type posAt = symbol.find('@'); - if (posAt != std::string::npos) { - symbol.erase(posAt); - } - } - // For i386 builds we need to remove _ - if (this->SymbolArch == Arch::I386 && symbol[0] == '_') { - symbol.erase(0, 1); - } - - // Check whether it is "Scalar deleting destructor" and "Vector - // deleting destructor" - // if scalarPrefix and vectorPrefix are not found then print - // the symbol - char const* scalarPrefix = "??_G"; - char const* vectorPrefix = "??_E"; - char const* vftablePrefix = "??_7"; - // The original code had a check for - // symbol.find("real@") == std::string::npos) - // but this disallows member functions with the name "real". - if (symbol.compare(0, 4, scalarPrefix) && - symbol.compare(0, 4, vectorPrefix)) { + NormalizeSymbolName(symbol, this->SymbolArch); + if (!SymbolIsExcluded(symbol, this->SymbolArch)) { SectChar = this->SectionHeaders[pSymbolTable->SectionNumber - 1] .Characteristics; - // Skip symbols containing a dot, are from managed code, - // or are C++ operators incorrectly declared extern "C". - if (symbol.find('.') == std::string::npos && - !SymbolIsFromManagedCode(symbol) && - !SymbolIsOperatorExternC(symbol)) { - // skip arm64ec thunk symbols - if (this->SymbolArch != Arch::ARM64EC || - (symbol.find("$ientry_thunk") == std::string::npos && - symbol.find("$entry_thunk") == std::string::npos && - symbol.find("$iexit_thunk") == std::string::npos && - symbol.find("$exit_thunk") == std::string::npos)) { - if ((!pSymbolTable->Type && - // Read only (i.e. constants) must be excluded - (SectChar & IMAGE_SCN_MEM_WRITE)) || - (this->SymbolArch == Arch::ARM64EC && - // vftable symbols are DATA on ARM64EC - symbol.compare(0, 4, vftablePrefix) == 0)) { - this->DataSymbols.insert(symbol); - } else if (pSymbolTable->Type || - !(SectChar & IMAGE_SCN_MEM_READ) || - (SectChar & IMAGE_SCN_MEM_EXECUTE) || - (this->SymbolArch != Arch::ARM64EC && - // vftable symbols fail if marked as DATA - symbol.compare(0, 4, vftablePrefix) == 0)) { - this->Symbols.insert(symbol); - } - } + if ((!pSymbolTable->Type && + // Read only (i.e. constants) must be excluded + (SectChar & IMAGE_SCN_MEM_WRITE)) || + (this->SymbolArch == Arch::ARM64EC && + // vftable symbols are DATA on ARM64EC + SymbolIsVftable(symbol))) { + this->DataSymbols.insert(symbol); + } else if (pSymbolTable->Type || + !(SectChar & IMAGE_SCN_MEM_READ) || + (SectChar & IMAGE_SCN_MEM_EXECUTE) || + (this->SymbolArch != Arch::ARM64EC && + // vftable symbols fail if marked as DATA + SymbolIsVftable(symbol))) { + this->Symbols.insert(symbol); } } } @@ -338,19 +421,6 @@ public: } private: - bool SymbolIsFromManagedCode(std::string const& symbol) - { - return symbol == "__t2m" || symbol == "__m2mep" || symbol == "__mep" || - symbol.find("$$F") != std::string::npos || - symbol.find("$$J") != std::string::npos; - } - - bool SymbolIsOperatorExternC(std::string const& symbol) - { - return symbol.find_first_not_of("=<>+-*/%,?|~!^&[]()") == - std::string::npos; - } - std::set& Symbols; std::set& DataSymbols; DWORD_PTR SymbolCount; @@ -415,6 +485,12 @@ static bool DumpFileWithNm(std::string const& nmPath, return false; } + // The COFF symbol table walker applies rules that depend on the object + // file's architecture, but nm does not report it. Read it from the + // object files named in the output, remembering it because one nm run + // may cover many object files. + std::map archOfObject; + std::istringstream ss(output); std::string line; while (std::getline(ss, line)) { @@ -440,10 +516,22 @@ static bool DumpFileWithNm(std::string const& nmPath, return false; } char const sym_type = line[sym_end + 1]; - std::string const symbol = line.substr(sym_start, sym_end - sym_start); + std::string const objectFile = line.substr(0, filename_end); + std::string symbol = line.substr(sym_start, sym_end - sym_start); + + auto archEntry = archOfObject.find(objectFile); + if (archEntry == archOfObject.end()) { + archEntry = + archOfObject.emplace(objectFile, GetObjectArch(objectFile)).first; + } + Arch const arch = archEntry->second; + + NormalizeSymbolName(symbol, arch); + if (SymbolIsExcluded(symbol, arch)) { + continue; + } switch (sym_type) { case 'B': - case 'C': case 'D': dataSymbols.insert(symbol); continue; @@ -451,11 +539,21 @@ static bool DumpFileWithNm(std::string const& nmPath, symbols.insert(symbol); continue; case 'R': - if (cmHasLiteralPrefix(symbol, "??_7")) { - dataSymbols.insert(symbol); + // Read only (i.e. constants) must be excluded, but vftables live + // there too. They fail if marked as DATA, except on ARM64EC where + // they must be marked as DATA. + if (SymbolIsVftable(symbol)) { + if (arch == Arch::ARM64EC) { + dataSymbols.insert(symbol); + } else { + symbols.insert(symbol); + } } continue; case 'A': + case 'C': + // Common symbols have no section, and the COFF symbol table walker + // rejects symbols whose section number is not positive. case 'I': case 'N': case 'S': @@ -477,7 +575,7 @@ static bool DumpFileWithNm(std::string const& nmPath, default: std::cerr << "Ignoring symbol '" << symbol << "' with unrecognized type '" << sym_type - << "' in object '" << line.substr(0, filename_end) << "'.\n"; + << "' in object '" << objectFile << "'.\n"; } } diff --git a/Tests/RunCMake/AutoExportDll/CreateDef.cmake b/Tests/RunCMake/AutoExportDll/CreateDef.cmake new file mode 100644 index 0000000000..94b0ba6707 --- /dev/null +++ b/Tests/RunCMake/AutoExportDll/CreateDef.cmake @@ -0,0 +1,46 @@ +# Run "cmake -E __create_def" on the checked-in object files and compare the +# generated .def files with the expected ones. The object files are compiled +# for Windows but are read on every host, so this checks that the COFF symbol +# table backend used on Windows hosts and the nm backend used on other +# hosts export the same symbols. +# +# The object files were generated from CreateDef/fixture.cxx with +# +# clang-cl --target=-pc-windows-msvc /c /GR- /EHs- /Os /Zl -nostdinc \ +# CreateDef/fixture.cxx /Fo:CreateDef/fixture-.obj +# +# once per architecture. + +set(failed "") +foreach(arch IN ITEMS x86_64 i386 arm64ec) + set(objlist "${bin}/${arch}-objects.txt") + set(def "${bin}/${arch}.def") + file(WRITE "${objlist}" "${src}/CreateDef/fixture-${arch}.obj\n") + file(REMOVE "${def}") + execute_process( + COMMAND ${cmake} -E __create_def "${def}" "${objlist}" ${nm} + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE output + ) + if(NOT result EQUAL 0) + string(APPEND failed + "__create_def failed for ${arch}: ${result}\n${output}") + continue() + endif() + file(READ "${def}" actual) + file(READ "${src}/CreateDef/fixture-${arch}.def" expect) + # Compare ignoring trailing whitespace, which __create_def emits. + string(REGEX REPLACE "[ \t\r]+\n" "\n" actual "${actual}") + string(REGEX REPLACE "[ \t\r]+\n" "\n" expect "${expect}") + if(NOT actual STREQUAL expect) + string(APPEND failed + "Wrong exports for ${arch}.\n" + "Expected:\n${expect}\n" + "Actual:\n${actual}\n") + endif() +endforeach() + +if(failed) + message(FATAL_ERROR "${failed}") +endif() diff --git a/Tests/RunCMake/AutoExportDll/CreateDef/fixture-arm64ec.def b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-arm64ec.def new file mode 100644 index 0000000000..7201ee2e1f --- /dev/null +++ b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-arm64ec.def @@ -0,0 +1,13 @@ +EXPORTS + ??_7Virt@@6B@ DATA + ?bssData@@3HA DATA + ?writableData@@3HA DATA + cWritableData DATA + #CFunction + ??0Virt@@$$hQEAA@XZ + ??1Virt@@$$hUEAA@XZ + ?CodeFunction@@$$hYAHXZ + ?DeleteOne@@$$hYAXPEAVVirt@@@Z + ?Method@Virt@@$$hUEAAHXZ + ?UseConstData@@$$hYAPEBHXZ + ?UseLocalFunction@@$$hYAHXZ diff --git a/Tests/RunCMake/AutoExportDll/CreateDef/fixture-arm64ec.obj b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-arm64ec.obj new file mode 100644 index 0000000000..f624dea6fd Binary files /dev/null and b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-arm64ec.obj differ diff --git a/Tests/RunCMake/AutoExportDll/CreateDef/fixture-i386.def b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-i386.def new file mode 100644 index 0000000000..a0685cdaae --- /dev/null +++ b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-i386.def @@ -0,0 +1,14 @@ +EXPORTS + ?bssData@@3HA DATA + ?writableData@@3HA DATA + cWritableData DATA + ??0Virt@@QAE@XZ + ??1Virt@@UAE@XZ + ??_7Virt@@6B@ + ?CodeFunction@@YAHXZ + ?DeleteOne@@YAXPAVVirt@@@Z + ?Method@Virt@@UAEHXZ + ?UseConstData@@YAPBHXZ + ?UseLocalFunction@@YAHXZ + CFunction + StdcallFunction diff --git a/Tests/RunCMake/AutoExportDll/CreateDef/fixture-i386.obj b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-i386.obj new file mode 100644 index 0000000000..cd7173d084 Binary files /dev/null and b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-i386.obj differ diff --git a/Tests/RunCMake/AutoExportDll/CreateDef/fixture-x86_64.def b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-x86_64.def new file mode 100644 index 0000000000..79f7440b09 --- /dev/null +++ b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-x86_64.def @@ -0,0 +1,13 @@ +EXPORTS + ?bssData@@3HA DATA + ?writableData@@3HA DATA + cWritableData DATA + ??0Virt@@QEAA@XZ + ??1Virt@@UEAA@XZ + ??_7Virt@@6B@ + ?CodeFunction@@YAHXZ + ?DeleteOne@@YAXPEAVVirt@@@Z + ?Method@Virt@@UEAAHXZ + ?UseConstData@@YAPEBHXZ + ?UseLocalFunction@@YAHXZ + CFunction diff --git a/Tests/RunCMake/AutoExportDll/CreateDef/fixture-x86_64.obj b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-x86_64.obj new file mode 100644 index 0000000000..f83ae83b82 Binary files /dev/null and b/Tests/RunCMake/AutoExportDll/CreateDef/fixture-x86_64.obj differ diff --git a/Tests/RunCMake/AutoExportDll/CreateDef/fixture.cxx b/Tests/RunCMake/AutoExportDll/CreateDef/fixture.cxx new file mode 100644 index 0000000000..9c52ca9dc4 --- /dev/null +++ b/Tests/RunCMake/AutoExportDll/CreateDef/fixture.cxx @@ -0,0 +1,64 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ + +/* Source of the checked-in object files used by the CreateDef test. + See CreateDef.cmake for the commands used to regenerate them. */ + +class Virt +{ +public: + Virt(); + virtual ~Virt(); + virtual int Method(); +}; +Virt::Virt() +{ +} +Virt::~Virt() +{ +} +int Virt::Method() +{ + return 1; +} +/* Makes the compiler emit the scalar deleting destructor "??_G". */ +void DeleteOne(Virt* v) +{ + delete v; +} + +int writableData = 1; +int bssData; +extern int const constData; +int const constData = 2; +int const* UseConstData() +{ + return &constData; +} + +int CodeFunction() +{ + return 3; +} +static int LocalFunction() +{ + return 4; +} +int UseLocalFunction() +{ + return LocalFunction(); +} + +/* On i386 these lose their leading underscore, and __stdcall loses its + "@" suffix. */ +extern "C" int CFunction(int a) +{ + return a; +} +extern "C" int cWritableData = 5; +#ifdef _M_IX86 +extern "C" int __stdcall StdcallFunction(int a) +{ + return a; +} +#endif diff --git a/Tests/RunCMake/AutoExportDll/RunCMakeTest.cmake b/Tests/RunCMake/AutoExportDll/RunCMakeTest.cmake index 5d9f26c9ff..d6e444e410 100644 --- a/Tests/RunCMake/AutoExportDll/RunCMakeTest.cmake +++ b/Tests/RunCMake/AutoExportDll/RunCMakeTest.cmake @@ -66,6 +66,38 @@ if (RunCMake_GENERATOR MATCHES "(Ninja|Makefiles|Visual Studio)") run_cmake_AutoExport(AutoExportShort ".o/0cb3d702") endif () +# Verify that the two bindexplib backends, the COFF symbol table walker used +# on Windows hosts and the nm reader used on other hosts, export the same +# symbols from a fixed set of object files. +function(run_create_def) + set(nm_arg "") + if(NOT CMAKE_HOST_WIN32) + # Reading COFF object files on other hosts requires llvm-nm. + find_program(LLVM_NM NAMES llvm-nm) + if(NOT LLVM_NM) + return() + endif() + # llvm-nm 16 is the first version that reads ARM64EC object files. + execute_process(COMMAND "${LLVM_NM}" --version + OUTPUT_VARIABLE nm_version ERROR_QUIET) + if(NOT nm_version MATCHES "LLVM version ([0-9]+)" OR CMAKE_MATCH_1 LESS 16) + return() + endif() + set(nm_arg "--nm=${LLVM_NM}") + endif() + set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/CreateDef-build") + file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}") + file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}") + run_cmake_command(CreateDef ${CMAKE_COMMAND} + "-Dcmake=${CMAKE_COMMAND}" + "-Dnm=${nm_arg}" + "-Dsrc=${RunCMake_SOURCE_DIR}" + "-Dbin=${RunCMake_TEST_BINARY_DIR}" + -P "${RunCMake_SOURCE_DIR}/CreateDef.cmake" + ) +endfunction() +run_create_def() + function(run_AIXExportExplicit) set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/AIXExportExplicit-build") run_cmake(AIXExportExplicit) diff --git a/Tests/RunCMake/CommandLine/E___create_def-nm-symbol-types-expected.def b/Tests/RunCMake/CommandLine/E___create_def-nm-symbol-types-expected.def index 9c8b05507a..878beafa14 100644 --- a/Tests/RunCMake/CommandLine/E___create_def-nm-symbol-types-expected.def +++ b/Tests/RunCMake/CommandLine/E___create_def-nm-symbol-types-expected.def @@ -1,6 +1,5 @@ EXPORTS - ??_7Example@@6B@ DATA bss DATA - common DATA data DATA + ??_7Example@@6B@ text