惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
Hugging Face - Blog
Hugging Face - Blog
V
Visual Studio Blog
博客园 - Franky
酷 壳 – CoolShell
酷 壳 – CoolShell
Last Week in AI
Last Week in AI
博客园 - 叶小钗
博客园_首页
阮一峰的网络日志
阮一峰的网络日志
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
TaoSecurity Blog
TaoSecurity Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
J
Java Code Geeks
爱范儿
爱范儿
宝玉的分享
宝玉的分享
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
量子位
N
News and Events Feed by Topic
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Commits to openclaw:main
Recent Commits to openclaw:main
SecWiki News
SecWiki News
MyScale Blog
MyScale Blog
AI
AI
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 【当耐特】
Security Archives - TechRepublic
Security Archives - TechRepublic
F
Fortinet All Blogs
V2EX - 技术
V2EX - 技术
T
Troy Hunt's Blog
有赞技术团队
有赞技术团队
W
WeLiveSecurity
Project Zero
Project Zero
T
Tor Project blog
Help Net Security
Help Net Security
L
LINUX DO - 最新话题
IT之家
IT之家
The Hacker News
The Hacker News
腾讯CDC
Schneier on Security
Schneier on Security
N
News and Events Feed by Topic
C
Cisco Blogs
博客园 - 聂微东
Webroot Blog
Webroot Blog
Forbes - Security
Forbes - Security
M
MIT News - Artificial intelligence
C
Cyber Attacks, Cyber Crime and Cyber Security
雷峰网
雷峰网
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
A
About on SuperTechFans

土土哥的Blog

好久没有更新博客=。= 阿里巴巴国际无线技术部 - 招人啦~求iOS、Android、Java 转-阿里巴巴国际无线技术部 - 在这里遇见最好的自己 反编译分析并模拟实现methodSignatureForSelector方法 反编译分析Xcode8的Bug, release下连续两次调用有二级指针参数的空方法会Crash 有趣的Autolayout示例5-Masonry实现 在对象dealloc的后期执行Task-开源库TTGDeallocTaskHelper 用QuartzCode快速实现一个收藏动画 开源项目-拼图验证控件TTGPuzzleVerify的实现 Swift开源Mac App - BingWallPaper 有趣的Autolayout示例4-Masonry实现 翻译-为什么objc_msgSend必须用汇编实现 API返回结果设计经验与总结 总结一些iOS项目中组织代码的方法 对组件化与模块化的思考与总结 开源项目-TTGTagCollectionView 有趣的Autolayout示例3-Masonry实现 Swift开源项目: TTGEmojiRate的实现 Swift写的库-TTGSnackbar 有趣的Autolayout示例2-Masonry实现 解决iOS项目的版本兼容问题-结合宏、Category和Runtime 用Runtime的手段填充任意NSObject对象的nil属性 有趣的Autolayout示例-Masonry实现 UITextView编辑时插入自定义表情-续-自定义表情图片的大小 RPC框架Thrift例子-PHP调用C++后端程序 GCD使用经验与技巧浅谈 为GCD队列绑定NSObject类型上下文数据-利用__bridge_retained(transfer)转移内存管理权 Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5 @autoreleasepool-内存的分配与释放 有关宏定义的经验与技巧-简化代码-增强Log Effective-Objective-C-读书笔记-Item-4-如何正确定义常量 UITextView编辑时插入自定义表情-简单的图文混编 关于评论不见了=。= Entity和Model的不同-关于代码的数据层 一次审核被拒的经历-关于iCloud到底应该备份什么数据 Block类型变量-缓存Http请求与回调 提升UITableView性能-复杂页面的优化 NSString的Copy与内存分配 利用NSProxy实现消息转发-模块化的网络接口层设计-原创 Effective-Objective-C-读书笔记-Item-3 Effective-Objective-C-读书笔记-Item-2 Effective-Objective-C-读书笔记-Item-1 iOS项目的目录结构-原创 Android开源库-LinkTextView-原创 第一篇Blog
结合访问Out Parameters出现EXC_BAD_ACCESS的例子,反编译汇编解读__autoreleasing
土土哥 · 2016-04-30 · via 土土哥的Blog

更新

2016-05-01: 补充了一点本文涉及的汇编知识

前言

本文结合一段访问Out Parameters出现了EXC_BAD_ACCESS错误的代码,通过反编译等手段验证Objective-C中__autoreleasing的一些特点。

2016-05-01更新 - 关于本文的反编译汇编代码

本文的反编译基于MachO 64bits,即System V X86_64,读懂本文需要的最简单Calling convention如下:

函数参数顺序:

1
2
3
4
5
6
7
8
rdi, rsi, rdx, rcx等寄存器
对应到
id objc_msgSend ( id object, SEL cmd, arg1, arg2 ... );
就是
rdi = object
rsi = cmd
rdx = arg1
rcx = arg2

函数返回值:

1
本文只涉及: rax寄存器

详细可参考:

Out Parameters - 指针的指针

所谓Out Parameters,其实就是指针的指针,熟悉C/C++的朋友应该不陌生,通过指针的指针可以改变指针的值,在Objective-C里面很多地方用到了这种方法,来在函数、方法内部改变参数的原始值,如:

1
2

- (BOOL)removeItemAtURL:(NSURL *)URL error:(NSError **)error

第二个参数(NSError **)error,就是Out Parameters,调用时传递NSError类型指针的地址即可:

1
2
3
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtURL:url error:&error];
// 处理error...

出现EXC_BAD_ACCESS的代码示例

假设有如下遍历数组检查数值零的方法:

1
2
3
4
5
6
7
8
9
10
11
- (void)checkZeroInArray:(NSArray <NSNumber *> *)array error:(NSError **)error {

[array enumerateObjectsUsingBlock:^(NSNumber * _Nonnull number, NSUInteger index, BOOL * _Nonnull stop) {
if (number.integerValue == 0) {
if (error) {
*error = [NSError errorWithDomain:@"me.tutuge" code:100 userInfo:nil];
}
*stop = TRUE;
}
}];
}

调用的时候如下:

1
2
3
4
5
NSError *error = nil;
[self checkZeroInArray:numbers error:&error];
if (error) {
NSLog(@"Error: %@", error);
}

可以得出,NSLog(@"Error: %@", error)访问error的时候,error的地址指向的内存空间已经被释放,所以才会出现EXC_BAD_ACCESS错误。

但是为什么会被释放?什么时候被释放的?经过一番查证,发现跟__autoreleasing@autoreleasepool有关。

下面先对__autoreleasing做点研究=。=

__autoreleasing - 变量所有权(ownership)修饰符

先看看__autoreleasing的定义及一些特点。

__autoreleasing是变量所有权修饰符的一种,除了它,还有__strong__weak__unsafe_unretained,详细的说明可以参考:Clang文档-Objective-C Automatic Reference Counting (ARC)

简单来说,就是被__autoreleasing修饰的变量会被加入到当前的autoreleasepool中,可以理解为如下两段分别在ARC和MRC中的代码等价:

1
2
3
4
5

id __autoreleasing obj = someObj;


id obj = [[someObj retain] autorelease];

再进一步,除开各种影响因素,假设有如下函数:

1
2
3
- (void)funcWithObj:(id)someObj {
id __autoreleasing obj = someObj;
}

用Hopper Disassembler反编译后为如下汇编代码(MachO 64bits):

funcWithObj:反汇编

var_18就是obj变量:

lea rax, qword [ss:rbp+var_18]mov rdi, rax取了var_18的地址放在rdi寄存器中,mov rsi, rdxsomeObj值放到了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
2
3
id objc_retainAutorelease(id value) {
return objc_autorelease(objc_retain(value));
}

objc_storeStrongobjc_retainAutorelease可参考:Clang文档-Objective-C Automatic Reference Counting (ARC)

可验证,用__autoreleasing修饰的变量会被添加到当前的autoreleasepool中。

方法的Out Parameters参数会自动添加__autoreleasing属性

当方法参数里面有Out Parameters参数时,就是有指针的指针类型时,编译器会自动为参数加上__autoreleasing属性,如以下两个方法:

1
2
3
4
5
6
7
- (void)generateError1:(NSError **)error {
*error = [NSError new];
}

- (void)generateError2:(NSError * __autoreleasing *)error {
*error = [NSError new];
}

编译时,generateError1:会对参数error自动添加__autoreleasing,然后就跟generateError2:的实现完全一致了。

通过反汇编也可看出两者完全一致:

Out Parameters参数会自动添加__autoreleasing属性

call imp___stubs__objc_msgSend完成后,rax寄存器保存了[NSError new]的对象,然后mov rdi, rax,转移到rdi寄存器,作为objc_autorelease函数的参数被调用,*error被加到了当前的autoreleasepool中。

如果传给Out Parameters参数的变量没有用__autoreleasing修饰,编译器会创建一个临时变量并以__autoreleasing修饰再传入

根据苹果的Transitioning to ARC Release Notes文档可知,如果有如下调用:

1
2
NSError *error; 
[self generateError1:&error];

编译器检测到generateError1:方法的Out Parameters类型参数,但是调用时的error又不是__autoreleasing修饰的,就会自动创建一个__autoreleasing修饰的临时变量,用来代替error传入,编译器重写后如下:

1
2
3
NSError * error; 
NSError * __autoreleasing tmp = error;
[self generateError1:&tmp];

通过汇编来验证一下:

假如有如下调用:

1
2
3
4
- (void)runTest {
NSError *error = nil;
[self generateError1:&error];
}

反汇编后,如下:

自动添加__autoreleasing临时变量

代码有点多=。=,从图中汇编可知:var_20就是自动生成的临时变量,var_18是我们定义的error变量。

mov qword [ss:rbp+var_18], 0x0var_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
2
3
4
lea rdx, qword [ss:rbp+var_18] # var_18为objc_storeStrong第一个参数
mov rsi, qword [ss:rbp+var_20] # var_20为objc_storeStrong第二个参数
mov rdi, rdx
call imp___stubs__objc_storeStrong

开头例子出现EXC_BAD_ACCESS错误的原因

经过上面一番对__autoreleasing的总结,再来看看开头例子的错误原因就比较容易懂了。

enumerateObjectsUsingBlock会在循环内部自动添加autoreleasepool

首先应该明确的就是enumerateObjectsUsingBlock:在用block迭代遍历NSArray的元素时,会自动添加autoreleasepool,对于例子来说,相当于:

1
2
3
4
5
6
7
8
9

[array enumerateObjectsUsingBlock:^(NSNumber * _Nonnull number, NSUInteger index, BOOL * _Nonnull stop) {
if (number.integerValue == 0) {
if (error) {
*error = [NSError errorWithDomain:@"me.tutuge" code:100 userInfo:nil];
}
*stop = TRUE;
}
}];

结合__autoreleasing后,重写为:

1
2
3
4
5
6
7
8
9
10
11
12
NSNumber *number = nil;
BOOL stop = FALSE;

for (NSUInteger index = 0; index < array.count && !stop; index++) {
@autoreleasepool {
number = array[index];
if (error) {
*error = [[NSError errorWithDomain:@"me.tutuge" code:100 userInfo:nil] autorelease];
}
stop = TRUE;
}
}

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错误。

总结

很多时候不能想当然的写代码=。=,要不然出了问题找都找不到,每个细节都很重要。
嗯,现在读汇编快多了。。。

参考