汇编语言知多少(四): AT&T 汇编语法

在前几篇文章里我们一直聊的是 Intel 格式的 8086汇编, 这篇文章我们聊聊 AT&T 格式的汇编语法.

AT&T VS Intel

  1. 基于 x86 架构 的处理器所使用的汇编指令一般有两种格式.
  • Intel 汇编
    • DOS(8086处理器), Windows
    • Windows 派系 -> VC 编译器
  • AT&T汇编
    • Linux, Unix, Mac OS, iOS(模拟器)
    • Unix派系 -> GCC编译器
  1. 基于ARM 架构 的处理器所使用的汇编指令一般有一种格式, 这种处理器常用语嵌入式设备, 移动设备, 以高性能, 低能耗见长
  • ARM 汇编, iOS 真机.
汇编语言知多少(四): AT&T 汇编语法_第1张图片

64位 AT&T汇编的寄存器

  1. 有16个常用的64位寄存器
  • %rax, %rbx, %rcx , %rdx, %rsi, %rdi, %rbp, %rsp (和 8086汇编类似 )
  • %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15
  1. 寄存器的具体用途
  • %rax 作为函数返回值使用.
  • %rsp 指向栈顶.
  • %rdi, %rsi, %rdx, %rcx, %r8, %r9, %r10等寄存器用于存放函数参数.

64位, 32位, 16位, 8位 寄存器的显示.


汇编语言知多少(四): AT&T 汇编语法_第2张图片

栈帧

汇编语言知多少(四): AT&T 汇编语法_第3张图片

这两张图虽然高地址的方向是反的, 但他们说的是同一个问题


汇编语言知多少(四): AT&T 汇编语法_第4张图片
  • 函数的调用流程(内存)
    • 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 这种操作, 所以我自定义实现了一个.

汇编语言知多少(四): AT&T 汇编语法_第5张图片

在 Xcode 的菜单栏中, Debug -> Debug workflow -> 选择 Always Show Disassembly, 这是控制是否显示汇编程序
在项目中设置断点, 程序运行到断点处, 触发中断, Xcode 界面显示当前程序的汇编界面.

汇编语言知多少(四): AT&T 汇编语法_第6张图片

接下来我们来解读一下这些汇编指令

0x10d9b6c96 <+118>: movq   $0x1, -0x28(%rbp)
0x10d9b6c9e <+126>: callq  0x10d9b6e10               ; Test_Swift_Assembly.++ postfix(inout Swift.Int) -> Swift.Int at ViewController.swift:24
    1. 我们的源代码经过编译器编译成汇编指令, 从左到右依次为
      指令在内存中的地址 <+(和上一个指令的偏移地址差)> 汇编指令 源操作数 目标操作数 ; 注释
    1. 汇编分析, 关键代码都有注释
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
    1. 复盘整个过程:
    • -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, 这类访问内存错误的问题, 原因大部分是 向一个已释放的对象发送消息

如果你对汇编比较熟悉的话, 直接观察这个汇编代码, 也可以定位问题位置.


汇编语言知多少(四): AT&T 汇编语法_第7张图片

但是, 如果你看不懂会汇编, 一时找不到错误, Xcode 已经内置了工具帮助我们调试.

在 Edit Scheme —> Diagnostics —> Memory Management —> Zombie Objects


汇编语言知多少(四): AT&T 汇编语法_第8张图片

打开 Zombie Objects 后,重新运行代码, 我们会发现

  • 错误提示由原来的EXC_BAD_Address 变为 EXC_BAD_INSTRUCTION
  • 控制台直接打印出错误信息, 向一个已释放的对象发送消息. 这个原来是没有的.
  • arr 对象 发生了改变. 由原来的NSArray -> _NSZombie__NSArrayl
开启前
开启后
  • 这新创建的 Zombie__NSArray 是什么呢? 我们可以合理猜测,
    • 开启 Zombie Objects 功能后, 在运行程序时, Xcode 内部会检测是否向已释放的对象发送消息,
    • 如果有, 创建 Zombie Object, 替换它, 并且向这个新的对象发消息, 在控制台打印错误信息.
    • 如果不创建新的Object, 原对象已经释放了, 无法向其发送消息, 导致无法定位问题.

本着大胆猜想, 小心求证的原则, 接下来我们验证一下.

验证猜想

验证第一步

没什么不是看源码不能解决的 :] 如果能找到 Runtime 的源码就好了.

Apple 是有提供 Runtime 的源码大致实现. 在这里可以下载到, 它是一个 OC 项目, 下载后打开就可以了.

汇编语言知多少(四): AT&T 汇编语法_第9张图片

在搜索框了搜索 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.
    汇编语言知多少(四): AT&T 汇编语法_第10张图片

为此, 我们需要添加 符号断点, 在程序运行时, 如果有调用 __CFZombifyNSObject, 就会触发中断.

汇编语言知多少(四): AT&T 汇编语法_第11张图片

汇编语言知多少(四): AT&T 汇编语法_第12张图片

在 Zombie Objects 开启的情况下, 运行程序, 我们会发现.


汇编语言知多少(四): AT&T 汇编语法_第13张图片

NSObejct 替换了 dealloc__dealloc_zombie 这两个方法.

我们继续设置符号断点为 __dealloc_zombie. 运行程序.

汇编语言知多少(四): AT&T 汇编语法_第14张图片

汇编语言知多少(四): AT&T 汇编语法_第15张图片

大致流程如下:

    1. 判断 __CFConstantStringClassReferencePtr + 7 是不是 等于 0 , 如果是,则函数执行完毕, 否则, 继续向下执行.(这个类索引值常量 我查到的结果是 与编译器内置的decl 匹配)
    1. object_getClass, class_getName 获取当前对象的类名
    1. 通过调用函数 asprintf , 按照 _NSZombie_%s 格式化, 并存储到寄存器 rdi 中.
    1. 通过调用函数 objc_lookUpClass,查找新类名的类是否存在,不存在,则创建.
    1. 通过调用函数 objc_lookUpClass,获取名为 _NSZombie_ 的类, 这个类 是系统类.
    1. 通过调用函数 objc_duplicateClass, 复制 _NSZombie_ 类,生成新的 _NSZombie_%s 类, 并将原来的 _NSZombie_ 类释放掉.
    1. 通过调用函数 object_setClass,将当前对象的类型设置成新的 _NSZombie_%s 类,
    1. 判断 __CFZombieEnabled 是否为 0 , 若是的, 则释放掉新的对象, 否则返回新的对象.

小结:

  • __CFZombifyNSObject(void) 的实现是这样的: 程序会替换掉当前对象 的 dealloc 方法, 实现 __dealloc_zombie 方法, 在方法中创建一个新的类. 即 Zombie Objecct.
  • 当对象的引用计数为0时, 会调用它的 dealloc方法, 将该对象转为 zombie object, 当向原来已经被释放的对象发送消息时, 内部会转到zombie object 代替旧的类接受消息, 由于新的类没有实现任何方法,所以程序会崩溃,最终被 Xcode 捕获到.

维基百科-汇编语言

你可能感兴趣的:(汇编语言知多少(四): AT&T 汇编语法)