002-OC对象原理探究(中)

713 阅读8分钟

通过这篇文章可以获得什么:

一:LLVM拦截优化

如何发现的:

在使用objc4源码进行对LGPerson这个对象的alloc函数进行跟踪的时候,与预想中的不一样,[LGPerson alloc],预想的是先响应的函数应该是alloc,可以真实调试下来并不是这样的,第一响应者是objc_alloc这个函数,直接懵逼......

图解:为LGPerson *p1 = [LGPerson alloc];添加断点1,为objc_alloc(Class cls)添加断点2

  • 断点跟踪: 发现问题-断点01.png

发现问题-断点02.png

  • 汇编跟踪:

发现问题-汇编01.png

发现问题-汇编02.png

出现问题,如何探索:

全局搜索objc_alloc这个函数,答案是迷茫的,根本就不知道要找什么,茫茫中没有目的的寻找,筛选有价值的信息。第一个重要发现:看到了一个修复的函数fixupMessageRef,里面的判断条件if (msg->sel == @selector(alloc))如果满足的情况下,就会将IMP替换成objc_alloc objc_alloc搜索-fixupMessageRef.png 看似找到了答案,这个时候第一直觉就是顺藤摸瓜,将调用顺序都找到,再看程序在什么时机下进入到了这了,替换了IMP

下面是我的探索查找的过程:

  • 逆向查找: 1、全局搜索fixupMessageRef,找到了调用者_read_images
    objc_alloc搜索-_read_image.png 2、_read_image函数说明里面提示本函数的调用者是map_images_nolock
    objc_alloc搜索-map_images_nolock.png 3、全局搜索map_images_nolock,找到了调用者map_images objc_alloc搜索-map_images.png 4、全局搜索map_images,找到了_dyld_objc_notify_register函数,此函数是在_objc_init函数内调用的,至此,跟fixupMessageRef相关的逆向执行路线应该是都找到了。objc_alloc搜索_objc_init.png
  • 正向验证: 探索到现在,主观上我以为应该可以了,就差最后执行程序验证正向流程了,将_objc_init_dyld_objc_notify_registermap_imagesmap_images_nolock_read_imagefixupMessageRef全部设置断点:正向执行流程验证.png 结论:通过逆向流程找源码,正向流程跑程序,双向验证下来得出的结论是alloc函数一定会被替换为objc_alloc,但是,并不在我们上述的fixupMessageRef修复函数内修改的。这里引发思考,为什么会提供一个不被执行的修复函数。难道说在编译阶段就有类似的事件,这里只是容错处理。怀着这样的猜想,找到了LLVM的源码:objc_alloc搜索-LLVM.png machoView验证在汇编阶段macho中就已经存在了objc_alloc符号:MachOView验证.png 最终结论:程序在LLVM编译阶段就已经完成了objc_alloc的替换,这里不止替换掉alloc,还有很多函数releaseretainautorelease等等,至于为什么要hook掉这些函数,推测系统对对象的创建、释放做了很多监控。

那么这里还有一个未探索到的问题,就是系统为什么要在obj4的源码内添加修复函数,容错处理?为什么会出错,什么条件下会llvm编译器会出错,从而触发fixupMessageRef函数?探索暂时被卡在了这里......

流程总结:

alloc等一些方法在编译阶段LLVM会对alloc方法进行Hook,此函数会被替换成objc_alloc函数,这样在运行时声明一个对象LGPerson并且为其开辟内存空间的时候调用alloc函数,第一响应方法为objc_alloc,接着会进入callAlloc函数,第一次永远不满足此判断条件fastpath(!cls->ISA()->hasCustomAWZ())会触发((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))objc_msgSend消息转发,为LGPerson对象发送了alloc消息,这个时候alloc函数才会真正被调用,然后进入_objc_rootAlloc->callAlloc->_objc_rootAllocWithZone->_class_createInstanceFromZone此方法里面做三件事:字节对齐开辟内存空间与对象绑定

流程图:

alloc流程.png

源码:

// alloc真是响应过程:
//第一个方法(objc_alloc):
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

//第二个方法(callAlloc):
//第三个方法:(objc_msgSend):
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

//第4个方法:(alloc)
+ (id)alloc {
    return _objc_rootAlloc(self);
}

//第5个方法(_objc_rootAlloc):
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

//低6个方法(再次进入callAlloc):
//第7个方法:_objc_rootAllocWithZone
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
//第9个方法:_class_createInstanceFromZone
//内部分别有三个事件:
//1、instanceSize,判断对象大小,进行内存对齐
//2、calloc堆空间上真实开辟内存空间
//3、绑定类和地址的指针
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

二:对象内存的影响因素

探索方向:

  • 空对象,不声明任何成员变量、属性和方法。 对象内存的影响因素-无.png

  • 只声明成员变量对象内存的影响因素-成员变量.png

  • 只添加方法对象内存的影响因素-方法.png

结论

  • 在不声明任何成员变量、属性、方法的时候,FFPerson实例对象默认开辟的内存大小是8字节。
  • 在添加方法的情况下,对类的实例对象内存大小没有任何影响,方法不存在对象内。
  • 在添加成员变量的过程中,由于成员变量的数据类型是不一致的,向最大数据类型的成员变量对齐。继承自NSObject对象的类,默认字节对齐方式是8字节。

三:字节对齐:

算法:

x是已知参数,类型是size_t,代表当前对象声明成员变量的大小instanceSize,WORD_MASK是宏定义,值为7,假设x=8(传入结构体指针isa),那么表达是就变成了

(8 + 7)& ~7

= 15 & ~7

= 0000 1111 & 1111 1000

= 0000 1000

= 8

那么得出结论8字节对齐

#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

原理图解:

内存对齐.gif

四:结构体内存对齐

内存对齐原则:

  • 数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的存储位置要从该成员大小或成员的子成员大小(只要该成员有子成员,比如说是数组结构体等)的整数倍开始(比如int是4字节,则要从4的整数倍地址开始存储)
  • 结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里面有struct b,b里面有char,int,double等元素,那b应该从8的整数倍开始存储)
  • 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员大小的整数倍,不足的要补齐。

案例:

  • 普通struct
  • 普通struct+交换内部成员变量的顺序
  • 普通struct+内部嵌套了另外一个结构体
  • 普通struct+内部嵌套的结构体内部再次嵌套一个普通的结构体(2层嵌套)

内存对齐案例01.png

案例源码:

struct LGStruct1 {
    double a;       // 8    [0 7]
    char b;         // 1    [8]
    int c;          // 4    (9 10 11 [12 13 14 15]
    short d;        // 2    [16 17] 24
}struct1;

struct LGStruct2 {
    double a;       // 8    [0 7]
    int b;          // 4    [8 9 10 11]
    char c;         // 1    [12]
    short d;        // 2    (13 [14 15] 16
}struct2;

struct LGStruct3 {
    double a;               //8     [0 7]
    int b;                  //4     [8 9 10 11]
    char c;                 //1     [12]
    short d;                //2     [14 15]
    int e;                  //4     [16 17 18 19]
    struct LGStruct1 str;   //24    (20 21 22 23)[24-47]
}struct3;

struct LGStruct4 {
    double a;               //8     [0 7]
    int b;                  //4     [8 9 10 11]
    char c;                 //1     [12]
    short d;                //2     [14 15]
    int e;                  //4     [16 17 18 19]
    struct LGStruct2 str;   //16    (20 21 22 23)[24 40]
}struct4;

struct LGStruct5 {
    double a;               //8     [0 7]
    int b;                  //4     [8 9 10 11]
    char c;                 //1     [12]
    short d;                //2     [14 15]
    int e;                  //4     [16 17 18 19]
    struct LGStruct3 str;   //16    (20 21 22 23)[24 72]
}struct5;

控制台打印:

2021-06-09 01:23:51.376836+0800 001-内存对齐原则[92187:1929587] 24-16
2021-06-09 01:23:51.378065+0800 001-内存对齐原则[92187:1929587] 48
2021-06-09 01:23:51.378170+0800 001-内存对齐原则[92187:1929587] 40
2021-06-09 01:23:51.378250+0800 001-内存对齐原则[92187:1929587] 72

五:malloc探索

为什么探索malloc

在使用malloc打印实例对象暂用内存的时候,出现了意料之外的答案:48

源码:

#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGPerson *person = [LGPerson alloc];
        person.name      = @"BBLv";
        person.nickName  = @"FF";
        person.age       = 180;
        person.height    = 50;
        // sizeof = 28 - 32 + 8 = 36
        // 对象 地址 8 = 结构体指针
        // 40 -> 48
        NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));
    }
    return 0;
}

控制台打印结果:

2021-06-09 01:44:16.748536+0800 002-系统内存开辟分析[92447:1948805] <LGPerson: 0x10053f1d0> - 8 - 40 - 48
Program ended with exit code: 0

malloc探索.png

在libmalloc-317.40.8源码中找到了核心代码,内存对齐以16字节的方式

#define SHIFT_NANO_QUANTUM		4
#define NANO_REGIME_QUANTA_SIZE	(1 << SHIFT_NANO_QUANTUM)	// 16

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
	size_t k, slot_bytes;

	if (0 == size) {
		size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
	}
	k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
	slot_bytes = k << SHIFT_NANO_QUANTUM;							// multiply by power of two quanta size
	*pKey = k - 1;													// Zero-based!

	return slot_bytes;
}

malloc流程图

malloc源码分析.png

只此一生,必须热情
少年,冲冲冲