通过这篇文章可以获得什么:
- 初始化一个对象底层真正的响应流程。
- LLVM编译器为什么要Hook alloc方法?
- LLVM优化的目的是什么?
- 发现问题,如何在源码层面上探索
- alloc加载流程图(包含llvm优化阶段)
- 对象内存的影响因素?
- 什么是字节对齐?
- 什么是结构体内存对齐?
- malloc实际开辟了多大内存?
- malloc算法规则是什么
一:LLVM拦截优化
如何发现的:
在使用objc4源码进行对LGPerson这个对象的alloc函数进行跟踪的时候,与预想中的不一样,[LGPerson alloc],预想的是先响应的函数应该是alloc,可以真实调试下来并不是这样的,第一响应者是objc_alloc这个函数,直接懵逼......
图解:为LGPerson *p1 = [LGPerson alloc];添加断点1,为objc_alloc(Class cls)添加断点2
- 断点跟踪:
- 汇编跟踪:
出现问题,如何探索:
全局搜索objc_alloc这个函数,答案是迷茫的,根本就不知道要找什么,茫茫中没有目的的寻找,筛选有价值的信息。第一个重要发现:看到了一个修复的函数fixupMessageRef,里面的判断条件if (msg->sel == @selector(alloc))如果满足的情况下,就会将IMP替换成objc_alloc
看似找到了答案,这个时候第一直觉就是顺藤摸瓜,将调用顺序都找到,再看程序在什么时机下进入到了这了,替换了
IMP。
下面是我的探索查找的过程:
- 逆向查找:
1、全局搜索
fixupMessageRef,找到了调用者_read_images
2、
_read_image函数说明里面提示本函数的调用者是map_images_nolock
3、全局搜索
map_images_nolock,找到了调用者map_images4、全局搜索
map_images,找到了_dyld_objc_notify_register函数,此函数是在_objc_init函数内调用的,至此,跟fixupMessageRef相关的逆向执行路线应该是都找到了。 - 正向验证:
探索到现在,主观上我以为应该可以了,就差最后执行程序验证正向流程了,将
_objc_init、_dyld_objc_notify_register、map_images、map_images_nolock、_read_image、fixupMessageRef全部设置断点:结论:通过逆向流程找源码,正向流程跑程序,双向验证下来得出的结论是
alloc函数一定会被替换为objc_alloc,但是,并不在我们上述的fixupMessageRef修复函数内修改的。这里引发思考,为什么会提供一个不被执行的修复函数。难道说在编译阶段就有类似的事件,这里只是容错处理。怀着这样的猜想,找到了LLVM的源码:machoView验证在汇编阶段macho中就已经存在了
objc_alloc符号:最终结论:程序在LLVM编译阶段就已经完成了
objc_alloc的替换,这里不止替换掉alloc,还有很多函数release、retain、autorelease等等,至于为什么要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真是响应过程:
//第一个方法(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);
}
二:对象内存的影响因素
探索方向:
-
空对象,不声明任何成员变量、属性和方法。
-
只声明成员变量
-
只添加方法
结论
- 在不声明任何成员变量、属性、方法的时候,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;
}
原理图解:
四:结构体内存对齐
内存对齐原则:
- 数据成员对齐规则:结构体(struct)或联合体(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的存储位置要从该成员大小或成员的子成员大小(只要该成员有子成员,比如说是数组结构体等)的整数倍开始(比如int是4字节,则要从4的整数倍地址开始存储)
- 结构体作为成员,如果一个结构体里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里面有struct b,b里面有char,int,double等元素,那b应该从8的整数倍开始存储)
- 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员大小的整数倍,不足的要补齐。
案例:
- 普通struct
- 普通struct+交换内部成员变量的顺序
- 普通struct+内部嵌套了另外一个结构体
- 普通struct+内部嵌套的结构体内部再次嵌套一个普通的结构体(2层嵌套)
案例源码:
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
在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;
}