iOS汇编教程:ARM(1)和ARM(2)

时间:2021-05-18 01:02:29

iOS汇编教程:ARM(1)和ARM(2)

发布于:2013-06-24 09:56阅读数:18007

注:本文由破船译自:raywenderlich。感谢唐巧抽出时间对本文进行double-check。 我们写的Objective-C代码,最终会被转换为机器代码 由ARM处理器能识别的1和0组成。实际上,在机器代码之间,还有一

注:本文由破船译自:raywenderlich。感谢唐巧抽出时间对本文进行double-check。 

iOS汇编教程:ARM(1)和ARM(2)
我们写的Objective-C代码,最终会被转换为机器代码 —— 由ARM处理器能识别的1和0组成。实际上,在机器代码之间,还有一门人类可以阅读的语言 —— 汇编语言。
 
了解汇编,可以深入到你的代码里面进行调试和优化的探索,并有助于你对Objective-C运行时(runtime)的理解,同时也能满足你内心的好奇!
 
在这篇iOS汇编教程中,你能学到:
什么是汇编 —— 以及为什么需要关注它。
如何阅读汇编 —— 特别是由Objective -C生成的汇编。
在调试的时候如何使用assembly view —— 遇到一个bug或者crash,看看到底是怎么回事,这非常有用。
 
为了有效吸收本文内容,建议本文的读者对象为已经熟悉Objective-C编程了。当然,你也应该要知道一些简单的计算机科学相关概念,例如栈、CPU以及它们是如何运行的。如果你对CPU不太熟悉,建议在阅读本文之前,先看看这里的内容:微处理器的工作原理。
 
——————————————————————-
iOS汇编教程:ARM(1)
开始:什么是汇编
函数调用约定
创建工程
加法(addFunction)
 
开始:什么是汇编
Objective-C是一门高级语言。编译器会将你的Objective-C代码编译为汇编语言代码:一门低级语言,不过还不是最低级的语言。
 
这些汇编会被汇编器(assembler)组装为机器代码——CPU可以识别的0和1。好在一般开发者并没有必要考虑机器代码,不过有时候详细的了解汇编,会非常有用。
iOS汇编教程:ARM(1)和ARM(2)
 
每一个汇编指令都会告诉CPU执行一个相关任务,例如“对两个数字执行加(add)操作”,或“从某个内存地址加载数据”。
 
除了主存外 ——如 iPhone 5有1GB的主存、Mac电脑可能会有8GB —— CPU还有少许的存储部件,称之为寄存器,寄存器的访问速度非常快,一个寄存器就像一个变量一样,可以存储单个值。
 
所有的iOS设备(实际上,现如今,几乎所有的移动设备)使用的CPU都是基于 ARM架构。 ARM芯片使用的指令集是 RISC(精简指令集),该指令集非常的精简,并且易读(比 x86的指令集精简多了)。
 
一个汇编指令(或者语句)看起来如下所示:
 
    
  1. mov r0, #42 
上面的这行汇编指令,涉及到好多命令(或操作)。mov的作用是对数据进行移动。在ARM汇编指令中,目标是第一个,所以,上面的指令是将值42移动到寄存器r0中。再来看看下面的代码:
 
    
  1. ldr r2, [r0] 
  2. ldr r3, [r1] 
  3. 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,工程的配置如下:
iOS汇编教程:ARM(1)和ARM(2)
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文件,然后将下面的函数拷贝并粘贴到文件的顶部。
 
    
  1. int addFunction(int a, int b) { 
  2.     int c = a + b; 
  3.     return c; 
 现在将Xcode中的scheme设置为为设备构建:选中iOS Device作为scheme target(如果你将设备连接到电脑中,会现实<你的设备名称>,如“Matt Galloway的iPhone 5”)——这样选择之后,生成的汇编就是针对ARM的,而不是针对x86(模拟器使用)。Xcode的选择效果如下图所示:
iOS汇编教程:ARM(1)和ARM(2)
 
然后选择: Product\Generate Output\Assembly File。过一会之后,Xcode会生成一个文件,这个文件里面有很多行都有下划线__。在文件的顶部,好多行都是以 .section开头。接着选中 Show Assembly Output For中的 Running
 
 注意:默认情况下,使用的是debug scheme中的设置信息,所以默认选中的就是Running。在debug模式下,编译器对代码没有做优化处理——首先观察没有进过优化处理的汇编,更利于理解代码具体都发生了什么。
 
在生成的文件中搜索_addFunction,会看到类似如下的代码:
 
    
  1.     .globl  _addFunction 
  2.     .align  2 
  3.     .code   16                      @ @addFunction 
  4.     .thumb_func _addFunction 
  5. _addFunction: 
  6.     .cfi_startproc 
  7. Lfunc_begin0: 
  8.     .loc    1 13 0                  @ main.m:13:0 
  9. @ BB#0: 
  10.     sub sp, #12 
  11.     str r0, [sp, #8] 
  12.     str r1, [sp, #4] 
  13.     .loc    1 14 18 prologue_end    @ main.m:14:18 
  14. Ltmp0: 
  15.     ldr r0, [sp, #8] 
  16.     ldr r1, [sp, #4] 
  17.     add r0, r1 
  18.     str r0, [sp] 
  19.     .loc    1 15 5                  @ main.m:15:5 
  20.     ldr r0, [sp] 
  21.     add sp, #12 
  22.     bx  lr 
  23. Ltmp1: 
  24. Lfunc_end0: 
  25.     .cfi_endproc 
 
上面的代码看起来有点凌乱,实际上也不难以读懂。我们来看看,首先,所有以”.”开头的代码行都不是汇编指令,我们可以忽略所有这些以”.”开头的代码行。
 
代码中以冒号结尾的的代码行(例如 _addFunction:Ltim0: ),我们称之为标签( label)。这些标签的作用是给汇编代码片段指定相关的名字.名为 _addFunction:的标签,实际上是一个函数的入口点.
 
这个标签(_addFunction: )是必须有的:别的代码调用addFunction函数时,并不需要知道该函数具体在什么地方,通过简单的一个符号或标签就可以进行调用.在最终生成程序二进制文件时,链接器会把这个标签转换到实际的地址.
 
我们需要注意的时,编译器总是会在函数名前面添加一个下划线——这仅仅是一个约定。另外,其他所有的标签都是以L开头——这些通常称为局部标签(local label),只会在函数内部使用。在上面的代码中,虽然没有实际用到局部标签,不过编译器还是为我们生成了一些——之所以会生成这些没有被使用到的局部标签,是由于代码还没有做任何的优化处理。
 
注释是以 @字符开头。通过上面的分析,这样一来,忽略掉注释和标签,代码看起来如下所示:
 
    
  1. _addFunction: 
  2. @ 1: 
  3.     sub sp, #12 
  4. @ 2: 
  5.     str r0, [sp, #8] 
  6.     str r1, [sp, #4] 
  7. @ 3: 
  8.     ldr r0, [sp, #8] 
  9.     ldr r1, [sp, #4] 
  10. @ 4: 
  11.     add r0, r1 
  12. @ 5: 
  13.     str r0, [sp] 
  14.     ldr r0, [sp] 
  15. @ 6: 
  16.     add sp, #12 
  17. @ 7: 
  18.     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:,会看到如下指令(只有这些):
 
    
  1. _addFunction: 
  2.     add r0, r1 
  3.     bx  lr 
 
这看起来非常简洁:只需要两条指令就完成了addFunction函数的功能。当然,在实际开发中,一个函数一般都会有好多指令。
现在,这个addFunction已经返回到调用的函数那里了.下面我们就来看看关于调用的函数的相关信息.


转载至: http://www.cocoachina.com/industry/20130624/6463.html