美文网首页
Objc4-818底层探索(二):内存对齐

Objc4-818底层探索(二):内存对齐

作者: ShawnAlex | 来源:发表于2021-06-08 23:58 被阅读0次

    建议先看下我之前的Objc4-818底层探索(一):alloc探索(一)

    首先补齐一些lldb命令, 有助于后面探索

    x: 相应的表示打印
    4: 打印4个, 内存段
    g: 代表8字节
    w: 代表4字节
    x: 一般末尾x表示16进制打印

    lldb命令

    举个例子:


    lldb读取信息
    • x/5gx test: 以5个片段, 16字节读出内存段
    • 0x101b4743: 内存段地址
    • 0x011d80010000834d: 内存段第一个默认是isa
    • 从第二个开始内存段为属性一些值, 留意下即使属性没赋值也会给开辟内存空间, 不然后续赋值不知道内存空间有多少

    接来下补齐些上篇文章遗留的问题 Objc4-818底层探索(一):alloc探索(一)

    问题1: 为什么对象alloc会先走objc_alloc

    当我们step into时候会发现alloc_objc_rootAlloccallAlloc, 但是我们实际走的时候发现走的却是objc_alloc

    objc_alloc

    我们搜索下objc_alloc, 在objc-runtime-new.mmfixupMessageRef可找到

    fixupMessageRef
    /***********************************************************************
    * fixupMessageRef
    * Repairs an old vtable dispatch call site. 
    * vtable dispatch itself is not supported.
    **********************************************************************/
    static void 
    fixupMessageRef(message_ref_t *msg)
    {    
        msg->sel = sel_registerName((const char *)msg->sel);
    
        if (msg->imp == &objc_msgSend_fixup) { 
            if (msg->sel == @selector(alloc)) {
                msg->imp = (IMP)&objc_alloc;
            } else if (msg->sel == @selector(allocWithZone:)) {
                msg->imp = (IMP)&objc_allocWithZone;
            } else if (msg->sel == @selector(retain)) {
                msg->imp = (IMP)&objc_retain;
            } else if (msg->sel == @selector(release)) {
                msg->imp = (IMP)&objc_release;
            } else if (msg->sel == @selector(autorelease)) {
                msg->imp = (IMP)&objc_autorelease;
            } else {
                msg->imp = &objc_msgSend_fixedup;
            }
        } 
        else if (msg->imp == &objc_msgSendSuper2_fixup) { 
            msg->imp = &objc_msgSendSuper2_fixedup;
        } 
        else if (msg->imp == &objc_msgSend_stret_fixup) { 
            msg->imp = &objc_msgSend_stret_fixedup;
        } 
        else if (msg->imp == &objc_msgSendSuper2_stret_fixup) { 
            msg->imp = &objc_msgSendSuper2_stret_fixedup;
        } 
    #if defined(__i386__)  ||  defined(__x86_64__)
        else if (msg->imp == &objc_msgSend_fpret_fixup) { 
            msg->imp = &objc_msgSend_fpret_fixedup;
        } 
    #endif
    #if defined(__x86_64__)
        else if (msg->imp == &objc_msgSend_fp2ret_fixup) { 
            msg->imp = &objc_msgSend_fp2ret_fixedup;
        } 
    #endif
    }
    

    这里先普及下selimp

    sel: 方法编号, 可理解成一本书的目录, 可通过对应名称找到页码
    imp: 函数指针地址, 可以理解成一本书的页面, 方便找到具体的实现函数

    然后看下fixupMessageRef(message_ref_t *msg)这个方法, 其中有这个,

     if (msg->sel == @selector(alloc)) {
         msg->imp = (IMP)&objc_alloc;
     } 
    

    如果sel == @selector(alloc)msg->imp = (IMP)&objc_alloc, 即 如果方法为alloc我就让你去走objc_alloc方法。

    但是要留意下 这个方法是fixup, 修正方法。如果没有问题是不会走这个修正的, 有问题才会走修正方法。这个方法源码其实在我们编译器LLVM里面

    LLVM

    LLVM构架编译器的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。

    这块简单介绍一些架构概念:

    • 前端编译器(Frontend): 编译器的前端任务是解析源代码, 会进行词法分析语法分析语义分析

    • 优化器(Optimizer): 负责各种优化, 改善代码的运行时间,如消除冗余计算等

    • 后端编译器(Backkend)/ 代码生成器(CodeGenerator): 将代码映射到目标指令集,生成机器语言,并进行机器相关的代码优化(目标指不同操作系统)。

    编译器架构

    iOS编译器架构

    Objective C / C / C++ 使用的编译器前端是ClangSwiftswift,后端都是LLVM

    iOS编译器架构

    继续回到alloc , 他的流程顺序依次是 OCClangLLVM机器代码。那么 alloc方法一进LLVM, 后端 LLVM 判断alloc, 这位是我们尊贵的VIP的顾客, 必须给走特殊方法, 所以走特殊的处理流程。

    接下来我们看下allocllvm是怎么处理的

    全局搜索alloc, 可在CGObjC.cpp找到tryGenerateSpecializedMessageSend方法

    tryGenerateSpecializedMessageSend

    这个方法是llvm针对于一些传入的sel方法, 做了一些不同的特殊处理, 其中就可以看到alloc。这里可看到, llvm判断如果进入的是alloc方法, 那么去执行EmitObjCAlloc, 我们点击进入EmitObjCAlloc

    EmitObjCAlloc

    可看到llvm让它去执行了objc_alloc方法
    由此alloc会走到objc+alloc, 其实这部分是由llvm做的特殊消息处理逻辑。

    第一次与第二次的区别:

    第一次是系统级别的alloc, 而第二次是消息转发objc_msgSend发起的alloc。其中objc_msgSend走慢速查找, 针对于当前类的ISA做了些处理, 所以第二次会走if (fastpath(!cls->ISA()->hasCustomAWZ()))这个判断。(这块后续补充)


    首先我们先看个例子, SATest.h里面创建一些属性

    SATest.h

    main里面调用一下, 并读取

    main调用 对应图标识
    • 冒号:左边的0x101c04100, 0x101c04110...是内存段的地址, 第一个0x101c04100alloc开辟内存的首地址, 也是SATest *test的地址。
    • 冒号:右边的0x011d8001000083b5, 0x0000001200006261...是内存段的值(po 读取一下对应内存段也可以看到)
    • 每片内存段占8字节
    • 0x61 = 97 = ASCII码中的 a
    • 浮点数要读取比较特殊e -f f -- XXX或``p/f XXX`, 当然我们也用一些函数方法, 还原下
    void lg_float2HEX(float f){
        union uuf { float f; char s[4]; } uf;
        uf.f = f;
        printf("0x");
        for (int i=3;i>=0;i--)
            printf("%02x", 0xff & uf.s[i]);
        printf("\n");
    }
    
    void lg_double2HEX(double d){
        union uud { double d; char s[8];} ud;
        ud.d = d;
        printf("0x");
        for (int i=7;i>=0;i--)
            printf("%02x",0xff & ud.s[i]);
        printf("\n");
    }
    

    可能问题就来了, 为什么会这样拍, 而且int age, char a, b为什么会放到一起, 接下来我们就探索下

    内存对齐

    内存对齐原则

    • 每个平台的编辑器都有自己的对齐系数, 程序员也可以通过预编译命令#pragma pack(n), n=1,2,4,8,16来改变这一系数, 其n就是你指定的对齐系数。在ios中xcode默认为8, 即8字节对齐

    • 数据成员对齐可以理解为min(m, n)公式, 其中m表示当前成员开始位置, n表示当前成员所需要的位数。如果满足 m 整除 n (m % n == 0), nm位置开始存储, 反之m循环+1, 直至可以整除, 从而确定了当前成员位置。

    • 数组成员为结构体, 当结构体嵌套结构体时, "成员"的结构体的自身长度为"成员"结构体中最大成员的内存大小, 例如: 结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8

    • 结构体的内存大小必须为结构体最大成员内存大小整数倍, 不足需要补齐

    先熟悉下 ios中数据类型的占用内存大小, 方便后面的例子计算

    iOS类型占用图

    例子

            struct Struct1 {
                double a;
                char b;
                int c;
                short d;
            }struct1;
    
            struct Struct2 {
                double a;
                int b;
                char c;
                short d;
            }struct2;
    
            struct Struct3 {
                double a;
                int b;
                char c;
                short d;
                int e;
                struct Struct1 str;
            }struct3;
    
            NSLog(@"结构体1占用的内存大小 %lu", sizeof(struct1));
            NSLog(@"结构体2占用的内存大小 %lu", sizeof(struct2));
            NSLog(@"结构体3占用的内存大小 %lu", sizeof(struct3));
    

    解题思路

    结构体1

    a: double类型占8字节, 即 0~7存double a
    b: char类型占1字节, 因为 8 % 1 = 0, 即8存char b
    c: int类型占4字节,
           9 % 4 != 0, 不满足 +1,
          10 % 4 != 0, 不满足 +1,
          11 % 4 != 0, 不满足 +1,
          12 % 4 == 0, 满足, 即, 12~15存 int c
    d: short类型占2字节, 16 % 4 != 0, 满足 +1, 即16, 17存short d

    结构体1需要内存18字节, 由于最大字节数为8(double的8), 结构体必须是8的倍数, 所以需要向上取整, 固最终结果为24

    结构体1
    结构体2

    a: double类型占8字节, 即 0~7存double a
    b: int类型占4字节, 8 % 4 == 0, 8~11存储int b
    c: char类型占1字节, 因为 12 % 1 = 0, 即9存char c
    d: short类型占2字节
          13 % 2 != 0, 不满足 +1,
          14 % 2 == 0, 满足, 即14, 15存short d

    结构体2需要内存16字节, 由于最大字节数为8(double的8), 结构体必须是8的倍数, 16满足8的倍数, 固最终结果为16

    结构体2

    之前的问题解决了, 我们接下来看下结构体嵌套结构体

    结构体3

    前四个跟结构体2一样, 从 int e;开始
    e: int类型占4字节, 16 % 4 == 0, 16~19存储int b
    str: 结构体类型, 就是结构体1, 结构体1占24字节, 留意下, 当结构体嵌套结构体时, "成员"的结构体的自身长度为"成员"结构体中最大成员的内存大小, 最大成员为double的8, 那么我们要找到位置满足, 位置 % 8 == 0, 开始存放结构体1
          20 % 8 != 0, 不满足 +1,
          21 % 8 != 0, 不满足 +1,
          22 % 8 != 0, 不满足 +1,
          23 % 8 != 0, 不满足 +1,
          24 % 8 == 0, 不满足, 24~47存struct Struct1 str

    结构体3需要内存48字节, 由于最大字节数为8, 48满足8的倍数, 固最终结果为48

    结构体3

    验证结果

    验证结果

    上面的例子也可看出结构体内存大小与结构体成员内存大小的顺序有关:

    • 如果是结构体中数据成员是根据内存从小到大的顺序定义的,根据内存对齐规则来计算结构体内存大小,需要增加有较大的内存padding即内存占位符,才能满足内存对齐规则,比较浪费内存

    • 如果是结构体中数据成员是根据内存从大到小的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足堆存对齐规则,这种方式就是苹果中采用的,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的


    属性重排

    接下来看一下之前那int age, 与char a char b排到一起的0x0000001200006261问题

    • 0x00000012: 18
    • 62: 0x62 = 98 = b
    • 61: 0x61 = 97 = a

    首先这里排在一起, 是苹果系统自动帮我们优化处理的, 做了内存优化属性重排

    虽然大部分的内存都是通过固定的内存块进行读取,尽管苹果会采用对齐的方式处理优化,但并不是所有的内存都可以进行浪费的,苹果会自动对属性进行重排,以此来优化内存。


    还是先看了例子

    例子1

    例子

    里面涉及三个读取内存知识点sizof, class_getInstanceSize, malloc_size

    sizof

    • sizeof: 对象类型占用大小, 里面可以放基础数据类型, 对象, 指针等, 比如
      int a, sizeof(a) = 4;
      short b, sizeof(b) = 2;
      NSString *c, sizeof(c) = 8

      • 如果放入基础数据类型, 则直接读出数据类型大小

      • 如果放入是NSObject/继承NSObject对象, 直接读它本质, NSObject本质结构体(struct objc_class : objc_object), 对象在64bit下为8字节。

    class_getInstanceSize

    • class_getInstanceSize: 实例对象中成员变量的内存大小。
    • class_getInstanceSizeruntime提供的api, 用于获取类的实例对象所占用的内存空间的大小, 并返回具体的字节数。

    malloc_size

    malloc_size: 系统实际分配内存大小, 这个是由系统完成的, 涉及16字节内存对齐

    那么上面的例子

    *test 指针类型, 64位占8字节; 对象里面只有isa, 所以class_getInstanceSize占用8字节; 而系统默认16字节对齐分配内存, 所以malloc_size为16

    例子2

    我们再加一个成员变量看一下

    #import <Foundation/Foundation.h>
    
    @interface SATest : NSObject {
        NSString *a;
    }
    
    @end
    
    例子2打印

    可看到class_getInstanceSize变了(isa + NSString = 8 +8 = 16), 说明成员变量大小的确影响实际开辟内存大小

    例子3

    我们再加个属性

    #import <Foundation/Foundation.h>
    
    @interface SATest : NSObject {
        NSString *a;
    }
    
    @property (nonatomic, strong) NSString *b;
    
    @end
    
    例子3打印

    可看到class_getInstanceSize也变了(isa + NSString + NSString = 8 + 8 + 8 = 24), 说明属性也会影响内存大小。malloc_size系统实际分配内存大小, 要满足16字节内存对齐

    例子4

    我们再加个方法

    @interface SATest : NSObject {
        NSString *a;
    }
    
    @property (nonatomic, strong) NSString *b;
    
    - (void)sayHello;
    
    @end
    
    
    例子4打印

    可看到class_getInstanceSize没有变, 说明方法不会影响内存大小(类方法也一样)
    其实属性在底层也是成员变量 + set/get方法, 所以影响开辟内存大小的只有成员变量


    malloc_size

    接下来我们看下malloc_size, 看下底层, 看下它是怎么样进行16字节对齐

    malloc代码在libmalloc里面

    我们先在main中, 加一个calloc跟一下内容

    #import <Foundation/Foundation.h>
    #import <malloc/malloc.h>
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
        
            void *p = calloc(1, 40);
            NSLog(@"%lu",malloc_size(p));
            NSLog(@"Hello, World!");
        }
        return 0;
    }
    
    void *
    calloc(size_t num_items, size_t size)
    {
        return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
    }
    
    MALLOC_NOINLINE
    static void *
    _malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
            malloc_zone_options_t mzo)
    {
        MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
    
        void *ptr;
        if (malloc_check_start) {
            internal_check();
        }
    
            // 关键代码, 因为最后需要返回的是ptr
        ptr = zone->calloc(zone, num_items, size);
    
        if (os_unlikely(malloc_logger)) {
            malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
                    (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
        }
    
        MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
        if (os_unlikely(ptr == NULL)) {
            malloc_set_errno_fast(mzo, ENOMEM);
        }
        return ptr;
    }
    
       void     *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
    

    跟到这里我们发现没法继续了, 找不到下一层了。

    当断点进入这块时候,
    1.可走汇编, 汇编能看出他走了什么方法
    2.可通过lldb命令 zone->calloc查看他存在哪里。因为有赋值就会有存储值, 有存储就能打印

    zone->calloc
    接下来, 查找default_zone_calloc方法 default_zone_calloc

    有些时候po读不出来, 我们也可以p一下, 读完整

    nano_calloc nano_calloc

    因为不会超过NANO_MAX_SIZE值(超过系统会发生问题), 所以其中关键代码是p这里

            void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
            if (p) {
                return p;
    

    因为void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);, 所以我们要找_nano_malloc_check_clear方法, 点击进入

    static void *
    _nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
    {
        MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
    
        void *ptr;
        size_t slot_key;
        size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
        mag_index_t mag_index = nano_mag_index(nanozone);
    
        nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
    
        ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
        if (ptr) {
            unsigned debug_flags = nanozone->debug_flags;
    #if NANO_FREE_DEQUEUE_DILIGENCE
            size_t gotSize;
            nano_blk_addr_t p; // the compiler holds this in a register
    
            p.addr = (uint64_t)ptr; // Begin the dissection of ptr
            if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
                malloc_zone_error(debug_flags, true,
                        "Invalid signature for pointer %p dequeued from free list\n",
                        ptr);
            }
    
            if (mag_index != p.fields.nano_mag_index) {
                malloc_zone_error(debug_flags, true,
                        "Mismatched magazine for pointer %p dequeued from free list\n",
                        ptr);
            }
    
            gotSize = _nano_vet_and_size_of_free(nanozone, ptr);
            if (0 == gotSize) {
                malloc_zone_error(debug_flags, true,
                        "Invalid pointer %p dequeued from free list\n", ptr);
            }
            if (gotSize != slot_bytes) {
                malloc_zone_error(debug_flags, true,
                        "Mismatched size for pointer %p dequeued from free list\n",
                        ptr);
            }
    
            if (!_nano_block_has_canary_value(nanozone, ptr)) {
                malloc_zone_error(debug_flags, true,
                        "Heap corruption detected, free list canary is damaged for %p\n"
                        "*** Incorrect guard value: %lu\n", ptr,
                        ((chained_block_t)ptr)->double_free_guard);
            }
    
    #if defined(DEBUG)
            void *next = (void *)(((chained_block_t)ptr)->next);
            if (next) {
                p.addr = (uint64_t)next; // Begin the dissection of next
                if (NANOZONE_SIGNATURE != p.fields.nano_signature) {
                    malloc_zone_error(debug_flags, true,
                            "Invalid next signature for pointer %p dequeued from free "
                            "list, next = %p\n", ptr, "next");
                }
    
                if (mag_index != p.fields.nano_mag_index) {
                    malloc_zone_error(debug_flags, true,
                            "Mismatched next magazine for pointer %p dequeued from "
                            "free list, next = %p\n", ptr, next);
                }
    
                gotSize = _nano_vet_and_size_of_free(nanozone, next);
                if (0 == gotSize) {
                    malloc_zone_error(debug_flags, true,
                            "Invalid next for pointer %p dequeued from free list, "
                            "next = %p\n", ptr, next);
                }
                if (gotSize != slot_bytes) {
                    malloc_zone_error(debug_flags, true,
                            "Mismatched next size for pointer %p dequeued from free "
                            "list, next = %p\n", ptr, next);
                }
            }
    #endif /* DEBUG */
    #endif /* NANO_FREE_DEQUEUE_DILIGENCE */
    
            ((chained_block_t)ptr)->double_free_guard = 0;
            ((chained_block_t)ptr)->next = NULL; // clear out next pointer to protect free list
        } else {
            ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
        }
    
        if (cleared_requested && ptr) {
            memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
        }
        return ptr;
    }
    

    这块代码很长, 我们找到关键代码即可, 因为返回的是ptr, 我们就寻找ptr赋值地方就可以。关键代码ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);

    也可进去看一下不过是一些很长回调, 我们实际要获取的是大小, 所以其实关键要找到是slot_bytes, 关键代码size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here

    segregated_size_to_fit segregated_size_to_fit
    
    #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16
    #define SHIFT_NANO_QUANTUM      4
    
    
    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;
    }
    

    segregated_size_to_fit这里边可看到对传入的size做了

    • size + 16 - 1 >> 4 再 <<4操作 后4位抹零, 即16字节对齐

    例如1: size = 8, 8 + 15 = 23 = 0001 1000

    0001 0111
    左移4位
    0000 0001
    右移4位
    0001 0000 = 16

    例如2: size = 23, 23 + 15 = 38 = 0010 0110

    0010 0110
    左移4位
    0000 0010
    右移4位
    0010 0000 = 32

    所以malloc_size其实做了16字节对齐操作

    相关文章

      网友评论

          本文标题:Objc4-818底层探索(二):内存对齐

          本文链接:https://www.haomeiwen.com/subject/nzydeltx.html