注:本文由破船译自:raywenderlich
转自破船http://beyondvincent.com/2013/06/19/ios%E6%B1%87%E7%BC%96%E6%95%99%E7%A8%8B%EF%BC%9Aarm/。
为自己方便查找转自这里
我们写的Objective-C代码,最终会被转换为机器代码 —— 由ARM处理器能识别的1和0组成。实际上,在机器代码之间,还有一门人类可以阅读的语言 —— 汇编语言。
了解汇编,可以深入到你的代码里面进行调试和优化的探索,并有助于你对Objective-C运行时(runtime)的理解,同时也能满足你内心的好奇!
在这篇iOS汇编教程中,你能学到:
- 什么是汇编 —— 以及为什么需要关注它。
- 如何阅读汇编 —— 特别是由Objective -C生成的汇编。
- 在调试的时候如何使用assembly view —— 遇到一个bug或者crash,看看到底是怎么回事,这非常有用。
为了有效吸收本文内容,建议本文的读者对象为已经熟悉Objective-C编程了。当然,你也应该要知道一些简单的计算机科学相关概念,例如栈、CPU以及它们是如何运行的。如果你对CPU不太熟悉,建议在阅读本文之前,先看看这里的内容:微处理器的工作原理。
目录[分两篇文章翻译]:
iOS汇编教程:ARM(1)
- 开始:什么是汇编
- 函数调用约定
- 创建工程
- 加法(addFunction)
iOS汇编教程:ARM(2)
- 函数的调用
- Objective -C 汇编
- Obj-C 消息发给了谁
- 你现在可以进行逆向工程了
- 何去何从
——————————————————————-
iOS汇编教程:ARM(1)
开始:什么是汇编
Objective-C是一门高级语言。编译器会将你的Objective-C代码编译为汇编语言代码:一门低级语言,不过还不是最低级的语言。
这些汇编会被汇编器(assembler)组装为机器代码——CPU可以识别的0和1。好在一般开发者并没有必要考虑机器代码,不过有时候详细的了解汇编,会非常有用。
每一个汇编指令都会告诉CPU执行一个相关任务,例如“对两个数字执行加(add)操作”,或“从某个内存地址加载数据”。
除了主存外 ——如 iPhone 5有1GB的主存、Mac电脑可能会有8GB —— CPU还有少许的存储部件,称之为寄存器,寄存器的访问速度非常快,一个寄存器就像一个变量一样,可以存储单个值。
所有的iOS设备(实际上,现如今,几乎所有的移动设备)使用的CPU都是基于ARM架构。 ARM芯片使用的指令集是RISC(精简指令集),该指令集非常的精简,并且易读(比x86的指令集精简多了)。
一个汇编指令(或者语句)看起来如下所示:
- mov r0, #42
上面的这行汇编指令,涉及到好多命令(或操作)。mov的作用是对数据进行移动。在ARM汇编指令中,目标是第一个,所以,上面的指令是将值42移动到寄存器r0中。再来看看下面的代码:
- ldr r2, [r0]
- ldr r3, [r1]
- add r4, r2, r3
上面汇编指令的作用是首先将寄存器r0和r1中的值装载到寄存器r2和r3中,然后对寄存器r2和r3中的值进行加(add)操作,加的结果存放到r4中。
很容易看懂吧!
函数调用约定
要想理解汇编代码,首先重要的事情就是理解代码之间的交互——意思是一个函数调用另一个函数的方式。这包括了参数如何传递以及如何从函数返回结果——称之为调用的约定。编译器必须严格的遵守相关标准进行代码编译,这样生成的代码,才能够相互兼容。
上面讨论过,寄存器是的存储空间非常少,并且靠近CPU——用来存储当前使用的一些值。ARM CPU有16个寄存器:r0到r15。每个寄存器为32bit。调用约定规定了这些寄存器的特定用途。如下:
- r0 – r3:存储传递给函数的参数值。
- r4 – r11:存储函数的局部变量。
- r12:是内部过程调用暂时寄存器(intra-procedure-call scratch register)。
- r13:存储栈指针(sp)。在计算机中,栈非常重要。这个寄存器保存着栈顶的指针。这里可以看到更多关于栈的信息:Wikipedia。
- r14:链接寄存器(link register)。存储着当被调用函数返回时,将要执行的下一条指令的地址。
- r15:用作程序计数器(program counter)。存储着当前执行指令的地址。每条执行被执行后,该计数器会进行自增(+1)。
这里可以看到更多相关ARM 调用约定的内容:this document from ARM。苹果公司也给出了一份文档详细介绍了在iOS开发中的调用约定: calling convention used for iOS development。
下面我们就从代码上开始真正的认识汇编。
创建工程
打开Xcode,File\New\New Project,选择iOS\Application\Single View Application,然后点击Next,工程的配置如下:
- Product name: ARMAssembly
- Company Identifier: 一般为反向的DNS标示
- Class Prefix: 空白
- Devices: iPhone
- Use Storyboards: No
- Use Automatic Reference Counting: Yes
- Include Unit Tests: No
点击 Next 选择工程存储的位置——完成工程的创建。
加法(addFunction)
下面我们写一个加法函数:对两个数进行相加,然后返回结果。这里我们先用C语法写,后面再介绍用OC来写(OC稍微复杂一点)。在工程的Supporting Files目录中打开main.m文件,然后将下面的函数拷贝并粘贴到文件的顶部。
- int addFunction(int a, int b) {
- int c = a + b;
- return c;
- }
现在将Xcode中的scheme设置为为设备构建:选中iOS Device作为scheme target(如果你将设备连接到电脑中,会现实<你的设备名称>,如“Matt Galloway的iPhone 5”)——这样选择之后,生成的汇编就是针对ARM的,而不是针对x86(模拟器使用)。Xcode的选择效果如下图所示:
然后选择:Product\Generate Output\Assembly File。过一会之后,Xcode会生成一个文件,这个文件里面有很多行都有下划线__。在文件的顶部,好多行都是以.section开头。接着选中Show Assembly Output For中的Running。
注意:默认情况下,使用的是debug scheme中的设置信息,所以默认选中的就是Running。在debug模式下,编译器对代码没有做优化处理——首先观察没有进过优化处理的汇编,更利于理解代码具体都发生了什么。
在生成的文件中搜索_addFunction,会看到类似如下的代码:
- .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
上面的代码看起来有点凌乱,实际上也不难以读懂。我们来看看,首先,所有以”.”开头的代码行都不是汇编指令,我们可以忽略所有这些以”.”开头的代码行。
代码中以冒号结尾的的代码行(例如_addFunction:和Ltim0: ),我们称之为标签(label)。这些标签的作用是给汇编代码片段指定相关的名字.名为_addFunction:的标签,实际上是一个函数的入口点.
这个标签(_addFunction: )是必须有的:别的代码调用addFunction函数时,并不需要知道该函数具体在什么地方,通过简单的一个符号或标签就可以进行调用.在最终生成程序二进制文件时,链接器会把这个标签转换到实际的地址.
我们需要注意的时,编译器总是会在函数名前面添加一个下划线——这仅仅是一个约定。另外,其他所有的标签都是以L开头——这些通常称为局部标签(local label),只会在函数内部使用。在上面的代码中,虽然没有实际用到局部标签,不过编译器还是为我们生成了一些——之所以会生成这些没有被使用到的局部标签,是由于代码还没有做任何的优化处理。
注释是以@字符开头。通过上面的分析,这样一来,忽略掉注释和标签,代码看起来如下所示:
- _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
下面我们来看看代码中每部分汇编都做了什么:
1、首先,在栈(stack)创建临时存储所需要的空间。栈提供了许多内存供函数使用。ARM中的栈是向下延伸的,也就是说,在栈上创建一些空间,需要从栈指针开始减去(subtract)一些空间。在这里,预留了12个字节。
2、r0和r1用来存储传递给调用函数的参数值。如果函数有4个参数,那么会把r2和r3当做第三个和第四个参数。如果函数的参数超过了4个,或者携带的参数不适合使用32位的寄存器(例如很大的数据结构),那么可以通过栈来传递这些参数。
在这里,两个参数被保存到栈中。这是由存储寄存器(str)指令完成的。
上面的指令可以指定一个偏移量,用来应用在某个值上面。所以[sp, #8]的意思是存储至“栈指针寄存器+8的地方”,因此,str r0, [sp, #8]的作用是:将寄存器r0中的内容存储到栈指针(加8)指向的内存地址.
3、将刚刚保存到栈中的值读取至相同的寄存器中(r0和r1)。这里,的ldr指令与str指令刚好相反,ldr(load register)会把指定内存位置中的的内容加载到寄存器中。ldr和str的语法非常相似:ldr r0, [sp, #8]的作用是“将栈指针加8后指向的地址内容加载到r0寄存器中”。
这里你可能会感觉到奇怪,为什么ro和r1寄存器中的值刚刚保存,马上又将其加载回来,答案是:这两行代码是冗余的,可以去掉!如果编译器做了优化处理,那么这些冗余的代码会被忽略掉.
4、这是该函数中最终的要一个指令:执行加操作。该执行的意思是:将r0和r1中的内容进行相加,然后把结果放到r0中。
add指令可以是两个参数,也可以是三个参数.如果指定三个参数,那么第一个参数就被当做目标寄存器,剩下的两个则为源寄存器.因此,这里的指令可以写成这样:add r0, r0, r1。
5、同样,编译器生成了一些冗余代码:将加的结果存储到栈中,接着立即从栈中读取回来。
6、终止函数的地方:将栈指针指向调用addFunction函数时的最初地方。addFunction开始于:sp减去12的地方:预留了12个字节。现在将12加回去即可。这里必须确保栈指针的正确操作,否则栈指针会指向错误的地方。
最后,执行bx指令会回到调用函数的地方.这里的寄存器lr是链接寄存器(link register),该存储器存储着将要执行的下一条指令。注意,addFunction返回之后,r0寄存器会存储着该函数相加的结果值——这也是调用约定中的一部分:函数的返回值永远都被存储在r0寄存器中。除非一个寄存器不够存储,这是可以使用r1-r3。
上面就是所有相关addFunction的介绍,并不复杂吧?预知关于这些指令的更多内容,请看这里: ARM website.
重申一下,上面的方法有好多冗余的地方:这是由于编译器处于debug模式,不会对代码做优化处理.如果对代码进行了优化处理,会看到生成的汇编代码非常的少。
选中Show Assembly Output For中的Archiving。然后搜索_addFunction:,会看到如下指令(只有这些):
- _addFunction:
- add r0, r1
- bx lr
这看起来非常简洁:只需要两条指令就完成了addFunction函数的功能。当然,在实际开发中,一个函数一般都会有好多指令。
现在,这个addFunction已经返回到调用的函数那里了.下面我们就来看看关于调用的函数的相关信息.
下面的内容会在第二篇文章中翻译: