Small valley bottom exploration collection

  • In my previous blog post about exploring the bits class in objc_class, we skipped cache_t in order to explore the structure. Today we’ll examine a wave

  • Here are two things I know you all know. However, still want to say, in case you don’t know. For example: I 😆)

    • SEL: It’s simply method numbering
    • IMP: a pointer to the implementation of a method.
  • Get down to business!!

1. Locate the structure of cache_t

We’ll take a look at cache_t first, and then we’ll walk through it step by step.

    1. First findobjc_class:
  • 2. We can observe it by clicking incache_tStructure!!

Source:


struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic<struct bucket_t* > _buckets;
    explicit_atomic<mask_t> _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;
    
    // How much the mask is shifted by.
    static constexpr uintptr_t maskShift = 48;
    
    // Additional bits after the mask which must be zero. msgSend
    // takes advantage of these additional bits to construct the value
    // `mask << 4` from `_maskAndBuckets` in a single instruction.
    static constexpr uintptr_t maskZeroBits = 4;
    
    // The largest mask value we can store.
    static constexpr uintptr_t maxMask = ((uintptr_t)1< < (64 - maskShift)) - 1;
    
    // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
    static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;
    
    // Ensure we have enough bits for the buckets pointer.
    static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers.");
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    // _maskAndBuckets stores the mask shift in the low 4 bits, and
    // the buckets pointer in the remainder of the value. The mask
    // shift is the value where (0xffff >> shift) produces the correct
    // mask. This is equal to 16 - log2(cache_size).
    explicit_atomic<uintptr_t> _maskAndBuckets;
    mask_t _mask_unused;

    static constexpr uintptr_t maskBits = 4;
    static constexpr uintptr_t maskMask = (1 << maskBits) - 1;
    static constexpr uintptr_t bucketsMask = ~maskMask;
#else
#error Unknown cache mask storage type.
#endif

#if __LP64__
    uint16_t _flags;
#endif
    uint16_t _occupied;

public:
    static bucket_t *emptyBuckets(a);
    
    struct bucket_t *buckets(a);
    mask_t mask(a);
    mask_t occupied(a);
    void incrementOccupied(a);
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty(a);

    unsigned capacity(a);
    bool isConstantEmptyCache(a);
    bool canBeFreed(a);
// There are too many copies. Conditional oneself can study next
Copy the code
    1. We can see thatcache_tStructure, however, such a long code, this is not to mess with our mentality. Take a look at the legendaryif-else~

Macro definition:

#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3

#if defined(__arm64__) && __LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#elifdefined(__arm64__) && ! __LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
Copy the code
  • CACHE_MASK_STORAGE_HIGH_16: true && 64-bit

  • CACHE_MASK_STORAGE_LOW_4: real machine && non-64-bit

  • Cache_mask_storage_may include: emulators and macs

We’re going to talk about it on the Mac.

2. Cache_t structure analysis

2.1 By viewing source code analysis

  • At first glance, we can refer to buckets, masks,flags, occupied,flags.

  • Just want to start analysis, but the code is long, English is not good, see this boring! But there is no good way! Can only bite the bullet (research bottom to try to explore, may explore the wrong direction, will do useless, but if you do not explore, do not know right!

  • Let’s take a look at the construction of buckets:

#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
Copy the code

This is fucking storageSELandIMPYao. Damn, I can’t wait to test a wave, guys

2.2. Debug and analyze using LLDB

2.2.1 Call a single method for analysis

    1. To create aXGTestClass:
@interface XGTest : NSObject
- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
@end

XGTest *test = [XGTest alloc];
Class tClass = [test class];
        
[test say1];
Copy the code

Locate the structure of the cache by calling the method say1.

    1. We can see what we want (_buckets._mask._flags._occupied), we looked to see if there was a way to retrieve the source code (and there is!). :
    static bucket_t *emptyBuckets(a);
    
    struct bucket_t *buckets(a);
    mask_t mask(a);
    mask_t occupied(a);
    void incrementOccupied(a);
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty(a);
Copy the code
    1. So we can do this by callingbuckets()To check the structure

Don’t you also store SEL and IMP?

    1. We can see that through observationbucket_tHave access toselandimpThe method of

    1. What are you waiting for? I want him!!

    1. Perfect access!

2.2.2 Call multiple methods for analysis

We already know what happens when we call a method cache, but how about multiple methods?

    1. Add a method!
XGTest *test = [XGTest alloc];
Class tClass = [test class];
        
[test say1];
[test say2];
Copy the code
    1. Let’s print the structure of the cache:

    1. We found that_occupiedThere are changes, and we’ll look at that in a minute, but we’ll look at that for nowbuckets.

Brothers! Watch me operate!!

Aye? What about our say2? Why didn’t. Why don’t I look for it?

    1. Let me hazard a guess!bucketsThere aresDoes he have more than one? Is it an array-like structure? Can I get what I want by pointer offset? Check it out!

    1. OK, we’ve got thatbucketsInside putselandimp.

3. Implementation principle analysis of cache_t

We have just found that _occupied has changed. Also a little interesting ~

  • Now, how does _occupied change? And as the methods increase, the mask changes. How do you keep buckets?

    1. We look atoccupiedThe change of… Zha of? , that can only see if there is a way to let him change!
    static bucket_t *emptyBuckets(a);
    
    struct bucket_t *buckets(a);
    mask_t mask(a);
    mask_t occupied(a);
    void incrementOccupied(a);
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void initializeToEmpty(a);
Copy the code

Take the trouble! Let me see incrementOccupied this! Then we follow the lead ~ to see who called it!

    1. By global search! There’s only one place
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
Copy the code
    1. Alas! Finally behind enemy lines! .insertIt’s just insertion! I have a feeling this is what we’re looking for! (But I have no proof 😆)
    1. Then I have to give evidence!
void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
{
#if CONFIG_USE_CACHE_LOCK
    cacheUpdateLock.assertLocked();
#else
    runtimeLock.assertLocked();
#endifASSERT(sel ! =0 && cls->isInitialized());

// Before the assertion, no!! It is divided into two parts

// The first part

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if(! capacity) capacity = INIT_CACHE_SIZE; reallocate(oldCapacity, capacity,/* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { // 4 3 + 1 bucket cache_t
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // Double the size
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);  // Memory capacity is complete
    }

// Part 2
    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot because the
    // minimum size is 4 and we resized at 3/4 full.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return; }}while(fastpath((i = cache_next(i, m)) ! = begin));cache_t::bad_cache(receiver, (SEL)sel, cls);
}
Copy the code

Just start by giving me a few assertions! Increase code length? You think you can bluff my desire to watch? Are you making me laugh!!

    1. To help with the analysis, I split the code into two parts: first, if-else judgment analysis:

Through the figure above we can clear the memory open up!

    1. So look at the second part of his code:

That’s how cache works, but there are a lot of methods that aren’t specified, such as cache_hash, which I’ll leave to you to explore.