您的位置:首页 > 移动开发 > IOS开发

ios 汇编教程

2013-06-23 10:53 447 查看
注:本文由破船译自: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已经返回到调用的函数那里了.下面我们就来看看关于调用的函数的相关信息.

下面的内容会在第二篇文章中翻译:
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: