iOS汇编教程:理解ARM
lmii1361
8年前
<h2>iOS汇编教程:理解ARM</h2> <p>当你写Objective-C代码时,它们最终转换成机器码---ARM处理器能理解的原始的0和1指令。在Objective-C和机器码之间,还有一种可直接理解的汇编语言。</p> <p>理解汇编会让你在调试和优化时更加深入了解你的代码,破译Objective-C运行时,而且能满足如呆子般痴迷于内核的好奇心。</p> <p>在这篇iOS汇编教程中,你将学到:</p> <ul> <li>汇编是什么及为什么你要关心它;</li> <li>看懂汇编。特别是Objective-C方法生成的汇编;</li> <li>在调试时如何使用汇编。对于看清代码运行状态及为什么bug或者崩溃发生非常有用;</li> </ul> <p>为了更好的理解这篇教程,你应该熟悉Objective-C编程。你也要理解一些简单的计算机术语,比如栈,处理器(CPU)及他们是如何工作的。如果你不熟悉CPU,你可能需要预先了解 <a href="/misc/goto?guid=4959715835289647672" rel="nofollow,noindex">这篇文章</a> 下再继续。</p> <p>打开你的XCode4(作者的文章写于2013年,当时还是4),让我们准备探索ARM的内部世界吧~</p> <h2>开始:什么是汇编</h2> <p>众所周知,Objective-C是一个高级语言。Objective-C语言通过编译器编译成汇编语言,它虽然是低级语言,但也不是最低级的。</p> <p>汇编语言通过汇编器转译成CPU可识别的01机器码。幸运的是,你不需要关心机器码,但理解汇编细节有时候非常有用。</p> <p><img src="https://simg.open-open.com/show/b5abf20491a7520e5a89e4f14cbfb92d.png"></p> <p>每个汇编指令都是告诉CPU去执行一项任务,比如将两个数相加,从内存中加载数据。</p> <p>除了主要的内存外---iPhone5是1GB,Mac上可能是8GB,CPU也有一些能够快速访问的小块工作内存。这小块工作内存被分割成寄存器,就像能持有值的变量一样。</p> <p>所有的iOS设备(事实上,目前几乎所有的手机设备)使用基于 <a href="/misc/goto?guid=4959715835371221355" rel="nofollow,noindex">ARM架构</a> 的CPU。幸运的是,这个指令集很容易读懂,不仅仅因为它是 <a href="/misc/goto?guid=4959715835458072027" rel="nofollow,noindex">精简指令集</a> (RISC)---意味着更少的指令。总之,它比 <a href="/misc/goto?guid=4959627545115878888" rel="nofollow,noindex">x86</a> 更易读。</p> <p>一个汇编指令看起来像这样:</p> <pre> mov r0, #42</pre> <p>汇编语言有许多指令或操作符。其中之一的mov操作符,用于移动数据。在ARM汇编里,第一个参数是移动数据的目的地,因此上面的指令就是把值42赋值给了r0寄存器。看看下面这个例子:</p> <pre> ldr r2, [r0] ldr r3, [r1] add r4, r2, r3</pre> <p>别担心,我并没有期望你能理解上面代码。但是你可能已经猜到了代码大致意思。指令的意思是从内存中加载数据并把他们存储到寄存器r2,r3中,然后将他们俩相加存储到寄存器r4中。</p> <p>现在你已经发现其实并没有那么恐怖,让我们来更深入详细些吧~</p> <h2>调用约定</h2> <p>理解汇编首先且最重要的事就是明白汇编代码与代码之间是如何交互的。这里我的意思是函数调用其他函数。包括参数是如何传递给函数,及函数返回值是如何返回的。</p> <p>这些事情执行的过程与实现被称为调用约定。编译器必须遵循它预定义的标准,这样才能让编译后的代码能和其他不同编译器编译出的代码能够交互。没有标准,编译器能编译出不相配的代码。</p> <p>如上讨论,寄存器是和CPU联系非常紧密的一小块内存,经常用于存储一些正在使用的数据。ARM处理器有16个寄存器,从r0到r15,每一个都是32位比特。调用约定指定他们其中的一些寄存器有特殊的用途,例如:</p> <ul> <li>r0-r3:用于存放传递给函数的参数;</li> <li>r4-r11:用于存放函数的本地参数;</li> <li>r12:是内部程序调用暂时寄存器。这个寄存器很特别是因为可以通过函数调用来改变它;</li> <li>r13:栈指针sp(stack pointer)。在计算机科学内栈是非常重要的术语。寄存器存放了一个指向栈顶的指针。看 <a href="/misc/goto?guid=4958546291291538731" rel="nofollow,noindex">这里</a> 了解更多关于栈的信息;</li> <li>r14:是链接寄存器lr(link register)。它保存了当目前函数返回时下一个函数的地址;</li> <li>r15:是程序计数器pc(program counter)。它存放了当前执行指令的地址。在每个指令执行完成后会自动增加;</li> </ul> <p>你可以在 <a href="/misc/goto?guid=4959715835588017518" rel="nofollow,noindex">ARM文档</a> 里了解更多关于ARM调用约定的信息。苹果也在文档 <a href="/misc/goto?guid=4959715835671201646" rel="nofollow,noindex">【iOS开发调用约定】</a> 内有做过详细描述。</p> <p>OK,前戏已足,是时候开始真正的码代码了~</p> <h2>创建工程</h2> <p>在这个汇编教程里,你不会创建一个应用,但还是得使用Xcode工程来说明其中含义。启动Xcode,再新建工程,选择iOS\应用\单个view的工程( iOS\Application\Single View Application)点击下一步,工程最终如下:</p> <p><img src="https://simg.open-open.com/show/19122115e2deb368de51a500d78361b9.png"></p> <ul> <li>Product name: ARMAssembly;</li> <li>Company Identifier: 你的通常的反向域名标识;</li> <li>Class Prefix:置空;</li> <li>Devices:iPhone;</li> <li>Use StoryBoards:否</li> <li>使用ARC:是</li> <li>Include Unit Tests:否</li> </ul> <p>最后点击下一步,选择一个保存你工程的地址。</p> <h2>1 + 1</h2> <p>你要做的第一件事就是写一个两个数相加后并返回的函数。没有比这更简单的事了。</p> <p>事实上,你可以先写一个C语言函数,因为Objective-C会是我们的复杂度增加。在Supporting Files里打开main.m,并把下面的代码贴到文件内:</p> <pre> int addFunction(int a, int b) { int c = a + b; return c; }</pre> <p>现在保证你选择的Scheme是iOS设备(或者你的设备名),比如,如果你连上了设备就是xx的iPhone。选择Scheme是设备的原因是这样才会用ARM汇编,而选择模拟器的话就会用x86的方式汇编。Scheme选择后的类似于这样:</p> <p><img src="https://simg.open-open.com/show/75252a5356016bd93e88f2409f405a46.png"></p> <p>现在去Product\Generate Output\Assembly File生成汇编码(其实现在Xcode7是Product\Perform Action\Assemble "main.m")。Xcode会展示一个奇怪的文件。在文件最顶部,你可以看到许多以.section开头的行。如果有那说明你生成成功了!</p> <p>注意:你默认运行的Scheme是Debug配置。在Debug模式下,编译器不会开启优化。你想看到无优化版本的汇编,这样你才能真正看清发生了什么。</p> <p>文件中搜索_addFunction。你会发现像以下的代码:</p> <pre> .globl _addFunction .align 2 .code 16 @ @addFunction .thumb_func _addFunction _addFunction: .cfi_startproc Lfunc_begin0: .loc 1 13 0 @ main.m:13:0 @ BB#0: sub sp, #12 str r0, [sp, #8] str r1, [sp, #4] .loc 1 14 18 prologue_end @ main.m:14:18 Ltmp0: ldr r0, [sp, #8] ldr r1, [sp, #4] add r0, r1 str r0, [sp] .loc 1 15 5 @ main.m:15:5 ldr r0, [sp] add sp, #12 bx lr Ltmp1: Lfunc_end0: .cfi_endproc</pre> <p>这代码看起来让人有点沮丧,但其实没那么难看懂。首先,所有以.号开始的行都不是汇编指令而是作用于汇编器的。你可以忽略所有这样的代码。</p> <p>这些以冒号为结束的行,例如:_addFunction:和Ltmp0:,被称为标签。他们是这部分的汇编代码名字。称为_addFunction:标签实际上是这个函数的入口。</p> <p>这个标签是必须的,这样其他代码可以通过使用addFunction标签来路由函数而不需要知道函数的位置。当最终应用二进制生成的时候,将这些标签转换成实际内存地址是链接器的工作。</p> <p>注意到编译器通常在函数名的前面添加一个下划线,这也是一个约定。其他的所有以L.开头的叫本地标签,这些标签只能用于函数内部。在这个简单的例子里,没有任何一个本地标签真正被使用,但编译器仍然生成了,因为这个代码并没有做任何编译优化。</p> <p>注意是以@字符开头的。在汇编代码后边注释上对应的main.c文件行数对我们看懂汇编代码非常有用。</p> <p>因此,忽略掉注释和标签,重要的代码如下:</p> <pre> _addFunction: @ 1: sub sp, #12 @ 2: str r0, [sp, #8] str r1, [sp, #4] @ 3: ldr r0, [sp, #8] ldr r1, [sp, #4] @ 4: add r0, r1 @ 5: str r0, [sp] ldr r0, [sp] @ 6: add sp, #12 @ 7: bx lr</pre> <ol> <li>首先,分配栈所需的所有临时存储空间。栈是一大块函数随时想使用的内存。ARM中的栈内存是高地址向低地址分布的,意味着你必须从栈指针开始减。在这里,分配了12个字节。</li> <li>r0和r1存放传给函数的参数。如果入参有四个参数,那么r2和r3就会分别存放第三和第四个参数。如果函数超过四个参数,或者一些例如结构体的参数超过了32位比特,那么参数将会通过栈来传递。 这里,两个参数被存入栈中。这是通过存储寄存指令(str)实现的。第一个参数是要存储的寄存器,第二个是存储的位置。方括号代表里面值是内存地址。 这个方括号指令允许你为一个值指定偏移量,因此 [sp, #8] 的意思『在栈指针的地址上加上8字节偏移量』。同样地, str r0, [sp, #8] 意味着『存储r0寄存器的值到栈指针地址加上8字节内存的位置』。</li> <li>刚被保存到栈的值又被读取到相同的寄存器内。和 str 指令相反的, ldr 指令是从一个内存中加载内容到寄存器。两者语法非常相似。因此, ldr r0, [sp, #8] 意思是『读取出在栈指针地址加上8字节内存的位置的内容,并将内容赋值给寄存器r0』。 如果你好奇为何r0和r1刚被存储又被加载出来,对,它是很奇怪,这两行明明就是多余的嘛!如果编译器允许基本的编译优化,那么这多余的就会被消除。</li> <li>这是该函数最重要的指令了:做加法。意思是将r0和r1中的内容相加,并将相加的值赋值给r0。 add 指令入参可以是两个或者三个,如果是三个,那第一个参数就是存储后两个参数相加的值的寄存器。所以,这行指令也可以写成: add r0, r0, r1 。</li> <li>再一次,编译器生成了一些多余的代码:将相加的结果存储起来,又读取到相同的位置。</li> <li>函数即将终止,因此栈指针放回原来的地方。函数开始时从sp(栈指针)上减去了12个字节而得到12个字节内存使用。现在它把12个字节还回去。函数必须保证栈指针操作平衡,否则栈指针可能漂移,最终可能超出了已分配的内存。你应该不希望那样...</li> <li>最后,间接分支调度指令 bx 被执行,用于返回到调用函数(调用本函数的函数)。 lr (link register)存放了调用函数执行完当前函数的下一条指令。注意到,在 addFunction 执行返回后,r0保存了相加的值。这是调用约定的另一部分。函数的返回值总是r0,除非一个寄存器不够存储,这种情况下才会使用r1-r3。</li> </ol> <p>其实并没有那么难,对不?想知道更多关于指令的信息,可以看看 <a href="/misc/goto?guid=4959715835756224996" rel="nofollow,noindex">这个文档</a> ,或者 <a href="/misc/goto?guid=4959715835832167982" rel="nofollow,noindex">看其他的中文</a> .</p> <p>你发现了这个函数很多汇编代码是多余的。因为一开始,我们的编译器就是Debug(调试)模式,没有任何编译器优化的。如果你把编译优化打开,你会得到一个非常精简的代码。</p> <p>改变 Show Assembly Output For 到选择器到 存档(Xcode7不一样,区别是如果你想获取精简的汇编,需要Edit Scheme成Release模式,另外切换后记得clean下工程)。搜索 _addFunction: ,你可以看到如下代码:</p> <pre> _addFunction: add r0, r1 bx lr</pre> <p>这非常简洁! 可以看到仅仅两个指令就写完了这个函数。你可能没想到仅用两个指令就完成了~ 当然,你平时写的函数一般更长也更有趣点~</p> <p>现在你已经有一个以返回到调用者分支为结束的函数。那么作为一个相互关系的另一个函数,调用该函数的调用者呢?</p> <h2>调用函数</h2> <p>首先,你需要给 addFunction 函数添加一个让编译器不做优化的属性。你已经发现如果我们开启优化,那么代码会移除掉不必要的指令,甚至连函数调用都会被移除,或者可能直接将函数作为内联函数使用。</p> <p>例如,编译器可能直接用 add 指令代替函数调用。实际上,编译器是非常强大智能的,它可能直接帮你计算好了相加后的值,连 add 指令都不需要生成。</p> <p>这个教程,我们不希望编译器做优化或者将函数内联。回到main.m文件,修改函数成如下:</p> <pre> __attribute__((noinline)) int addFunction(int a, int b) { int c = a + b; return c; }</pre> <p>继续在下方添加另一个函数如下:</p> <pre> void fooFunction() { int add = addFunction(12, 34); printf("add = %i", add); }</pre> <p>fooFunction 函数简单地用 addFunction 让12和34相加并且打印出值。这里使用的是C语言的printf而不是Objective-C的NSLog函数的原因是后者的汇编结果更加复杂。</p> <p>再一次生成汇编代码,搜索 _fooFunction ,你可以看到如下代码:</p> <pre> _fooFunction: @ 1: push {r7, lr} @ 2: movs r0, #12 movs r1, #34 @ 3: mov r7, sp @ 4: bl _addFunction @ 5: mov r1, r0 @ 6: movw r0, :lower16:(L_.str-(LPC1_0+4)) movt r0, :upper16:(L_.str-(LPC1_0+4)) LPC1_0: add r0, pc @ 7: blx _printf @ 8: pop {r7, pc}</pre> <p>这里引入了一些教程之前没有介绍过的指令,但不用担心,他们并不复杂,我们来看:</p> <ol> <li>这个指令跟我们之前的 add sp, #12 指令做的事情差不多。这里,r7和lr被推入到栈,意味着sp(栈指针)减掉了8字节(栈指针始终指向栈顶,所以在push的时候会变小),因为r7和lr都是4字节。注意到栈指针变小了而且通过一个指令存储了两个值。r7的值需要存储起来的原始是之后函数执行时它会被使用到并重写。lr被存储的原因在函数最后将会知晓;</li> <li>这两个指令(mov)是Move组的成员之一。有时候你会看到 movs ,或者 movw ,或者其他,但他们的作用都是用一个值来填充寄存器。你可以将一个寄存器的值移动到另一个寄存器,因此 mov r0, r1 会将r1寄存器内容填充到r0,r1的值不变。在这两行代码中,r0和r1是用函数中定义的两个常量赋值的。注意到他们是被加载到r0和r1中,刚好是 addFunction 的入参。</li> <li>在函数调用边界时,栈指针应该被保存起来,因此作为可存储本地变量的寄存器之一r7被使用了。你会发现剩余的函数代码中并没有使用栈指针或者r7,因此这是个小小的多余处理。有时候开启了优化也优化不掉。</li> <li>指令 bl 意味着函数调用。记得函数的入参已经放入相关的寄存器r0及r1了吧。这个指令执行的代码我们称之为分支。因为是指令 bl 而不是指令 b ,指令 bl 全称是『branch with link』,意味着在执行分支代码之前,需要将lr(链接寄存器)置为当前函数的下一个指令。回想下,当 addFunction 方法返回时,lr就是指向下一个要执行的指令。</li> <li>这是将两个数相加的 addFunction 分支返回的节点。记得之前说明过函数的返回值是存放在r0的吧~ 这个值会作为printf函数的第二个参数,因此 mov 指令用于将r0的内容赋值给r1。</li> <li>printf函数的第一个参数是一个字符串。这三条指令加载指向所需的字符串的开始地址的指针到r0寄存器。字符串存储在我们称之为二机制『数据段』的位置。但只有最终二进制被链接时才能知道该数据的具体位置。 字符串可以在main.m生成的目标文件例找到。如果你在生成的汇编代码内搜索『L.str』,便可找到它。前两个指令加载常量的地址,并减去标签的地址(LPC1_0加上4字节)。看到第三个指令这么做的目的就很明显了。r0加上pc(程序计数器),这样无论L.str在二进制文件的什么位置都能够准确的存放字符串的位置。 下面的图展示了内存分布。 L_.str - (LPC1_0 + 4) 差值可以随便变动,但是加载r0的代码却不用变化。 <p><img src="https://simg.open-open.com/show/48fed23e0443c5226ea75009117f71ea.png"></p> </li> <li>这条指令是调用printf函数。这里的 blx 跟 bl 指令有点不同,x代表交换,当有需要时,处理器可以改变当前运行模式。处理器运行模式有点超越了本教程的范围,ARM处理器有两种运行模式:ARM和Thumb。Thumb指令集是16位宽,而ARM是32位。Thumb指令比ARM少,使用Thumb意味着更少的代码大小及更好的CPU缓存。通常使用有限的Thumb指令集可以让你从更小的包大小中获益。想了解更多Thumb的知识 <a href="/misc/goto?guid=4959715835918132051" rel="nofollow,noindex">请戳这里</a></li> <li>最后一条指令是推出第一条指令推入的值。这次列表中的寄存器的值是用栈中的值填充的,且栈指针增加了。回想下,r7和lr之前是被推入到栈中,那么此时为何是推出来的值存入到了r7和pc中,而不是r7和lr呢? 好的,记得lr是存储当前函数执行完成后的下一个指令地址吧。当你把lr推出栈赋值给pc后,执行将会从本函数调用的地方继续执行。这通常是一个函数返回的实现方式,而不是像 addFunction 那样切分支的方式。</li> </ol> <p>以上是对ARM指令集大致的介绍。还有很多其他指令集,但这些是开始理解指令集最重要的的指令。让我们来用伪代码快速回忆一下代码做的事情:</p> <pre> mov r0, r1 => r0 = r1 mov r0, #10 => r0 = 10 ldr r0, [sp] => r0 = *sp str r0, [sp] => *sp = r0 add r0, r1, r2 => r0 = r1 + r2 add r0, r1 => r0 = r0 + r1 push {r0, r1, r2} => r0, r1 r2 入栈 pop {r0, r1, r2} => 栈顶出三个, 并赋值给r0, r1 and r2. b _label => pc = _label bl _label => lr = pc + 4; pc = _label</pre> <p>哇哦~ 现在你可以读懂一些ARM汇编代码了~</p> <p><img src="https://simg.open-open.com/show/8ad452291bd43c1b37ee2a060ba0d09e.png"></p> <h2>Objective-C 汇编</h2> <p>至此,你看到的函数都是C语言的。Objective-C代码要复杂点,不过让我们来检验一下。在ViewController.m代码中添加以下代码实现:</p> <pre> - (int)addValue:(int)a toValue:(int)b { int c = a + b; return c; }</pre> <p>让我们再次重复之前精简的汇编方式,搜索 addValue:toValue: 函数,可以看到:</p> <pre> "-[ViewController addValue:toValue:]": adds r0, r3, r2 bx lr</pre> <p>首先你会注意到标签名字。这次便签名字包含了类名及全部的方法名。</p> <p>如果你和之前的 addFunction 汇编代码相比较,你会发现两个入参存储在了r2及r3而不是r0和r1。为什么呢?</p> <p>OK,因为Objective-C函数在C函数的基础上多传了两个隐式的参数。 addValue:toValue: 方法语法上和以下方法相同:</p> <pre> int ViewController_addValue_toValue(id self, SEL _cmd, int a, int b) { int c = a + b; return c; }</pre> <p>这就是为什么a和b变量分别存储在r2和r3内了。现在你大概知道了前两个隐式参数的含义了,你总是可以使用self这个变量。</p> <p>但是, _cmd 可能之前你没有见过。像self变量一样,在Objective-C代码中它是可获取的,而且代表着当前函数的selector。你一般从不会用到它,这就是你为何没听过的原因了。</p> <p>为了看清Objective-C函数是如何被调用的,我们在ViewController中添加如下代码:</p> <pre> - (void)foo { int add = [self addValue:12 toValue:34]; NSLog(@"add = %i", add); }</pre> <p>生成代码并找到该方法,你可以看到类似下面的代码(Xcode7生成的有点不一样了):</p> <pre> "-[ViewController foo]": @ 1: push {r7, lr} @ 2: movw r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC1_0+4)) movt r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC1_0+4)) LPC1_0: add r1, pc @ 3: ldr r1, [r1] @ 4: movs r2, #12 movs r3, #34 @ 5: mov r7, sp @ 6: blx _objc_msgSend @ 7: mov r1, r0 @ 8: movw r0, :lower16:(L__unnamed_cfstring_-(LPC1_1+4)) movt r0, :upper16:(L__unnamed_cfstring_-(LPC1_1+4)) LPC1_1: add r0, pc @ 9: blx _NSLog @ 10: pop {r7, pc}</pre> <p>再次,和之前我们看到的C语言函数差不多。分解它:</p> <ol> <li>将r7及lr入栈;</li> <li>使用同样加载字符串的方式,加载在名为 L_OBJC_SELECTOR_REFERENCES_ 的标签处的字符串指针到r1。像便签名字一样,它是一个selector的引用。其实selector就是存储在数据段的字符串。</li> <li>如果你在汇编代码里查找 L_OBJC_SELECTOR_REFERENCES_ ,你会发现: L_OBJC_SELECTOR_REFERENCES_:.long L_OBJC_METH_VAR_NAME_ ,这说明r1指向的是 L_OBJC_METH_VAR_NAME_ 标签的地址。如果你继续查看该便签,你将找到 addValue:toValue: 字符串。 指令 ldr r1, [r1] 表示加载存储在r1指针内的内容并赋值给r1。用伪代码是这么表述的 r1 = *r1 。再仔细想想,r1其实已经指向 addValue:toValue: 字符串地址。</li> <li>加载常量到r2和r3。</li> <li>保存栈指针。</li> <li>以保存lr指针且可更换模式的方式切分支到 objc_msgSend 。这个方式是Objective-C语言的核心。它调用它的入参selector的实现。参数最终和传给这个方法的参数一样,r0是self,r1是_cmd,r2和r3是剩下的参数。这就是为何selector要赋值给r1,剩余参数赋值给r2和r3,r0是隐式加载的,因为self变量已经存在了。</li> <li>addValue:toValue: 方法的返回值还是r0。这个指令将r0的值赋值给r1,因为r0之后要作为C函数NSLog的参数。</li> <li>加载NSLog需要的字符串给r0,像printf函数一样。</li> <li>以保存lr指针且可更换模式的方式切分支到 NSLog 。</li> <li>两个值被推出来,一个赋值给r7一个给pc。这个指令将使函数返回。</li> </ol> <p>如你所见,当生成汇编代码时,C函数和Objective-C没有多大差别。两者的主要差别在于,Objective-C隐式的传递了两个参数,且selector是在保存在数据段内的。</p> <h2>Objective-C函数执行过程</h2> <p>你已经大致看到了 objc_msgSend 函数,你可能也在Crash日志内见过它。这个函数是Objective-C运行时的核心。运行时是胶合Objective-C应用的代码,包括所有的内存管理方法及类处理。</p> <p>每一次Objective-C函数调用,都需要 objc_msgSend C函数来派发消息。它会去对应的对象方法列表内搜索方法的实现。 objc_msgSend 函数签名如下:</p> <pre> id objc_msgSend(id self, SEL _cmd, ...)</pre> <p>在函数执行当中的第一个参数是self对象。因此当我们写诸如 self.someProperty 代码时,self就是这么来的。</p> <p>第二个参数是少有人知的隐藏参数。你可以试试,在Objective-C方法里写这样一句代码: NSLog(@"%@", NSStringFromSelector(_cmd)); ,你可以看到当前的方法被打印出来。明白了不?</p> <p>剩下的参数就是方法所需的参数了。像 addValue:toValue: 方法有两个参数的方法,初次外,还有另外两个参数。因此,不调用Objective-C函数,你可以直接这样写也可达到同样的效果:</p> <pre> - (void)foo { int add = (int)objc_msgSend(self, NSSelectorFromString(@"addValue:toValue:"), 12, 34); NSLog(@"add = %i", add); }</pre> <p>注: objc_msgSend 函数的返回值是id类型,但被强转成int类型。这没问题是因为他们的大小都是一样的。如果方法返回不同大小的返回值,实际上是另一个方法被调用了。你可以 <a href="/misc/goto?guid=4959715836001868689" rel="nofollow,noindex">在这里</a> 了解更多信息。如果返回值是浮点型,那么另一个 objc_msgSend 方法的 <a href="/misc/goto?guid=4959715836077446222" rel="nofollow,noindex">变种被调用</a> .</p> <p>回想下上面Objective-C方法生成的等量C函数的签名如下:</p> <pre> int ViewController_addValue_toValue(id self, SEL _cmd, int a, int b)</pre> <p>现在对于这个写法应该没什么惊讶的。可以看出它跟我们 objc_msgSend 的签名非常匹配!意味着当 objc_msgSend 方法找到了对应方法实现时,调用的各个参数都正确了。你可以 <a href="/misc/goto?guid=4959715836161824859" rel="nofollow,noindex">在这里</a> 阅读更多关于 objc_msgSend 方法的信息。</p> <h2>现在,你可以逆向工程</h2> <p>获得了ARM汇编的一些知识,你应该对程序中一些中断,崩溃或者运行不正确有种感觉了。为何你会想去看汇编代码?因为你找到更多信息看清到底是哪一步导致bug发生。</p> <p><img src="https://simg.open-open.com/show/3148a2f4929c8e7b973023d887d41d6c.png"></p> <p>有时候,你并没有源码,例如,你崩溃发生在第三方库或者系统框架内。若能通过汇编调查将会让你快速找到问题所在。iOS SDK所有的框架都装在这个目录下:</p> <p><Path_to_Xcode>/Contents/Developer/Platforms/iPhoneOS.platform/Developer/ SDKs/iPhoneOS6.1.sdk/System/Library/Frameworks</p> <p>为调查这些库,我建议你买这个软件 <a href="/misc/goto?guid=4959629262669530379" rel="nofollow,noindex">HopperApp</a> ,该软件可以反汇编既而你可以查看。例如,打开UIKit库,你可以看到每个方法做了啥,看清来像这样:</p> <p><img src="https://simg.open-open.com/show/648c082b55fc1df00c9097e29f5fe4a5.png"></p> <p>这是 http://www.raywenderlich.com/wp-content/uploads/2013/04/05-HopperApp-1.png 方法的汇编代码。运用你新得到的ARM汇编知识,你应该可以知道方法做了什么。</p> <p>第一个selector指针加载到r1,为 objc_msgSend 方法做准备。注意到并没有动过其他寄存器,那么r0中的self就和 shouldAutorotateToInterfaceOrientation 方法一样。</p> <p>同样地,你也发现被调用的函数也是只有一个参数,因为他的名字里只有一个冒号。因为只剩下r2未处理了,那么传给 shouldAutorotateToInterfaceOrientation 的第一个参数就是我们需要传给调用函数的参数。</p> <p>最后,调用函数后,r0没有动过。那么调用函数的返回值,就是当前函数的返回值。</p> <p>因此你可以推论出这个函数是这么实现的:</p> <pre> - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return [self _doesTopViewControllerSupportInterfaceOrientation:interfaceOrientation]; }</pre> <p>哇哦!太简单了!通常一个函数的逻辑比这个复杂一些,但通常你都可以把他们组织起来,并快速的想明白一些代码做了什么。</p> <h2>何去何从?</h2> <p>iOS汇编教程给了你一些洞察ARM内核的方法。你也学会了一些关于C和Objective-C的一些调用约定。</p> <p>装备了这些知识,当你的应用崩溃在系统库深部时,你就有了理解这些随机代码的方法。或者你只想通过看汇编代码了解你的代码到底是如何运行的。</p> <p>如果你对深入研究ARM感兴趣,我建议你买一个 <a href="/misc/goto?guid=4958348874254940848" rel="nofollow,noindex">Raspberry Pi</a> . 这些晓得设备拥有一个和iOS设备非常类似的ARM处理器,而且有许多文档教你如何为它编写代码。</p> <p>另一个值得关注的是NEON。这是在所有除了iPhone 3GS之前的处理器上的额外的指令集。它提供单指令多数据流(SIMD)能力让你非常高效地处理数据。运用这些指令的应用一般是图像处理的。如果你需要高效地处理事情,学习如何写NEON指令使用内联汇编将会对你有益。这真的非常高端~</p> <p>这个事情应该够你忙一会了,让我们在论坛上了解下你的ARM探索之旅吧~</p> <p> </p> <p>来自:http://www.jianshu.com/p/544464a5e630</p> <p> </p>