汇编语言知多少(四): 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 真机.

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位 寄存器的显示.


栈帧


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


  • 函数的调用流程(内存)
    • 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
    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, 这类访问内存错误的问题, 原因大部分是 向一个已释放的对象发送消息

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


但是, 如果你看不懂会汇编, 一时找不到错误, 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. 运行程序.

大致流程如下:

    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 捕获到.

维基百科-汇编语言

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容