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
- 断点跟踪:
- 汇编跟踪:
探索LLVM的hook方法:
1、通过全局搜索方法objc_alloc逆向查找到fixupMessageRef
2、通过全局搜索fixupMessageRef,找到了调用者_read_images
3、_read_images函数说明里面提示本函数的调用者是map_images_nolock
4、全局搜索map_images_nolock,找到了调用者map_images
5、全局搜索map_images,找到了_dyld_objc_notify_register函数,此函数是在_objc_init函数内调用的,至此,跟fixupMessageRef相关的逆向执行路线应该是都找到了。
6、通过逆向流程找源码,得出的结论是alloc函数一定会被替换为objc_alloc。而替换代码在LLVM上,找到了LLVM的源码:
machoView验证在汇编阶段macho中就已经存在了objc_alloc符号:
最终结论:程序在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));
}
复制代码
二:对象内存的影响因素
- 探索方向:
空对象,不声明任何成员变量、属性和方法。
只声明成员变量
只添加方法
- 结论:
在不声明任何成员变量、属性、方法的时候,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;
}
复制代码
四:结构体内存对齐
内存对齐原则:
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;
}
复制代码