转自 http://www.cocoachina.com/applenews/devnews/2013/0624/6463.html
注:本文由破船译自:raywenderlich。感谢唐巧抽出时间对本文进行double-check。
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624102539_1.png)
我们写的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。好在一般开发者并没有必要考虑机器代码,不过有时候详细的了解汇编,会非常有用。
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624102806_1.png)
每一个汇编指令都会告诉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,工程的配置如下:
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624103117_1.png)
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的选择效果如下图所示:
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624100559_1.png)
然后选择:
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已经返回到调用的函数那里了.下面我们就来看看关于调用的函数的相关信息.
iOS汇编教程:ARM(2)
函数的调用
Objective – C 汇编
Obj-C 消息发给了谁
你现在可以进行逆向工程了
何去何从
函数的调用
首先,给函数addFunction函数添加一个属性(
__attribute__) ,告诉编译器不要进行特定的优化处理。通过上一篇文章,你已经看到编译器可以对代码进行优化,移除掉不需要的指令,另外,编译器甚至可以移除掉函数的调用,直接把被调用函数的相关代码进行内嵌到调用函数中。
例如,编译器可能会在调用函数中适当的添加add指令,而不是调用addFunction本身。实际上,现如今的编译器已经非常的智能了,针对类似addFunction这样的函数,编译器本身就可以进行加法操作,而不用在代码中添加一条add指令。
本文中,我们不希望编译器对代码进行优化——把代码进行内嵌处理。现在回到工程的main.m文件中,并按照如下方式修改addFunction:
- __attribute__((noinline))
- int addFunction(int a, int b) {
- int c = a + b;
- return c;
- }
紧接着,在该函数下面添加另外一个函数:
- void fooFunction() {
- int add = addFunction(12, 44);
- printf("add = %i", add);
- }
如上代码所示,fooFunction通过调用addFunction来计算12+44,然后将结果打印出来。这里使用C函数printf进行打印,而没有使用Objective-C的NSLog(NSLog要稍微复杂一点)。
接着再次选择Xcode中的Product\Generate Output\Assembly File,并确保输出设置为Archiving。然后搜索_fooFunction,会看到如下一些内容:
提醒:在Scheme中一定要选择iOS Device,不要选择模拟器。
- _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}
上面的代码中,涉及到了一些还没有介绍过的指令,不用担心,它们都不复杂。下面我们就分别来看看上面代码中的指令都做了什么操作:
1、这里的指令作用跟之前介绍的
add sp, #12类似——r7和lr被“pushed”到栈中,也就是说栈指针(sp)被减去8(因为r7和lr都是4个字节)。需要注意,通过这条指令,栈指针被递减,两个值也被存储到栈中!需要存储r7是因为在这个函数中,该寄存器会被覆盖,而之后又需要还原最初的值;而存储lr寄存器中的值是因为在函数结束时,要使用。
注意:lr是寄存器(Link Register, LR——R14寄存器)。
2、这两个指令属于
move(mov)指令集中的一个。有时候你会看到movs,而有时候则会看到mov,或者其它类似的名称。它们都是把一个值装载到寄存器中。你可以把数据从一个寄存器“mov”到另外一个寄存器,例如
mov ro, r1指令,将把r1中的数据装载到r0中,而r1中的数据不会改变。
上面的两行汇编指令中,会将定义在函数中的两个常量装载到r0和r1中。注意,需要将这两个常量装载到r0和r1中,才能够被addFunction正确的使用。
3、 在调用函数的时候,应该将栈指针保存起来,而这里使用r7来保存栈指针(r7是可以用来存储局部变量存储器中的一个)。可能你已经注意到,在该函数中剩下的代码里面并没有再次使用到栈指针或者r7,所以这条指令在这里是多余的——
有时候,即使开启了编译器的优化,但还是不能做到最佳优化。
4、这条指令(
bl)对函数进行调用。请记住被调用函数需要的参数已经存储到相关的寄存器中了(r0和r1)。这条指令的执行一般被当做一个分支(branch)。可以理解为
执行带链接的分支,也就是说,在跳转到分支之前,会将lr(link register)的值设置为当前函数中将要执行的下一条指令,当从分支(被调函数)中返回时,通过lr中的值可以知道当前函数执行到哪里了。
5、当addFunction函数执行完毕,返回后,执行的第一条指令——将addFunction的返回值(存储在r0中)保存起来,以供后续的printf使用。也就是利用
mov将r0中的值存储到r1中。
6、printf的的第一个参数是一个字符串。这里使用了3条指令将指向字符串首地址的指针装载到r0寄存器中。这个字符串存储在二进制文件的
数据段“data segment”中,不过该字符串的准确位置在二进制文件被链接之前是不知道的。
字符串其实是在由main.m文件生成的目标文件(object file)中的数据段里。如果你在汇编代码中搜索L_.str,就能找到这个字符串。这三个指令中的前两个作用是装载这个常量的地址(减去本地标签加4后的地址)。
第三条指令中将程序计数器(pc)的值加到r0中。因此,现在r0已保存着字符串的地址,也不用考虑L_.str在二进制文件中的确切位置。
下面的这个图演示了内存的布局。其中
L_.str – (LPC1_0 + 4)的改变并不用对r0进行改动。
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624101528_1.png)
7、这条指令(
blx)调用printf函数。这跟bl指令有明显的区别。
blx中的x标示交换“exchange”,意思是如果有必要,处理器将对指令集模式进行切换。
现在的ARM处理器有两种模式:
ARM和Thumb。Thumb指令是16位的宽度,而ARM指令是32位的宽度。Thumb指令比较少,不过使用Thumb指令意味着代码容量更小,以及更利于CPU缓存。
因此,使用Thumb尺寸得到的好处就是让你的代码更少。这里可以看到更多的Thumb信息:
Wikipedia。
8、最后一条指令是将在第一条指令中push到栈里面的值pop出来。这里列出来的寄存器将被从栈中pop出来的值填充,然后栈指针递增。回想一下第一条指令是这样的:r7和lr寄存器中的值被push到栈中,但是为什么这里的指令是将栈中的值pop到r7和pc寄存器中,而不是r7和lr寄存器中呢?
相信你还记得之前说过的:lr寄存器保存的是从一个函数返回时,下一条将被执行指令的地址。所以,如果将push到栈中的这个地址pop给pc(program counter),那么将继续从函数被调用的地方执行——这也是从一个被调用函数返回调用函数的常规做法,而不是像调用addFunction一样返回到调用函数fooFunction中。
现在我们来看看ARM中的一些指令的简短总结。ARM中又许多指令,不过下面列出来的指令对于初步了解ARM指令非常重要。现在就通过伪代码(pseudo-code)和相关描述来快速的回顾一下这些指令都做有什么作用:
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 和 r2push到栈中.
pop {r0, r1, r2} => 将3个值从栈中pop出来,并存放到r0, r1 和 r2中.
b _label => pc = _label
bl _label => lr = pc + 4; pc = _label
现在是时候恭喜你了:你能够阅读一些ARM汇编代码了!
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624101746_1.png)
Objective -C 汇编
到现在为止,我们涉及到的函数都是用C语言来写的。Objective-C在C语言的基础上稍微增加了一点复杂度。下面我们就来看看用Objective-C代码编译出来的汇编指令。打开
ViewController.m文件,然后将下面的方法添加到类的实现中:
- - (int)addValue:(int)a toValue:(int)b {
- int c = a + b;
- return c;
- }
同样,通过这样的步骤来查看汇编代码:
Product\Generate Output\Assembly File。记得将output类型设置为Archiving,然后搜索
addValue:toValue: ,你会发现类似如下的汇编代码:
- "-[ViewController addValue:toValue:]":
- adds r0, r3, r2
- bx lr
首先看到的是一个
标签(label)名称——”-[ViewController addValue:toValue:]“,这个名称包含类名和完整的Objective-C方法名称。
把上面的汇编代码与之前的addFunction相关汇编代码进行比较,你会发现这里是将r2和r3进行加法运算,而不是r0与r1相加——这意味着传递给addValue:toValue:方法的参数使用了r2和r3寄存器(没有使用r0和r1),这是为什么呢?
这是因为:在调用Objective-C方法时,除了传递明确指定的参数外,还会在明确参数之前传递两个隐含的参数(implicit parameter)。addValue:toValue:方法跟下面的C函数是等价的:
- int ViewController_addValue_toValue(id self, SEL _cmd, int a, int b) {
- int c = a + b;
- return c;
- }
这就是为什么a和b两个参数分别存储到r2和r3的原因。可能你之前已经听说过前两个参数了(经常使用self吧)。
提醒:self和_cmd占用了r0和r1寄存器。
可能之前你还没有见过
_cmd。其实跟
self一样,在Objective-C函数中,_cmd是可以直接使用的,它存储着当前执行方法的
selector。一般来说,你并不需要使用_cmd(这也可能是为什么你从来没有听说过_cmd的原因)。
为了观察Objective-C方法是如何被调用的,现在将如下方法添加到
ViewController中:
- - (void)foo {
- int add = [self addValue:12 toValue:34];
- NSLog(@"add = %i", add);
- }
重新生成汇编文件,然后寻找“-[ViewController foo]“:,应该能看到类似如下的代码:
- "-[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}
同样,这与之前C语言产生的汇编代码非常相似,我们也来看看具体都做了些什么:
1、将r7和lr push到栈中。
2、利用pc(program counter)将标签L_OBJC_SELECTOR_REFERENCES_对应的值装载到r1寄存器中。这个标签引用到一个selector。实际上selector就是一个字符串,并且存储在数据段中(data segment)。
3、如果在汇编文件中搜索L_OBJC_SELECTOR_REFERENCES_,会看到如下内容:
- L_OBJC_SELECTOR_REFERENCES_:? .long L_OBJC_METH_VAR_NAME_
r1会指向这里(L_OBJC_SELECTOR_REFERENCES_),这个标签包含了另外一个标签:L_OBJC_METH_VAR_NAME_。在文件中查找这个标签(L_OBJC_METH_VAR_NAME_),会找到这样的字符串:addValue:toValue:。
而指令ldr r1, [r1]的作用:对r1中存储的地址进行解引用(dereferencing),然后将得到的值放到r1中。如果用C伪代码看起来应该是这样的:r1 = *r1。仔细想想的话,可能你应该知道r1将会存储着指向字符串addValue:toValue: 的指针。
4、将常量装载到r2和r3中。
5、 将sp保持到r7寄存器中。
6、这是一个分支(branch),以带链接跳转和根据情况切换指令集的模式来调用objc_msgSend方法。这是Objective-C runtime中非常重要的一个方法——它根据传递的参数找到并调用相关的函数。
该方法使用到了4个参数(r0-r3)。因此,在上面的代码中,将selector装载到r1中,另外两个参数(12和34)装载到r2和r3中。注意:在此并没有明确的装载r0,因为r0已经存储着self变量了。
7、调用addValue:toValue:的返回值被存放在r0中。这里的指令将这个结果值保持到r1中。在接下来调用NSLog函数时会用到这个值。
8、将NSLog用到的第一个字符串参数装载到r0中。这跟之前介绍的用C函数里面调用printf一样。
9、这是一个分支(branch),以带链接跳转和根据情况切换指令集的模式来调用NSLog方法。
10、从栈中pop出两个值,并放入r7和pc寄存器中。这跟之前一样,从foo方法中返回。
如上所见,由C和Objective-C代码生成汇编指令,区别不是太大。只不过在Objective-C生成的汇编指令中,会隐示的给方法传递两个参数,以及使用到的selector以字符串的形式存放在数据段中(data segment)。
Obj-C 消息发给了谁
上面我们看到了objc_msgSend方法。可能你在crash log中已经看到过这个方法。该方法是Objective-C runtime中的一个核心方法。runtime包含了内存管理以及类的相关处理。
每次调用Objective-C方法时,都由objc_msgSend方法(这是一个C方法)处理消息的派送(dispatching)。该方法根据传递的消息类型在类的方法列表中查找被调用方法的实现。objc_msgSend方法的签名(signature)看起来是这样的:
- id objc_msgSend(id self, SEL _cmd, ...)
在方法执行期间,第一个参数是self。在方法中写的一些代码,例如self.someProperty,其中self就是来自自objc_msgSend方法中的self参数。
第二个参数很少人会知道,这也是一个被隐藏的参数(hidden parameter)。如果在Objective-C方法中,写这类似这样的代码:NSLog(@”%@”, NSStringFromSelector(_cmd)); ,会看到控制台输出了当前的selector。
剩下的参数一般就是开发者传递给该方法的参数了。所以如果一个方法携带两个参数,例如上面的addValue:toValue:,那么还会携带额外的两个参数。因此,我们也可以用下面的代码来代替通过Objective-C方式的调用:
- - (void)foo {
- int add = (int)objc_msgSend(self, NSSelectorFromString(@"addValue:toValue:", 12, 34);
- NSLog(@"add = %i", add);
- }
注意:虽然objc_msgSend的返回值类型是id,不过在上面的代码中将其转换为int类型了。因为它们 的size是相同的,所以转换为int不会有问题。如果该方法返回的是不同的size,那么实际上是别的函数被调用了,更多内容请看这里:
here
。同样,如果返回的是一个floating指针,那么则是objc_msgSend的另一个变种被调用了,更多内容请看这里:
here
。
当一个Objective-C方法被编译的时候,上面用C写的等效方法签名应该是这样的:
- int ViewController_addValue_toValue(id self, SEL _cmd, int a, int b)
对此为什么会这样,现在应该不会感觉到奇怪——这样的签名是为了与objc_msgSend相匹配!也就是说当objc_msgSend在查找并跳转到对应方法时,所有的这些参数都应该在正确的地方。
你现在可以进行逆向工程了
根据上面对ARM汇编的介绍,你应该可以能够知道为什么有些代码被breaking、crashing或者没有正确的执行。
通过观察相关的汇编代码,可以更加清楚的获知到引起bug的详细步骤。
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624102317_1.png)
有时候,你可能无法查看源代码——例如,你遇到的bug是发生在第三方库或者系统的framework中。此时,通过汇编指令进行分析可以帮助你迅速的找到问题。下面的目录存放着iOS SDK中所有的framework:
<Path_to_Xcode>/Contents/Developer/Platforms/iPhoneOS.platform/Developer/ SDKs/iPhoneOS6.1.sdk/System/Library/Frameworks
我建议使用
HopperApp对这些库进行分析。该软件能够对二进制文件进行反汇编——这样你就可以看库中的内容了——这样做是没有问题的!!!例如,打开UIKit,就可以看到每个方法都做了什么。如下图所示:
![](http://www.cocoachina.com/cms/uploads/allimg/130624/4196_130624102348_1.png)
上图中的汇编代码是
[UINavigationController shouldAutorotateToInterfaceOrientation] 方法相关的。结合之前介绍的ARM汇编知识,相信上面的汇编代码具体做了些什么你应该能看出来。
首先是将一个selector引用装载到ri寄存器中,以供后续调用objc_msgSend使用。然后可以看到,别的寄存器并没有做任何改动,所以我们可以知道传递给objc_msgSend方法的self指针(存储在r0中),跟传递给shouldAutorotateToInterfaceOrientation方法的self是同一个。
同理,我们可以知道被调用方法携带一个参数(代码中有一列是用来显示相关名称的)。由于r2寄存器没有改动过,所以这个参数就是从
shouldAutorotateToInterfaceOrientation方法传入的。
最后,函数调用之后,r0没有改动过,所以被调用函数的返回值就是调用函数的返回值。
这样一来,就可以推断出这个方法的实现应该是这样的了:
- - (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation {
- return [self _doesTopViewControllerSupportInterfaceOrientation:interfaceOrientation];
- }
cool!很容易吧!虽然大多数方法都比上面的这个要复杂,不过你可以根据汇编指令拼凑出一些代码,进而快速的确定这些代码做了些什么。
何去何从
这篇关于iOS汇编的教程向你介绍了一些运行在iOS设备中的ARM汇编指令核心概念。你应该学习到了C和Objective-C相关的一些调用约定。
通过本文介绍的知识,当你的程序在使用系统库crash时,你可以对所有能看到的随机代码进行分析。当然,你也可以通过汇编指令来准确的分析你自己写的方法。
如果你希望更加深入的了解ARM,请看这里:
Raspberry Pi。这里的涉及到的小型设备都拥有ARM处理器,跟iOS设备非常相似,同时也有许多教程可以教你如何对这些设备进行编程。
另外,NEON也值得去学习了解。这是另外扩展的一套指令集,自iPhone 3GS以来设备中的所有处理器,都支持NEON指令集。该指令集提供了SIMD(单指令,多数据——Single Instruction Multiple Data)指令,对数据的处理非常高效,例如,图片的处理。如果你需要对数据进行高效的处理,那么最好学习一下如何直接写NEON指令,并结合使用内联汇编(inline assembly)。这个指令集非常的先进!