[lld] [lld][MachO] Preserve class-address addends in ObjC category merging (PR #211431)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Jul 28 08:18:35 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-lld-macho
Author: Karim Alweheshy (karim-alweheshy)
<details>
<summary>Changes</summary>
Mach-O category merging can encounter Swift class references as an enclosing
metadata symbol plus a non-zero addend after LTO removes the exact
class-address alias. For example, a category can refer to a `CMf` symbol plus
the offset of the class object within that metadata record.
`tryGetSymbolAtIsecOffset()` previously resolved such a relocation to the
enclosing symbol and discarded the residual addend. `getClassRo()` then read
the class layout at the wrong address. For valid Swift metadata this can return
null; before the defensive check in the first commit the linker dereferenced
that result and crashed, while the check alone safely skipped a category that
could have been merged.
Preserve the class reference as a symbol and residual addend throughout:
- category grouping;
- class and metaclass RO-data lookup;
- source-language detection; and
- relocations synthesized for merged categories.
An exact symbol at the target address is still preferred. When no exact alias
survives, retain the offset from the closest containing symbol. The defensive
checks remain in place for genuinely incomplete or unsupported metadata.
The Swift regression now omits the exact class-address alias and points the
category classlist directly into the enclosing metadata record, matching the
shape observed after ThinLTO.
Testing:
- Release + assertions AArch64 LLVM/LLD build
- focused `llvm-lit` coverage for the alias-free Swift metadata and
missing-metadata cases
- full Swift 6 application link with `-objc_category_merging`
The application result is integration evidence for correctness and folding
coverage
---
Full diff: https://github.com/llvm/llvm-project/pull/211431.diff
3 Files Affected:
- (modified) lld/MachO/ObjC.cpp (+96-50)
- (modified) lld/test/MachO/objc-category-merging-minimal.s (+13)
- (modified) lld/test/MachO/objc-category-merging-swift.s (+2-6)
``````````diff
diff --git a/lld/MachO/ObjC.cpp b/lld/MachO/ObjC.cpp
index 4522c574c137f..5689f3b0f5145 100644
--- a/lld/MachO/ObjC.cpp
+++ b/lld/MachO/ObjC.cpp
@@ -395,6 +395,7 @@ class ObjcCategoryMerger {
std::string mergedContainerName;
std::string baseClassName;
const Symbol *baseClass = nullptr;
+ int64_t baseClassAddend = 0;
SourceLanguage baseClassSourceLanguage = SourceLanguage::Unknown;
CategoryLayout &catLayout;
@@ -461,19 +462,26 @@ class ObjcCategoryMerger {
const std::string &forBaseClassName,
ObjFile *objFile);
Defined *emitCategoryBody(const std::string &name, const Defined *nameSym,
- const Symbol *baseClassSym,
+ const Symbol *baseClassSym, int64_t baseClassAddend,
const std::string &baseClassName, ObjFile *objFile);
Defined *emitCategoryName(const std::string &name, ObjFile *objFile);
void createSymbolReference(Defined *refFrom, const Symbol *refTo,
- uint32_t offset, const Relocation &relocTemplate);
+ uint32_t offset, const Relocation &relocTemplate,
+ int64_t addend = 0);
Defined *tryFindDefinedOnIsec(const InputSection *isec, uint32_t offset);
+ std::pair<Symbol *, int64_t>
+ tryGetSymbolReferenceAtIsecOffset(const ConcatInputSection *isec,
+ uint32_t offset);
Symbol *tryGetSymbolAtIsecOffset(const ConcatInputSection *isec,
uint32_t offset);
Defined *tryGetDefinedAtIsecOffset(const ConcatInputSection *isec,
uint32_t offset);
- Defined *getClassRo(const Defined *classSym, bool getMetaRo);
- SourceLanguage getClassSymSourceLang(const Defined *classSym);
+ Defined *getClassRo(const Defined *classSym, int64_t classAddend,
+ bool getMetaRo);
+ SourceLanguage getClassSymSourceLang(const Defined *classSym,
+ int64_t classAddend);
bool mergeCategoriesIntoBaseClass(const Defined *baseClass,
+ int64_t baseClassAddend,
std::vector<InfoInputCategory> &categories);
void eraseSymbolAtIsecOffset(ConcatInputSection *isec, uint32_t offset);
void tryEraseDefinedAtIsecOffset(const ConcatInputSection *isec,
@@ -493,8 +501,10 @@ class ObjcCategoryMerger {
InfoCategoryWriter infoCategoryWriter;
std::vector<ConcatInputSection *> &allInputSections;
- // Map of base class Symbol to list of InfoInputCategory's for it
- MapVector<const Symbol *, std::vector<InfoInputCategory>> categoryMap;
+ // Map of base class Symbol + residual addend to its categories. Swift class
+ // address points can be interior to a larger metadata symbol after LTO.
+ MapVector<std::pair<const Symbol *, int64_t>, std::vector<InfoInputCategory>>
+ categoryMap;
// Normally, the binary data comes from the input files, but since we're
// generating binary data ourselves, we use the below array to store it in.
@@ -532,35 +542,48 @@ void ObjcCategoryMerger::collectSectionWriteInfoFromIsec(
catWriteInfo.valid = true;
}
-Symbol *
-ObjcCategoryMerger::tryGetSymbolAtIsecOffset(const ConcatInputSection *isec,
- uint32_t offset) {
+std::pair<Symbol *, int64_t>
+ObjcCategoryMerger::tryGetSymbolReferenceAtIsecOffset(
+ const ConcatInputSection *isec, uint32_t offset) {
if (!isec)
- return nullptr;
+ return {nullptr, 0};
const Relocation *reloc = isec->getRelocAt(offset);
if (!reloc)
- return nullptr;
+ return {nullptr, 0};
Symbol *sym = dyn_cast_if_present<Symbol *>(reloc->referent);
if (reloc->addend && sym) {
assert(isa<Defined>(sym) && "Expected defined for non-zero addend");
Defined *definedSym = cast<Defined>(sym);
- sym = tryFindDefinedOnIsec(definedSym->isec(),
- definedSym->value + reloc->addend);
+ uint64_t targetOffset = definedSym->value + reloc->addend;
+ if (Defined *targetSym =
+ tryFindDefinedOnIsec(definedSym->isec(), targetOffset))
+ return {targetSym, targetOffset - targetSym->value};
}
- return sym;
+ return {sym, reloc->addend};
+}
+
+Symbol *
+ObjcCategoryMerger::tryGetSymbolAtIsecOffset(const ConcatInputSection *isec,
+ uint32_t offset) {
+ return tryGetSymbolReferenceAtIsecOffset(isec, offset).first;
}
Defined *ObjcCategoryMerger::tryFindDefinedOnIsec(const InputSection *isec,
uint32_t offset) {
- for (Defined *sym : isec->symbols)
- if ((sym->value <= offset) && (sym->value + sym->size > offset))
+ Defined *containing = nullptr;
+ for (Defined *sym : isec->symbols) {
+ if (sym->value == offset)
return sym;
+ if (sym->value < offset && sym->value + sym->size > offset &&
+ (!containing || sym->value > containing->value))
+ containing = sym;
+ }
- return nullptr;
+ return containing;
}
Defined *
@@ -574,23 +597,25 @@ ObjcCategoryMerger::tryGetDefinedAtIsecOffset(const ConcatInputSection *isec,
// the meta-class's ro_data symbol. Otherwise, we will return the class
// (instance) ro_data symbol.
Defined *ObjcCategoryMerger::getClassRo(const Defined *classSym,
- bool getMetaRo) {
+ int64_t classAddend, bool getMetaRo) {
ConcatInputSection *isec = dyn_cast<ConcatInputSection>(classSym->isec());
if (!isec)
return nullptr;
+ uint64_t classOffset = classSym->value + classAddend;
if (!getMetaRo)
- return tryGetDefinedAtIsecOffset(isec, classLayout.roDataOffset +
- classSym->value);
+ return tryGetDefinedAtIsecOffset(isec,
+ classLayout.roDataOffset + classOffset);
- Defined *metaClass = tryGetDefinedAtIsecOffset(
- isec, classLayout.metaClassOffset + classSym->value);
+ auto [metaClassSym, metaClassAddend] = tryGetSymbolReferenceAtIsecOffset(
+ isec, classLayout.metaClassOffset + classOffset);
+ Defined *metaClass = dyn_cast_or_null<Defined>(metaClassSym);
if (!metaClass)
return nullptr;
return tryGetDefinedAtIsecOffset(
dyn_cast<ConcatInputSection>(metaClass->isec()),
- classLayout.roDataOffset);
+ classLayout.roDataOffset + metaClass->value + metaClassAddend);
}
// Given an ConcatInputSection or CStringInputSection and an offset, if there is
@@ -808,18 +833,22 @@ bool ObjcCategoryMerger::parseCatInfoToExtInfo(const InfoInputCategory &catInfo,
// Parse base class
if (!extInfo.baseClass) {
- Symbol *classSym =
- tryGetSymbolAtIsecOffset(catInfo.catBodyIsec, catLayout.klassOffset);
+ auto [classSym, classAddend] = tryGetSymbolReferenceAtIsecOffset(
+ catInfo.catBodyIsec, catLayout.klassOffset);
assert(extInfo.baseClassName.empty());
extInfo.baseClass = classSym;
+ extInfo.baseClassAddend = classAddend;
llvm::StringRef classPrefix(objc::symbol_names::klass);
- assert(classSym->getName().starts_with(classPrefix) &&
- "Base class symbol does not start with expected prefix");
- extInfo.baseClassName = classSym->getName().substr(classPrefix.size());
+ if (classSym->getName().starts_with(classPrefix))
+ extInfo.baseClassName =
+ classSym->getName().substr(classPrefix.size()).str();
+ else
+ extInfo.baseClassName = classSym->getName().str();
} else {
- assert((extInfo.baseClass ==
- tryGetSymbolAtIsecOffset(catInfo.catBodyIsec,
- catLayout.klassOffset)) &&
+ auto [classSym, classAddend] = tryGetSymbolReferenceAtIsecOffset(
+ catInfo.catBodyIsec, catLayout.klassOffset);
+ assert((extInfo.baseClass == classSym &&
+ extInfo.baseClassAddend == classAddend) &&
"Trying to parse category info into container with different base "
"class");
}
@@ -1000,6 +1029,7 @@ ObjcCategoryMerger::emitCatListEntrySec(const std::string &forCategoryName,
Defined *ObjcCategoryMerger::emitCategoryBody(const std::string &name,
const Defined *nameSym,
const Symbol *baseClassSym,
+ int64_t baseClassAddend,
const std::string &baseClassName,
ObjFile *objFile) {
llvm::ArrayRef<uint8_t> bodyData = newSectionData(catLayout.totalSize);
@@ -1032,7 +1062,8 @@ Defined *ObjcCategoryMerger::emitCategoryBody(const std::string &name,
// Create a reloc to the base class (either external or internal)
createSymbolReference(catBodySym, baseClassSym, catLayout.klassOffset,
- infoCategoryWriter.catBodyInfo.relocTemplate);
+ infoCategoryWriter.catBodyInfo.relocTemplate,
+ baseClassAddend);
return catBodySym;
}
@@ -1081,9 +1112,10 @@ Defined *ObjcCategoryMerger::emitCategory(const ClassExtensionInfo &extInfo) {
Defined *catNameSym = emitCategoryName(extInfo.mergedContainerName,
extInfo.objFileForMergeData);
- Defined *catBodySym = emitCategoryBody(
- extInfo.mergedContainerName, catNameSym, extInfo.baseClass,
- extInfo.baseClassName, extInfo.objFileForMergeData);
+ Defined *catBodySym =
+ emitCategoryBody(extInfo.mergedContainerName, catNameSym,
+ extInfo.baseClass, extInfo.baseClassAddend,
+ extInfo.baseClassName, extInfo.objFileForMergeData);
Defined *catListSym =
emitCatListEntrySec(extInfo.mergedContainerName, extInfo.baseClassName,
@@ -1135,12 +1167,14 @@ bool ObjcCategoryMerger::mergeCategoriesIntoSingleCategory(
return true;
}
-void ObjcCategoryMerger::createSymbolReference(
- Defined *refFrom, const Symbol *refTo, uint32_t offset,
- const Relocation &relocTemplate) {
+void ObjcCategoryMerger::createSymbolReference(Defined *refFrom,
+ const Symbol *refTo,
+ uint32_t offset,
+ const Relocation &relocTemplate,
+ int64_t addend) {
Relocation r = relocTemplate;
r.offset = offset;
- r.addend = 0;
+ r.addend = addend;
r.referent = const_cast<Symbol *>(refTo);
refFrom->isec()->relocs.push_back(r);
}
@@ -1200,14 +1234,14 @@ void ObjcCategoryMerger::collectAndValidateCategoriesData() {
// Check that the category has a reloc at 'klassOffset' (which is
// a pointer to the class symbol)
- Symbol *classSym =
- tryGetSymbolAtIsecOffset(catBodyIsec, catLayout.klassOffset);
+ auto [classSym, classAddend] =
+ tryGetSymbolReferenceAtIsecOffset(catBodyIsec, catLayout.klassOffset);
assert(classSym && "Category does not have a valid base class");
if (!collectCategoryWriterInfoFromCategory(catInputInfo))
continue;
- categoryMap[classSym].push_back(catInputInfo);
+ categoryMap[{classSym, classAddend}].push_back(catInputInfo);
}
}
}
@@ -1334,11 +1368,13 @@ void ObjcCategoryMerger::eraseMergedCategories() {
void ObjcCategoryMerger::doMerge() {
collectAndValidateCategoriesData();
- for (auto &[baseClass, catInfos] : categoryMap) {
+ for (auto &[baseClassRef, catInfos] : categoryMap) {
+ const auto &[baseClass, baseClassAddend] = baseClassRef;
bool merged = false;
if (auto *baseClassDef = dyn_cast<Defined>(baseClass)) {
// Merge all categories into the base class
- merged = mergeCategoriesIntoBaseClass(baseClassDef, catInfos);
+ merged =
+ mergeCategoriesIntoBaseClass(baseClassDef, baseClassAddend, catInfos);
} else if (catInfos.size() > 1) {
// Merge all categories into a new, single category
merged = mergeCategoriesIntoSingleCategory(catInfos);
@@ -1382,7 +1418,8 @@ void objc::mergeCategories() {
void objc::doCleanup() { ObjcCategoryMerger::doCleanup(); }
ObjcCategoryMerger::SourceLanguage
-ObjcCategoryMerger::getClassSymSourceLang(const Defined *classSym) {
+ObjcCategoryMerger::getClassSymSourceLang(const Defined *classSym,
+ int64_t classAddend) {
if (classSym->getName().starts_with(objc::symbol_names::swift_objc_klass))
return SourceLanguage::Swift;
@@ -1396,7 +1433,7 @@ ObjcCategoryMerger::getClassSymSourceLang(const Defined *classSym) {
// So we scan for symbols with the same address and check for the Swift class
if (classSym->getName().starts_with(objc::symbol_names::klass)) {
for (auto &sym : classSym->originalIsec->symbols)
- if (sym->value == classSym->value)
+ if (sym->value == classSym->value + classAddend)
if (sym->getName().starts_with(objc::symbol_names::swift_objc_klass))
return SourceLanguage::Swift;
return SourceLanguage::ObjC;
@@ -1406,23 +1443,32 @@ ObjcCategoryMerger::getClassSymSourceLang(const Defined *classSym) {
}
bool ObjcCategoryMerger::mergeCategoriesIntoBaseClass(
- const Defined *baseClass, std::vector<InfoInputCategory> &categories) {
+ const Defined *baseClass, int64_t baseClassAddend,
+ std::vector<InfoInputCategory> &categories) {
assert(categories.size() >= 1 && "Expected at least one category to merge");
// Collect all the info from the categories
ClassExtensionInfo extInfo(catLayout);
extInfo.baseClass = baseClass;
- extInfo.baseClassSourceLanguage = getClassSymSourceLang(baseClass);
+ extInfo.baseClassAddend = baseClassAddend;
+ extInfo.baseClassSourceLanguage =
+ getClassSymSourceLang(baseClass, baseClassAddend);
for (auto &catInfo : categories)
if (!parseCatInfoToExtInfo(catInfo, extInfo))
return false;
// Get metadata for the base class
- Defined *metaRo = getClassRo(baseClass, /*getMetaRo=*/true);
+ Defined *metaRo = getClassRo(baseClass, baseClassAddend, /*getMetaRo=*/true);
+ Defined *classRo =
+ getClassRo(baseClass, baseClassAddend, /*getMetaRo=*/false);
+ if (!metaRo || !classRo)
+ return false;
+
ConcatInputSection *metaIsec = dyn_cast<ConcatInputSection>(metaRo->isec());
- Defined *classRo = getClassRo(baseClass, /*getMetaRo=*/false);
ConcatInputSection *classIsec = dyn_cast<ConcatInputSection>(classRo->isec());
+ if (!metaIsec || !classIsec)
+ return false;
// Now collect the info from the base class from the various lists in the
// class metadata
diff --git a/lld/test/MachO/objc-category-merging-minimal.s b/lld/test/MachO/objc-category-merging-minimal.s
index d4d5933aa5ec8..da717b1af054d 100644
--- a/lld/test/MachO/objc-category-merging-minimal.s
+++ b/lld/test/MachO/objc-category-merging-minimal.s
@@ -44,6 +44,19 @@
# Check that lld emitted the warning about skipping category merging
MERGE_WARNING: warning: ObjC category merging skipped for class symbol' _OBJC_CLASS_$_MyBaseClass'
+############ Test merging skipped due to missing base class metadata ############
+# Replace the base class's metaclass pointer with null. The class symbol is
+# still defined, but it does not contain the metadata required for merging.
+# RUN: awk '/^_OBJC_CLASS_\$_MyBaseClass:/ { print; getline; sub(/^[ \t]*\.quad[ \t]+_OBJC_METACLASS_\$_MyBaseClass$/, "\t.quad\t0"); print; next } { print }' merge_base_class_minimal.s > merge_base_class_missing_metadata.s
+
+# RUN: llvm-mc -filetype=obj -triple=arm64-apple-macos -o merge_base_class_missing_metadata.o merge_base_class_missing_metadata.s
+
+# RUN: %no-fatal-warnings-lld -arch arm64 -dylib -objc_category_merging -o merge_base_class_missing_metadata.dylib merge_base_class_missing_metadata.o merge_cat_minimal.o 2>&1 | FileCheck %s --check-prefix=MISSING_METADATA_WARNING
+# RUN: llvm-objdump --objc-meta-data --macho merge_base_class_missing_metadata.dylib | FileCheck %s --check-prefixes=NO_MERGE_INTO_BASE
+
+# The unsupported base class is skipped instead of being dereferenced.
+MISSING_METADATA_WARNING: warning: ObjC category merging skipped for class symbol' _OBJC_CLASS_$_MyBaseClass'
+
#### Check merge categories enabled ###
# Check that the original categories are not there
MERGE_CATS-NOT: __OBJC_$_CATEGORY_MyBaseClass_$_Category01
diff --git a/lld/test/MachO/objc-category-merging-swift.s b/lld/test/MachO/objc-category-merging-swift.s
index 7a084d9310d6c..61711ee7fd138 100644
--- a/lld/test/MachO/objc-category-merging-swift.s
+++ b/lld/test/MachO/objc-category-merging-swift.s
@@ -9,7 +9,7 @@
# RUN: llvm-objdump --objc-meta-data --macho cat_swift.dylib | FileCheck %s --check-prefixes=CHECK-MERGE
; CHECK-MERGE: Contents of (__DATA_CONST,__objc_classlist) section
-; CHECK-MERGE-NEXT: _$s11SimpleClassAACN
+; CHECK-MERGE-NEXT: {{[0-9a-f]+ 0x[0-9a-f]+}}
; CHECK-MERGE-NEXT: isa {{.+}} _OBJC_METACLASS_$__TtC11SimpleClass11SimpleClass
; CHECK-MERGE-NEXT: superclass 0x0
; CHECK-MERGE-NEXT: cache 0x0
@@ -372,7 +372,7 @@ ___swift_reflection_version:
.section __DATA,__objc_classlist,regular,no_dead_strip
.p2align 3, 0x0
_objc_classes_$s11SimpleClassAACN:
- .quad _$s11SimpleClassAACN
+ .quad _$s11SimpleClassAACMf+24
.section __DATA,__objc_catlist,regular,no_dead_strip
.p2align 3, 0x0
@@ -395,10 +395,6 @@ L_OBJC_IMAGE_INFO:
.private_extern _$s11SimpleClassAAC04baseB14InstanceMethods5Int32VyFTq
.alt_entry _$s11SimpleClassAAC04baseB14InstanceMethods5Int32VyFTq
.set _$s11SimpleClassAAC04baseB14InstanceMethods5Int32VyFTq, _$s11SimpleClassAACMn+52
- .globl _$s11SimpleClassAACN
- .private_extern _$s11SimpleClassAACN
- .alt_entry _$s11SimpleClassAACN
-.set _$s11SimpleClassAACN, _$s11SimpleClassAACMf+24
.globl _OBJC_CLASS_$__TtC11SimpleClass11SimpleClass
.private_extern _OBJC_CLASS_$__TtC11SimpleClass11SimpleClass
.subsections_via_symbols
``````````
</details>
https://github.com/llvm/llvm-project/pull/211431
More information about the llvm-commits
mailing list