函数的调用
首先,给函数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进行改动。
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汇编代码了!
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在查找并跳转到对应方法时,所有的这些参数都应该在正确的地方。
这里可以看到更多关于objc_msgSend相关内容:
文章1
,
文章2
。
你现在可以进行逆向工程了
根据上面对ARM汇编的介绍,你应该可以能够知道为什么有些代码被breaking、crashing或者没有正确的执行。
通过观察相关的汇编代码,可以更加清楚的获知到引起bug的详细步骤。
有时候,你可能无法查看源代码——例如,你遇到的bug是发生在第三方库或者系统的framework中。此时,通过汇编指令进行分析可以帮助你迅速的找到问题。下面的目录存放着iOS SDK中所有的framework:
/Contents/Developer/Platforms/iPhoneOS.platform/Developer/ SDKs/iPhoneOS6.1.sdk/System/Library/Frameworks
我建议使用
HopperApp对这些库进行分析。该软件能够对二进制文件进行反汇编——这样你就可以看库中的内容了——这样做是没有问题的!!!例如,打开UIKit,就可以看到每个方法都做了什么。如下图所示:
上图中的汇编代码是
[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)。这个指令集非常的先进!