在前几篇文章里我们一直聊的是 Intel 格式的 8086汇编, 这篇文章我们聊聊 AT&T 格式的汇编语法.
AT&T VS Intel
- 基于 x86 架构 的处理器所使用的汇编指令一般有两种格式.
-
Intel 汇编
- DOS(8086处理器), Windows
- Windows 派系 -> VC 编译器
-
AT&T汇编
- Linux, Unix, Mac OS, iOS(模拟器)
- Unix派系 -> GCC编译器
- 基于ARM 架构 的处理器所使用的汇编指令一般有一种格式, 这种处理器常用语嵌入式设备, 移动设备, 以高性能, 低能耗见长
- ARM 汇编, iOS 真机.
64位 AT&T汇编的寄存器
- 有16个常用的64位寄存器
- %rax, %rbx, %rcx , %rdx, %rsi, %rdi, %rbp, %rsp (和 8086汇编类似 )
- %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15
- 寄存器的具体用途
- %rax 作为函数返回值使用.
- %rsp 指向栈顶.
- %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10等寄存器用于存放函数参数.
64位, 32位, 16位, 8位 寄存器的显示.
栈帧
这两张图虽然高地址的方向是反的, 但他们说的是同一个问题
- 函数的调用流程(内存)
- 1.push 参数
- 2.push 函数的返回地址
- 3.push bp (保留bp之前的值,方便以后恢复)
- 4.mov bp, sp (保留sp之前的值,方便以后恢复)
- 5.sub sp,空间大小 (分配空间给局部变量)
- 6.保护可能要用到的寄存器
- 7.使用CC(int 3)填充局部变量的空间
- 8.--------执行业务逻辑--------
- 9.恢复寄存器之前的值
- 10.mov sp, bp (恢复sp之前的值)
- 11.pop bp (恢复bp之前的值)
- 12.ret (将函数的返回地址出栈,执行下一条指令)
- 13.恢复栈平衡 (add sp,参数所占的空间)
调试
在解析汇编程序的时候, 有一些 LLDB 指令是很好用的
- 读取寄存器的值: register read/x $rax, 这里x 指 16进制格式, 还有 f 浮点数, d 十进制数
- 修改寄存器的值: register write $rax 0
- 读取内存中的值:
- x/数量-格式-字节大小 内存地址
- x/3xw 0x0000010, 这里 w 指的是4个字节大小
- b, byte, 1字节; h, hard word, 2字节; w, word, 4字节; g, giant word, 8字节.
- 修改内存中的值:
- memory write 内存地址 数值
- memory write 0x0000010 10
- 寻址: image lookup --address 内存地址
还有 JCC 的指令表
指令 | 解释 | 描述 |
---|---|---|
JE, JZ | equal, zero | 结果为零则跳转(相等时跳转) |
JNE, JNZ | not equal, not zero | 结果不为零则跳转(不相等时跳转) |
JS | sign(有符号\有负号) | 结果为负则跳转 |
JNS | not sign(无符号\无负号) | 结果为非负则跳转 |
JP, JPE | parity even | 结果中1的个数为偶数则跳转 |
JNP, JPO | parity odd | 结果中1的个数为偶数则跳转 |
JO | overflow | 结果溢出了则跳转 |
JNO | not overflow | 结果没有溢出则跳转 |
JB, JNAE | below, not above equal | 小于则跳转 (无符号数) |
JNB, JAE | not below, above equal | 大于等于则跳转 (无符号数) |
JBE, JNA | below equal, not above | 小于等于则跳转 (无符号数) |
JNBE, JA | not below equal, above | 大于则跳转(无符号数) |
JL, JNGE | little, not great equal | 小于则跳转 (有符号数) |
JNL, JGE | not little, great equal | 大于等于则跳转 (有符号数) |
JLE, JNG | little equal, not great | 小于等于则跳转 (有符号数) |
JNLE, JG | not little equal, great | 大于则跳转(有符号数) |
实战1: 计算 (a++) + (a++) + (a++) = ?
这次我们选择创建一个简单的 Swift 项目, 运行在iOS模拟器中. 代码如下, 由于 Swift 已经不支持 a++, ++a 这种操作, 所以我自定义实现了一个.
在 Xcode 的菜单栏中, Debug -> Debug workflow -> 选择 Always Show Disassembly, 这是控制是否显示汇编程序
在项目中设置断点, 程序运行到断点处, 触发中断, Xcode 界面显示当前程序的汇编界面.
接下来我们来解读一下这些汇编指令
0x10d9b6c96 <+118>: movq $0x1, -0x28(%rbp)
0x10d9b6c9e <+126>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++ postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
- 我们的源代码经过编译器编译成汇编指令, 从左到右依次为
指令在内存中的地址 <+(和上一个指令的偏移地址差)> 汇编指令 源操作数 目标操作数 ; 注释
- 我们的源代码经过编译器编译成汇编指令, 从左到右依次为
- 汇编分析, 关键代码都有注释
0x10d9b6c79 <+89>: movq 0x45f8(%rip), %rsi ; "viewDidLoad"
0x10d9b6c80 <+96>: movq %rdx, -0x50(%rbp)
0x10d9b6c84 <+100>: callq 0x10d9b8354 ; symbol stub for: objc_msgSendSuper2
0x10d9b6c89 <+105>: movq -0x48(%rbp), %rdi
0x10d9b6c8d <+109>: callq 0x10d9b835a ; symbol stub for: objc_release
调用完 super.viewDidLoad()
0x10d9b6c92 <+114>: leaq -0x28(%rbp), %rdi
0x10d9b6c96 <+118>: movq $0x1, -0x28(%rbp)
<注释>上面可以翻译成 mov $0x1 [rbp-0x28] 将立即数1 赋值到 [rbp-0x28] 所指的内存单元
<注释>这是一个 局部变量, 对应源代码中的 int a = 1.
0x10d9b6c9e <+126>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++ postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
<注释> 调用 ++ 函数
0x10d9b6ca3 <+131>: leaq -0x28(%rbp), %rdi
0x10d9b6ca7 <+135>: movq %rax, -0x58(%rbp)
<注释> 此时 %rax 中的值为 1
0x10d9b6cab <+139>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++
<注释> 调用 ++ 函数
postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
0x10d9b6cb0 <+144>: movq -0x58(%rbp), %rdx
0x10d9b6cb4 <+148>: addq %rax, %rdx
<注释> %ax 中的值(2) + %rdx 中的值(1) 存储在 %rdx 寄存器中(3)
0x10d9b6cb7 <+151>: seto %r8b
0x10d9b6cbb <+155>: movq %rdx, -0x60(%rbp)
<注释>将%rdx中的值赋值给 -0x60(%rbp)
0x10d9b6cbf <+159>: movb %r8b, -0x61(%rbp)
0x10d9b6cc3 <+163>: jo 0x10d9b6daf ; <+399> at ViewController.swift:17
0x10d9b6cc9 <+169>: leaq -0x28(%rbp), %rdi
0x10d9b6ccd <+173>: callq 0x10d9b6e10 ; Test_Swift_Assembly.++ postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
0x10d9b6cd2 <+178>: movq -0x60(%rbp), %rdi
<注释> %rax 的值为3, %rdi 的值为3
0x10d9b6cd6 <+182>: addq %rax, %rdi
<注释>3 + 3 = %rdi 的值为 6
0x10d9b6cd9 <+185>: seto %cl
0x10d9b6cdc <+188>: movq %rdi, -0x70(%rbp)
<注释> 将 %rdi 的值赋给 -0x70(%rbp), 值为6
0x10d9b6ce0 <+192>: movb %cl, -0x71(%rbp)
0x10d9b6ce3 <+195>: jo 0x10d9b6db1 ; <+401> at ViewController.swift:17
0x10d9b6ce9 <+201>: movq -0x70(%rbp), %rax
<注释>将 -0x70(%rbp) 的值赋给 %rax, 值为6
<注释>接下来是传递参数打印 c 的值
-> 0x10d9b6ced <+205>: movl $0x1, %ecx
-
- 复盘整个过程:
- -0x28(%rbp) 对应 局部变量a, -0x70(%rbp) 对应 局部变量c
- %rax 存放的是每次运算的值, 分别为 1, 2, 3,
- %rdi 存放每次相加后的值, 分别为 1, 3, 6. 这里面有一个 %rdx, 存储过内部运算的值.
- 最终结果是 6
下面是一个挑战
var a = 2
let c = a++ + a++ + a++ // 2 + 3 + 4 = 9 , a = 5
let c2 = ++a + a++ + a++ // 6 + 6 + 7 = 19, a = 8
let c3 = ++a + ++a + a++ // 9 + 10 + 10 = 29, a = 11
print(c3, a) // 29, 11
实战2: 解读 zombieObject
在 MRC 环境下, 我们运行下面这段代码.
NSArray *arr = @[@"a", @"b", @"c"];
NSLog(@"1==>%ld", arr.retainCount); // 1
[arr release]; // 0
NSLog(@"1==>%ld", arr.retainCount); // 报错
[arr release];
NSLog(@"1==>%ld", arr.retainCount);
程序肯定会报错, EXC_BAD_Address, 这类访问内存错误的问题, 原因大部分是 向一个已释放的对象发送消息
如果你对汇编比较熟悉的话, 直接观察这个汇编代码, 也可以定位问题位置.
但是, 如果你看不懂会汇编, 一时找不到错误, Xcode 已经内置了工具帮助我们调试.
在 Edit Scheme —> Diagnostics —> Memory Management —> Zombie Objects
打开 Zombie Objects 后,重新运行代码, 我们会发现
- 错误提示由原来的EXC_BAD_Address 变为 EXC_BAD_INSTRUCTION
- 控制台直接打印出错误信息, 向一个已释放的对象发送消息. 这个原来是没有的.
- arr 对象 发生了改变. 由原来的NSArray -> _NSZombie__NSArrayl
开启前 | |
---|---|
开启后 |
- 这新创建的 Zombie__NSArray 是什么呢? 我们可以合理猜测,
- 开启 Zombie Objects 功能后, 在运行程序时, Xcode 内部会检测是否向已释放的对象发送消息,
- 如果有, 创建 Zombie Object, 替换它, 并且向这个新的对象发消息, 在控制台打印错误信息.
- 如果不创建新的Object, 原对象已经释放了, 无法向其发送消息, 导致无法定位问题.
本着大胆猜想, 小心求证的原则, 接下来我们验证一下.
验证猜想
验证第一步
没什么不是看源码不能解决的 :] 如果能找到 Runtime 的源码就好了.
Apple 是有提供 Runtime 的源码大致实现. 在这里可以下载到, 它是一个 OC 项目, 下载后打开就可以了.
在搜索框了搜索 zombie, 大致找到了相关信息, 我整理一下
// Replaced by CF (throws an NSException)
+ (void)dealloc {
}
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}
由这我们可以猜想: 对象在被销毁的时候, 程序会创建 Zombie对象, 调用实例方法
_objc_rootDealloc
,
void
_objc_rootDealloc(id obj)
{
显示断言, 显示被释放的对象信息
assert(obj);
obj->rootDealloc();
}
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?
判断是否该对象应该释放
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
正式释放
free(this);
}
else {
继续使用
object_dispose((id)this);
}
}
id
object_dispose(id obj)
{
if (!obj) return nil;
在不释放内存的情况下销毁实例
删除关联引用
objc_destructInstance(obj);
正式销毁
free(obj);
return nil;
}
到这里其实还是不能看出实际的东西, 到底是什么时候被替换的, 替换的过程中做了什么, 在这里没有体现出来.
验证第二步.
刚才使用的 Runtime 源码 是.mm文件, 里面除了 OC 和 C 代码以外还包含C++代码, 苹果开源了这一部分的底层代码.
- 在 CFRuntime.c 中, 同样是搜索 Zombies, 我们发现了一个有趣的函数
__CFZombifyNSObject(void)
, 翻译过来就是 zombie 化 Object.
为此, 我们需要添加 符号断点, 在程序运行时, 如果有调用 __CFZombifyNSObject, 就会触发中断.
在 Zombie Objects 开启的情况下, 运行程序, 我们会发现.
NSObejct 替换了 dealloc
和 __dealloc_zombie
这两个方法.
我们继续设置符号断点为 __dealloc_zombie
. 运行程序.
大致流程如下:
- 判断 __CFConstantStringClassReferencePtr + 7 是不是 等于 0 , 如果是,则函数执行完毕, 否则, 继续向下执行.(这个类索引值常量 我查到的结果是 与编译器内置的decl 匹配)
- object_getClass, class_getName 获取当前对象的类名
- 通过调用函数 asprintf , 按照
_NSZombie_%s
格式化, 并存储到寄存器 rdi 中.
- 通过调用函数 asprintf , 按照
- 通过调用函数 objc_lookUpClass,查找新类名的类是否存在,不存在,则创建.
- 通过调用函数 objc_lookUpClass,获取名为
_NSZombie_
的类, 这个类 是系统类.
- 通过调用函数 objc_lookUpClass,获取名为
- 通过调用函数 objc_duplicateClass, 复制
_NSZombie_
类,生成新的_NSZombie_%s
类, 并将原来的_NSZombie_
类释放掉.
- 通过调用函数 objc_duplicateClass, 复制
- 通过调用函数 object_setClass,将当前对象的类型设置成新的
_NSZombie_%s
类,
- 通过调用函数 object_setClass,将当前对象的类型设置成新的
- 判断 __CFZombieEnabled 是否为 0 , 若是的, 则释放掉新的对象, 否则返回新的对象.
小结:
- __CFZombifyNSObject(void) 的实现是这样的: 程序会替换掉当前对象 的 dealloc 方法, 实现 __dealloc_zombie 方法, 在方法中创建一个新的类. 即 Zombie Objecct.
- 当对象的引用计数为0时, 会调用它的 dealloc方法, 将该对象转为 zombie object, 当向原来已经被释放的对象发送消息时, 内部会转到zombie object 代替旧的类接受消息, 由于新的类没有实现任何方法,所以程序会崩溃,最终被 Xcode 捕获到.