Skip to content

[LLD][COFF] Add support for including native ARM64 objects in ARM64EC images #137653

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

Merged
merged 2 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lld/COFF/COFFLinkerContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class COFFLinkerContext : public CommonLinkerContext {
f(symtab);
}

// Invoke the specified callback for each active symbol table,
// skipping the native symbol table on pure ARM64EC targets.
void forEachActiveSymtab(std::function<void(SymbolTable &symtab)> f) {
if (symtab.ctx.config.machine == ARM64X)
f(*hybridSymtab);
f(symtab);
}

std::vector<ObjFile *> objFileInstances;
std::map<std::string, PDBInputFile *> pdbInputFileInstances;
std::vector<ImportFile *> importFileInstances;
Expand Down
2 changes: 1 addition & 1 deletion lld/COFF/Chunks.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ void SectionChunk::getBaserels(std::vector<Baserel> *res) {
// to match the value in the EC load config, which is expected to be
// a relocatable pointer to the __chpe_metadata symbol.
COFFLinkerContext &ctx = file->symtab.ctx;
if (ctx.hybridSymtab && ctx.hybridSymtab->loadConfigSym &&
if (ctx.config.machine == ARM64X && ctx.hybridSymtab->loadConfigSym &&
ctx.hybridSymtab->loadConfigSym->getChunk() == this &&
ctx.symtab.loadConfigSym &&
ctx.hybridSymtab->loadConfigSize >=
Expand Down
74 changes: 43 additions & 31 deletions lld/COFF/DLL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,8 @@ class TailMergeChunkARM64 : public NonSectionCodeChunk {
memcpy(buf, tailMergeARM64, sizeof(tailMergeARM64));
applyArm64Addr(buf + 44, desc->getRVA(), rva + 44, 12);
applyArm64Imm(buf + 48, desc->getRVA() & 0xfff, 0);
applyArm64Branch26(buf + 52, helper->getRVA() - rva - 52);
if (helper)
applyArm64Branch26(buf + 52, helper->getRVA() - rva - 52);
}

Chunk *desc = nullptr;
Expand Down Expand Up @@ -781,6 +782,7 @@ void IdataContents::create(COFFLinkerContext &ctx) {
// ordinal values to the table.
size_t base = lookups.size();
Chunk *lookupsTerminator = nullptr, *addressesTerminator = nullptr;
uint32_t nativeOnly = 0;
for (DefinedImportData *s : syms) {
uint16_t ord = s->getOrdinal();
HintNameChunk *hintChunk = nullptr;
Expand All @@ -806,8 +808,8 @@ void IdataContents::create(COFFLinkerContext &ctx) {
// the native terminator, they will be ignored in the native view.
// In the EC view, they should act as terminators, so emit ZEROFILL
// relocations overriding them.
if (ctx.hybridSymtab && !lookupsTerminator && s->file->isEC() &&
!s->file->hybridFile) {
if (ctx.config.machine == ARM64X && !lookupsTerminator &&
s->file->isEC() && !s->file->hybridFile) {
lookupsTerminator = lookupsChunk;
addressesTerminator = addressesChunk;
lookupsChunk = make<NullChunk>(ctx);
Expand Down Expand Up @@ -841,6 +843,7 @@ void IdataContents::create(COFFLinkerContext &ctx) {
// Fill the auxiliary IAT with null chunks for native-only imports.
auxIat.push_back(make<NullChunk>(ctx));
auxIatCopy.push_back(make<NullChunk>(ctx));
++nativeOnly;
}
}
// Terminate with null values.
Expand All @@ -862,18 +865,15 @@ void IdataContents::create(COFFLinkerContext &ctx) {
// Create the import table header.
dllNames.push_back(make<StringChunk>(syms[0]->getDLLName()));
auto *dir = make<ImportDirectoryChunk>(dllNames.back());
dir->lookupTab = lookups[base];
dir->addressTab = addresses[base];
dirs.push_back(dir);

if (ctx.hybridSymtab) {
// If native-only imports exist, they will appear as a prefix to all
// imports. Emit ARM64X relocations to skip them in the EC view.
uint32_t nativeOnly =
llvm::find_if(syms,
[](DefinedImportData *s) { return s->file->isEC(); }) -
syms.begin();
if (nativeOnly) {
if (ctx.hybridSymtab && nativeOnly) {
if (ctx.config.machine != ARM64X)
// On pure ARM64EC targets, skip native-only imports in the import
// directory.
base += nativeOnly;
else if (nativeOnly) {
// If native-only imports exist, they will appear as a prefix to all
// imports. Emit ARM64X relocations to skip them in the EC view.
ctx.dynamicRelocs->add(
IMAGE_DVRT_ARM64X_FIXUP_TYPE_DELTA, 0,
Arm64XRelocVal(
Expand All @@ -886,6 +886,10 @@ void IdataContents::create(COFFLinkerContext &ctx) {
nativeOnly * sizeof(uint64_t));
}
}

dir->lookupTab = lookups[base];
dir->addressTab = addresses[base];
dirs.push_back(dir);
}
// Add null terminator.
dirs.push_back(make<NullChunk>(sizeof(ImportDirectoryTableEntry), 4));
Expand Down Expand Up @@ -922,21 +926,25 @@ void DelayLoadContents::create() {

size_t base = addresses.size();
ctx.forEachSymtab([&](SymbolTable &symtab) {
if (ctx.hybridSymtab && symtab.isEC()) {
// For hybrid images, emit null-terminated native import entries
// followed by null-terminated EC entries. If a view is missing imports
// for a given module, only terminators are emitted. Emit ARM64X
// relocations to skip native entries in the EC view.
ctx.dynamicRelocs->add(
IMAGE_DVRT_ARM64X_FIXUP_TYPE_DELTA, 0,
Arm64XRelocVal(dir, offsetof(delay_import_directory_table_entry,
DelayImportAddressTable)),
(addresses.size() - base) * sizeof(uint64_t));
ctx.dynamicRelocs->add(
IMAGE_DVRT_ARM64X_FIXUP_TYPE_DELTA, 0,
Arm64XRelocVal(dir, offsetof(delay_import_directory_table_entry,
DelayImportNameTable)),
(addresses.size() - base) * sizeof(uint64_t));
if (symtab.isEC()) {
if (ctx.config.machine == ARM64X) {
// For hybrid images, emit null-terminated native import entries
// followed by null-terminated EC entries. If a view is missing
// imports for a given module, only terminators are emitted. Emit
// ARM64X relocations to skip native entries in the EC view.
ctx.dynamicRelocs->add(
IMAGE_DVRT_ARM64X_FIXUP_TYPE_DELTA, 0,
Arm64XRelocVal(dir, offsetof(delay_import_directory_table_entry,
DelayImportAddressTable)),
(addresses.size() - base) * sizeof(uint64_t));
ctx.dynamicRelocs->add(
IMAGE_DVRT_ARM64X_FIXUP_TYPE_DELTA, 0,
Arm64XRelocVal(dir, offsetof(delay_import_directory_table_entry,
DelayImportNameTable)),
(addresses.size() - base) * sizeof(uint64_t));
} else {
base = addresses.size();
}
}

Chunk *tm = nullptr;
Expand Down Expand Up @@ -981,7 +989,7 @@ void DelayLoadContents::create() {
chunk = make<AuxImportChunk>(s->file);
auxIatCopy.push_back(chunk);
s->file->auxImpCopySym->setLocation(chunk);
} else if (ctx.hybridSymtab) {
} else if (ctx.config.machine == ARM64X) {
// Fill the auxiliary IAT with null chunks for native imports.
auxIat.push_back(make<NullChunk>(ctx));
auxIatCopy.push_back(make<NullChunk>(ctx));
Expand All @@ -995,6 +1003,10 @@ void DelayLoadContents::create() {
symtab.addSynthetic(tmName, tm);
}

// Skip terminators on pure ARM64EC target if there are no native imports.
if (!tm && !symtab.isEC() && ctx.config.machine != ARM64X)
return;

// Terminate with null values.
addresses.push_back(make<NullChunk>(ctx, 8));
names.push_back(make<NullChunk>(ctx, 8));
Expand Down Expand Up @@ -1024,7 +1036,7 @@ void DelayLoadContents::create() {
}

Chunk *DelayLoadContents::newTailMergeChunk(SymbolTable &symtab, Chunk *dir) {
auto helper = cast<Defined>(symtab.delayLoadHelper);
auto helper = cast_or_null<Defined>(symtab.delayLoadHelper);
switch (symtab.machine) {
case AMD64:
case ARM64EC:
Expand Down
34 changes: 20 additions & 14 deletions lld/COFF/Driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ static bool compatibleMachineType(COFFLinkerContext &ctx, MachineTypes mt) {
case ARM64:
return mt == ARM64 || mt == ARM64X;
case ARM64EC:
return isArm64EC(mt) || mt == AMD64;
case ARM64X:
return isAnyArm64(mt) || mt == AMD64;
case IMAGE_FILE_MACHINE_UNKNOWN:
Expand Down Expand Up @@ -499,7 +498,7 @@ void LinkerDriver::parseDirectives(InputFile *file) {
case OPT_entry:
if (!arg->getValue()[0])
Fatal(ctx) << "missing entry point symbol name";
ctx.forEachSymtab([&](SymbolTable &symtab) {
ctx.forEachActiveSymtab([&](SymbolTable &symtab) {
symtab.entry = symtab.addGCRoot(symtab.mangle(arg->getValue()), true);
});
break;
Expand Down Expand Up @@ -657,9 +656,13 @@ void LinkerDriver::setMachine(MachineTypes machine) {

ctx.config.machine = machine;

if (machine != ARM64X) {
if (!isArm64EC(machine)) {
Copy link
Member

Choose a reason for hiding this comment

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

So this change makes us create two symbol tables for all arm64ec linking, even if the very fast majority of them ever only will use one of them?

That feels odd, but I guess it's good for consistency - otherwise we'd probably have lots of bugs for the very rare cases when we do need both symbol tables for arm64ec. And I guess this is the change which forces the extra (EC symbol) to be printed in many tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that’s right, this is the core part of the change; the rest of the patch updates various components to handle it properly. The extra (EC symbol) annotation doesn’t add much value when there’s only a single symbol table, but it becomes quite useful with this PR. If someone accidentally passes a native object file, error messages that include (native symbol) should make the issue easy to diagnose.

ctx.symtab.machine = machine;
} else {
// Set up a hybrid symbol table on ARM64EC/ARM64X. This is primarily useful
// on ARM64X, where both the native and EC symbol tables are meaningful.
// However, since ARM64EC can include native object files, we also need to
// support a hybrid symbol table there.
ctx.symtab.machine = ARM64EC;
Copy link
Member

Choose a reason for hiding this comment

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

It would probably be good to add a comment here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a comment, thanks for review!

ctx.hybridSymtab.emplace(ctx, ARM64);
}
Expand Down Expand Up @@ -979,7 +982,7 @@ void LinkerDriver::createImportLibrary(bool asLib) {
};

getExports(ctx.symtab, exports);
if (ctx.hybridSymtab)
if (ctx.config.machine == ARM64X)
getExports(*ctx.hybridSymtab, nativeExports);

std::string libName = getImportName(asLib);
Expand Down Expand Up @@ -1383,13 +1386,13 @@ void LinkerDriver::maybeExportMinGWSymbols(const opt::InputArgList &args) {
return;

if (ctx.symtab.hadExplicitExports ||
(ctx.hybridSymtab && ctx.hybridSymtab->hadExplicitExports))
(ctx.config.machine == ARM64X && ctx.hybridSymtab->hadExplicitExports))
return;
if (args.hasArg(OPT_exclude_all_symbols))
return;
}

ctx.forEachSymtab([&](SymbolTable &symtab) {
ctx.forEachActiveSymtab([&](SymbolTable &symtab) {
AutoExporter exporter(symtab, excludedSymbols);

for (auto *arg : args.filtered(OPT_wholearchive_file))
Expand Down Expand Up @@ -2305,7 +2308,7 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
if (auto *arg = args.getLastArg(OPT_deffile)) {
// parseModuleDefs mutates Config object.
ctx.symtab.parseModuleDefs(arg->getValue());
if (ctx.hybridSymtab) {
if (ctx.config.machine == ARM64X) {
// MSVC ignores the /defArm64Native argument on non-ARM64X targets.
// It is also ignored if the /def option is not specified.
if (auto *arg = args.getLastArg(OPT_defarm64native))
Expand All @@ -2332,7 +2335,7 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
}

// Handle /entry and /dll
ctx.forEachSymtab([&](SymbolTable &symtab) {
ctx.forEachActiveSymtab([&](SymbolTable &symtab) {
llvm::TimeTraceScope timeScope("Entry point");
if (auto *arg = args.getLastArg(OPT_entry)) {
if (!arg->getValue()[0])
Expand Down Expand Up @@ -2364,7 +2367,7 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
llvm::TimeTraceScope timeScope("Delay load");
for (auto *arg : args.filtered(OPT_delayload)) {
config->delayLoads.insert(StringRef(arg->getValue()).lower());
ctx.forEachSymtab([&](SymbolTable &symtab) {
ctx.forEachActiveSymtab([&](SymbolTable &symtab) {
if (symtab.machine == I386) {
symtab.delayLoadHelper = symtab.addGCRoot("___delayLoadHelper2@8");
} else {
Expand Down Expand Up @@ -2538,7 +2541,9 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
u->setWeakAlias(symtab.addUndefined(to));
}
}
});

ctx.forEachActiveSymtab([&](SymbolTable &symtab) {
// If any inputs are bitcode files, the LTO code generator may create
// references to library functions that are not explicit in the bitcode
// file's symbol table. If any of those library functions are defined in
Expand Down Expand Up @@ -2568,7 +2573,7 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {

// Handle /includeglob
for (StringRef pat : args::getStrings(args, OPT_incl_glob))
ctx.forEachSymtab(
ctx.forEachActiveSymtab(
[&](SymbolTable &symtab) { symtab.addUndefinedGlob(pat); });

// Create wrapped symbols for -wrap option.
Expand Down Expand Up @@ -2685,12 +2690,12 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {
// need to create a .lib file. In MinGW mode, we only do that when the
// -implib option is given explicitly, for compatibility with GNU ld.
if (config->dll || !ctx.symtab.exports.empty() ||
(ctx.hybridSymtab && !ctx.hybridSymtab->exports.empty())) {
(ctx.config.machine == ARM64X && !ctx.hybridSymtab->exports.empty())) {
llvm::TimeTraceScope timeScope("Create .lib exports");
ctx.forEachSymtab([](SymbolTable &symtab) { symtab.fixupExports(); });
ctx.forEachActiveSymtab([](SymbolTable &symtab) { symtab.fixupExports(); });
if (!config->noimplib && (!config->mingw || !config->implib.empty()))
createImportLibrary(/*asLib=*/false);
ctx.forEachSymtab(
ctx.forEachActiveSymtab(
[](SymbolTable &symtab) { symtab.assignExportOrdinals(); });
}

Expand Down Expand Up @@ -2756,7 +2761,8 @@ void LinkerDriver::linkerMain(ArrayRef<const char *> argsArr) {

if (ctx.symtab.isEC())
ctx.symtab.initializeECThunks();
ctx.forEachSymtab([](SymbolTable &symtab) { symtab.initializeLoadConfig(); });
ctx.forEachActiveSymtab(
[](SymbolTable &symtab) { symtab.initializeLoadConfig(); });

// Identify unreferenced COMDAT sections.
if (config->doGC) {
Expand Down
4 changes: 1 addition & 3 deletions lld/COFF/InputFiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,8 @@ void ArchiveFile::parse() {
ctx.symtab.addLazyArchive(this, sym);

// Read both EC and native symbols on ARM64X.
if (!ctx.hybridSymtab)
return;
archiveSymtab = &*ctx.hybridSymtab;
} else if (ctx.hybridSymtab) {
} else {
// If the ECSYMBOLS section is missing in the archive, the archive could
// be either a native-only ARM64 or x86_64 archive. Check the machine type
// of the object containing a symbol to determine which symbol table to
Expand Down
2 changes: 1 addition & 1 deletion lld/COFF/SymbolTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ void SymbolTable::initializeLoadConfig() {
Warn(ctx) << "EC version of '_load_config_used' is missing";
return;
}
if (ctx.hybridSymtab) {
if (ctx.config.machine == ARM64X) {
Warn(ctx) << "native version of '_load_config_used' is missing for "
"ARM64X target";
return;
Expand Down
13 changes: 7 additions & 6 deletions lld/COFF/Writer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1374,7 +1374,7 @@ void Writer::createExportTable() {
}
}
}
ctx.forEachSymtab([&](SymbolTable &symtab) {
ctx.forEachActiveSymtab([&](SymbolTable &symtab) {
if (symtab.edataStart) {
if (symtab.hadExplicitExports)
Warn(ctx) << "literal .edata sections override exports";
Expand Down Expand Up @@ -1776,7 +1776,8 @@ template <typename PEHeaderTy> void Writer::writeHeader() {
assert(coffHeaderOffset == buf - buffer->getBufferStart());
auto *coff = reinterpret_cast<coff_file_header *>(buf);
buf += sizeof(*coff);
SymbolTable &symtab = ctx.hybridSymtab ? *ctx.hybridSymtab : ctx.symtab;
SymbolTable &symtab =
ctx.config.machine == ARM64X ? *ctx.hybridSymtab : ctx.symtab;
coff->Machine = symtab.isEC() ? AMD64 : symtab.machine;
coff->NumberOfSections = ctx.outputSections.size();
coff->Characteristics = IMAGE_FILE_EXECUTABLE_IMAGE;
Expand Down Expand Up @@ -2433,7 +2434,7 @@ void Writer::setECSymbols() {
return a.first->getRVA() < b.first->getRVA();
});

ChunkRange &chpePdata = ctx.hybridSymtab ? hybridPdata : pdata;
ChunkRange &chpePdata = ctx.config.machine == ARM64X ? hybridPdata : pdata;
Symbol *rfeTableSym = ctx.symtab.findUnderscore("__arm64x_extra_rfe_table");
replaceSymbol<DefinedSynthetic>(rfeTableSym, "__arm64x_extra_rfe_table",
chpePdata.first);
Expand Down Expand Up @@ -2478,7 +2479,7 @@ void Writer::setECSymbols() {
delayIdata.getAuxIatCopy().empty() ? nullptr
: delayIdata.getAuxIatCopy().front());

if (ctx.hybridSymtab) {
if (ctx.config.machine == ARM64X) {
// For the hybrid image, set the alternate entry point to the EC entry
// point. In the hybrid view, it is swapped to the native entry point
// using ARM64X relocations.
Expand Down Expand Up @@ -2868,7 +2869,7 @@ void Writer::fixTlsAlignment() {
}

void Writer::prepareLoadConfig() {
ctx.forEachSymtab([&](SymbolTable &symtab) {
ctx.forEachActiveSymtab([&](SymbolTable &symtab) {
if (!symtab.loadConfigSym)
return;

Expand Down Expand Up @@ -2928,7 +2929,7 @@ void Writer::prepareLoadConfig(SymbolTable &symtab, T *loadConfig) {
IF_CONTAINS(CHPEMetadataPointer) {
// On ARM64X, only the EC version of the load config contains
// CHPEMetadataPointer. Copy its value to the native load config.
if (ctx.hybridSymtab && !symtab.isEC() &&
if (ctx.config.machine == ARM64X && !symtab.isEC() &&
ctx.symtab.loadConfigSize >=
offsetof(T, CHPEMetadataPointer) + sizeof(T::CHPEMetadataPointer)) {
OutputSection *sec =
Expand Down
2 changes: 1 addition & 1 deletion lld/test/COFF/arm64ec-entry-mangle.test
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ RUN: not lld-link -machine:arm64ec -dll -out:test.dll demangled-func.obj loadcon
RUN: "-entry:#func" 2>&1 | FileCheck -check-prefix=FUNC-NOT-FOUND %s
RUN: not lld-link -machine:arm64ec -dll -out:test.dll demangled-func.obj loadconfig-arm64ec.obj \
RUN: -noentry "-export:#func" 2>&1 | FileCheck -check-prefix=FUNC-NOT-FOUND %s
FUNC-NOT-FOUND: undefined symbol: #func
FUNC-NOT-FOUND: undefined symbol: #func (EC symbol)

Verify that the linker recognizes the demangled x86_64 _DllMainCRTStartup.
RUN: lld-link -machine:arm64ec -dll -out:test.dll x64-dll-main.obj loadconfig-arm64ec.obj
Expand Down
Loading