TLDR

  1. 介绍了两种较为罕见的 Block 循环引证,并供给了解决办法。
  2. 宏的替换在 Preprocessed 阶段,super 的变更在 compile 阶段,因而对 self 进行 weak/strong dance 并解决不了 super 导致的循环引证问题。
  3. 一起通过 RewriteObjc 与 Disassembly 的办法从底层原理上剖析了 super 与 Block 调用的实质,辅以内存实践状况与示意图方便加深了解。交叉了 objc_msgSend Stub 相关的常识。
  4. 验证了在 Block 中调用 super 时,RewriteObjc 生成代码的一个 bug 。

问题抛出

问题都来自实在事例改编。Demo

@interface TestObj : NSObject
@property (nonatomic, copy) void(^captureSelf)(void);
@property (nonatomic, assign) BOOL capture;
@end
@implementation TestObj
- (instancetype)init {
    if (self = [super init]) {
    // Q1:这样会不会走漏
        self.captureSelf = ^() {
            _capture = YES;
        };
    }
    return self;
}
@end
@interface TestChildObj : TestObj
@end
@implementation TestChildObj
- (instancetype)init {
    if (self = [super init]) {
        @weakify(self);
        // Q2:这样会不会导致走漏?
        self.captureSelf = ^() {
            @strongify(self);
            super.capture = YES;
        };
    }
    return self;
}
@end

代码其实非常简略,一共是两个问题,分别是否会循环引证。

前置常识

环境 Xcode 14.2 (14C18),M1 Mac。

Block 内存布局

想看懂这个需求一些对 block 汇编的了解,可以可以检查:Block-ABI-Apple 。源码都来自 libclosure 。

struct Block_layout {
    void * __ptrauth_objc_isa_pointer isa; // 8
    volatile int32_t flags; // contains ref count // 4
    int32_t reserved; // 4
    BlockInvokeFunction invoke; // 8
    struct Block_descriptor_1 *descriptor; // 8
    // imported variables 
};

假如一个 Block 长这样,在 栈 上,内存布局类似如此:

Block 内调用 super 引发的循环引用

Block 内调用 super 引发的循环引用

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
struct __TestChildObj__simpleBlockOnStack_block_impl_0 {
  struct __block_impl impl; // 8+4+4+8 = 0x18
  struct __TestChildObj__simpleBlockOnStack_block_desc_0* Desc; // 0x8
  int a; // 4,这个偏移量正好便是 0x20
  int b;
  int c;
  int d;
  int e;
  __TestChildObj__simpleBlockOnStack_block_impl_0(void *fp, struct __TestChildObj__simpleBlockOnStack_block_desc_0 *desc, int _a, int _b, int _c, int _d, int _e, int flags=0) : a(_a), b(_b), c(_c), d(_d), e(_e) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

这儿咱们就可以简略得记住,偏移量 + 0x20 便是第一个被 capture 的值。

类推下咱们 void(^captureSelf)(void); 的长什么样子呢?

self.captureSelf = ^() {
    self.capture = YES;
};

以此类推,其实就长这样。

struct Block_layout {
    void * __ptrauth_objc_isa_pointer isa; // 8
    volatile int32_t flags; // contains ref count // 4
    int32_t reserved; // 4
    BlockInvokeFunction invoke; // 8
    struct Block_descriptor_1 *descriptor; // 8
    TestChildObjc *obj; // 8,这个偏移量正好便是 0x20
};

但假如是 capture 了 super 呢?

self.captureSelf = ^() {
    super.capture = YES;
};

在 Rewrite C++ 之后其实就没有捕获了 super(self) 变量了,这个是有疑问的。并且生成的 C++ 代码里是有显着反常的,在函数体中 self 是如何传递进来的?正常的调用是从 __cself 里拿。估计是 Rewrite C++ 有 bug,后文剖析汇编的时分会证明其实实践上是有 capture 住 TestChildObjc * 目标的(一起通过偏移量与 block size 两点来证明)。Child.cpp。

struct __TestChildObj__testSuperCapture_block_impl_0 {
  struct __block_impl impl;
  struct __TestChildObj__testSuperCapture_block_desc_0* Desc;
  __TestChildObj__testSuperCapture_block_impl_0(void *fp, struct __TestChildObj__testSuperCapture_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __TestChildObj__testSuperCapture_block_func_0(struct __TestChildObj__testSuperCapture_block_impl_0 *__cself) {
        ((void (*)(__rw_objc_super *, SEL, BOOL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("TestChildObj"))}, sel_registerName("setCapture:"), ((bool)1));
    }

objc_msgSendSuper2

还需求对 objc_msgSendSuper2 有一些了解,了解每个传入的参数是什么,才能更好了解汇编。其实传入的 objc_super * 是栈上的地址(由所以 struct),栈上会进行分配并赋值。

/// Specifies the superclass of an instance.
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */
};
// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);

Q1:

这个有点经验的都知道走漏了,由于隐式包含调用了 self->_xxx ,仍是通过 self 再进行偏移量获取的。这儿就不再进行剖析了,看完剖析 Q2 之后你也可以尝试剖析 Q1 的问题。

解法也很简略,留意由所以读取偏移量,假如不对 self 判空的话,当 self 为 null 时,进行 self->_capture 的拜访是会出现 MACH 类型的崩溃的。

@weakify(self);
self.captureSelf = ^() {
    @strongify(self);
    if (!self) return;
    self->_capture = YES;
    // _capture = YES;
};

Q2:

其实我一开端以为这是没有问题的,网上也有对 super 的介绍(老八股),咱们知道会 super 被替换为 objc_msgSendSuper(self) 。因而在我浅显的认知中,觉得既然会被换成 self ,那就会被 weak/strong dance 所替换,然后就万事大吉了。

developer.apple.com/documentati…

When it encounters a method call, the compiler generates a call to one of the functions objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, or objc_msgSendSuper_stret. Messages sent to an object’s superclass (using the super keyword) are sent using objc_msgSendSuper; other messages are sent using objc_msgSend. Methods that have data structures as return values are sent using objc_msgSendSuper_stret and objc_msgSend_stret.

但真的有这么简略吗?编译期间 llvm 提示咱们 self 变量是未被运用的,履行时的内存状态也可以证明这样确实是现已很引起了循环引证。

Block 内调用 super 引发的循环引用

Block 内调用 super 引发的循环引用

那这是为什么呢,难道 宏替换 与 objc_msgSendSuper 不是在同一个步骤中进行的嘛?

Preprocessed

咱们先来看 Preprocess 的成果,咱们只摘取 -[TestChildObj init] 的部分,此刻咱们可以看到 weak/strong 的宏是现已展开了的,可是 super.capture 仍是原样。

@implementation TestChildObj
- (instancetype)init {
    if (self = [super init]) {
        @try {} @catch (...) {} __attribute__((objc_ownership(weak))) __typeof__(self) self_weak_ = (self);;
        self.captureSelf = ^() {
            @try {} @catch (...) {}
# 63 "/Users/bytedance/Desktop/demo/demo/ViewController.m"
#pragma clang diagnostic push
# 63 "/Users/bytedance/Desktop/demo/demo/ViewController.m"
#pragma clang diagnostic ignored "-Wshadow"
# 63 "/Users/bytedance/Desktop/demo/demo/ViewController.m"
             __attribute__((objc_ownership(strong))) __typeof__(self) self = self_weak_;
# 63 "/Users/bytedance/Desktop/demo/demo/ViewController.m"
#pragma clang diagnostic pop
# 63 "/Users/bytedance/Desktop/demo/demo/ViewController.m"
                            ;
            super.capture = __objc_yes;
        };
    }
    return self;
}
@end

因而咱们至少可以知道,将 super 替换为 objc_msgSendSuper 至少不在 Preprocess 阶段,并且至少在 Preprocess 阶段之后。

LLVM(Fronted)

接着咱们再来持续探寻详细是在哪个步骤中进行 super 以及正常 OC 办法重写为 objc_msgsend() 的。

github.com/llvm-mirror…

github.com/llvm-mirror…

咱们通过命令 -rewrite-objc 可以将 OC 代码转为 OC ,因而在 llvm 库房中查找 objc_msgSendSuper 可以找到两个文件,可以进行检查。

RewriteModernObjC

咱们就以 Frontend/Rewrite/RewriteModernObjC.cpp 中的代码为例:

// https://github.com/llvm-mirror/clang/blob/aa231e4be75ac4759c236b755c57876f76e3cf05/lib/Frontend/Rewrite/RewriteModernObjC.cpp#L2439
// SynthMsgSendSuperFunctionDecl - id objc_msgSendSuper(void);
void RewriteModernObjC::SynthMsgSendSuperFunctionDecl() {
  IdentifierInfo *msgSendIdent = &Context->Idents.get("objc_msgSendSuper");
  SmallVector<QualType, 2> ArgTys;
  ArgTys.push_back(Context->VoidTy);
  QualType msgSendType = getSimpleFunctionType(Context->getObjCIdType(),
                                               ArgTys, /*variadic=*/true);
  // 初始化成员变量,Synth 的语义是 组成,可以猜到后边实践变的时分会用这个 MsgSendSuperFunctionDecl
  MsgSendSuperFunctionDecl = FunctionDecl::Create(*Context, TUDecl,
                                                  SourceLocation(),
                                                  SourceLocation(),
                                                  msgSendIdent, msgSendType,
                                                  nullptr, SC_Extern);
}

因而持续寻找哪里运用了 MsgSendSuperFunctionDecl :

// https://github.com/llvm-mirror/clang/blob/aa231e4be75ac4759c236b755c57876f76e3cf05/lib/Frontend/Rewrite/RewriteModernObjC.cpp#L3224
Stmt *RewriteModernObjC::SynthMessageExpr(ObjCMessageExpr *Exp,
                                    SourceLocation StartLoc,
                                    SourceLocation EndLoc) {
  // 省掉大段代码
  // Synthesize a call to objc_msgSend().
  SmallVector<Expr*, 8> MsgExprs;
  switch (Exp->getReceiverKind()) {
  case ObjCMessageExpr::SuperClass: {
    MsgSendFlavor = MsgSendSuperFunctionDecl;
    if (MsgSendStretFlavor)
      MsgSendStretFlavor = MsgSendSuperStretFunctionDecl;
    assert(MsgSendFlavor && "MsgSendFlavor is NULL!");
    ObjCInterfaceDecl *ClassDecl = CurMethodDef->getClassInterface();
    SmallVector<Expr*, 4> InitExprs;
    // set the receiver to self, the first argument to all methods.
    InitExprs.push_back(
      NoTypeInfoCStyleCastExpr(Context, Context->getObjCIdType(),
                               CK_BitCast,
                   new (Context) DeclRefExpr(*Context,
                                             CurMethodDef->getSelfDecl(),
                                             false,
                                             Context->getObjCIdType(),
                                             VK_RValue,
                                             SourceLocation()))
                        ); // set the 'receiver'.
    // (id)class_getSuperclass((Class)objc_getClass("CurrentClass"))
    SmallVector<Expr*, 8> ClsExprs;
    ClsExprs.push_back(getStringLiteral(ClassDecl->getIdentifier()->getName()));
    // (Class)objc_getClass("CurrentClass")
    CallExpr *Cls = SynthesizeCallToFunctionDecl(GetMetaClassFunctionDecl,
                                                 ClsExprs, StartLoc, EndLoc);
    ClsExprs.clear();
    ClsExprs.push_back(Cls);
    Cls = SynthesizeCallToFunctionDecl(GetSuperClassFunctionDecl, ClsExprs,
                                       StartLoc, EndLoc);
    // (id)class_getSuperclass((Class)objc_getClass("CurrentClass"))
    // To turn off a warning, type-cast to 'id'
    InitExprs.push_back( // set 'super class', using class_getSuperclass().
                        NoTypeInfoCStyleCastExpr(Context,
                                                 Context->getObjCIdType(),
                                                 CK_BitCast, Cls));
    // struct __rw_objc_super
    QualType superType = getSuperStructType();
    Expr *SuperRep;
    if (LangOpts.MicrosoftExt) {
      SynthSuperConstructorFunctionDecl();
      // Simulate a constructor call...
      DeclRefExpr *DRE = new (Context)
          DeclRefExpr(*Context, SuperConstructorFunctionDecl, false, superType,
                      VK_LValue, SourceLocation());
      SuperRep = CallExpr::Create(*Context, DRE, InitExprs, superType,
                                  VK_LValue, SourceLocation());
      // The code for super is a little tricky to prevent collision with
      // the structure definition in the header. The rewriter has it's own
      // internal definition (__rw_objc_super) that is uses. This is why
      // we need the cast below. For example:
      // (struct __rw_objc_super *)&__rw_objc_super((id)self, (id)objc_getClass("SUPER"))
      //
      SuperRep = new (Context) UnaryOperator(SuperRep, UO_AddrOf,
                               Context->getPointerType(SuperRep->getType()),
                                             VK_RValue, OK_Ordinary,
                                             SourceLocation(), false);
      SuperRep = NoTypeInfoCStyleCastExpr(Context,
                                          Context->getPointerType(superType),
                                          CK_BitCast, SuperRep);
    } else {
      // (struct __rw_objc_super) { <exprs from above> }
      InitListExpr *ILE =
        new (Context) InitListExpr(*Context, SourceLocation(), InitExprs,
                                   SourceLocation());
      TypeSourceInfo *superTInfo
        = Context->getTrivialTypeSourceInfo(superType);
      SuperRep = new (Context) CompoundLiteralExpr(SourceLocation(), superTInfo,
                                                   superType, VK_LValue,
                                                   ILE, false);
      // struct __rw_objc_super *
      SuperRep = new (Context) UnaryOperator(SuperRep, UO_AddrOf,
                               Context->getPointerType(SuperRep->getType()),
                                             VK_RValue, OK_Ordinary,
                                             SourceLocation(), false);
    }
    MsgExprs.push_back(SuperRep);
    break;
  }
  // 然后持续拼调用
  // Create a call to sel_registerName("selName"), it will be the 2nd argument.
  // Now push any user supplied arguments.
}

最终 super 生成的代码类似:(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass(“TestChildObj”))} 。

也便是在 Stack 上创建了一个结构体:__rw_objc_super,其实便是咱们一开端介绍的 objc_super 。

[super init] 生成的类似:

((TestChildObj *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("TestChildObj"))}, sel_registerName("init"))
// 去掉类型转换后,缩略版如下
objc_msgSendSuper({self, class_getSuperclass(objc_getClass("TestChildObj"))}, sel_registerName("init"))

而 super.capture = YES; 生成的类似:

 ((void (*)(__rw_objc_super *, SEL, BOOL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("TestChildObj"))}, sel_registerName("setCapture:"), ((bool)1));

-ccc-print-phases

通过参数 -ccc-print-phases 咱们可以打印出编译的进程,可以看到在有 -rewrite-objc 参数的时分,compiler 步骤是在 preprocessor 之后的。

➜  demo git:(main) ✗ clang -ccc-print-phases -rewrite-objc Child.m
      +- 0: input, "Child.m", objective-c
   +- 1: preprocessor, {0}, objective-c-cpp-output
+- 2: compiler, {1}, rewritten-objc
3: bind-arch, "arm64", {2}, rewritten-objc

咱们可以再看下正常的编译,正常的编译是不会有 rewritten-objc 的步骤的,可是原理上是类似的,因而上面也通过这个手段进行了剖析。

➜  demo git:(main) ✗ clang -ccc-print-phases Child.m
               +- 0: input, "Child.m", objective-c
            +- 1: preprocessor, {0}, objective-c-cpp-output
         +- 2: compiler, {1}, ir
      +- 3: backend, {2}, assembler
   +- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "arm64", {5}, image

Disassembly

咱们最终来看下最终的汇编,验证下 super 被替换的成果。

Caller

咱们替换为更简略的版本。

- (void)testCapture {
->  self.captureSelf = ^() {
        super.capture = YES;
    };
    self.captureSelf(); // only for into block
}
demo`-[TestChildObj testCapture]:
    0x1047b1a98 <+0>:   sub    sp, sp, #0x70
    0x1047b1a9c <+4>:   stp    x29, x30, [sp, #0x60]
    0x1047b1aa0 <+8>:   add    x29, sp, #0x60
    0x1047b1aa4 <+12>:  stur   x0, [x29, #-0x8] // *(x29 - 0x8) = x0 = TestChildObj *
    0x1047b1aa8 <+16>:  stur   x1, [x29, #-0x10] // *(x29 - 0x10) = x1 = @SEL(testCapture)
    0x1047b1aac <+20>:  add    x8, sp, #0x28 // x8 = sp + 0x28
    0x1047b1ab0 <+24>:  str    x8, [sp, #0x8] // *(sp + 0x8) = x8 ,把 sp + 0x28 的地址存到了 sp + 0x8 的方位
    0x1047b1ab4 <+28>:  adrp   x9, 3
    0x1047b1ab8 <+32>:  ldr    x9, [x9, #0x10]
    0x1047b1abc <+36>:  str    x9, [sp, #0x28] // 这儿 x9 算完其实是 __NSStackBlock__ ,存到 sp + 0x28 的方位,正好是 sp + 0x8 指向的当地,但断到这一步的时分其实还没有赋值
    0x1047b1ac0 <+40>:  mov    w9, #-0x3e000000 
    0x1047b1ac4 <+44>:  str    w9, [sp, #0x30] // *(sp + 0x30) = 0xc2000000 = (trim 成 4  Byte 的 -0x3e000000)
    0x1047b1ac8 <+48>:  str    wzr, [sp, #0x34] // *(sp + 0x34) = 0x0 
    0x1047b1acc <+52>:  adrp   x9, 0
    0x1047b1ad0 <+56>:  add    x9, x9, #0xb48            ; __27-[TestChildObj testCapture]_block_invoke at ViewController.m:50
    0x1047b1ad4 <+60>:  str    x9, [sp, #0x38] // *(sp + 0x38) = 函数指针(-[TestChildObj testCapture]_block_invoke)
    0x1047b1ad8 <+64>:  adrp   x9, 3
    0x1047b1adc <+68>:  add    x9, x9, #0x80             ; __block_descriptor_40_e8_32s_e5_v8?0l
    0x1047b1ae0 <+72>:  str    x9, [sp, #0x40] // *(sp + 0x40) = x9 ,其实是指向 __block_descriptor
    0x1047b1ae4 <+76>:  add    x8, x8, #0x20 // 重新开端用 x8 了,x8 = x8 + 0x20,之前 x8 的存储在 0x1047b1ab0 <+24> 这一行里边,值为 sp + 0x28 的地址,现在履行完变成了 sp + 0x48
    0x1047b1ae8 <+80>:  str    x8, [sp, #0x20] // *(sp + 0x20) = x8 = sp + 0x48
    0x1047b1aec <+84>:  ldur   x0, [x29, #-0x8] // x0 = x29 - 0x8 ,这块区域其实等于 sp + 0x58,方才存的是 TestChildObj *
    0x1047b1af0 <+88>:  bl     0x1047b21f0               ; symbol stub for: objc_retain // retain TestChildObj *
    0x1047b1af4 <+92>:  ldr    x2, [sp, #0x8] // x2 = *(sp + 0x8),其实便是方才存下来的 (sp+0x28) 地址
    0x1047b1af8 <+96>:  ldr    x1, [sp, #0x10] // x1 = *(sp + 0x10) ,但这块内存是未初始化的,这儿 x1 后续会被 Selector Stub 掩盖,因而这儿随便是什么都行:)
    0x1047b1afc <+100>: str    x0, [sp, #0x48] // *(sp + 0x48) = x0 = TestChildObj *
    0x1047b1b00 <+104>: ldur   x0, [x29, #-0x8] // x0 
    0x1047b1b04 <+108>: bl     0x1047b2280               ; objc_msgSend$setCaptureSelf:
    0x1047b1b08 <+112>: ldr    x1, [sp, #0x10]
    0x1047b1b0c <+116>: ldur   x0, [x29, #-0x8]
    0x1047b1b10 <+120>: bl     0x1047b2220               ; objc_msgSend$captureSelf
    0x1047b1b14 <+124>: mov    x29, x29
    0x1047b1b18 <+128>: bl     0x1047b21fc               ; symbol stub for: objc_retainAutoreleasedReturnValue
    0x1047b1b1c <+132>: str    x0, [sp, #0x18]
    0x1047b1b20 <+136>: ldr    x8, [x0, #0x10]
    0x1047b1b24 <+140>: blr    x8
    0x1047b1b28 <+144>: ldr    x0, [sp, #0x18]
    0x1047b1b2c <+148>: bl     0x1047b21e4               ; symbol stub for: objc_release
    0x1047b1b30 <+152>: ldr    x0, [sp, #0x20]
    0x1047b1b34 <+156>: mov    x1, #0x0
    0x1047b1b38 <+160>: bl     0x1047b2214               ; symbol stub for: objc_storeStrong
    0x1047b1b3c <+164>: ldp    x29, x30, [sp, #0x60]
    0x1047b1b40 <+168>: add    sp, sp, #0x70
    0x1047b1b44 <+172>: ret    

没想到短短几句赋值,在汇编会有这么长,让咱们一步一步来剖析下。

我会供给履行到这儿的时分的内存示意图与 po 输出。

Step 1

对于栈上的操作既可以用 sp 为基准核算偏移,也可以用 fp(x29) 为基准核算偏移,因而图中我都会标出。

    0x1047b1a98 <+0>:   sub    sp, sp, #0x70
    0x1047b1a9c <+4>:   stp    x29, x30, [sp, #0x60]
    0x1047b1aa0 <+8>:   add    x29, sp, #0x60

前三行便是正常的 push Stack 操作,这次在 Stack 上占用了 0x70 的空间,然后把 x29,x30 存到了 Stack 上。

    0x1047b1aa4 <+12>:  stur   x0, [x29, #-0x8] // *(x29 - 0x8) = x0 = TestChildObj *
    0x1047b1aa8 <+16>:  stur   x1, [x29, #-0x10] // *(x29 - 0x10) = x1 = @SEL(testCapture)
    0x1047b1aac <+20>:  add    x8, sp, #0x28 // x8 = sp + 0x28
    0x1047b1ab0 <+24>:  str    x8, [sp, #0x8] // *(sp + 0x8) = x8 ,把 sp + 0x28 的地址存到了 sp + 0x8 的方位
    0x1047b1ab4 <+28>:  adrp   x9, 3
    0x1047b1ab8 <+32>:  ldr    x9, [x9, #0x10]
 -> 0x1047b1abc <+36>:  str    x9, [sp, #0x28] // 这儿 x9 算完其实是 __NSStackBlock__ ,存到 sp + 0x28 的方位,正好是 sp + 0x8 指向的当地,但断到这一步的时分其实还没有赋值

正式逻辑开端,前两步是把 x0 , x1 存到了对应 Stack 的内存里。

<+20 ~ +24> 两行实践做的便是把 sp + 0x28 的地址存在了 sp + 0x8 的方位。

<+28 ~ +36> 把 Block 的 isa 指针存到了 sp + 0x28 方位,也便是说从这儿开端现已在 Stack 上构建 Block 的区域了,也因而 isa 指针指向了 NSStackBlock

Block 内调用 super 引发的循环引用

(lldb) reg read x0
      x0 = 0x0000600002220c80
(lldb) po 0x0000600002220c80
<TestChildObj: 0x600002220c80>
(lldb) reg read x1
      x1 = 0x00000001047b2489  "testCapture"
(lldb) reg read sp
      sp = 0x000000016b64b930
(lldb) memory read 0x000000016b64b930 (0x000000016b64b930+0x70)
                                     ⬇️ 指向 sp + 0x28 = 0x16b64b958
0x16b64b930: 00 00 00 00 00 90 78 c0 58 b9 64 6b 01 00 00 00  ......x.X.dk....
0x16b64b940: 80 0c 22 02 00 60 00 00 89 24 7b 04 01 00 00 00  .."..`...${.....
0x16b64b950: 08 92 7b 04 01 00 00 00 b4 98 18 80 01 00 00 00  ..{.............
0x16b64b960: 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x16b64b970: 73 73 5f 62 61 72 73 00 d0 07 00 00 00 00 00 00  ss_bars.........
                ⬇️@SEL(testCapture)       ⬇️TestChildObj *
0x16b64b980: 89 24 7b 04 01 00 00 00 80 0c 22 02 00 60 00 00  .${......."..`..
0x16b64b990: f0 b9 64 6b 01 00 00 00 60 1a 7b 04 01 00 00 00  ..dk....`.{.....
(lldb) reg read x9
      x9 = 0x00000001bb8c0f60  (void *)0x00000001bb8c0f38: __NSStackBlock__

memory read 0x000000016b64b930 (0x000000016b64b930+0x70) 便是打出了与示意图对应的内存区域。可以在内存中看到对应的目标,我现已标注在内存中了。

Step 2
    0x1047b1abc <+36>:  str    x9, [sp, #0x28] // 这儿 x9 算完其实是 __NSStackBlock__ ,存到 sp + 0x28 的方位,正好是 sp + 0x8 指向的当地,但断到这一步的时分其实还没有赋值
    0x1047b1ac0 <+40>:  mov    w9, #-0x3e000000 
    0x1047b1ac4 <+44>:  str    w9, [sp, #0x30] // *(sp + 0x30) = 0xc2000000 = (trim 成 4  Byte 的 -0x3e000000)
    0x1047b1ac8 <+48>:  str    wzr, [sp, #0x34] // *(sp + 0x34) = 0x0 
    0x1047b1acc <+52>:  adrp   x9, 0
    0x1047b1ad0 <+56>:  add    x9, x9, #0xb48            ; __27-[TestChildObj testCapture]_block_invoke at ViewController.m:50
    0x1047b1ad4 <+60>:  str    x9, [sp, #0x38] // *(sp + 0x38) = 函数指针(-[TestChildObj testCapture]_block_invoke)
->  0x1047b1ad8 <+64>:  adrp   x9, 3

<+40 ~ +44> 先把 w9 是 x9 寄存器的低 32 位,存进去的值会被 trim 成 4 Byte 的,最终写入 sp + 0x30 ~ sp + 0x34 方位的是 0xc2000000 。这儿的含义是 Block_layout 中的 volatile int32_t flags; 。

<+48> 是把 sp + 0x34 ~ sp + 0x38 置空。wzr 是 32 位的 零寄存器。这儿的含义是 Block_layout 中的 int32_t reserved; 。

<+52 ~ +60> 是 adrp 对应的惯例操作,把函数指针存到 sp + 0x38 的方位。这儿的含义是 Block_layout 中的 BlockInvokeFunction invoke; 。

Block 内调用 super 引发的循环引用

(lldb) memory read 0x000000016b64b930 (0x000000016b64b930+0x70)
0x16b64b930: 00 00 00 00 00 90 78 c0 58 b9 64 6b 01 00 00 00  ......x.X.dk....
0x16b64b940: 80 0c 22 02 00 60 00 00 89 24 7b 04 01 00 00 00  .."..`...${.....
0x16b64b950: 08 92 7b 04 01 00 00 00 60 0f 8c bb 01 00 00 00  ..{.....`.......
                 ⬇️          ⬇️0x0     ⬇️0x01047b1b48(函数指针)
0x16b64b960: 00 00 00 c2 00 00 00 00 48 1b 7b 04 01 00 00 00  ........H.{.....
0x16b64b970: 80 40 7b 04 01 00 00 00 d0 07 00 00 00 00 00 00  .@{.............
0x16b64b980: 89 24 7b 04 01 00 00 00 80 0c 22 02 00 60 00 00  .${......."..`..
0x16b64b990: f0 b9 64 6b 01 00 00 00 60 1a 7b 04 01 00 00 00  ..dk....`.{.....
Step 3
    0x1047b1ad8 <+64>:  adrp   x9, 3
    0x1047b1adc <+68>:  add    x9, x9, #0x80             ; __block_descriptor_40_e8_32s_e5_v8?0l
    0x1047b1ae0 <+72>:  str    x9, [sp, #0x40] // *(sp + 0x40) = x9 ,其实是指向 __block_descriptor
    0x1047b1ae4 <+76>:  add    x8, x8, #0x20 // 重新开端用 x8 了,x8 = x8 + 0x20,之前 x8 的存储在 0x1047b1ab0 <+24> 这一行里边,值为 sp + 0x28 的地址,现在履行完变成了 sp + 0x48
    0x1047b1ae8 <+80>:  str    x8, [sp, #0x20] // *(sp + 0x20) = x8 = sp + 0x48
    0x1047b1aec <+84>:  ldur   x0, [x29, #-0x8] // x0 = x29 - 0x8 ,这块区域其实等于 sp + 0x58,方才存的是 TestChildObj *
    0x1047b1af0 <+88>:  bl     0x1047b21f0               ; symbol stub for: objc_retain // retain TestChildObj *
    0x1047b1af4 <+92>:  ldr    x2, [sp, #0x8] // x2 = *(sp + 0x8),其实便是方才存下来的 (sp+0x28) 地址
    0x1047b1af8 <+96>:  ldr    x1, [sp, #0x10] // x1 = *(sp + 0x10) ,但这块内存是未初始化的, x1 的值应该是无效的。
    0x1047b1afc <+100>: str    x0, [sp, #0x48] // *(sp + 0x48) = x0 = TestChildObj *
    0x1047b1b00 <+104>: ldur   x0, [x29, #-0x8] // x0 
->  0x1047b1b04 <+108>: bl     0x1047b2280               ; objc_msgSend$setCaptureSelf:

<+64 ~ +72> 把 __block_descriptor 的指针存到了 sp + 0x40 的方位。这儿的含义是 Block_layout 中的 struct Block_descriptor_1 *descriptor; 。

<+76 ~ +80> 把 sp + 0x20 的值指向了 sp + 0x48 。

<+84 ~ +88> 把 x0 变成了 TestChildObj *,并调用了一次 retain 。

<+92 ~ +108> 是在做调用 @SEL(setCaptureSelf:) 的预备(赋值 x0,x1,x2)并进行调用。这儿需求留意的是,x1 的值是不确定的(由于从一块未初始化的内存区域中读取了值),没有在 <+108> 之前指向对应的 SEL 。咱们可以在下面内存的输出中看到。

咱们可以看到在 sp + 0x48 得方位存的是 TestChildObj * ,而从 sp + 0x28 开端便是 Block_layout 的栈内存。这儿还可以说 sp + 0x48 方位放了一个 TestChildObj * 只是刚好这么分布,只是巧合。咱们可以还可以通过别的方式验证。

Block 内调用 super 引发的循环引用

咱们回想下 Block_descriptor 的结构,对应的 size 是 0x28 ,可以印证巨细不是 0x20,而是还有一个 8 个 Byte 的 id 类型指针,至此,完成实锤,rewrite 生成的代码是过错的。

(lldb) reg read x9
      x9 = 0x00000001047b4080  demo`__block_descriptor_40_e8_32s_e5_v8?0l
(lldb) memory read 0x00000001047b4080
0x1047b4080: 00 00 00 00 00 00 00 00 28 00 00 00 00 00 00 00  ........(.......
0x1047b4090: 9c 1b 7b 04 01 00 00 00 d4 1b 7b 04 01 00 00 00  ..{.......{.....
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

咱们持续看内存分布与寄存器的值。

(lldb) memory read 0x000000016b64b930 (0x000000016b64b930+0x70)
0x16b64b930: 00 00 00 00 00 90 78 c0 58 b9 64 6b 01 00 00 00  ......x.X.dk....
0x16b64b940: 80 0c 22 02 00 60 00 00 89 24 7b 04 01 00 00 00  .."..`...${.....
0x16b64b950: 78 b9 64 6b 01 00 00 00 60 0f 8c bb 01 00 00 00  x.dk....`.......
0x16b64b960: 00 00 00 c2 00 00 00 00 48 1b 7b 04 01 00 00 00  ........H.{.....
0x16b64b970: 80 40 7b 04 01 00 00 00 80 0c 22 02 00 60 00 00  .@{......."..`..
0x16b64b980: 89 24 7b 04 01 00 00 00 80 0c 22 02 00 60 00 00  .${......."..`..
0x16b64b990: f0 b9 64 6b 01 00 00 00 60 1a 7b 04 01 00 00 00  ..dk....`.{.....
(lldb) reg read 
General Purpose Registers:
        x0 = 0x0000600002220c80
        x1 = 0x0000600002220c80 // 这儿 x1 跟 x0 一样是巧合,x1 是从未初始化的 Stack 区域中获取的
        x2 = 0x000000016b64b958
(lldb) po 0x0000600002220c80 // x0
<TestChildObj: 0x600002220c80>
(lldb) po 0x0000600002220c80 // x1,这个不是 @SEL(setCaptureSelf:),原因看 objc_msgSend Stub
<TestChildObj: 0x600002220c80>
(lldb) po 0x000000016b64b958 // x2
<__NSStackBlock__: 0x16b64b958>
 signature: "v8@?0"
 invoke   : 0x1047b1b48
 copy     : 0x1047b1b9c
 dispose  : 0x1047b1bd4 

所以这儿得到了实锤,因而也更可有底气得得到结论: sp + 0x28 ~ sp + 0x50 是对应 Block_layout 的巨细,变量可以一一对应。RewriteObjC 生成的 C++ 代码存在 bug 。

objc_msgSend Stub

关于这个 x1 不是 @SEL 的状况,请教了下相关同学后了解到 ,实践是 Xcode 14 新引入的优化:objc_msgSend Stub 的改动,只需运用 Xcode 14 打包,即便在之前的 OS 体系上对包巨细也有帮助。

实质便是不再直接调用 _objc_msgsend 了,通过 Selector stub 转了一层,在 stub 里对 x1 进行了赋值,外部调用 objc_msgSend$setCaptureSelf: 时 x1 是未界说变得合理了起来。

Block 内调用 super 引发的循环引用

可以看:【WWDC22 110363】App 包巨细优化和 Runtime 上的功能提高 – 小专栏,ld64.lld: Add support for _objc_msgSend stubs from Xcode 14 Issue #56034 llvm/llvm-projec…

剖析 Caller 的最终,咱们再对应下 C++ 代码,加深一下了解。

((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setCaptureSelf:"), ((void (*)())&__TestChildObj__testSuperCapture_block_impl_0((void *)__TestChildObj__testSuperCapture_block_func_0, &__TestChildObj__testSuperCapture_block_desc_0_DATA)));

Callee

接着咱们再剖析下 _block_invoke 内的调用。

// #0        0x0000000100909b64 in __27-[TestChildObj testCapture]_block_invoke
(lldb) dis
demo`:
    0x100909b48 <+0>:  sub    sp, sp, #0x30
    0x100909b4c <+4>:  stp    x29, x30, [sp, #0x20]
    0x100909b50 <+8>:  add    x29, sp, #0x20
    0x100909b54 <+12>: mov    x8, x0 // 逻辑正式开端, x8 = x0, x0 是 __NSMallocBlock__ 目标, type == Block_layout
    0x100909b58 <+16>: stur   x8, [x29, #-0x8] // 把 x0(x8) 存到 x29 - 0x8 的方位
    0x100909b5c <+20>: mov    x8, x0 // x8 = x0
    0x100909b60 <+24>: str    x8, [sp, #0x10] // *(sp + 0x10) = x8(x0)
    0x100909b64 <+28>: ldr    x8, [x0, #0x20] // x8 = *(x0 + 0x20),x0 + 0x20 的方位其实是 TestChildObjc * ,原因咱们之前现已解说过了,这儿可以再次实锤 capture 了 TestChildObj 目标
    0x100909b68 <+32>: mov    x0, sp // x0 = sp,栈上的地址,留意这儿是直接赋值,不是 x0 = *sp ,sp 的内存是未界说的,不了解为什么内存为界说可以看 [OC 局部变量未初始化的危险性](https://tech.bytedance.net/articles/7187275573624832061)。这儿需求知道下 objc_msgSendSuper2 的第一个参数传了什么(答案是:栈上的地址),可以更好的了解
    0x100909b6c <+36>: str    x8, [sp] // *sp = x8 = TestChildObjc * 
    0x100909b70 <+40>: adrp   x8, 8
    0x100909b74 <+44>: ldr    x8, [x8, #0x198]
    0x100909b78 <+48>: str    x8, [sp, #0x8] // *(sp + 0x8) = x8 ,x8 其实是 [TestChildObj class], Class 类型
    0x100909b7c <+52>: adrp   x8, 8
    0x100909b80 <+56>: ldr    x1, [x8, #0x148] // 通过一系列操作(略),x1 变成了 @selector(setCapture:)
    0x100909b84 <+60>: mov    w8, #0x1
    0x100909b88 <+64>: and    w2, w8, #0x1 // w2 = YES
    0x100909b8c <+68>: bl     0x10090a1cc               ; symbol stub for: objc_msgSendSuper2 
    0x100909b90 <+72>: ldp    x29, x30, [sp, #0x20]
    0x100909b94 <+76>: add    sp, sp, #0x30
    0x100909b98 <+80>: ret 
Step 1
    0x100909b48 <+0>:  sub    sp, sp, #0x30
    0x100909b4c <+4>:  stp    x29, x30, [sp, #0x20]
    0x100909b50 <+8>:  add    x29, sp, #0x20
    0x100909b54 <+12>: mov    x8, x0 # 逻辑正式开端, x8 = x0, x0 是 __NSMallocBlock__ 目标, type == Block_layout
    0x100909b58 <+16>: stur   x8, [x29, #-0x8] // 把 x0(x8) 存到 x29 - 0x8 的方位
    0x100909b5c <+20>: mov    x8, x0 // x8 = x0
    0x100909b60 <+24>: str    x8, [sp, #0x10] // *(sp + 0x10) = x8(x0)
->  0x100909b64 <+28>: ldr    x8, [x0, #0x20] // x8 = *(x0 + 0x20),x0 + 0x20 的方位其实是 TestChildObjc * ,原因咱们之前现已解说过了,这儿可以再次实锤 capture 了 TestChildObj 目标

<+12 ~ +28>做的是把 NSMallocBlock 指针塞到 sp + 0x10 跟 sp + 0x18 两个方位。

Block 内调用 super 引发的循环引用

这儿额定说一句,Block 之所以可以 po 出信息,是由于第一个变量其实也是 isa 指针,可以当一个更广义的目标。

// 刚履行到 <+28> 时内存的样子
(lldb) reg read x29
      fp = 0x000000016f4f3920
(lldb) reg read sp
      sp = 0x000000016f4f3900
(lldb) memory read 0x000000016f4f3900
0x16f4f3900: 20 39 4f 6f 01 00 00 00 7c 1f 0b 80 01 00 00 00   9Oo....|.......
0x16f4f3910: 90 1e ec 00 00 60 00 00 90 1e ec 00 00 60 00 00  .....`.......`..
(lldb) reg read x0
      x0 = 0x0000600000ec1e90
(lldb) po 0x0000600000ec1e90
<__NSMallocBlock__: 0x600000ec1e90>
 signature: "v8@?0"
 invoke   : 0x100909b48 (/Users/bytedance/Library/Developer/CoreSimulator/Devices/900AF9D9-1C4F-47B0-83CA-D4D14D8503D3/data/Containers/Bundle/Application/E3764F89-79A4-4C8F-8E2C-029EE7895F8B/demo.app/demo`__27-[TestChildObj testCapture]_block_invoke)
 copy     : 0x100909b9c (/Users/bytedance/Library/Developer/CoreSimulator/Devices/900AF9D9-1C4F-47B0-83CA-D4D14D8503D3/data/Containers/Bundle/Application/E3764F89-79A4-4C8F-8E2C-029EE7895F8B/demo.app/demo`__copy_helper_block_e8_32s)
 dispose  : 0x100909bd4 (/Users/bytedance/Library/Developer/CoreSimulator/Devices/900AF9D9-1C4F-47B0-83CA-D4D14D8503D3/data/Containers/Bundle/Application/E3764F89-79A4-4C8F-8E2C-029EE7895F8B/demo.app/demo`__destroy_helper_block_e8_32s)
Step 2
    0x100909b64 <+28>: ldr    x8, [x0, #0x20] // x8 = *(x0 + 0x20),x0 + 0x20 的方位其实是 TestChildObjc * ,原因咱们之前现已解说过了,这儿可以再次实锤 capture 了 TestChildObj 目标
    0x100909b68 <+32>: mov    x0, sp // x0 = sp,栈上的地址,留意这儿是直接赋值,不是 x0 = *sp ,sp 的内存是未界说的。这儿需求知道下 objc_msgSendSuper2 的第一个参数传了什么(答案是:栈上的地址),可以更好的了解
    0x100909b6c <+36>: str    x8, [sp] // *sp = x8 = TestChildObjc * 
->  0x100909b70 <+40>: adrp   x8, 8

<+28> 把 x8 赋值为了 TestChildObjc *。

<+32> 把 x0 赋值为了 sp,sp 是栈上的地址,留意这儿是直接赋值,不是 x0 = *sp ,sp 的内存是未界说的。这儿需求知道下 objc_msgSendSuper2 的第一个参数传了什么,可以回想下 Rewrite C++ 时看到的成果(答案是:栈上的地址),可以更好的了解。

 ((void (*)(__rw_objc_super *, SEL, BOOL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("TestChildObj"))}, sel_registerName("setCapture:"), ((bool)1));

<+36> 把 x8 塞到了 sp 指向的地址,sp 的内存完成了初始化。

Block 内调用 super 引发的循环引用

Step 3
    0x100909b70 <+40>: adrp   x8, 8
    0x100909b74 <+44>: ldr    x8, [x8, #0x198]
    0x100909b78 <+48>: str    x8, [sp, #0x8] // *(sp + 0x8) = x8 ,x8 其实是 [TestChildObj class], Class 类型
->  0x100909b7c <+52>: adrp   x8, 8

<+40 ~ +48> 这儿就很简略了,把 [TestChildObj class] 存到了 sp + 0x8 的方位。到这儿,struct objc_super 就初始化完成了。后边就能正常调用了。

Block 内调用 super 引发的循环引用

Step 4
    0x100909b7c <+52>: adrp   x8, 8
    0x100909b80 <+56>: ldr    x1, [x8, #0x148] // 通过一系列操作(略),x1 变成了 @selector(setCapture:)
    0x100909b84 <+60>: mov    w8, #0x1
    0x100909b88 <+64>: and    w2, w8, #0x1 // w2 = YES
    0x100909b8c <+68>: bl     0x10090a1cc               ; symbol stub for: objc_msgSendSuper2 

<+52 ~ +56> 将 x1 赋值成了 @selector(setCapture:)。

<+60 ~ +64> 将 x2 赋值为了 YES。

<+68> 进行了 objc_msgSendSuper2 的调用。回想下 Step 2 的时分,x0 是 sp 的地址,也便是 struct objc_super * 。

至此,一切剖析结束。

Q2 的 解法

第一种解法便是额定开一个函数,通过 self 去调用。例如这样:

@implementation TestChildObj
- (instancetype)init {
    if (self = [super init]) {
        @weakify(self);
        self.captureSelf = ^() {
            @strongify(self);
            [self callSetSuperCapture:YES];
            super.capture = YES;
        };
    }
    return self;
}
- (void)callSetSuperCapture:(BOOL)capture {
    super.capture = YES;
}
@end

但这种解法不是很优雅,去看完成的时分还得跳转,仍是直接在 Block 内调用会适宜一些。

因而咱们给出第二个解法,来自公司内 APM 团队的完成启发,XIG 是自己工程自己界说的前缀,详细可以依据实践调用场景再弥补一些界说。

#define xig_create_super_info(thisSelf, superClass) \
struct objc_super xig_super_info = {                \
.receiver = (thisSelf),                         \
.super_class = (superClass) }
#define xig_void_msgSendSuper_void(super_info, selector) \
((void (*)(struct objc_super *, SEL))objc_msgSendSuper)((super_info), (selector))
#define xig_void_msgSendSuper_BOOL(super_info, selector, BOOLValue) \
((void (*)(struct objc_super *, SEL, BOOL))objc_msgSendSuper)((super_info), (selector), (BOOLValue))

运用示例的话,就可以这样

 @weakify(self);
 self.captureSelf = ^() {
    @strongify(self);
    xig_create_super_info(self, SuperClass.class); // SuperClass 需求替换成各自实在的类
    xig_void_msgSendSuper_BOOL(&xig_super_info, @selector(setCapture:), YES);  //  super.capture = YES;
 };

结论

因而成果很清晰了,宏替换的步骤在 Rewrite(Fronted) 之前,因而在宏替换时,super 仍旧仍是 super,不能被 weakify/strongify(self) 替换为弱引证。

参考

iOS汇编教程(五)Objc Block 的内存布局和汇编表明 –

02-探究iOS底层原理|编译器LLVM项目【Clang、SwiftC、优化器、LLVM、Xcode编译的进程】 –

objc_msgSend | Apple Developer Documentation

Block-ABI-Apple

github.com/llvm-mirror…

tech.bytedance.net/articles/71…

OC 局部变量未初始化的危险性 –

【WWDC22 110363】App 包巨细优化和 Runtime 上的功能提高 – 小专栏

ld64.lld: Add support for _objc_msgSend stubs from Xcode 14 Issue #56034 llvm/llvm-projec…

CallSuperInBlockDemo**