OC底层原理(二):alloc内存对齐

OC底层原理(二):alloc内存对齐

一、LLVM拦截与alloc的hook

断点调试追查:

对于使用objck4-818.2源码的工程,进行对LGPerson这个对象的alloc函数进行断点跟踪的时候,第一响应者是objc_alloc这个函数,原因是LLVM底层hook了alloc方法!

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

  • 断点跟踪:

截屏2021-06-16 下午4.22.42.png

截屏2021-06-16 下午4.35.01.png

  • 汇编跟踪:

截屏2021-06-16 下午5.03.09.png

探索LLVM的hook方法:

1、通过全局搜索方法objc_alloc逆向查找到fixupMessageRef

截屏2021-06-16 下午5.27.46.png

2、通过全局搜索fixupMessageRef,找到了调用者_read_images

截屏2021-06-16 下午5.39.36.png

3、_read_images函数说明里面提示本函数的调用者是map_images_nolock

截屏2021-06-16 下午5.42.27.png

4、全局搜索map_images_nolock,找到了调用者map_images

截屏2021-06-16 下午5.43.49.png

5、全局搜索map_images,找到了_dyld_objc_notify_register函数,此函数是在_objc_init函数内调用的,至此,跟fixupMessageRef相关的逆向执行路线应该是都找到了。

截屏2021-06-16 下午5.46.05.png

6、通过逆向流程找源码,得出的结论是alloc函数一定会被替换为objc_alloc。而替换代码在LLVM上,找到了LLVM的源码:

image.png

machoView验证在汇编阶段macho中就已经存在了objc_alloc符号:

image.png

最终结论:程序在LLVM编译阶段就已经完成了objc_alloc的替换,这里不止替换掉alloc,还有很多函数release、retain、autorelease等等,至于为什么要hook掉这些函数,推测系统对对象的创建、释放做了很多监控。

流程总结:

[LGPerson 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的方法调用:
+ (id)alloc {
    return _objc_rootAlloc(self);
}

//进入第一个方法(objc_alloc):
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

// 进入第二个方法(callAlloc):
// cooci 2021.01.05
// KC 重磅提示 这里是核心方法
// 首次进入执行的是 (_objc_rootAllocWithZone)
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));
}

// 进入第三个方法(_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);
}

// 进入第四个方法(_class_createInstanceFromZone):
// 进入第四个方法返回到上级,直至alloc方法(+(id)alloc)
// 然后从新从第一个方法进入,但是走第二步
//内部分别有三个事件:
//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);
}

//
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));
}

// cooci 2021.01.05
// KC 重磅提示 这里是核心方法
// 第二次进入这个方法,算是第六个方法,进入发送消息方法
//结束 alloc
(((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc)))
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));
}



复制代码

二:对象内存的影响因素

  • 探索方向:

空对象,不声明任何成员变量、属性和方法。

截屏2021-06-16 下午6.48.18.png

只声明成员变量

截屏2021-06-16 下午6.50.28.png

只添加方法

截屏2021-06-16 下午6.54.51.png

  • 结论:

在不声明任何成员变量、属性、方法的时候,FFPerson实例对象默认开辟的内存大小是8字节。

在添加方法的情况下,对类的实例对象内存大小没有任何影响,方法不存在对象内。

在添加成员变量的过程中,由于成员变量的数据类型是不一致的,向最大数据类型的成员变量对齐。继承自NSObject对象的类,默认字节对齐方式是8字节。

三:字节对齐:

变量占用字节与计算方式:

  • 字节表:

截屏2021-06-16 下午3.37.18.png

  • 计算方式:

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;
}
复制代码

四:结构体内存对齐

内存对齐原则:

1.数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的存储位置要从该成员大小或成员的子成员大小(只要该成员有子成员,比如说是数组结构体等)的整数倍开始(比如int是4字节,则要从4的整数倍地址开始存储)

2.结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里面有struct b,b里面有char,int,double等元素,那b应该从8的整数倍开始存储)

3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员大小的整数倍,不足的要补齐。

Test内容:

1.struct包含double、char、int、short等类型变量,更改顺序的内存分配

2.struct的内部嵌套了其他结构体的内存分配

案例源码:

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     (13 [14 15]
    int e;         // 4     [16 17 18 19]
    struct LGStruct1 str;// 24 (20 21 22 23 [24 25 ... 47] 48
}struct3;

复制代码

控制台打印:

2021-06-16 11:17:37.434547+0800 001-内存对齐原则[484:37513] 24
2021-06-16 11:17:37.435028+0800 001-内存对齐原则[484:37513] 16
2021-06-16 11:17:37.435046+0800 001-内存对齐原则[484:37513] 48
复制代码

五:malloc探索

为什么探索malloc

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

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

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        person.name      = @"Cooci";
        person.nickName  = @"KC";
        person.age       = 18;
        person.height    = 190.5;
        person.c1        = 'a';
        person.c2        = 'b';

        NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LGPerson class]),malloc_size((__bridge const void *)(person)));
     
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

复制代码

控制台打印结果:

2021-06-16 16:40:09.109622+0800 001-内存对齐原则[654:138271] <LGPerson: 0x282724810> - 8 - 48 - 48


复制代码

在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;
}

复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享