3 // dyld_shared_cache_builder
5 // Created by Thomas Deniau on 18/12/2019.
8 #include "FileAbstraction.hpp"
9 #include "IMPCaches.hpp"
10 #include "IMPCachesBuilder.hpp"
11 #include "JSONReader.h"
13 #include <unordered_map>
25 std::string
ClassData::description() const
28 ss
<< name
<< " modulo:" << modulo();
29 if (isMetaclass
) ss
<< " (metaclass)";
33 std::string
ClassData::PlacementAttempt::description() const
35 std::stringstream stream
;
36 stream
<< "needed bits: " << neededBits
<< ", shift: " << shift
;
40 typename
ClassData::PlacementAttempt
ClassData::attemptForShift(int shiftToTry
, int neededBitsToTry
) const
42 int totalNumberOfBitsToSet
= 0;
43 int mask
= (1 << neededBitsToTry
) - 1;
44 for (const Method
& method
: methods
) {
45 totalNumberOfBitsToSet
+= method
.selector
->numberOfBitsToSet(shiftToTry
, mask
);
48 return ClassData::PlacementAttempt(totalNumberOfBitsToSet
, shiftToTry
, neededBitsToTry
);
51 std::vector
<typename
ClassData::PlacementAttempt
> ClassData::attempts() const
53 // We have 26 MB of selectors, and among them only ~ 7MB are deemed
54 // "interesting" to include in our hash tables.
56 // So we should be able to fit on 24 bits of address space (~ 16 MB of selectors),
57 // but need to keep the low 7 bits available so that we
58 // don't have to worry about selector length and so that
59 // we don't have to worry about collisions. (i.e. multiple selectors
60 // end up at the same address with this algorithm and then we play
61 // in another step with
62 // the low 7 bits to give them all an unique address).
63 // Then if there are holes given this 128-byte alignment, we can
64 // fill the holes with selectors excluded from the algorithm.
66 // Let us grow the hash tables to one more bit if needed
67 // as the full problem is too difficult.
68 std::vector
<int> allowedNeededBits
{ neededBits
, neededBits
+ 1 };
69 std::vector
<PlacementAttempt
> attempts
;
70 for (auto i
: allowedNeededBits
) {
71 // Go thrugh all the possible shifts, starting at 0, knowing that
72 // shift+neededBits needs to fit on 17 bits.
74 for (int shiftToTry
= 0; shiftToTry
<= 17 - i
; shiftToTry
++) {
75 attempts
.push_back(attemptForShift(shiftToTry
, i
));
78 sort(attempts
.begin(), attempts
.end());
82 void ClassData::resetSlots()
85 slots
.assign(slots
.size(), nullptr);
87 slots
.assign(slots
.size(), false);
91 void ClassData::backtrack(typename
ClassData::PlacementAttempt::Result
& resultToBacktrackFrom
)
93 if (!resultToBacktrackFrom
.success
) {
94 // We backtrack from a failure if we decided to skip a class that was too difficult to place.
95 // In this case there's nothing to do.
100 // Restore the addresses and masks we had in place before we did the step that let to resultToBacktrackFrom
101 typename
PlacementAttempt::PreviousState previousState
= resultToBacktrackFrom
.previousState
;
102 for (const Method
& method
: methods
) {
103 Selector
* selector
= method
.selector
;
104 typename
PlacementAttempt::PreviousMethodAddress previousMethod
= previousState
.methods
[selector
];
105 selector
->inProgressBucketIndex
= previousMethod
.address
;
106 selector
->fixedBitsMask
= previousMethod
.fixedBitsMask
;
109 shift
= previousState
.shift
;
110 neededBits
= previousState
.neededBits
;
113 // Compute the number of needed bits for the hash table now that all the methods have been added
114 void ClassData::didFinishAddingMethods()
116 if (methods
.size() == 0) {
119 neededBits
= (int)ceil(log2(double(methods
.size())));
123 bool ClassData::hadToIncreaseSize() const
125 if (methods
.size() == 0) return false;
126 return neededBits
> (int)(ceil(log2((double)(methods
.size()))));
129 /// Try to see if we can make the shift and mask in @param attempt work.
130 typename
ClassData::PlacementAttempt::Result
ClassData::applyAttempt(const PlacementAttempt
& attempt
, std::minstd_rand
& randomNumberGenerator
)
132 std::vector
<Selector
*> sortedMethods
;
133 sortedMethods
.reserve(methods
.size());
134 for (const Method
& method
: methods
) {
135 sortedMethods
.push_back(method
.selector
);
138 // Solve from most constrained to least constrained
139 std::sort(sortedMethods
.begin(), sortedMethods
.end(), [attempt
](Selector
* m1
, Selector
* m2
) {
140 return m1
->numberOfBitsToSet(attempt
.shift
, attempt
.mask()) < m2
->numberOfBitsToSet(attempt
.shift
, attempt
.mask());
143 if (slots
.size() < (1 << attempt
.neededBits
)) {
145 slots
.resize(1 << attempt
.neededBits
, nullptr);
147 slots
.resize(1 << attempt
.neededBits
, false);
153 std::vector
<int> addresses
;
154 for (auto m
: sortedMethods
) {
157 // Check if all the bits are already assigned
158 int shiftedMask
= attempt
.mask() << attempt
.shift
;
159 if ((m
->fixedBitsMask
& shiftedMask
) == shiftedMask
) {
160 int index
= (m
->inProgressBucketIndex
>> attempt
.shift
) & attempt
.mask();
162 typename
ClassData::PlacementAttempt::Result result
;
163 result
.success
= false;
172 addresses
.push_back(index
);
174 // Some bits are not assigned yet, so try to find an address that would be compatible
175 // with the existing bits for this method.
177 int attemptModulo
= 1 << attempt
.neededBits
;
179 // possibleAddresses = 0..<attemptModulo
180 std::vector
<int> possibleAddresses(attemptModulo
);
181 std::iota(possibleAddresses
.begin(), possibleAddresses
.end(), 0);
183 // We randomize the addresses to try so that two random selectors
184 // have as many ranges of different bits as possible, in order
185 // to find a satisfying shift for every class.
187 std::shuffle(possibleAddresses
.begin(), possibleAddresses
.end(), randomNumberGenerator
);
188 for (auto i
: possibleAddresses
) {
189 int futureAddress
= m
->inProgressBucketIndex
| (i
<< attempt
.shift
);
190 int slot
= (futureAddress
>> attempt
.shift
) & attempt
.mask();
192 // Make sure the new address is compatible with the existing bits
193 bool addressesMatch
= (futureAddress
& m
->fixedBitsMask
) == (m
->inProgressBucketIndex
& m
->fixedBitsMask
);
194 if (addressesMatch
&& !slots
[slot
]) {
201 addresses
.push_back(i
);
206 typename
ClassData::PlacementAttempt::Result result
;
207 result
.success
= false;
213 // We succeeded, record the state so that we can backtrack if needed
214 std::unordered_map
<Selector
*, typename
PlacementAttempt::PreviousMethodAddress
> previousMethods
;
215 for (unsigned long i
= 0; i
< sortedMethods
.size(); i
++) {
216 Selector
* m
= sortedMethods
[i
];
217 int previousAddress
= m
->inProgressBucketIndex
;
218 int previousMask
= m
->fixedBitsMask
;
219 m
->inProgressBucketIndex
|= addresses
[i
] << attempt
.shift
;
220 m
->fixedBitsMask
|= attempt
.mask() << attempt
.shift
;
221 previousMethods
[m
] = typename
PlacementAttempt::PreviousMethodAddress
{
222 .address
= previousAddress
,
223 .fixedBitsMask
= previousMask
227 typename
PlacementAttempt::PreviousState previousState
{
228 .neededBits
= neededBits
,
230 .methods
= previousMethods
232 shift
= attempt
.shift
;
233 neededBits
= attempt
.neededBits
;
235 typename
PlacementAttempt::Result result
{
237 .previousState
= previousState
243 bool ClassData::checkConsistency() {
245 for (const Method
& method
: methods
) {
246 const Selector
* s
= method
.selector
;
247 int slotIndex
= (s
->inProgressBucketIndex
>> shift
) & mask();
248 if (slots
[slotIndex
]) {
252 slots
[slotIndex
] = s
;
254 slots
[slotIndex
] = true;
260 Constraint
ClassData::constraintForMethod(const Selector
* method
) {
262 allowedValues
.clear();
264 // Fill the slots with all our methods except `method`
265 for (const Method
& m
: methods
) {
266 const Selector
* s
= m
.selector
;
271 int slotIndex
= (s
->inProgressBucketIndex
>> shift
) & mask();
273 assert(slots
[slotIndex
] == nullptr);
274 slots
[slotIndex
] = s
;
276 assert(!slots
[slotIndex
]);
277 slots
[slotIndex
] = true;
281 // What are the remaining empty slots in which we could put `method`?
282 int max
= 1 << neededBits
;
283 for (int i
= 0 ; i
< max
; i
++) {
285 allowedValues
.push_back(i
);
289 auto allowedSet
= std::unordered_set
<int>(allowedValues
.begin(), allowedValues
.end());
293 .allowedValues
= allowedSet
297 size_t ClassData::sizeInSharedCache() const {
298 return IMPCaches::sizeForImpCacheWithCount(modulo());
301 int AddressSpace::sizeAtIndex(int idx
) const
303 if (sizes
.find(idx
) != sizes
.end()) {
304 return sizes
.at(idx
);
310 void AddressSpace::removeUninterestingSelectors() {
311 for (auto& [k
,v
] : methodsByIndex
) {
312 v
.erase(std::remove_if(v
.begin(), v
.end(), [](const Selector
* s
){
313 return s
->classes
.size() == 0;
318 int AddressSpace::sizeAvailableAfterIndex(int idx
) const
320 int availableAfterThisAddress
= bagSizeAtIndex(idx
) - sizeAtIndex(idx
);
321 for (int j
= idx
+ 1; j
< maximumIndex
; j
++) {
322 if (methodsByIndex
.find(j
) != methodsByIndex
.end()) {
325 availableAfterThisAddress
+= bagSizeAtIndex(j
);
329 return availableAfterThisAddress
;
332 // Because some selectors are longer than 128 bytes, we have to sometimes let
333 // them overflow into the next 128-byte bucket. This method tells you if you
334 // can place a method in a bucket without colliding with an overflowing selector
335 // from one of the previous buckets.
336 bool AddressSpace::canPlaceWithoutFillingOverflowCellAtIndex(int idx
) const
338 if ((idx
== 0) || (sizeAtIndex(idx
) > 0)) {
343 int availableOnOrBefore
= 0;
345 while (j
> 0 && (sizeAtIndex(j
) == 0)) {
346 availableOnOrBefore
+= bagSizeAtIndex(j
);
350 int sizeOfFirstNonEmptyCellBefore
= sizeAtIndex(j
);
351 return (sizeOfFirstNonEmptyCellBefore
< availableOnOrBefore
);
354 bool AddressSpace::canPlaceMethodAtIndex(const Selector
* method
, int idx
) const
356 int existingSize
= sizeAtIndex(idx
);
357 bool canPlaceWithoutFillingOverflow
= canPlaceWithoutFillingOverflowCellAtIndex(idx
);
359 if (!canPlaceWithoutFillingOverflow
) {
363 int available
= bagSizeAtIndex(idx
) - existingSize
;
364 int methodSize
= method
->size();
365 bool enoughRoom
= available
> methodSize
;
371 bool tooBigButOverflowExists
= (methodSize
> 64) && (available
> 0) && (sizeAvailableAfterIndex(idx
) > methodSize
);
373 return tooBigButOverflowExists
;
376 void AddressSpace::placeMethodAtIndex(Selector
* method
, int idx
)
378 auto [it
, success
] = methodsByIndex
.try_emplace(idx
, std::vector
<Selector
*>());
379 it
->second
.push_back(method
);
381 auto [it2
, success2
] = sizes
.try_emplace(idx
, 0);
382 it2
->second
+= method
->size();
385 // At this point selected are already sorted into 128-byte buckets.
386 // Now fill in the low 7 bits of each address, and return a list
387 // of intervals [ ..... selector data....][ ...... hole ....][.... selector data...]
388 // so that we can stuff in selectors that don't participate in
390 void AddressSpace::computeLowBits(HoleMap
& selectors
) const {
391 int currentEndOffset
= magicSelector
.length() + 1;
393 std::set
<IMPCaches::Hole
> & holes
= selectors
.holes
;
396 std::vector
<int> orderedIndices
;
397 for (const auto& [index
, selectors
] : methodsByIndex
) {
398 orderedIndices
.push_back(index
);
400 std::sort(orderedIndices
.begin(), orderedIndices
.end());
401 for (int index
: orderedIndices
) {
402 const std::vector
<Selector
*> & selectorsAtThisIndex
= methodsByIndex
.at(index
);
403 int bucketOffset
= index
<< bagSizeShift
;
404 if (bucketOffset
> currentEndOffset
) {
406 .startAddress
= currentEndOffset
,
407 .endAddress
= bucketOffset
,
409 currentEndOffset
= bucketOffset
;
411 for (Selector
* s
: selectorsAtThisIndex
) {
412 s
->offset
= currentEndOffset
;
413 currentEndOffset
+= s
->size();
417 selectors
.endAddress
= currentEndOffset
;
420 int HoleMap::addStringOfSize(unsigned size
) {
422 // if (i++ % 1000 == 0) {
423 // printf("Inserted 1000 more strings, number of holes = %lu\n", holes.size());
425 Hole neededHole
= Hole
{
427 .endAddress
= static_cast<int>(size
)
429 std::set
<Hole
>::iterator it
= holes
.lower_bound(neededHole
);
430 if (it
== holes
.end()) {
432 int end
= endAddress
;
436 // Remove this hole and insert a smaller one instead
438 int address
= it
->startAddress
;
439 Hole updatedHole
= *it
;
440 updatedHole
.startAddress
+= size
;
443 // Don't insert if the hole is empty or won't fit any selector
444 if (updatedHole
.size() > 1) {
445 holes
.insert(updatedHole
);
451 void HoleMap::clear() {
456 unsigned long HoleMap::totalHoleSize() const {
457 unsigned long result
= 0;
458 for (const Hole
& hole
: holes
) {
459 result
+= hole
.size();
464 std::ostream
& operator<<(std::ostream
& o
, const HoleMap
& m
) {
467 for (const Hole
& h
: m
.holes
) {
468 if (h
.size() == size
) {
471 o
<< count
<< " holes of size " << size
<< std::endl
;
479 std::ostream
& operator<<(std::ostream
& o
, const AddressSpace
& a
)
481 int maximumIndex
= 0;
482 for (const auto& kvp
: a
.methodsByIndex
) {
483 maximumIndex
= std::max(maximumIndex
, kvp
.first
);
486 std::vector
<double> lengths
;
487 std::map
<int, std::vector
<Selector
*>> sortedMethodsByAddress
;
488 sortedMethodsByAddress
.insert(a
.methodsByIndex
.begin(), a
.methodsByIndex
.end());
490 std::map
<int, int> sortedSizes
;
491 sortedSizes
.insert(a
.sizes
.begin(), a
.sizes
.end());
493 for (const auto& kvp
: sortedSizes
) {
494 lengths
.push_back(kvp
.second
);
497 for (const auto& kvp
: sortedMethodsByAddress
) {
498 o
<< std::setw(5) << kvp
.first
<< ": ";
499 for (Selector
* m
: kvp
.second
) {
505 o
<< "Max address " << maximumIndex
<< "\n";
506 o
<< "Average length " << (double)std::accumulate(lengths
.begin(), lengths
.end(), 0) / lengths
.size() << "\n";
511 // Returns a constraint that is the intersection of "this" and "other", i.e. a constraint for which the allowed values
512 // are the intersection of the allowed values of "this" and "other" (taking into account shift and mask)
513 Constraint
Constraint::intersecting(const Constraint
& other
) const
515 if ((mask
== other
.mask
) && (shift
== other
.shift
)) {
517 std::unordered_set
<int> intersection
;
518 for (int allowedValue
: allowedValues
) {
519 if (other
.allowedValues
.find(allowedValue
) != other
.allowedValues
.end()) {
520 intersection
.insert(allowedValue
);
527 .allowedValues
= intersection
531 int shiftedMask
= mask
<< shift
;
532 int otherShift
= other
.shift
;
533 int otherMask
= other
.mask
;
534 int otherShiftedMask
= other
.mask
<< otherShift
;
535 int intersectionMask
= shiftedMask
& otherShiftedMask
;
536 const std::unordered_set
<int>& otherAllowedValues
= other
.allowedValues
;
538 // Always make sure we start with the left-most mask as self
539 if (shiftedMask
< otherShiftedMask
) {
540 return other
.intersecting(*this);
543 // If there are no real constraints on our side, just return the other
544 if ((mask
== 0) && (allowedValues
.size() == 1) && (*(allowedValues
.begin()) == 0)) {
548 // If there are no real constraints on the other side, just return our constraint
549 if ((otherMask
== 0) && (otherAllowedValues
.size() == 1) && (*(otherAllowedValues
.begin()) == 0)) {
553 if (otherShift
>= shift
) {
554 // [self..[other]..self]
555 // Restrict the allowed values to make sure they have the right bits.
556 int shiftDifference
= otherShift
- shift
;
557 std::unordered_set
<int> combinedAllowedValues
;
558 for (int v
: allowedValues
) {
559 int val
= (v
>> shiftDifference
) & otherMask
;
560 if (otherAllowedValues
.find(val
) != otherAllowedValues
.end()) {
561 combinedAllowedValues
.insert(v
);
568 .allowedValues
= combinedAllowedValues
572 int highestBit
= (int)fls(shiftedMask
) - 1;
573 int otherHighestBit
= (int)fls(otherShiftedMask
) - 1;
574 int otherMaskLength
= fls(otherMask
+ 1) - 1;
576 if (otherShiftedMask
< (1 << shift
)) {
578 // Start by shifting all the allowed values in self
579 int numberOfUnconstrainedBits
= shift
- otherHighestBit
- 1;
580 int maxUnconstrained
= 1 << numberOfUnconstrainedBits
;
581 std::set
<int> includingUnrestrictedBits
;
583 if (numberOfUnconstrainedBits
> 0) {
584 for (const int allowed
: allowedValues
) {
585 int shifted
= allowed
<< numberOfUnconstrainedBits
;
586 for (int unconstrained
= 0; unconstrained
< maxUnconstrained
; unconstrained
++) {
587 // Mix in unrestricted bits, then shift by [other]'s length
588 includingUnrestrictedBits
.insert((shifted
| unconstrained
) << otherMaskLength
);
592 for (const int allowed
: allowedValues
) {
593 // Shift all the values by [other]'s length
594 includingUnrestrictedBits
.insert(allowed
<< otherMaskLength
);
598 // Or in the values for [other]
599 std::unordered_set
<int> finalAllowedValues
;
600 for (const int allowed
: includingUnrestrictedBits
) {
601 for (const int otherValue
: otherAllowedValues
) {
602 finalAllowedValues
.insert(allowed
| otherValue
);
607 .mask
= ((1 << (highestBit
+ 1)) - 1) >> otherShift
,
609 .allowedValues
= finalAllowedValues
614 // [self....[other....self].....other].......
616 // * determine the set of bits allowed in the intersection
617 // * filter each set of values to keep only these
618 // * do the cross-product
620 // Bits in the intersection
622 int shiftDifference
= shift
- otherShift
;
623 std::set
<int> selfIntersectingBits
;
624 for (const int v
: allowedValues
) {
625 selfIntersectingBits
.insert(((v
<< shift
) & intersectionMask
) >> shift
);
627 std::set
<int> otherIntersectingBits
;
628 for (const int v
: otherAllowedValues
) {
629 otherIntersectingBits
.insert(((v
<< otherShift
) & intersectionMask
) >> shift
);
632 std::set
<int> intersectingBits
;
633 std::set_intersection(selfIntersectingBits
.begin(), selfIntersectingBits
.end(), otherIntersectingBits
.begin(), otherIntersectingBits
.end(), std::inserter(intersectingBits
, intersectingBits
.begin()));
635 std::unordered_set
<int> values
;
636 // Swift here was constructing a list of values for self and other
637 // filtered on which elements had the right values for intersectionMask
638 // This would avoid the n^3 loop at the expense of some storage... FIXME
639 for (const int intersecting
: intersectingBits
) {
640 int intersectingShifted
= intersecting
<< shift
;
641 for (int selfAllowed
: allowedValues
) {
642 if (((selfAllowed
<< shift
) & intersectionMask
) == intersectingShifted
) {
643 for (int otherAllowed
: otherAllowedValues
) {
644 if (((otherAllowed
<< otherShift
) & intersectionMask
) == intersectingShifted
) {
645 values
.insert((selfAllowed
<< shiftDifference
) | otherAllowed
);
653 .mask
= (shiftedMask
| otherShiftedMask
) >> otherShift
,
655 .allowedValues
= values
660 std::ostream
& operator << (std::ostream
& o
, const std::unordered_set
<int> & s
) {
669 std::ostream
& operator << (std::ostream
& o
, const Constraint
& c
) {
670 o
<< "(x >> " << c
.shift
<< " & " << c
.mask
<< " == " << c
.allowedValues
;
674 bool ConstraintSet::add(const Constraint
& c
) {
675 if (constraints
.find(c
) != constraints
.end()) {
678 constraints
.insert(c
);
679 if (mergedConstraint
.has_value()) {
680 mergedConstraint
= mergedConstraint
->intersecting(c
);
682 mergedConstraint
= c
;
687 void ConstraintSet::clear() {
689 mergedConstraint
.reset();
692 bool IMPCachesBuilder::isClassInteresting(const ObjCClass
& theClass
) const {
693 std::string_view
classNameStr(theClass
.className
);
694 if (theClass
.isMetaClass
) {
695 return neededMetaclasses
.find(classNameStr
) != neededMetaclasses
.end();
697 return neededClasses
.find(classNameStr
) != neededClasses
.end();
701 bool IMPCachesBuilder::isClassInterestingOrTracked(const ObjCClass
& theClass
) const {
702 std::string_view
classNameStr(theClass
.className
);
703 auto& neededArray
= theClass
.isMetaClass
? neededMetaclasses
: neededClasses
;
704 auto& trackedArray
= theClass
.isMetaClass
? trackedMetaclasses
: trackedClasses
;
706 return (neededArray
.find(classNameStr
) != neededArray
.end()) ||
707 (trackedArray
.find(classNameStr
) != trackedArray
.end());
710 void IMPCachesBuilder::addMethod(IMPCaches::ClassData
* classDataPtr
, const char* methodName
, const char* installName
, const char* className
, const char* catName
, bool inlined
, bool fromFlattening
) {
711 std::string_view
methodNameView(methodName
);
713 auto [selectorIterator
, success
] = selectors
.map
.try_emplace(methodNameView
, std::make_unique
<Selector
>());
714 IMPCaches::Selector
* thisSelectorData
= selectorIterator
->second
.get();
716 thisSelectorData
->name
= methodName
;
719 std::vector
<ClassData::Method
> & methods
= classDataPtr
->methods
;
720 // Check in the existing methods to see if the method already exists...
722 for (const ClassData::Method
& m
: methods
) {
723 if (m
.selector
== thisSelectorData
) {
724 // printf("Preventing insertion of duplicate %s.%s\n", classDataPtr->name, methodName);
731 ClassData::Method m
{
732 .installName
= installName
,
733 .className
= className
,
734 .categoryName
= catName
,
735 .selector
= thisSelectorData
,
736 .wasInlined
= inlined
,
737 .fromFlattening
= fromFlattening
740 thisSelectorData
->classes
.push_back(classDataPtr
);
741 classDataPtr
->methods
.push_back(m
);
745 void IMPCachesBuilder::inlineMethodIfNeeded(IMPCaches::ClassData
* classToInlineIn
, const char* classToInlineFrom
, const char* catToInlineFrom
, const char* installNameToInlineFrom
, const char* name
, std::set
<Selector
*>& seenSelectors
, bool isFlattening
) {
746 std::string_view
nameView(name
);
748 if ((nameView
== ".cxx_construct") || (nameView
== ".cxx_destruct")) {
749 // These selectors should never be inherited.
750 // object_cxxConstructFromClass / object_cxxDestructFromClass walk the class hierarchy and call them all.
755 bool shouldInline
= isFlattening
|| (selectorsToInline
.find(nameView
) != selectorsToInline
.end());
757 // The selector hasn't necessarily been seen at this point: eg. we don't build an IMP cache for UIView, so we haven't seen -[UIView superview] yet.
759 auto [it
, inserted
] = selectors
.map
.try_emplace(nameView
, std::make_unique
<Selector
>());
760 IMPCaches::Selector
* thisSelectorData
= it
->second
.get();
763 thisSelectorData
->name
= name
;
766 if (inserted
|| seenSelectors
.find(thisSelectorData
) == seenSelectors
.end()) {
767 seenSelectors
.insert(thisSelectorData
);
768 const bool inlined
= true;
769 addMethod(classToInlineIn
, name
, installNameToInlineFrom
, classToInlineFrom
, catToInlineFrom
, inlined
, isFlattening
);
774 // This goes through all the superclasses of the interesting classes so that
775 // we can track their methods for inlining.
776 // Since it goes through the superclasses, we also take this opportunity
777 // to add subclassses of duplicate classes to the duplicate classes set.
778 void IMPCachesBuilder::buildTrackedClasses(CacheBuilder::DylibInfo
& dylib
, const dyld3::MachOAnalyzer
* ma
) {
779 dyld3::MachOAnalyzer::VMAddrConverter vmAddrConverter
= ma
->makeVMAddrConverter(false);
780 ma
->forEachObjCClass(_diagnostics
, vmAddrConverter
, ^(Diagnostics
& diag
, uint64_t classVMAddr
, uint64_t classSuperclassVMAddr
, uint64_t classDataVMAddr
, const dyld3::MachOAnalyzer::ObjCClassInfo
& objcClass
, bool isMetaClass
) {
781 const uint8_t* classLocation
= (classVMAddr
- ma
->preferredLoadAddress()) + (uint8_t*)ma
;
783 // The class might not be in the map as we exclude classes with missing weak superclasses
784 auto classIt
= objcClasses
.find(classLocation
);
785 if ( classIt
== objcClasses
.end() )
787 const auto& theClass
= classIt
->second
;
788 ClassKey theClassKey
{
789 .name
= theClass
.className
,
790 .metaclass
= theClass
.isMetaClass
793 if (!isClassInteresting(theClass
)) return;
795 // Go through superclasses and add them to the tracked set.
796 const IMPCaches::IMPCachesBuilder::ObjCClass
* currentClass
= &theClass
;
797 bool rootClass
= false;
800 .name
= currentClass
->className
,
801 .metaclass
= currentClass
->isMetaClass
804 // If one of the superclasses of theClass is in the duplicate classes set,
805 // add theClass to the duplicate classes as well.
806 if (duplicateClasses
.find(k
) != duplicateClasses
.end()) {
807 duplicateClasses
.insert(theClassKey
);
810 if (currentClass
->isMetaClass
) {
811 trackedMetaclasses
.insert(currentClass
->className
);
813 trackedClasses
.insert(currentClass
->className
);
815 rootClass
= currentClass
->isRootClass
;
817 // The superclass might not be in the map as we exclude classes with missing weak superclasses
818 auto superclassIt
= objcClasses
.find(currentClass
->superclass
);
819 if ( superclassIt
== objcClasses
.end() )
821 currentClass
= &superclassIt
->second
;
823 } while (!rootClass
);
827 /// Parses the method lists of all the classes in @param dylib so that we populate the methods we want in each IMP cache skeleton.
828 void IMPCachesBuilder::populateMethodLists(CacheBuilder::DylibInfo
& dylib
, const dyld3::MachOAnalyzer
* ma
, int* duplicateClassCount
) {
829 const uint32_t pointerSize
= ma
->pointerSize();
830 dyld3::MachOAnalyzer::VMAddrConverter vmAddrConverter
= ma
->makeVMAddrConverter(false);
832 ma
->forEachObjCClass(_diagnostics
, vmAddrConverter
, ^(Diagnostics
& diag
, uint64_t classVMAddr
, uint64_t classSuperclassVMAddr
, uint64_t classDataVMAddr
, const dyld3::MachOAnalyzer::ObjCClassInfo
& objcClass
, bool isMetaClass
) {
834 const uint8_t* classLocation
= (classVMAddr
- ma
->preferredLoadAddress()) + (uint8_t*)ma
;
836 // The class might not be in the map as we exclude classes with missing weak superclasses
837 auto classIt
= objcClasses
.find(classLocation
);
838 if ( classIt
== objcClasses
.end() )
840 const auto& theClass
= classIt
->second
;
842 if (!isClassInterestingOrTracked(theClass
)) return;
843 bool interesting
= isClassInteresting(theClass
);
845 std::string_view
classNameString(theClass
.className
);
846 std::unique_ptr
<IMPCaches::ClassData
> thisData
= std::make_unique
<ClassData
>();
847 IMPCaches::ClassData
* thisDataPtr
= thisData
.get();
848 thisData
->name
= theClass
.className
;
849 thisData
->isMetaclass
= isMetaClass
;
850 thisData
->shouldGenerateImpCache
= interesting
;
852 ma
->forEachObjCMethod(objcClass
.baseMethodsVMAddr(pointerSize
), vmAddrConverter
, ^(uint64_t methodVMAddr
, const dyld3::MachOAnalyzer::ObjCMethod
& method
) {
853 dyld3::MachOAnalyzer::PrintableStringResult printableStringResult
;
854 const char* methodName
= ma
->getPrintableString(method
.nameVMAddr
, printableStringResult
);
855 if (printableStringResult
!= dyld3::MachOAnalyzer::PrintableStringResult::CanPrint
) {
859 const bool inlined
= false;
860 const bool fromFlattening
= false;
861 addMethod(thisDataPtr
, methodName
, ma
->installName(), theClass
.className
, NULL
, inlined
, fromFlattening
);
865 .name
= classNameString
,
866 .metaclass
= isMetaClass
868 assert(dylib
.impCachesClassData
.find(key
) == dylib
.impCachesClassData
.end());
870 if (duplicateClasses
.find(key
) != duplicateClasses
.end()) {
871 // We can't just set shouldGenerateImpCache to false ; we do it later
872 // when we have built the flattening hierarchies in order to drop
873 // any related classes as well.
875 thisData
->isPartOfDuplicateSet
= true;
876 *duplicateClassCount
+= 1;
879 dylib
.impCachesClassData
[key
] = std::move(thisData
);
883 /// Parses all the categories within the same image as a class so that we can add the corresponding methods to the IMP cache skeletons, too.
884 void IMPCachesBuilder::attachCategories(CacheBuilder::DylibInfo
& dylib
, const dyld3::MachOAnalyzer
* ma
) {
885 dyld3::MachOAnalyzer::VMAddrConverter vmAddrConverter
= ma
->makeVMAddrConverter(false);
886 ma
->forEachObjCCategory(_diagnostics
, vmAddrConverter
, ^(Diagnostics
& diag
, uint64_t categoryVMAddr
, const dyld3::MachOAnalyzer::ObjCCategory
& objcCategory
) {
888 const uint8_t* categoryLocation
= (categoryVMAddr
- ma
->preferredLoadAddress()) + (uint8_t*)ma
;
889 ObjCCategory previouslyFoundCategory
= objcCategories
.at(categoryLocation
);
891 __block
dyld3::MachOAnalyzer::PrintableStringResult printableStringResult
;
892 const char* catName
= ma
->getPrintableString(objcCategory
.nameVMAddr
, printableStringResult
);
894 if (previouslyFoundCategory
.classMA
!= ma
) {
895 // Cross-image category
899 // The class might not be in the map as we exclude classes with missing weak superclasses
900 auto classIt
= objcClasses
.find(previouslyFoundCategory
.cls
);
901 if ( classIt
== objcClasses
.end() )
903 const auto& theClass
= classIt
->second
;
905 auto& theMetaClass
= objcClasses
[theClass
.metaClass
];
906 auto classNameString
= std::string_view(theClass
.className
);
908 if (isClassInterestingOrTracked(theClass
)) {
910 .name
= classNameString
,
913 ClassData
* clsData
= dylib
.impCachesClassData
.at(key
).get();
915 ma
->forEachObjCMethod(objcCategory
.instanceMethodsVMAddr
, vmAddrConverter
, ^(uint64_t methodVMAddr
, const dyld3::MachOAnalyzer::ObjCMethod
& method
) {
916 // The config file should specify only classes without cross-image categories, so we should have found a class here
917 assert(clsData
!= NULL
);
918 const char* methodName
= ma
->getPrintableString(method
.nameVMAddr
, printableStringResult
);
919 const bool inlined
= false;
920 const bool fromFlattening
= false;
921 addMethod(clsData
, methodName
, ma
->installName(), theClass
.className
, catName
, inlined
, fromFlattening
);
924 if (isClassInterestingOrTracked(theMetaClass
)) {
926 .name
= classNameString
,
929 ClassData
* metaclsData
= dylib
.impCachesClassData
.at(key
).get();
931 ma
->forEachObjCMethod(objcCategory
.classMethodsVMAddr
, vmAddrConverter
, ^(uint64_t methodVMAddr
, const dyld3::MachOAnalyzer::ObjCMethod
& method
) {
932 assert(metaclsData
!= NULL
);
933 const char* methodName
= ma
->getPrintableString(method
.nameVMAddr
, printableStringResult
);
934 const bool inlined
= false;
935 const bool fromFlattening
= false;
936 addMethod(metaclsData
, methodName
, ma
->installName(), theMetaClass
.className
, catName
, inlined
, fromFlattening
);
942 struct FlatteningRootLookupResult
{
943 bool isInFlatteningHierarchy
;
945 const uint8_t * flatteningRootSuperclassLocation
;
946 ClassData::ClassLocator flatteningRootSuperclass
;
947 std::set
<std::string_view
> superclassesInFlatteningHierarchy
;
948 const char * flatteningRootName
;
951 static FlatteningRootLookupResult
findFlatteningRoot(const uint8_t* classLocation
,
952 const std::unordered_map
<const uint8_t*, IMPCachesBuilder::ObjCClass
> & objcClasses
,
953 const std::unordered_set
<std::string_view
> & classHierarchiesToFlatten
,
954 const std::unordered_set
<std::string_view
> & metaclassHierarchiesToFlatten
,
955 bool storeSuperclasses
) {
956 FlatteningRootLookupResult result
;
957 const uint8_t* superclassLocation
= classLocation
;
958 __block
bool rootClass
= false;
959 bool success
= false;
962 const auto it
= objcClasses
.find(superclassLocation
);
963 if (it
== objcClasses
.end()) {
966 const IMPCachesBuilder::ObjCClass
& iteratedClass
= it
->second
;
967 rootClass
= iteratedClass
.isRootClass
;
968 superclassLocation
= iteratedClass
.superclass
;
970 if (storeSuperclasses
) {
971 result
.superclassesInFlatteningHierarchy
.insert(iteratedClass
.className
);
974 bool metaClassBeingFlattened
= iteratedClass
.isMetaClass
&& metaclassHierarchiesToFlatten
.find(iteratedClass
.className
) != metaclassHierarchiesToFlatten
.end();
975 bool classBeingFlattened
= !iteratedClass
.isMetaClass
&& classHierarchiesToFlatten
.find(iteratedClass
.className
) != classHierarchiesToFlatten
.end();
977 if (metaClassBeingFlattened
|| classBeingFlattened
) {
978 result
.flatteningRootName
= iteratedClass
.className
;
979 result
.flatteningRootSuperclassLocation
= iteratedClass
.superclass
;
980 result
.flatteningRootSuperclass
= iteratedClass
.superclassLocator();
986 result
.isInFlatteningHierarchy
= success
;
990 /// Inline selectors from parent classes into child classes for performance
991 void IMPCachesBuilder::inlineSelectors(CacheBuilder::DylibInfo
& dylib
, std::unordered_map
<std::string_view
, CacheBuilder::DylibInfo
*> & dylibsByInstallName
, const dyld3::MachOAnalyzer
* ma
) {
992 dyld3::MachOAnalyzer::VMAddrConverter vmAddrConverter
= ma
->makeVMAddrConverter(false);
993 ma
->forEachObjCClass(_diagnostics
, vmAddrConverter
, ^(Diagnostics
& diag
, uint64_t classVMAddr
, uint64_t classSuperclassVMAddr
, uint64_t classDataVMAddr
, const dyld3::MachOAnalyzer::ObjCClassInfo
& objcClass
, bool isMetaClass
) {
995 const uint8_t* classLocation
= (classVMAddr
- ma
->preferredLoadAddress()) + (uint8_t*)ma
;
997 // The class might not be in the map as we exclude classes with missing weak superclasses
998 auto classIt
= objcClasses
.find(classLocation
);
999 if ( classIt
== objcClasses
.end() )
1001 const auto& theClass
= classIt
->second
;
1003 if (!isClassInteresting(theClass
)) {
1007 std::string_view
classNameString(theClass
.className
);
1009 .name
= classNameString
,
1010 .metaclass
= isMetaClass
1013 IMPCaches::ClassData
* thisDataPtr
= dylib
.impCachesClassData
.at(key
).get();
1014 assert(thisDataPtr
!= NULL
);
1016 __block
std::set
<Selector
*> seenSelectors
;
1017 for (const ClassData::Method
& method
: thisDataPtr
->methods
) {
1018 seenSelectors
.insert(method
.selector
);
1021 // Check the superclass hierarchy to see if we're in a flattened hierarchy
1022 // (meaning we should inline all of the selectors up to the flattening root)
1023 FlatteningRootLookupResult flatteningInfo
= findFlatteningRoot(classLocation
, objcClasses
, classHierarchiesToFlatten
, metaclassHierarchiesToFlatten
, false);
1025 if (flatteningInfo
.isInFlatteningHierarchy
) {
1026 // try again and record superclasses this time
1027 // (maybe premature optimization, but given the small number of classes where flattening
1028 // is actually happening I did not want to gather this set every time)
1030 flatteningInfo
= findFlatteningRoot(classLocation
, objcClasses
, classHierarchiesToFlatten
, metaclassHierarchiesToFlatten
, true);
1032 assert(flatteningInfo
.isInFlatteningHierarchy
);
1034 thisDataPtr
->flatteningRootSuperclass
= flatteningInfo
.flatteningRootSuperclass
;
1035 thisDataPtr
->flatteningRootName
= flatteningInfo
.flatteningRootName
;
1036 thisDataPtr
->flattenedSuperclasses
= flatteningInfo
.superclassesInFlatteningHierarchy
;
1039 // Iterate again to actually flatten/inline the selectors
1040 const uint8_t *superclassLocation
= classLocation
;
1041 __block
const dyld3::MachOAnalyzer
* currentMA
= ma
;
1042 bool isRootClass
= false;
1043 bool isFlattening
= flatteningInfo
.isInFlatteningHierarchy
;
1045 while (!isRootClass
) {
1046 const auto it
= objcClasses
.find(superclassLocation
);
1047 if (it
== objcClasses
.end()) {
1050 ObjCClass
& iteratedClass
= it
->second
;
1051 isRootClass
= iteratedClass
.isRootClass
;
1053 CacheBuilder::DylibInfo
* classDylib
= dylibsByInstallName
.at(currentMA
->installName());
1054 ClassKey keyForIteratedClass
{
1055 .name
= std::string_view(iteratedClass
.className
),
1056 .metaclass
= iteratedClass
.isMetaClass
1058 auto classDataIt
= classDylib
->impCachesClassData
.find(keyForIteratedClass
);
1060 // We should have added this class to our data in populateMethodLists()
1061 assert(classDataIt
!= classDylib
->impCachesClassData
.end());
1063 IMPCaches::ClassData
* classData
= classDataIt
->second
.get();
1065 for (const ClassData::Method
& m
: classData
->methods
) {
1066 // If the method found in the superclass was inlined from a further superclass, we'll inline it
1067 // when we reach that class (otherwise the install name / class name the method is coming from will be wrong)
1068 if (!m
.wasInlined
) {
1069 inlineMethodIfNeeded(thisDataPtr
, m
.className
, m
.categoryName
, currentMA
->installName(), m
.selector
->name
, seenSelectors
, isFlattening
);
1073 currentMA
= iteratedClass
.superclassMA
;
1074 assert(isRootClass
|| (currentMA
!= nullptr));
1075 superclassLocation
= iteratedClass
.superclass
;
1077 if (isFlattening
&& (iteratedClass
.superclass
== flatteningInfo
.flatteningRootSuperclassLocation
)) {
1078 // we reached the flattening root, turn flattening off
1079 isFlattening
= false;
1085 /// Parses all the source dylibs to fill the IMP cache skeletons with all the methods we want to have there.
1086 bool IMPCachesBuilder::parseDylibs(Diagnostics
& diag
) {
1087 std::unordered_map
<std::string_view
, CacheBuilder::DylibInfo
*> dylibsByInstallName
;
1089 for (CacheBuilder::DylibInfo
& d
: dylibs
) {
1090 const DyldSharedCache::MappedMachO
& mapped
= d
.input
->mappedFile
;
1091 const dyld3::MachOAnalyzer
* ma
= mapped
.mh
;
1093 dylibsByInstallName
.insert(std::make_pair(std::string_view(ma
->installName()), &d
));
1095 // Build the set of tracked classes (interesting classes + their superclasses)
1096 buildTrackedClasses(d
, ma
);
1099 int totalDuplicateClassCount
= 0;
1101 for (CacheBuilder::DylibInfo
& d
: dylibs
) {
1102 const DyldSharedCache::MappedMachO
& mapped
= d
.input
->mappedFile
;
1103 const dyld3::MachOAnalyzer
* ma
= mapped
.mh
;
1105 int duplicateClassCount
= 0;
1106 // First, go through all classes and populate their method lists.
1107 populateMethodLists(d
, ma
, &duplicateClassCount
);
1108 totalDuplicateClassCount
+= duplicateClassCount
;
1110 // Now go through all categories and attach them as well
1111 attachCategories(d
, ma
);
1114 _diagnostics
.verbose("[IMP caches] Not generating caches for %d duplicate classes or children of duplicate classes\n", totalDuplicateClassCount
);
1116 // Ensure that all the selectors will fit on 16 MB as that's the constant
1117 // embedded in the placement algorithm
1118 uint32_t totalSize
= 0;
1119 for (const auto& [k
,v
] : selectors
.map
) {
1120 totalSize
+= v
->size();
1122 if (totalSize
>= (1 << 24)) {
1123 diag
.warning("Dropping all IMP caches ; too many selectors\n");
1127 for (CacheBuilder::DylibInfo
& d
: dylibs
) {
1128 const DyldSharedCache::MappedMachO
& mapped
= d
.input
->mappedFile
;
1129 const dyld3::MachOAnalyzer
* ma
= mapped
.mh
;
1131 // Now that all categories are attached, handle any selector inheritance if needed
1132 // (Do this after category attachment so that inlined selectors don't override categories)
1134 inlineSelectors(d
, dylibsByInstallName
, ma
);
1137 removeUninterestingClasses();
1140 for (CacheBuilder::DylibInfo
& d
: dylibs
) {
1141 count
+= d
.impCachesClassData
.size();
1144 constexpr bool logAllSelectors
= false;
1146 diag
.verbose("[IMP Caches] parsed %u classes\n", count
);
1147 for (CacheBuilder::DylibInfo
& d
: dylibs
) {
1148 for (auto it
= d
.impCachesClassData
.begin() ; it
!= d
.impCachesClassData
.end() ; it
++) {
1149 auto& c
= it
->second
;
1150 c
->didFinishAddingMethods();
1151 if (logAllSelectors
) {
1152 printf("%s\n", c
->description().c_str());
1153 std::vector
<ClassData::Method
> sortedMethods(c
->methods
.begin(), c
->methods
.end());
1154 std::sort(sortedMethods
.begin(), sortedMethods
.end(), [](const ClassData::Method
& a
, const ClassData::Method
& b
) {
1155 return strcmp(a
.selector
->name
, b
.selector
->name
) < 0;
1157 for (const ClassData::Method
& m
: sortedMethods
) {
1158 const Selector
* s
= m
.selector
;
1159 printf(" %s", s
->name
);
1160 if (m
.categoryName
!= nullptr) {
1161 printf(" (from %s::%s+%s)\n", m
.installName
, m
.className
, m
.categoryName
);
1162 } else if (m
.className
!= c
->name
) {
1163 printf(" (from %s::%s)\n", m
.installName
, m
.className
);
1172 for (auto& s
: selectorsToInline
) {
1173 auto selectorsIt
= selectors
.map
.find(s
);
1174 if (selectorsIt
== selectors
.map
.end()) {
1175 diag
.warning("Requested selector to inline not found in any classes: %s\n", s
.data());
1178 inlinedSelectors
.push_back(selectors
.map
.at(s
).get());
1180 std::sort(inlinedSelectors
.begin(), inlinedSelectors
.end(), [](const Selector
* a
, const Selector
*b
) {
1181 return a
->offset
< b
->offset
;
1187 IMPCachesBuilder::TargetClassFindingResult
IMPCachesBuilder::findTargetClass(const dyld3::MachOAnalyzer
* ma
, uint64_t targetClassVMAddr
, uint64_t targetClassPointerVMAddr
,const char* logContext
, const std::unordered_map
<uint64_t, uint64_t> & bindLocations
, std::vector
<BindTarget
> & bindTargets
, std::unordered_map
<std::string
, DylibAndDeps
> &dylibMap
) {
1188 constexpr bool log
= false;
1189 uint64_t loadAddress
= ma
->preferredLoadAddress();
1191 uint64_t superclassRuntimeOffset
= targetClassPointerVMAddr
- loadAddress
;
1192 auto bindIt
= bindLocations
.find(superclassRuntimeOffset
);
1194 if ( bindIt
== bindLocations
.end() ) {
1195 if (targetClassVMAddr
!= 0) {
1196 // A rebase, ie, a fixup to a class in this dylib
1198 printf("%s: %s -> this dylib\n", ma
->installName(), logContext
);
1199 superclassRuntimeOffset
= targetClassVMAddr
- loadAddress
;
1200 return TargetClassFindingResult
{
1203 .location
= (const uint8_t*)ma
+ superclassRuntimeOffset
1206 // A bind, ie, a fixup to a class in another dylib
1207 return TargetClassFindingResult
{ .success
= false };
1211 const BindTarget
& bindTarget
= bindTargets
[bindIt
->second
];
1213 printf("%s: %s -> %s: %s\n", ma
->installName(), logContext
,
1214 bindTarget
.targetDylib
->installName(), bindTarget
.symbolName
.c_str());
1216 dyld3::MachOLoaded::DependentToMachOLoaded finder
= ^(const dyld3::MachOLoaded
* mh
, uint32_t depIndex
) {
1217 auto dylibIt
= dylibMap
.find(mh
->installName());
1218 if ( dylibIt
== dylibMap
.end() ) {
1219 // Missing weak dylib?
1220 return (const dyld3::MachOLoaded
*)nullptr;
1223 if ( depIndex
>= dylibIt
->second
.dependentLibraries
.size() ) {
1224 // Out of bounds dependent
1228 auto depIt
= dylibMap
.find(dylibIt
->second
.dependentLibraries
[depIndex
]);
1229 if ( depIt
== dylibMap
.end() ) {
1230 // Missing weak dylib?
1231 return (const dyld3::MachOLoaded
*)nullptr;
1233 return (const dyld3::MachOLoaded
*)depIt
->second
.ma
;
1236 if (bindTarget
.targetDylib
!= nullptr) {
1237 dyld3::MachOAnalyzer::FoundSymbol foundInfo
;
1238 bool found
= bindTarget
.targetDylib
->findExportedSymbol(_diagnostics
, bindTarget
.symbolName
.c_str(),
1239 false, foundInfo
, finder
);
1241 if ( !bindTarget
.isWeakImport
) {
1242 _diagnostics
.verbose("Could not find non-weak target class: '%s'\n", bindTarget
.symbolName
.c_str());
1244 // We couldn't find the target class. This is an error if the symbol is not a weak-import, but
1245 // we'll let symbol resolution work that out later if we really have a missing symbol which matters
1246 // For IMP caches, we'll just ignore this class.
1247 return TargetClassFindingResult
{ .success
= false };
1250 assert(foundInfo
.kind
== dyld3::MachOAnalyzer::FoundSymbol::Kind::headerOffset
);
1251 if (foundInfo
.foundInDylib
== nullptr) {
1253 printf("null foundInDylib\n");
1255 return TargetClassFindingResult
{
1257 .foundInDylib
= foundInfo
.foundInDylib
,
1258 .location
= (uint8_t*)foundInfo
.foundInDylib
+ foundInfo
.value
,
1262 printf("No target dylib found for %s\n", logContext
);
1263 return TargetClassFindingResult
{ .success
= false };
1267 void IMPCachesBuilder::buildClassesMap(Diagnostics
& diag
) {
1268 const uint32_t pointerSize
= 8;
1270 __block
std::unordered_map
<std::string
, DylibAndDeps
> dylibMap
;
1272 for (CacheBuilder::DylibInfo
& dylib
: dylibs
) {
1273 const dyld3::MachOAnalyzer
* ma
= dylib
.input
->mappedFile
.mh
;
1274 __block
std::vector
<std::string
> dependentLibraries
;
1275 ma
->forEachDependentDylib(^(const char* loadPath
, bool isWeak
, bool isReExport
,
1276 bool isUpward
, uint32_t compatVersion
, uint32_t curVersion
, bool &stop
) {
1277 dependentLibraries
.push_back(loadPath
);
1279 dylibMap
[ma
->installName()] = { ma
, dependentLibraries
};
1282 const bool log
= false;
1284 __block ClassSet seenClasses
;
1286 for (CacheBuilder::DylibInfo
& dylib
: dylibs
) {
1287 const dyld3::MachOAnalyzer
* ma
= dylib
.input
->mappedFile
.mh
;
1288 __block
std::vector
<const dyld3::MachOAnalyzer
*> dependentLibraries
;
1289 ma
->forEachDependentDylib(^(const char* loadPath
, bool isWeak
, bool isReExport
,
1290 bool isUpward
, uint32_t compatVersion
, uint32_t curVersion
, bool& stop
) {
1291 auto it
= dylibMap
.find(loadPath
);
1292 if (it
== dylibMap
.end()) {
1293 char resolvedSymlinkPath
[PATH_MAX
];
1295 // The dylib may link on a dylib which has moved and has a symlink to a new path.
1296 if (_fileSystem
.getRealPath(loadPath
, resolvedSymlinkPath
)) {
1297 it
= dylibMap
.find(resolvedSymlinkPath
);
1300 if (it
== dylibMap
.end()) {
1302 dependentLibraries
.push_back(nullptr);
1304 dependentLibraries
.push_back(it
->second
.ma
);
1307 __block
std::vector
<BindTarget
> bindTargets
;
1309 // Map from vmOffsets in this binary to which bind target they point to
1310 __block
std::unordered_map
<uint64_t, uint64_t> bindLocations
;
1311 if ( ma
->hasChainedFixups() ) {
1313 ma
->forEachChainedFixupTarget(diag
, ^(int libOrdinal
, const char* symbolName
, uint64_t addend
, bool weakImport
, bool &stop
) {
1314 if (libOrdinal
== BIND_SPECIAL_DYLIB_SELF
) {
1315 bindTargets
.push_back({ symbolName
, ma
, weakImport
});
1316 } else if ( libOrdinal
< 0 ) {
1317 // A special ordinal such as weak. Just put in a placeholder for now
1318 bindTargets
.push_back({ symbolName
, nullptr, weakImport
});
1320 assert(libOrdinal
<= (int)dependentLibraries
.size());
1321 const dyld3::MachOAnalyzer
*target
= dependentLibraries
[libOrdinal
-1];
1322 assert(weakImport
|| (target
!= nullptr));
1323 bindTargets
.push_back({ symbolName
, target
, weakImport
});
1326 ma
->withChainStarts(diag
, 0, ^(const dyld_chained_starts_in_image
* startsInfo
) {
1327 ma
->forEachFixupInAllChains(diag
, startsInfo
, false, ^(dyld3::MachOLoaded::ChainedFixupPointerOnDisk
* fixupLoc
,
1328 const dyld_chained_starts_in_segment
* segInfo
, bool& fixupsStop
) {
1329 uint64_t fixupOffset
= (uint8_t*)fixupLoc
- (uint8_t*)ma
;
1330 uint32_t bindOrdinal
;
1332 if ( fixupLoc
->isBind(segInfo
->pointer_format
, bindOrdinal
, addend
) ) {
1333 if ( bindOrdinal
< bindTargets
.size() ) {
1334 bindLocations
[fixupOffset
] = bindOrdinal
;
1337 diag
.error("out of range bind ordinal %d (max %lu)", bindOrdinal
, bindTargets
.size());
1344 // Non-arm64e (for now...)
1345 ma
->forEachBind(diag
, ^(uint64_t runtimeOffset
, int libOrdinal
, const char* symbolName
,
1346 bool weakImport
, bool lazyBind
, uint64_t addend
, bool &stop
) {
1348 printf("0x%llx %s: %s\n", runtimeOffset
, ma
->installName(), symbolName
);
1349 if ( libOrdinal
< 0 ) {
1350 // A special ordinal such as weak. Just put in a placeholder for now
1351 bindTargets
.push_back({ symbolName
, nullptr, weakImport
});
1352 } else if (libOrdinal
== BIND_SPECIAL_DYLIB_SELF
) {
1353 bindTargets
.push_back({ symbolName
, ma
, weakImport
});
1355 assert(libOrdinal
<= (int)dependentLibraries
.size());
1356 bindTargets
.push_back({ symbolName
, dependentLibraries
[libOrdinal
- 1], weakImport
});
1358 bindLocations
[runtimeOffset
] = bindTargets
.size() - 1;
1359 }, ^(const char* symbolName
) {
1360 // We don't need this as its only for weak def coalescing
1363 const uint64_t loadAddress
= ma
->preferredLoadAddress();
1364 dyld3::MachOAnalyzer::VMAddrConverter vmAddrConverter
= ma
->makeVMAddrConverter(false);
1365 ma
->forEachObjCClass(diag
, vmAddrConverter
, ^(Diagnostics
& maDiag
, uint64_t classVMAddr
,
1366 uint64_t classSuperclassVMAddr
, uint64_t classDataVMAddr
,
1367 const dyld3::MachOAnalyzer::ObjCClassInfo
& objcClass
, bool isMetaClass
) {
1368 uint64_t classNameVMAddr
= objcClass
.nameVMAddr(pointerSize
);
1369 dyld3::MachOAnalyzer::PrintableStringResult classNameResult
= dyld3::MachOAnalyzer::PrintableStringResult::UnknownSection
;
1370 const char* className
= ma
->getPrintableString(classNameVMAddr
, classNameResult
);
1371 assert(classNameResult
== dyld3::MachOAnalyzer::PrintableStringResult::CanPrint
);
1373 printf("%s: %s\n", ma
->installName(), className
);
1375 const uint32_t RO_ROOT
= (1<<1);
1376 bool isRootClass
= (objcClass
.flags(pointerSize
) & RO_ROOT
) != 0;
1377 auto result
= findTargetClass(ma
, objcClass
.superclassVMAddr
, classSuperclassVMAddr
, className
, bindLocations
, bindTargets
, dylibMap
);
1379 if (result
.success
|| isRootClass
) {
1380 uint64_t classRuntimeOffset
= classVMAddr
- loadAddress
;
1381 const uint8_t* classLocation
= (const uint8_t*)ma
+ classRuntimeOffset
;
1382 objcClasses
[classLocation
] = ObjCClass
{
1383 .superclassMA
= (const dyld3::MachOAnalyzer
*) result
.foundInDylib
,
1384 .metaClass
= isMetaClass
? nullptr : ((const uint8_t*)ma
+ objcClass
.isaVMAddr
- loadAddress
),
1385 .superclass
= result
.location
,
1386 .methodListVMaddr
= objcClass
.baseMethodsVMAddr(pointerSize
),
1387 .className
= className
,
1388 .isRootClass
= isRootClass
,
1389 .isMetaClass
= isMetaClass
,
1394 .metaclass
= isMetaClass
1397 auto [it
, success
] = seenClasses
.insert(k
);
1399 duplicateClasses
.insert(k
);
1404 ma
->forEachObjCCategory(diag
, vmAddrConverter
, ^(Diagnostics
& maDiag
, uint64_t categoryVMAddr
, const dyld3::MachOAnalyzer::ObjCCategory
& objcCategory
) {
1405 dyld3::MachOAnalyzer::PrintableStringResult catNameResult
= dyld3::MachOAnalyzer::PrintableStringResult::UnknownSection
;
1406 const char *catName
= ma
->getPrintableString(objcCategory
.nameVMAddr
, catNameResult
);
1408 uint64_t clsVMAddr
= categoryVMAddr
+ pointerSize
;
1409 TargetClassFindingResult result
= findTargetClass(ma
, objcCategory
.clsVMAddr
, clsVMAddr
, catName
, bindLocations
, bindTargets
, dylibMap
);
1410 uint64_t catRuntimeOffset
= categoryVMAddr
- loadAddress
;
1411 const uint8_t *catLocation
= (const uint8_t*)ma
+ catRuntimeOffset
;
1412 if (result
.success
) {
1413 objcCategories
[catLocation
] = ObjCCategory
{
1414 .classMA
= (const dyld3::MachOAnalyzer
*)result
.foundInDylib
,
1415 .cls
= result
.location
1418 // This happens for categories on weak classes that may be missing.
1419 objcCategories
[catLocation
] = ObjCCategory
{
1420 .classMA
= (const dyld3::MachOAnalyzer
*)nullptr,
1427 // Print the class hierarchy just to see that we found everything
1429 for (const auto& [location
, theClass
] : objcClasses
) {
1430 printf("%p %c%s", location
, theClass
.isMetaClass
? '+' : '-', theClass
.className
);
1431 bool isRoot
= theClass
.isRootClass
;
1432 const uint8_t* superclass
= theClass
.superclass
;
1434 auto it
= objcClasses
.find(superclass
);
1435 // assert(it != objcClasses.end());
1436 if (it
== objcClasses
.end()) {
1437 printf(": missing");
1440 printf(" : %c%s", it
->second
.isMetaClass
? '+' : '-', it
->second
.className
);
1441 isRoot
= it
->second
.isRootClass
;
1442 superclass
= it
->second
.superclass
;
1449 const std::string
* IMPCachesBuilder::nameAndIsMetaclassPairFromNode(const dyld3::json::Node
& node
, bool* metaclass
) {
1450 const dyld3::json::Node
& metaclassNode
= dyld3::json::getRequiredValue(_diagnostics
, node
, "metaclass");
1451 if (_diagnostics
.hasError()) return nullptr;
1453 if (metaclass
!= nullptr) *metaclass
= dyld3::json::parseRequiredInt(_diagnostics
, metaclassNode
) != 0;
1454 const dyld3::json::Node
& nameNode
= dyld3::json::getRequiredValue(_diagnostics
, node
, "name");
1455 if (_diagnostics
.hasError()) return nullptr;
1457 return &(dyld3::json::parseRequiredString(_diagnostics
, nameNode
));
1460 IMPCachesBuilder::IMPCachesBuilder(std::vector
<CacheBuilder::DylibInfo
>& allDylibs
, const dyld3::json::Node
& optimizerConfiguration
, Diagnostics
& diag
, TimeRecorder
& timeRecorder
, const dyld3::closure::FileSystem
& fileSystem
) : dylibs(allDylibs
), _diagnostics(diag
), _timeRecorder(timeRecorder
), _fileSystem(fileSystem
) {
1461 const dyld3::json::Node
* version
= dyld3::json::getOptionalValue(diag
, optimizerConfiguration
, "version");
1462 int64_t versionInt
= (version
!= NULL
) ? dyld3::json::parseRequiredInt(diag
, *version
) : 1;
1463 if (versionInt
== 2) {
1464 // v2 has a single neededClasses array, with a key to know if it's a metaclass
1465 // or class. This lets us order them by importance so that we handle the important
1466 // cases first in the algorithm, while it's still easy to place things (as we process
1467 // more classes, constraints build up and we risk dropping difficult classes)
1469 const dyld3::json::Node
& classes
= dyld3::json::getRequiredValue(diag
, optimizerConfiguration
, "neededClasses");
1470 if (diag
.hasError()) return;
1473 for (const dyld3::json::Node
& n
: classes
.array
) {
1474 bool metaclass
= false;
1475 const std::string
*name
= nameAndIsMetaclassPairFromNode(n
, &metaclass
);
1476 if (name
!= nullptr) {
1478 neededMetaclasses
[*name
] = i
++;
1480 neededClasses
[*name
] = i
++;
1483 // nameAndIsMetaclassPairFromNode already logs an error in this case,
1484 // so nothing to do here
1488 auto metaclasses
= optimizerConfiguration
.map
.find("neededMetaclasses");
1491 if (metaclasses
!= optimizerConfiguration
.map
.cend()) {
1492 for (const dyld3::json::Node
& n
: metaclasses
->second
.array
) {
1493 neededMetaclasses
[n
.value
] = i
++;
1497 auto classes
= optimizerConfiguration
.map
.find("neededClasses");
1499 if (classes
!= optimizerConfiguration
.map
.cend()) {
1500 for (const dyld3::json::Node
& n
: classes
->second
.array
) {
1501 neededClasses
[n
.value
] = i
++;
1506 auto sels
= optimizerConfiguration
.map
.find("selectorsToInline");
1507 if (sels
!= optimizerConfiguration
.map
.cend()) {
1508 for (const dyld3::json::Node
& n
: sels
->second
.array
) {
1509 selectorsToInline
.insert(n
.value
);
1513 const dyld3::json::Node
* classHierarchiesToFlattenNode
= dyld3::json::getOptionalValue(diag
, optimizerConfiguration
, "flatteningRoots");
1514 if (classHierarchiesToFlattenNode
!= nullptr) {
1515 for (const dyld3::json::Node
& n
: classHierarchiesToFlattenNode
->array
) {
1516 bool metaclass
= false;
1517 const std::string
*name
= nameAndIsMetaclassPairFromNode(n
, &metaclass
);
1519 metaclassHierarchiesToFlatten
.insert(*name
);
1521 classHierarchiesToFlatten
.insert(*name
);
1525 // For old files, we assume we should flatten OS_object, this was implied
1526 // before we decided to extend this set.
1528 metaclassHierarchiesToFlatten
.insert("OS_object");
1529 classHierarchiesToFlatten
.insert("OS_object");
1533 struct BacktrackingState
{
1534 // Index into the next array that we are currently trying
1535 int currentAttemptIndex
;
1537 // Possible placement attempts for this class
1538 std::vector
<typename
IMPCaches::ClassData::PlacementAttempt
> attempts
;
1540 // What we had to modify to attempt the current placement. This needs
1541 // to be reversed if we backtrack.
1542 std::optional
<IMPCaches::ClassData::PlacementAttempt::Result
> result
;
1544 // We also need to store the state of the random number generator,
1545 // because when reverting to a snapshot we need to apply exactly the same
1546 // steps as last time.
1547 std::minstd_rand randomNumberGenerator
;
1549 bool operator== (const BacktrackingState
& other
) {
1550 // Don't test attempts, they will be the same as long as the class index is
1551 // the same, and we never compare states for different indices
1552 return (currentAttemptIndex
== other
.currentAttemptIndex
) && (randomNumberGenerator
== other
.randomNumberGenerator
);
1555 bool operator!= (const BacktrackingState
& other
) {
1556 return !(*this == other
);
1560 static void backtrack(std::vector
<BacktrackingState
>& backtrackingStack
, int& numberOfDroppedClasses
, std::vector
<IMPCaches::ClassData
*>& allClasses
) {
1561 int i
= (int)backtrackingStack
.size() - 1;
1563 BacktrackingState
& last
= backtrackingStack
.back();
1565 // backtracking over a skipped class
1566 numberOfDroppedClasses
--;
1568 allClasses
[i
]->backtrack(*(last
.result
));
1569 backtrackingStack
.pop_back();
1572 template <typename Func
>
1573 static void forEachClassInFlatteningHierarchy(const IMPCaches::ClassData
*parentClass
, const std::vector
<IMPCaches::ClassData
*>& allClasses
, const Func
&callback
) {
1574 if (!parentClass
->flatteningRootSuperclass
.has_value()) {
1577 for (IMPCaches::ClassData
* c
: allClasses
) {
1578 // If c has parentClass in its flattening hierarchy
1579 if ((c
!= parentClass
)
1580 && c
->flatteningRootSuperclass
.has_value()
1581 && (*(c
->flatteningRootSuperclass
) == *(parentClass
->flatteningRootSuperclass
))
1582 && (c
->flatteningRootName
== parentClass
->flatteningRootName
)
1583 && (c
->flattenedSuperclasses
.find(parentClass
->name
) != c
->flattenedSuperclasses
.end())) {
1589 static void dropClass(Diagnostics
& diagnostics
, unsigned long& currentClassIndex
, int& numberOfDroppedClasses
, std::vector
<BacktrackingState
>& backtrackingStack
, std::minstd_rand
& randomNumberGenerator
, std::vector
<IMPCaches::ClassData
*>& allClasses
, const char* reason
) {
1590 IMPCaches::ClassData
* droppedClass
= allClasses
[currentClassIndex
];
1592 diagnostics
.verbose("%lu: dropping class %s (%s) because %s\n", currentClassIndex
, droppedClass
->name
, droppedClass
->isMetaclass
? "metaclass" : "class", reason
);
1593 droppedClass
->shouldGenerateImpCache
= false;
1595 // If we are inside a flattened hierarchy, we need to also drop any classes inheriting from us,
1596 // as objc relies on all classes inside a flattened hierarchy having constant caches to do invalidation
1598 forEachClassInFlatteningHierarchy(droppedClass
, allClasses
, [&numberOfDroppedClasses
, &diagnostics
, currentClassIndex
](IMPCaches::ClassData
*c
){
1600 // We could undrop them if we undrop droppedClass while backtracking or restoring
1601 // a snapshot, but it's not worth the effort.
1603 if (c
->shouldGenerateImpCache
) {
1604 numberOfDroppedClasses
++;
1605 c
->shouldGenerateImpCache
= false;
1606 c
->droppedBecauseFlatteningSuperclassWasDropped
= true;
1607 diagnostics
.verbose("%lu: also dropping %s (%s) in the same flattening hierarchy\n", currentClassIndex
, c
->name
, c
->isMetaclass
? "metaclass" : "class");
1611 currentClassIndex
++;
1612 numberOfDroppedClasses
++;
1614 BacktrackingState state
= {
1615 .currentAttemptIndex
= 0,
1616 .randomNumberGenerator
= randomNumberGenerator
1619 backtrackingStack
.push_back(state
);
1622 static void resetToSnapshot(std::vector
<BacktrackingState
>& backtrackingStack
, std::vector
<BacktrackingState
>& bestSolutionSnapshot
, std::vector
<IMPCaches::ClassData
*>& allClasses
, int& numberOfDroppedClasses
) {
1624 // First, backtrack if needed until we reach the first different step.
1625 int firstDifferentStep
= -1;
1626 for (int i
= 0 ; i
< backtrackingStack
.size() && i
< bestSolutionSnapshot
.size() ; i
++) {
1627 if (backtrackingStack
[i
] != bestSolutionSnapshot
[i
]) {
1628 firstDifferentStep
= i
;
1633 if (firstDifferentStep
== -1) {
1634 firstDifferentStep
= MIN((int)backtrackingStack
.size(), (int)bestSolutionSnapshot
.size());
1637 while (backtrackingStack
.size() > firstDifferentStep
) {
1638 backtrack(backtrackingStack
, numberOfDroppedClasses
, allClasses
);
1641 // Then apply the steps needed to get to the snapshot.
1642 if (firstDifferentStep
< bestSolutionSnapshot
.size()) {
1643 for (int i
= (int)backtrackingStack
.size() ; i
< bestSolutionSnapshot
.size() ; i
++) {
1644 BacktrackingState
& state
= bestSolutionSnapshot
[i
];
1646 // Make a copy in order not to mutate it should we need to go back...
1647 std::minstd_rand stateRandomNumberGenerator
= state
.randomNumberGenerator
;
1649 assert(state
.currentAttemptIndex
< state
.attempts
.size());
1650 typename
IMPCaches::ClassData::PlacementAttempt::Result result
= allClasses
[i
]->applyAttempt(state
.attempts
[state
.currentAttemptIndex
], stateRandomNumberGenerator
);
1651 assert(result
.success
);
1653 if (!allClasses
[i
]->droppedBecauseFlatteningSuperclassWasDropped
) {
1654 // shouldGenerateImpCache might have been flipped to false during backtracking
1655 // we're restoring to a snapshot where we did place this class, so restore
1656 // the success bit...
1657 // ... unless we had decided to drop it because other classes were dropped
1658 // (in that case give up and don't attempt to generate a cache for it,
1659 // but still apply the attempt above in order to set the right constraints
1660 // on each selector, which is necessary for snapshot reproducibility)
1662 allClasses
[i
]->shouldGenerateImpCache
= true;
1665 numberOfDroppedClasses
++;
1668 backtrackingStack
.push_back(state
);
1673 /// Finds a shift and mask for each class, and start assigning the bits of the selector addresses
1674 int IMPCachesBuilder::findShiftsAndMasks() {
1675 // Always seed the random number generator with 0 to get reproducibility.
1676 // Note: in overflow scenarios, findShiftsAndMasks can be called more than once,
1677 // so make sure to always use the same value when we enter this method.
1678 std::minstd_rand
randomNumberGenerator(0);
1680 // This is a backtracking algorithm, so we need a stack to store our state
1681 // (It goes too deep to do it recursively)
1682 std::vector
<BacktrackingState
> backtrackingStack
;
1684 // Index of the class we're currently looking at.
1685 unsigned long currentClassIndex
= 0;
1687 // This lets us backtrack by more than one step, going back eg. 4 classes at a time.
1688 // Yes, this means we're not exploring the full solution space, but it's OK because
1689 // there are many solutions out there and we prefer dropping a few classes here and
1690 // there rather than take hours to find the perfect solution.
1691 unsigned long backtrackingLength
= 1;
1693 // Indices of the attempt we had chosen for each class last time we reached the maximum
1694 // number of classes placed so far.
1695 std::vector
<BacktrackingState
> bestSolutionSnapshot
;
1698 // Debugging facilities where we store the full state corresponding
1699 // to bestSolutionSnapshot to make sure restoring snapshots works.
1700 std::vector
<IMPCaches::ClassData
> bestSolutionSnapshotClasses
;
1701 std::unordered_map
<std::string_view
, IMPCaches::Selector
> bestSolutionSnapshotSelectors
;
1704 // Number of times we have backtracked. When this becomes too high, we go back to the
1705 // previous snapshot and drop the faulty class.
1706 unsigned long backtrackingAttempts
= 0;
1708 // Go through all the classes and find a shift and mask for each,
1709 // backtracking if needed.
1710 std::vector
<IMPCaches::ClassData
*> allClasses
;
1711 fillAllClasses(allClasses
);
1713 int numberOfDroppedClasses
= 0;
1715 while (currentClassIndex
< allClasses
.size()) {
1716 /* for (int i = 0 ; i < backtrackingStack.size() ; i++) {
1717 assert(backtrackingStack[i].attempts.size() > 0 || !allClasses[i]->shouldGenerateImpCache);
1720 assert(// Either we are adding a new state...
1721 (currentClassIndex
== backtrackingStack
.size())
1722 // Or we are backtracking and building on the last state recorded
1723 || (currentClassIndex
== (backtrackingStack
.size() - 1)));
1725 IMPCaches::ClassData
* c
= allClasses
[currentClassIndex
];
1727 if (!c
->shouldGenerateImpCache
) {
1728 // We have decided to drop this one before, so don't waste time.
1729 dropClass(_diagnostics
, currentClassIndex
, numberOfDroppedClasses
, backtrackingStack
, randomNumberGenerator
, allClasses
, "we have dropped it before");
1733 if (c
->isPartOfDuplicateSet
) {
1734 dropClass(_diagnostics
, currentClassIndex
, numberOfDroppedClasses
, backtrackingStack
, randomNumberGenerator
, allClasses
, "it is part of a duplicate set");
1738 if (currentClassIndex
>= backtrackingStack
.size()) {
1739 // We're at the top of the stack. Make a fresh state.
1741 BacktrackingState state
;
1742 state
.attempts
= c
->attempts();
1743 state
.currentAttemptIndex
= 0;
1744 state
.randomNumberGenerator
= randomNumberGenerator
;
1745 backtrackingStack
.push_back(state
);
1747 // We are backtracking ; don't retry the attempt we tried before, use the next one.
1748 backtrackingStack
[currentClassIndex
].currentAttemptIndex
++;
1750 // Note that we do not reset randomNumberGenerator to state.randomNumberGenerator
1751 // here, because when backtracking we want to explore a different set of
1752 // possibilities, so let's try other placements.
1755 assert(backtrackingStack
.size() == currentClassIndex
+ 1);
1757 BacktrackingState
& state
= backtrackingStack
[currentClassIndex
];
1759 bool placed
= false;
1761 // Go through all the possible placement attempts for this class.
1762 // If one succeeds, place the next class, and if needed we'll backtrack and try the next attempt, etc.
1763 // This is basically an iterative backtracking because
1764 // we don't want the stack to get too deep.
1765 std::vector
<typename
IMPCaches::ClassData::PlacementAttempt
> & attempts
= state
.attempts
;
1766 for (int operationIndex
= state
.currentAttemptIndex
; operationIndex
< (int)attempts
.size() ; operationIndex
++) {
1767 // Save the state of the random number generator so that we can save its
1768 // state before applying the attempt in the backtracking stack if needed.
1769 std::minstd_rand maybeSuccessfulRNG
= randomNumberGenerator
;
1770 typename
IMPCaches::ClassData::PlacementAttempt::Result result
= c
->applyAttempt(attempts
[operationIndex
], randomNumberGenerator
);
1771 if (result
.success
) {
1772 if (currentClassIndex
% 1000 == 0) {
1773 _diagnostics
.verbose("[IMP Caches] Placed %lu / %lu classes\n", currentClassIndex
, allClasses
.size());
1776 //fprintf(stderr, "%lu / %lu: placed %s with operation %d/%lu (%s)\n", currentClassIndex, allClasses.size(), c->description().c_str(), operationIndex, attempts.size(), attempts[operationIndex].description().c_str());
1778 state
.result
= result
;
1779 state
.currentAttemptIndex
= operationIndex
;
1780 state
.randomNumberGenerator
= maybeSuccessfulRNG
;
1786 currentClassIndex
+= 1;
1788 // Remove the current state, which has just failed and does not matter.
1789 // (It was never applied).
1790 backtrackingStack
.pop_back();
1792 backtrackingAttempts
++;
1793 if (backtrackingAttempts
> 10) {
1794 // Reset to the best snapshot and drop the next class
1796 resetToSnapshot(backtrackingStack
, bestSolutionSnapshot
, allClasses
, numberOfDroppedClasses
);
1799 // Expensive book-keeping to make sure that resetting to the snapshot worked.
1800 for (const auto & [k
,v
] : selectors
.map
) {
1801 const IMPCaches::Selector
& theoretical
= bestSolutionSnapshotSelectors
[k
];
1802 const IMPCaches::Selector
& actual
= *v
;
1804 if (theoretical
!= actual
) {
1805 fprintf(stderr
, "Failed to restore snapshot of %lu classes; method %s differs, (%x, %x) vs (%x, %x)\n", bestSolutionSnapshot
.size(), k
.data(), theoretical
.inProgressBucketIndex
, theoretical
.fixedBitsMask
, actual
.inProgressBucketIndex
, actual
.fixedBitsMask
);
1806 assert(theoretical
== actual
);
1811 _diagnostics
.verbose("*** SNAPSHOT: successfully reset to snapshot of size %lu\n", bestSolutionSnapshot
.size());
1813 currentClassIndex
= backtrackingStack
.size();
1814 dropClass(_diagnostics
, currentClassIndex
, numberOfDroppedClasses
, backtrackingStack
, randomNumberGenerator
, allClasses
, "it's too difficult to place");
1816 // FIXME: we should consider resetting backtrackingLength to the value it had when we snapshotted here (the risk makes this not worth trying at this point in the release).
1818 backtrackingAttempts
= 0;
1821 if (currentClassIndex
> bestSolutionSnapshot
.size()) {
1822 _diagnostics
.verbose("*** SNAPSHOT *** %lu / %lu (%s)\n", currentClassIndex
, allClasses
.size(), c
->description().c_str());
1823 bestSolutionSnapshot
= backtrackingStack
;
1826 // Make a full copy of the state so that we can debug resetting to snapshots
1827 bestSolutionSnapshotClasses
.clear();
1828 bestSolutionSnapshotSelectors
.clear();
1829 for (const auto & [k
,v
] : selectors
.map
) {
1830 bestSolutionSnapshotSelectors
[k
] = *v
;
1835 _diagnostics
.verbose("%lu / %lu (%s): backtracking\n", currentClassIndex
, allClasses
.size(), c
->description().c_str());
1836 assert(currentClassIndex
!= 0); // Backtracked all the way to the beginning, no solution
1838 for (unsigned long j
= 0 ; j
< backtrackingLength
; j
++) {
1839 backtrack(backtrackingStack
, numberOfDroppedClasses
, allClasses
);
1840 currentClassIndex
--;
1843 backtrackingLength
= std::max(1ul,std::min(std::min(currentClassIndex
, backtrackingLength
* 2), 1024ul));
1848 if (numberOfDroppedClasses
> 0) {
1849 _diagnostics
.verbose("Dropped %d classes that were too difficult to place\n", numberOfDroppedClasses
);
1852 return numberOfDroppedClasses
;
1855 void IMPCachesBuilder::fillAllClasses(std::vector
<IMPCaches::ClassData
*> & allClasses
) {
1856 for (const CacheBuilder::DylibInfo
& d
: dylibs
) {
1857 typedef typename
decltype(d
.impCachesClassData
)::value_type classpair
;
1859 for (const auto& [key
, thisClassData
]: d
.impCachesClassData
) {
1860 if (thisClassData
->methods
.size() > 0 && thisClassData
->shouldGenerateImpCache
) {
1861 allClasses
.push_back(thisClassData
.get());
1867 // Only include the classes for which there is actual work to do,
1868 // otherwise we have classes with only 1 choice which makes our
1869 // partial backtracking more difficult.
1871 std::sort(allClasses
.begin(), allClasses
.end(), [this](IMPCaches::ClassData
* a
, IMPCaches::ClassData
* b
) {
1872 int indexA
= a
->isMetaclass
? neededMetaclasses
[a
->name
] : neededClasses
[a
->name
];
1873 int indexB
= b
->isMetaclass
? neededMetaclasses
[b
->name
] : neededClasses
[b
->name
];
1875 return (indexA
< indexB
);
1879 void IMPCachesBuilder::removeUninterestingClasses() {
1880 // Remove any empty classes and classes for which we don't generate IMP caches now that we've inlined all selectors
1881 // (These classes were just used for inlining purposes)
1883 for (CacheBuilder::DylibInfo
& d
: dylibs
) {
1884 for (auto it
= d
.impCachesClassData
.begin() ; it
!= d
.impCachesClassData
.end() ; /* no increment here */) {
1885 auto& c
= it
->second
;
1886 if (((c
->methods
.size() == 0) && !(c
->flatteningRootSuperclass
.has_value()))
1887 || !c
->shouldGenerateImpCache
) {
1888 // Remove this useless class: delete it from the selectors, and from the master class map
1889 // Note that it is not useless if it is in a flattening hierarchy: all classes in a flattening
1890 // hierarchy must have preopt caches so that objc correctly invalidates the caches on children
1891 // when you attach a category to one of the classes in a flattening hierarchy
1893 for (auto& m
: c
->methods
) {
1894 auto& classes
= m
.selector
->classes
;
1895 classes
.erase(std::remove(classes
.begin(), classes
.end(), c
.get()), classes
.end());
1898 it
= d
.impCachesClassData
.erase(it
);
1906 // Now remove from the selector map any selectors that are not used by any classes
1908 addressSpace
.removeUninterestingSelectors();
1909 for (auto it
= selectors
.map
.begin() ; it
!= selectors
.map
.end() ; /* no increment */ ) {
1910 Selector
& s
= *(it
->second
);
1912 if ((s
.classes
.size() == 0) && (it
->first
!= magicSelector
)) {
1913 it
= selectors
.map
.erase(it
);
1920 void IMPCachesBuilder::fillAllMethods(std::vector
<IMPCaches::Selector
*> & allMethods
) {
1921 typedef typename
decltype(selectors
.map
)::value_type methodpair
;
1922 for (auto& [name
, selectorData
] : selectors
.map
) {
1923 // Remove all non-interesting classes that were added only for inlining tracking.
1924 if (selectorData
->classes
.size() > 0) {
1925 allMethods
.push_back(selectorData
.get());
1930 // Main entry point of the algorithm, chaining all the steps.
1931 void IMPCachesBuilder::buildPerfectHashes(IMPCaches::HoleMap
& holeMap
, Diagnostics
& diag
) {
1932 _timeRecorder
.pushTimedSection();
1933 int droppedClasses
= findShiftsAndMasks();
1934 _timeRecorder
.recordTime("find shifts and masks");
1936 if (droppedClasses
> 0) {
1937 removeUninterestingClasses();
1940 droppedClasses
= solveGivenShiftsAndMasks();
1942 if (droppedClasses
> 0) {
1943 removeUninterestingClasses();
1946 computeLowBits(holeMap
);
1948 _timeRecorder
.recordTime("assign selector addresses");
1949 _timeRecorder
.popTimedSection();
1952 size_t IMPCachesBuilder::totalIMPCachesSize() const {
1954 for (CacheBuilder::DylibInfo
& d
: dylibs
) {
1955 for (const auto& [k
,v
] : d
.impCachesClassData
) {
1956 assert(v
->shouldGenerateImpCache
);
1957 size
+= v
->sizeInSharedCache();
1963 void IMPCachesBuilder::computeLowBits(IMPCaches::HoleMap
& holeMap
) {
1965 addressSpace
.computeLowBits(holeMap
);
1968 // Shuffles selectors around to satisfy size constraints
1969 int IMPCachesBuilder::solveGivenShiftsAndMasks() {
1970 std::vector
<IMPCaches::ClassData
*> allClasses
;
1971 fillAllClasses(allClasses
);
1973 int hadToIncreaseSizeCount
= 0;
1974 int droppedClasses
= 0;
1976 // Sanity check: all methods should have a fixed bits mask
1977 // that at least encompasses the masks of all the classes they are in.
1978 for (const IMPCaches::ClassData
* c
: allClasses
) {
1979 for (const ClassData::Method
& m
: c
->methods
) {
1980 assert(((m
.selector
->fixedBitsMask
>> c
->shift
) & c
->mask()) == c
->mask());
1982 if (c
->hadToIncreaseSize()) {
1983 hadToIncreaseSizeCount
++;
1987 // Sanity check: all classes should have a valid shift and mask.
1988 for (IMPCaches::ClassData
* c
: allClasses
) {
1989 assert(c
->checkConsistency());
1993 // Now that everything is placed, try to adjust placement within the
1994 // constraints so that we can respect alignment
1996 _diagnostics
.verbose("[IMP Caches] Placed %lu classes, increasing hash table size for %d\n", allClasses
.size(), hadToIncreaseSizeCount
);
1998 std::vector
<IMPCaches::Selector
*> methodsSortedByNumberOfFixedBits
;
1999 fillAllMethods(methodsSortedByNumberOfFixedBits
);
2001 std::sort(methodsSortedByNumberOfFixedBits
.begin(), methodsSortedByNumberOfFixedBits
.end(), [](const IMPCaches::Selector
* a
, const IMPCaches::Selector
* b
) -> bool {
2003 // Place the methods with the greatest number of fixed bits first
2004 // as they will have the most constraints
2006 // If we have the same number of fixed bits, place the methods
2007 // in the largest number of classes first, as they will likely
2008 // have more constraints on their bits
2010 std::tuple
<int, int, std::string_view
> ta
= std::make_tuple(a
->numberOfSetBits(), a
->classes
.size(), std::string_view(a
->name
));
2011 std::tuple
<int, int, std::string_view
> tb
= std::make_tuple(b
->numberOfSetBits(), b
->classes
.size(), std::string_view(b
->name
));
2016 std::default_random_engine generator
;
2018 _diagnostics
.verbose("[IMP Caches] Rearranging selectors in 128-byte buckets…\n");
2020 IMPCaches::ConstraintSet cs
;
2021 for (unsigned long methodIndex
= 0 ; methodIndex
< methodsSortedByNumberOfFixedBits
.size() ; methodIndex
++) {
2022 IMPCaches::Selector
* m
= methodsSortedByNumberOfFixedBits
[methodIndex
];
2023 if (addressSpace
.canPlaceMethodAtIndex(m
, m
->inProgressBucketIndex
)) {
2024 addressSpace
.placeMethodAtIndex(m
, m
->inProgressBucketIndex
);
2026 // Try to find another address for m
2030 std::vector
<IMPCaches::ClassData
*> sortedClasses
= m
->classes
;
2032 // Sort the classes so that we can always debug the same thing.
2033 std::sort(sortedClasses
.begin(), sortedClasses
.end(), [](const IMPCaches::ClassData
* a
, const IMPCaches::ClassData
* b
) {
2037 std::vector
<IMPCaches::ClassData
*> & classes
= sortedClasses
;
2039 std::vector
<IMPCaches::ClassData
*> & classes
= m
->classes
;
2042 bool atLeastOneConstraint
= false;
2044 // Go through all the classes the method is used in and add constraints
2045 for (IMPCaches::ClassData
* c
: classes
) {
2046 if (!c
->shouldGenerateImpCache
) continue;
2047 atLeastOneConstraint
= true;
2048 IMPCaches::Constraint constraint
= c
->constraintForMethod(m
);
2052 if (!atLeastOneConstraint
) {
2053 // This method is only used in classes we have just dropped.
2057 auto dropClassesWithThisMethod
= [this, &classes
, &allClasses
, &droppedClasses
](){
2058 for (IMPCaches::ClassData
* c
: classes
) {
2059 c
->shouldGenerateImpCache
= false;
2060 _diagnostics
.verbose("Dropping class %s, selectors too difficult to place\n", c
->name
);
2062 forEachClassInFlatteningHierarchy(c
, allClasses
, [this](IMPCaches::ClassData
*toDrop
) {
2063 if (toDrop
->shouldGenerateImpCache
) {
2064 toDrop
->shouldGenerateImpCache
= false;
2065 toDrop
->droppedBecauseFlatteningSuperclassWasDropped
= true;
2066 _diagnostics
.verbose("Dropping class %s in the same flattening hierarchy\n", toDrop
->name
);
2072 IMPCaches::Constraint
& mergedConstraint
= *(cs
.mergedConstraint
);
2074 if (mergedConstraint
.allowedValues
.size() == 0) {
2075 dropClassesWithThisMethod();
2079 bool foundValue
= false;
2080 std::unordered_set
<int> & allowedValues
= mergedConstraint
.allowedValues
;
2081 int modulo
= mergedConstraint
.mask
+ 1;
2082 int multiplier
= 1 << mergedConstraint
.shift
;
2083 // We want to go through:
2084 // [((0 + allowedValues) << shift) + k, ((modulo + allowedValues) << shift) + k, ((2*modulo + allowedValue) << shift) + k, ....] etc.
2085 // but we want to randomize this so that we don't completely
2086 // fill up the small addresses. If we do, and we end up with a
2087 // constraint that forces us to zero the high bits, we'll fail
2088 // to find room for the selector.
2090 // Range for the multiplier of the modulo above
2091 int addressesCount
= std::max(((addressSpace
.maximumIndex
+ 1) >> mergedConstraint
.shift
) / modulo
, 1);
2093 // Fill "addresses" with [0, addressesCount[ so that we can shuffle it below
2094 std::vector
<int> addresses(addressesCount
);
2095 std::iota(addresses
.begin(), addresses
.end(), 0);
2097 for (int i
= 0 ; i
< addressesCount
; i
++) {
2102 // Manual Fisher-Yates:
2103 // Pick a random element in [i, end[. Swap it with the i^th element. Repeat if the random element didn't work.
2104 // We don't do std::shuffle because it wastes time to shuffle the whole range if we find happiness in the beginning.
2105 std::uniform_int_distribution
<int> distribution(i
,addressesCount
-1);
2106 int rd
= distribution(generator
);
2107 int baseAddress
= addresses
[rd
];
2108 std::swap(addresses
[i
], addresses
[rd
]);
2110 for (int j
: allowedValues
) {
2115 for (int k
= 0 ; k
< multiplier
; k
++) {
2116 int bucketIndex
= ((baseAddress
* modulo
+ j
) << mergedConstraint
.shift
) | k
;
2117 if (bucketIndex
>= addressSpace
.maximumIndex
) {
2121 bool canPlace
= addressSpace
.canPlaceMethodAtIndex(m
, bucketIndex
);
2127 //std::cerr << methodIndex << "/" << methodsSortedByNumberOfFixedBits.size() << " Moving " << m->name << " to " << address << "\n";
2128 m
->inProgressBucketIndex
= bucketIndex
;
2129 addressSpace
.placeMethodAtIndex(m
, bucketIndex
);
2136 _diagnostics
.verbose("Failed to place %s\n", m
->name
);
2137 dropClassesWithThisMethod();
2141 if (methodIndex
% 1000 == 0) {
2142 _diagnostics
.verbose(" %lu/%lu…\n", methodIndex
, methodsSortedByNumberOfFixedBits
.size());
2147 if (droppedClasses
== 0) {
2148 _diagnostics
.verbose("[IMP Caches] Placed all methods\n");
2150 _diagnostics
.verbose("[IMP Caches] Finished placing methods, dropping %d classes\n", droppedClasses
);
2153 constexpr bool log
= false;
2155 std::cerr
<< addressSpace
<< "\n";
2158 return droppedClasses
;
2161 SelectorMap::SelectorMap() {
2162 std::unique_ptr
<IMPCaches::Selector
> magicSelectorStruct
= std::make_unique
<IMPCaches::Selector
>();
2163 magicSelectorStruct
->name
= magicSelector
.data();
2164 magicSelectorStruct
->offset
= 0;
2165 map
[magicSelector
] = std::move(magicSelectorStruct
);
2168 HoleMap::HoleMap() {
2169 addStringOfSize(magicSelector
.length() + 1);
2172 bool ClassData::ClassLocator::operator==(const ClassData::ClassLocator
& other
) {
2173 return (segmentIndex
== other
.segmentIndex
)
2174 && (segmentOffset
== other
.segmentOffset
)
2175 && (strcmp(installName
, other
.installName
) == 0);
2178 bool ClassData::operator==(const ClassData
& other
) {
2179 if (strcmp(name
, other
.name
) != 0) {
2182 if (methods
.size() != other
.methods
.size()) {
2185 for (unsigned i
= 0 ; i
< methods
.size() ; i
++) {
2186 if (*(methods
[i
].selector
) != *(other
.methods
[i
].selector
)) {