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

3,866 阅读16分钟

系列文章

  1. iOS汇编入门教程(一)ARM64汇编基础
  2. iOS汇编入门教程(二)在Xcode工程中嵌入汇编代码
  3. iOS汇编入门教程(三)汇编中的 Section 与数据存取
  4. iOS汇编教程(四)基于 LLDB 动态调试快速分析系统函数的实现

前言

在 Objc 中,Block 是一个特殊的对象,它的实例并非是常规的对象结构,而是以 Block_layout 结构体的形式存在。在声明时,Block 的结构体会以值类型的形式直接存储在栈上,随后会被 copy 到堆上,成为一个特殊的对象,学习 Block 的底层原理一方面能够掌握复杂值类型的存储和传递方式,另一方面也能在逆向分析遇到 Block 时快速定位与分析相关逻辑。

Block 的结构

Block 的结构可以在 Runtime 的开源代码 Objc4-706 中找到,它位于 Block-private.h 中:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

对比常规的 OC 对象 objc_object 结构:

struct objc_object {
private:
    isa_t isa; // union contains Class
    // ivar instances
}

可以发现 Block 和常规对象有异曲同工之妙,都是通过 isa 指向的类对象记录基本信息,区别在于 Block 对象后面跟的是捕获的变量列表,而常规对象后面跟的是 ivar 实例列表。

Block 的汇编表示

下面我们用一个简单的例子来分析生成的汇编代码:

// block.m
#import <Foundation/Foundation.h>

typedef int (^CommonBlock)(void);

CommonBlock simpleBlockOnStack() {
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int (^theBlock)(void) = ^int {
        return a + b + c + d + e;
    };
    return theBlock;
}

void invokeStackBlock() {
    CommonBlock block = simpleBlockOnStack();
    block();
}

int main(int argc, char *argv[]) {
    invokeStackBlock();
    return 0;
}

使用 clang 生成 a.out:

clang -arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` block.m -framework Foundation -fobjc-arc

将 a.out 拖入 IDA 或 Hopper 中进行反汇编,结合 simpleBlockOnStack 和 invokeStackBlock 两个符号来分析 Block 的创建、传递和调用过程。

注意,var_XY 的值是 -0xXY,var_s0 的值是 0。

Block 的创建过程

下面是 _simpleBlockOnStack 符号的反汇编结果:

__text:0000000100007D9C                 SUB             SP, SP, #0x70
__text:0000000100007DA0                 STP             X29, X30, [SP,#0x60+var_s0]
__text:0000000100007DA4                 ADD             X29, SP, #0x60
__text:0000000100007DA8                 MOV             W8, #1
__text:0000000100007DAC                 STUR            W8, [X29,#var_4]
__text:0000000100007DB0                 MOV             W8, #2
__text:0000000100007DB4                 STUR            W8, [X29,#var_8]
__text:0000000100007DB8                 MOV             W8, #3
__text:0000000100007DBC                 STUR            W8, [X29,#var_C]
__text:0000000100007DC0                 MOV             W8, #4
__text:0000000100007DC4                 STUR            W8, [X29,#var_10]
__text:0000000100007DC8                 MOV             W8, #5
__text:0000000100007DCC                 STUR            W8, [X29,#var_14]
__text:0000000100007DD0                 ADRP            X9, #__NSConcreteStackBlock_ptr@PAGE
__text:0000000100007DD4                 LDR             X9, [X9,#__NSConcreteStackBlock_ptr@PAGEOFF]
__text:0000000100007DD8                 STR             X9, [SP,#0x60+var_58]
__text:0000000100007DDC                 MOV             W8, #0xC0000000
__text:0000000100007DE0                 STR             W8, [SP,#0x60+var_50]
__text:0000000100007DE4                 MOV             W8, #0
__text:0000000100007DE8                 STR             W8, [SP,#0x60+var_4C]
__text:0000000100007DEC                 ADRP            X9, #___simpleBlockOnStack_block_invoke@PAGE
__text:0000000100007DF0                 ADD             X9, X9, #___simpleBlockOnStack_block_invoke@PAGEOFF
__text:0000000100007DF4                 STR             X9, [SP,#0x60+var_48]
__text:0000000100007DF8                 ADRP            X9, #___block_descriptor_52_e5_i8__0l@PAGE
__text:0000000100007DFC                 ADD             X9, X9, #___block_descriptor_52_e5_i8__0l@PAGEOFF
__text:0000000100007E00                 STR             X9, [SP,#0x60+var_40]
__text:0000000100007E04                 LDUR            W8, [X29,#var_4]
__text:0000000100007E08                 STR             W8, [SP,#0x60+var_38]
__text:0000000100007E0C                 LDUR            W8, [X29,#var_8]
__text:0000000100007E10                 STR             W8, [SP,#0x60+var_34]
__text:0000000100007E14                 LDUR            W8, [X29,#var_C]
__text:0000000100007E18                 STR             W8, [SP,#0x60+var_30]
__text:0000000100007E1C                 LDUR            W8, [X29,#var_10]
__text:0000000100007E20                 STR             W8, [SP,#0x60+var_2C]
__text:0000000100007E24                 LDUR            W8, [X29,#var_14]
__text:0000000100007E28                 STR             W8, [SP,#0x60+var_28]
__text:0000000100007E2C                 ADD             X0, SP, #0x60+var_58
__text:0000000100007E30                 BL              _objc_retainBlock
__text:0000000100007E34                 STUR            X0, [X29,#var_20]
__text:0000000100007E38                 LDUR            X0, [X29,#var_20]
__text:0000000100007E3C                 BL              _objc_retainBlock
__text:0000000100007E40                 SUB             X9, X29, #-var_20
__text:0000000100007E44                 MOV             X30, #0
__text:0000000100007E48                 STR             X0, [SP,#0x60+var_60]
__text:0000000100007E4C                 MOV             X0, X9
__text:0000000100007E50                 MOV             X1, X30
__text:0000000100007E54                 BL              _objc_storeStrong
__text:0000000100007E58                 LDR             X0, [SP,#0x60+var_60]
__text:0000000100007E5C                 LDP             X29, X30, [SP,#0x60+var_s0]
__text:0000000100007E60                 ADD             SP, SP, #0x70
__text:0000000100007E64                 B               _objc_autoreleaseReturnValue

显然,从 7DA8 到 7DCC 的部分是对函数 simpleBlockOnStack 开头的五个 int 变量 a-e 的定义,以当前栈帧的起始地址为零点(后面讨论栈上地址时都以此为前提),变量 a-e 分别被存储在栈的 -0x14 ~ -0x24 区域,

Block ISA

接下来 7DD0 - 7DD4 的代码取出的 __NSConcreteStackBlock_ptr 是指向 __NSConcreteStackBlock 的指针,而 NSConcreteStackBlock 就是 Block 的 isa 数据。

__text:0000000100007DD0                 ADRP            X9, #__NSConcreteStackBlock_ptr@PAGE
__text:0000000100007DD4                 LDR             X9, [X9,#__NSConcreteStackBlock_ptr@PAGEOFF]
__text:0000000100007DD8                 STR             X9, [SP,#0x60+var_58]

它被存储在了栈的 -0x68 区域(IDA中,var_XY = -0xXY,SP 指向 -0x70,-0x70 + 0x60 + (-0x58) = -0x68)。

Flags & Reserved

随后紧接着的 4 句是 flags 和 reserved 的存储逻辑,根据文章开头给出的结构,他们是两个 int 变量,Wn 寄存器取的是 Xn 的低 32 位,即一个 Word = 4B,正好是一个 int 的长度,他们分别存储在栈的 -0x60 和 -0x5C 区域。

__text:0000000100007DDC                 MOV             W8, #0xC0000000
__text:0000000100007DE0                 STR             W8, [SP,#0x60+var_50]
__text:0000000100007DE4                 MOV             W8, #0
__text:0000000100007DE8                 STR             W8, [SP,#0x60+var_4C]

Block Invoker

接下来 3 句是 Block Invoker 的存储逻辑,Block Invoker 就是 Block 的逻辑的函数指针,它被存储在了栈的 -0x58 区域。

__text:0000000100007DEC                 ADRP            X9, #___simpleBlockOnStack_block_invoke@PAGE
__text:0000000100007DF0                 ADD             X9, X9, #___simpleBlockOnStack_block_invoke@PAGEOFF
__text:0000000100007DF4                 STR             X9, [SP,#0x60+var_48]

Block Descriptor

接下来是 Block Descriptor 的存储逻辑,Descriptor 的结构为:

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

uintptr_t 是 unsigned long 的 alias:

typedef unsigned long           uintptr_t;

由此可计算出在 AArch64 下这是一个 16B 大小的结构体,注意内存中存储的是它的指针,也就是 8B,它的存储逻辑定义在 7DF8 - 7E00 处,它被存储在栈的 -0x50 处。

__text:0000000100007DF8                 ADRP            X9, #___block_descriptor_52_e5_i8__0l@PAGE
__text:0000000100007DFC                 ADD             X9, X9, #___block_descriptor_52_e5_i8__0l@PAGEOFF
__text:0000000100007E00                 STR             X9, [SP,#0x60+var_40]

Imported Variables

从 7E04 - 7E28 区域是 Block 捕获的变量存储逻辑,由于未声明 __block,这些值只是简单的静态拷贝。

__text:0000000100007E04                 LDUR            W8, [X29,#var_4]
__text:0000000100007E08                 STR             W8, [SP,#0x60+var_38]
__text:0000000100007E0C                 LDUR            W8, [X29,#var_8]
__text:0000000100007E10                 STR             W8, [SP,#0x60+var_34]
__text:0000000100007E14                 LDUR            W8, [X29,#var_C]
__text:0000000100007E18                 STR             W8, [SP,#0x60+var_30]
__text:0000000100007E1C                 LDUR            W8, [X29,#var_10]
__text:0000000100007E20                 STR             W8, [SP,#0x60+var_2C]
__text:0000000100007E24                 LDUR            W8, [X29,#var_14]
__text:0000000100007E28                 STR             W8, [SP,#0x60+var_28]

这段逻辑分别取出了栈上 -0x14 ~ -0x24 区域的变量拷贝到 -0x48 ~ -0x38 区域,结合上文的分析,这是将局部变量 a-e 拷贝到了 Block 的变量捕获区。

Stack Layout

有了上面的分析,我们就可以画出 Block 在栈上的内存布局了,其中浅蓝色区域即为 Block Layout 的全部内容。

Block 的传递

在 MRC 时代,栈上的 Block 不会自动拷贝到堆,这就意味着在使用 Block 时直接访问的即是上图中从 -0x68 ~ -0x34 的内容,在这种情况下如果在调用 Block 前涉及到了其他函数调用,Block 的存储区会被覆盖从而出错,因此在 ARC 下在 Block 创建完成后会被立即拷贝到堆区,这段代码在 7E2C ~ 7E48 区域:

__text:0000000100007E2C                 ADD             X0, SP, #0x60+var_58
__text:0000000100007E30                 BL              _objc_retainBlock
__text:0000000100007E34                 STUR            X0, [X29,#var_20]
__text:0000000100007E38                 LDUR            X0, [X29,#var_20]
__text:0000000100007E3C                 BL              _objc_retainBlock
__text:0000000100007E40                 SUB             X9, X29, #-var_20
__text:0000000100007E44                 MOV             X30, #0
__text:0000000100007E48                 STR             X0, [SP,#0x60+var_60]

在 7E2C 处首先计算了 Block ISA 的地址,X0 = -0x70 + 0x60 - 0x58 = -0x68 = &Block_ISA,随后以 isa 的地址为参数调用了 objc_retainBlock 函数:

BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    
id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

实际调用了 BuiltIn 的函数 _Block_copy 来将整个 Block 复制到堆区,并返回堆区的 Block ISA 地址,将其存储在栈的 -0x70 区域,并作为函数的返回值。

综上所述,Block 传递时实际上传递的是 Block ISA 的地址,根据 Block ISA 地址向高地址取值即可获得完整的 Block Layout 数据。

Block 的调用

Caller 分析

invokeStackBlock 函数是 Block Caller,我们先分析下它的实现:

__text:0000000100007EA4                 SUB             SP, SP, #0x30
__text:0000000100007EA8                 STP             X29, X30, [SP,#0x20+var_s0]
__text:0000000100007EAC                 ADD             X29, SP, #0x20
__text:0000000100007EB0                 BL              _simpleBlockOnStack
__text:0000000100007EB4                 MOV             X29, X29
__text:0000000100007EB8                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000100007EBC                 STUR            X0, [X29,#var_8]
__text:0000000100007EC0                 LDUR            X0, [X29,#var_8]
__text:0000000100007EC4                 MOV             X30, X0
__text:0000000100007EC8                 LDR             X0, [X0,#0x10]
__text:0000000100007ECC                 STR             X0, [SP,#0x20+var_10]
__text:0000000100007ED0                 MOV             X0, X30
__text:0000000100007ED4                 LDR             X30, [SP,#0x20+var_10]
__text:0000000100007ED8                 BLR             X30
__text:0000000100007EDC                 SUB             X30, X29, #-var_8
__text:0000000100007EE0                 STR             W0, [SP,#0x20+var_14]
__text:0000000100007EE4                 MOV             X0, X30
__text:0000000100007EE8                 MOV             X30, #0
__text:0000000100007EEC                 MOV             X1, X30
__text:0000000100007EF0                 BL              _objc_storeStrong
__text:0000000100007EF4                 LDP             X29, X30, [SP,#0x20+var_s0]
__text:0000000100007EF8                 ADD             SP, SP, #0x30
__text:0000000100007EFC                 RET

重点看 7EB0 ~ 7ED8 区域,这是从 simpleBlockOnStack 函数返回 Block 并调用的过程:

__text:0000000100007EB0                 BL              _simpleBlockOnStack
__text:0000000100007EB4                 MOV             X29, X29
__text:0000000100007EB8                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000100007EBC                 STUR            X0, [X29,#var_8]
__text:0000000100007EC0                 LDUR            X0, [X29,#var_8]
__text:0000000100007EC4                 MOV             X30, X0
__text:0000000100007EC8                 LDR             X0, [X0,#0x10]
__text:0000000100007ECC                 STR             X0, [SP,#0x20+var_10]
__text:0000000100007ED0                 MOV             X0, X30
__text:0000000100007ED4                 LDR             X30, [SP,#0x20+var_10]
__text:0000000100007ED8                 BLR             X30

simpleBlockOnStack 返回的是堆区 Block 的 ISA 地址,在 7EC8 处,X0 = ISA + 0x10,根据上面的分析,ISA + 0x10 指向的是 Block Invoker,随后它被赋给 X30 作为 BLR 的参数,实现对 Block Invoker 的调用,注意 7EC4 和 7ED0 两句,前者先备份了 X0 = ISA 的值,随后还原,因此 Block Invoker 的入参是 Block ISA 的地址,这是为了能够在实现中取出 Block 信息,例如捕获的变量。

Callee 分析

下面我们分析 ___simpleBlockOnStack_block_invoke 的实现,它指向的代码如下:

__text:0000000100007E68                 SUB             SP, SP, #0x10
__text:0000000100007E6C                 STR             X0, [SP,#0x10+var_8]
__text:0000000100007E70                 MOV             X8, X0
__text:0000000100007E74                 STR             X8, [SP,#0x10+var_10]
__text:0000000100007E78                 LDR             W9, [X0,#0x20]
__text:0000000100007E7C                 LDR             W10, [X0,#0x24]
__text:0000000100007E80                 ADD             W9, W9, W10
__text:0000000100007E84                 LDR             W10, [X0,#0x28]
__text:0000000100007E88                 ADD             W9, W9, W10
__text:0000000100007E8C                 LDR             W10, [X0,#0x2C]
__text:0000000100007E90                 ADD             W9, W9, W10
__text:0000000100007E94                 LDR             W10, [X0,#0x30]
__text:0000000100007E98                 ADD             W0, W9, W10
__text:0000000100007E9C                 ADD             SP, SP, #0x10
__text:0000000100007EA0                 RET

根据上面的分析,这里的 X0 = &Block_ISA,看一下 7E78 ~ 7E80 的代码,它从 X0 + 0x20 和 X0 + 0x24 处取出值相加,回到上文 Block Layout 的图中查看,从 ISA 开始向上偏移 0x20 和 0x24,分别是被捕获的 a、b 的地址,到这里 Block Invoker 的实现基本就清晰了:通过传入 Block ISA 来获取 Block 信息,其他逻辑与一般函数一致。

有参 Block

不过这里依然有个问题,如果 Block 本身就有参数,那么 ISA 如何传入呢?下面我们来做个实验,在文首的代码中再加入两个函数:

typedef int (^CommonBlockWithParams)(int);

CommonBlockWithParams simpleBlockWithParamsOnStack() {
    int a = 1, b = 2, c = 3, d = 4, e = 5;
    int (^theBlock)(int) = ^int (int f) {
        return a + b + c + d + e + f;
    };
    return theBlock;
}

void invokeStackBlockWithParams() {
    CommonBlockWithParams block = simpleBlockWithParamsOnStack();
    block(100);
}

随后分析一下 invokeStackBlockWithParams 的实现,依然是节选从调用 simpleBlockWithParamsOnStack 获取 Block 到调用的片段。

__text:0000000100007E98                 BL              _simpleBlockWithParamsOnStack
__text:0000000100007E9C                 MOV             X29, X29
__text:0000000100007EA0                 BL              _objc_retainAutoreleasedReturnValue
__text:0000000100007EA4                 STUR            X0, [X29,#var_8]
__text:0000000100007EA8                 LDUR            X0, [X29,#var_8]
__text:0000000100007EAC                 MOV             X30, X0
__text:0000000100007EB0                 LDR             X0, [X0,#0x10]
__text:0000000100007EB4                 STR             X0, [SP,#0x20+var_10]
__text:0000000100007EB8                 MOV             X0, X30
__text:0000000100007EBC                 MOV             W1, #0x64
__text:0000000100007EC0                 LDR             X30, [SP,#0x20+var_10]
__text:0000000100007EC4                 BLR             X30

重点看 7EB8 和 7EBC,可见 Block 的 ISA 依然使用 X0 传递,而 Block 的入参则是使用了 X1,因此我们可以得到结论,Block 有固定入参 ISA 使用 X0 传递,函数的入参从 X1 开始。

Block 的动态捕获

默认情况下 Block 采用 Copy 的形式捕获成员,这使得无法在 Block Invoker 中修改原变量的值,若要修改,则需要将变量用 __block 修饰,使其拷贝到堆区,这个部分较为复杂,将在下一篇文章中介绍。

总结

Block 是一种特殊的对象,如果将其类比普通 OC 对象,它只是没有 SEL,结构与 objc_object 基本一致;其实例在内存中的结构是一个 Block_layout 结构体附加捕获列表,这类似于普通 OC 对象的 isa + ivar list,在 Block 调用时,Block 的固定入参 X0 = Block ISA,这类似于 OC 方法的固定入参 X0 = self,X1 = SEL,Block 函数的参数从 X1 开始顺次存储。