
























2016-05-01: 补充了一点本文涉及的汇编知识
本文结合一段访问Out Parameters出现了EXC_BAD_ACCESS错误的代码,通过反编译等手段验证Objective-C中__autoreleasing的一些特点。
本文的反编译基于MachO 64bits,即System V X86_64,读懂本文需要的最简单Calling convention如下:
函数参数顺序:
1 | rdi, rsi, rdx, rcx等寄存器 |
函数返回值:
1 | 本文只涉及: rax寄存器 |
详细可参考:
所谓Out Parameters,其实就是指针的指针,熟悉C/C++的朋友应该不陌生,通过指针的指针可以改变指针的值,在Objective-C里面很多地方用到了这种方法,来在函数、方法内部改变参数的原始值,如:
1 |
|
第二个参数(NSError **)error,就是Out Parameters,调用时传递NSError类型指针的地址即可:
1 | NSError *error = nil; |
假设有如下遍历数组检查数值零的方法:
1 | - (void)checkZeroInArray:(NSArray <NSNumber *> *)array error:(NSError **)error { |
调用的时候如下:
1 | NSError *error = nil; |
可以得出,NSLog(@"Error: %@", error)访问error的时候,error的地址指向的内存空间已经被释放,所以才会出现EXC_BAD_ACCESS错误。
但是为什么会被释放?什么时候被释放的?经过一番查证,发现跟__autoreleasing和@autoreleasepool有关。
下面先对__autoreleasing做点研究=。=
先看看__autoreleasing的定义及一些特点。
__autoreleasing是变量所有权修饰符的一种,除了它,还有__strong、__weak和__unsafe_unretained,详细的说明可以参考:Clang文档-Objective-C Automatic Reference Counting (ARC)
简单来说,就是被__autoreleasing修饰的变量会被加入到当前的autoreleasepool中,可以理解为如下两段分别在ARC和MRC中的代码等价:
1 |
|
再进一步,除开各种影响因素,假设有如下函数:
1 | - (void)funcWithObj:(id)someObj { |
用Hopper Disassembler反编译后为如下汇编代码(MachO 64bits):

var_18就是obj变量:
lea rax, qword [ss:rbp+var_18]和mov rdi, rax取了var_18的地址放在rdi寄存器中,mov rsi, rdx将someObj值放到了rsi寄存器中,然后调用id objc_storeStrong(id *object, id value)函数最终将someObj值保存在var_18变量中。
__autoreleasing导致了id objc_retainAutorelease(id value)函数的调用:
call imp___stubs__objc_retainAutorelease,就是对obj变量,也就是var_18,调用了objc_retainAutorelease函数,先retain然后autorelease了一次,其实现大致如下:
1 | id objc_retainAutorelease(id value) { |
objc_storeStrong和objc_retainAutorelease可参考:Clang文档-Objective-C Automatic Reference Counting (ARC)
可验证,用__autoreleasing修饰的变量会被添加到当前的autoreleasepool中。
当方法参数里面有Out Parameters参数时,就是有指针的指针类型时,编译器会自动为参数加上__autoreleasing属性,如以下两个方法:
1 | - (void)generateError1:(NSError **)error { |
编译时,generateError1:会对参数error自动添加__autoreleasing,然后就跟generateError2:的实现完全一致了。
通过反汇编也可看出两者完全一致:

在call imp___stubs__objc_msgSend完成后,rax寄存器保存了[NSError new]的对象,然后mov rdi, rax,转移到rdi寄存器,作为objc_autorelease函数的参数被调用,*error被加到了当前的autoreleasepool中。
根据苹果的Transitioning to ARC Release Notes文档可知,如果有如下调用:
1 | NSError *error; |
编译器检测到generateError1:方法的Out Parameters类型参数,但是调用时的error又不是__autoreleasing修饰的,就会自动创建一个__autoreleasing修饰的临时变量,用来代替error传入,编译器重写后如下:
1 | NSError * error; |
通过汇编来验证一下:
假如有如下调用:
1 | - (void)runTest { |
反汇编后,如下:

代码有点多=。=,从图中汇编可知:var_20就是自动生成的临时变量,var_18是我们定义的error变量。
mov qword [ss:rbp+var_18], 0x0对var_18做初始化,也就是赋nil值,然后mov rdi, qword [ss:rbp+var_18]和mov qword [ss:rbp+var_20], rdi就是用var_18初始化了var_20临时变量。
lea rdx, qword [ss:rbp+var_20]将var_20临时变量的地址存到了rdx寄存器中,作为objc_msgSend实现generateError1:调用时的第三个参数,也就是(NSError **):error参数,完成调用。
调用完成后,通过如下调用,将临时变量var_20的值保存到var_18,即error变量中。
1 | lea rdx, qword [ss:rbp+var_18] # var_18为objc_storeStrong第一个参数 |
经过上面一番对__autoreleasing的总结,再来看看开头例子的错误原因就比较容易懂了。
enumerateObjectsUsingBlock会在循环内部自动添加autoreleasepool
首先应该明确的就是enumerateObjectsUsingBlock:在用block迭代遍历NSArray的元素时,会自动添加autoreleasepool,对于例子来说,相当于:
1 |
|
结合__autoreleasing后,重写为:
1 | NSNumber *number = nil; |
而autorelease对应的函数id objc_autorelease(id value)的官方解释:
If value is null, this call has no effect. Otherwise, it adds the object to the innermost autorelease pool exactly as if the object had been sent the autorelease message.
其中的innermost autorelease pool表示的就是“最内层”的autoreleasepool,对于例子来说就是*error被添加到了循环内的autoreleasepool中,当然,导致的结果就是本次循环结束后,*error也随着一起被释放了。
最终导致了外部访问了已经被释放的*error,出现了EXC_BAD_ACCESS错误。
很多时候不能想当然的写代码=。=,要不然出了问题找都找不到,每个细节都很重要。
嗯,现在读汇编快多了。。。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。