从C的伪代码到汇编动手实现objcmsgSend.docx
《从C的伪代码到汇编动手实现objcmsgSend.docx》由会员分享,可在线阅读,更多相关《从C的伪代码到汇编动手实现objcmsgSend.docx(12页珍藏版)》请在冰豆网上搜索。
从C的伪代码到汇编动手实现objcmsgSend
从C的伪代码到汇编-动手实现objc-msgSend
从C的伪代码到汇编,动手实现objc_msgSend
objc_msgSend函数支撑了我们使用Objective-C实现的一切。
GwynneRaskind,FridayQ&A的读者,建议我谈谈objc_msgSend的内部实现。
要理解某件事还有比自己动手实现一次更好的方法吗?
咱们来自己动手实现一个objc_msgSend。
Tramapoline!
Trampopoline!
(蹦床)
当你写了一个发送Objective-C消息的方法:
1.[obj message]
编译器会生成一个objc_msgSend调用:
1.objc_msgSend(obj, @selector(message));
之后objc_msgSend会负责转发这个消息。
它都做了什么?
它会查找合适的函数指针或者IMP,然后调用,最后跳转。
任何传给objc_msgSend的参数,最终都会成为IMP的参数。
IMP的返回值成为了最开始被调用的方法的返回值。
1.
2.Class c = object_getClass(self);
3.IMP imp = class_getMethodImplementation(c, _cmd);
4.return imp(self, _cmd, ...);
5.}
这有点过于简单。
事实上会有一个方法缓存来提升查找速度,像这样:
1.id objc_msgSend(id self, SEL _cmd, ...)
2.{
3.Class c = object_getClass(self);
4.IMP imp = cache_lookup(c, _cmd);
5.if(!
imp)
6.imp = class_getMethodImplementation(c, _cmd);
7.return imp(self, _cmd, ...);
8.}
通常为了速度,cache_lookup使用inline函数实现。
汇编
在Apple版的runtime中,为了最大化速度,整个函数是使用汇编实现的。
在Objective-C中每次发送消息都会调用objc_msgSend,在一个应用中最简单的动作都会有成千或者上百万的消息。
为了让事情更简单,我自己的实现中会尽可能少的使用汇编,使用独立的C函数抽象复杂度。
汇编代码会实现下面的功能:
1.id objc_msgSend(id self, SEL _cmd, ...)
2.{
3.IMP imp = GetImplementation(self, _cmd);
4.imp(self, _cmd, ...);
5.}
6.
7.GetImplementation 可以用更可读的方式工作。
汇编代码需要:
1.把所有潜在的参数存储在安全的地方,确保GetImplementation不会覆盖它们。
2.调用GetImplementation。
3.把返回值保存在某处。
4.恢复所有的参数值。
5.跳转到GetImplementation返回的IMP。
让我们开始吧!
这里我会尝试使用x86-64汇编,这样可以很方便的在Mac上工作。
这些概念也可以应用于i386或者ARM。
这个函数会保存在独立的文件中,叫做msgsend-asm.s。
这个文件可以像源文件那样传递给编译器,然后会被编译并链接到程序中。
第一件事要做的是声明全局的符号(globalsymbol)。
因为一些无聊的历史原因,C函数的globalsymbol会在名字前有个下划线:
1..globl _objc_msgSend
2._objc_msgSend:
编译器会很高兴的链接最近可使用的(nearestavailable)objc_msgSend。
简单的链接这个到一个测试app已经可以让[objmessage]表达式使用我们自己的代码而不是苹果的runtime,这样可以相当方便的测试我们的代码确保它可以工作。
整型数和指针参数会被传入寄存器%rsi,%rdi,%rdx,%rcx,%r8和%r9。
其他类型的参数会被传进栈(stack)中。
这个函数最先做的事情是把这六个寄存器中的值保存在栈中,这样它们可以在之后被恢复:
1.pushq %rsi
2.pushq %rdi
3.pushq %rdx
4.pushq %rcx
5.pushq %r8
6.pushq %r9
除了这些寄存器,寄存器%rax扮演了一个隐藏的参数。
它用于变参的调用,并保存传入的向量寄存器(vectorregisters)的数量,用于被调用的函数可以正确的准备变参列表。
以防目标函数是个变参的方法,我同样也保存了这个寄存器中的值:
1.pushq %rax
为了完整性,用于传入浮点类型参数的寄存器%xmm也应该被保存。
但是,要是我能确保GetImplementation不会传入任何的浮点数,我就可以忽略掉它们,这样我就可以让代码更简洁。
接着,对齐栈。
MacOSX要求一个函数调用栈需要对齐16字节边界。
上面的代码已经是栈对齐的,但是还是需要显式手动处理下,这样可以确保所有都是对齐的,就不用担心动态调用函数时会崩溃。
要对齐栈,在保存%r12的原始值到栈中后,我把当前的栈指针保存到了%r12中。
%r12是随便选的,任何保存的调用者寄存器(caller-savedregister)都可以。
重要的是在调用完GetImplementation后这些值仍然存在。
然后我把栈指针按位与(and)上-0x10,这样可以清除栈底的四位:
1.pushq %r12
2.mov %rsp, %r12
3.andq $-0x10, %rsp
现在栈指针是对齐的了。
这样可以安全的避开上面(above)保存的寄存器,因为栈是向下增长的,这种对齐的方法会让它更向下(moveitfurtherdown)。
是时候该调用GetImplementation了。
它接收两个参数,self和_cmd。
调用习惯是把这两个参数分别保存到%rsi和%rdi中。
然而传入objc_msgSend中时就是那样了,它们没有被移动过,所以不需要改变它们。
所有要做的事情实际上是调用GetImplementation,方法名前面也要有一个下划线:
1.callq _GetImplementation
整型数和指针类型的返回值保存在%rax中,这就是找到返回的IMP的地方。
因为%rax需要被恢复到初始的状态,返回的IMP需要被移动到别的地方。
我随便选了个%r11。
1.mov %rax, %r11
现在是时候该恢复原样了。
首先要恢复之前保存在%r12中的栈指针,然后恢复旧的%r12的值:
1.mov %r12, %rsp
2.popq %r12
然后按压入栈的相反顺序恢复寄存器的值:
1.popq %rax
2.popq %r9
3.popq %r8
4.popq %rcx
5.popq %rdx
6.popq %rdi
7.popq %rsi
现在一切都已经准备好了。
参数寄存器(argumentregisters)都恢复到了之前的样子。
目标函数需要的参数都在合适的位置了。
IMP在寄存器%r11中,现在要做的是跳转到那里:
1.jmp *%r11
就这样!
不需要其他的汇编代码了。
jump把控制权交给了方法实现。
从代码的角度看,就好像发送消息者直接调用的这个方法。
之前的那些迂回的调用方法都消失了。
当方法返回,它会直接放回到objc_msgSend的调用处,不需要其他的操作。
这个方法的返回值可以在合适的地方找到。
非常规的返回值有一些细节需要注意。
比如大的结构体(不能用一个寄存器大小保存的返回值)。
在x86-64,大的结构体使用隐藏的第一个参数返回。
当你像这样调用:
1.NSRect r = SomeFunc(a, b, c);
这个调用会被翻译成这样:
1.NSRect r;
2.SomeFunc(&r, a, b, c);
用于返回值的内存地址被传入到%rdi中。
因为objc_msgSend期望%rdi和%rsi中包含self和_cmd,当一个消息返回大的结构体时不会起作用的。
同样的问题存在于多个不同平台上。
runtime提供了objc_msgSend_stret用于返回结构体,工作原理和objc_msgSend类似,只是知道在%rsi中寻找self和在%rdx中寻找_cmd。
相似的问题发生在一些平台上发送消息(messages)返回浮点类型值。
在这些平台上,runtime提供了objc_msgSend_fpret(在x86-64,objc_msgSend_fpret2用于特别极端的情况)。
方法查找
让我们继续实现GetImplementation。
上面的汇编蹦床意味着这些代码可以用C实现。
记得吗,在真正的runtime中,这些代码都是直接用汇编写的,是为了尽可能的保证最快的速度。
这样不仅可以更好的控制代码,也可以避免重复像上面那样保存并恢复寄存器的代码。
GetImplementation可以简单的调用class_getMethodImplementation实现,混入Objective-Cruntime的实现。
这有点无聊。
真正的objc_msgSend为了最大化速度首先会查找类的方法缓存。
因为GetImplementation想模仿objc_msgSend,所以它也会这么做。
要是缓存中不包含给定的selector入口点(entry),它会继续查找runtime(itfallbacktoqueryingtheruntime)。
我们现在需要的是一些结构体定义。
方法缓存是类(class)结构体中的私有结构体,为了得到它我们需要定义自己的版本。
尽管是私有的,这些结构体的定义还是可以通过苹果的Objective-Cruntime开源实现获得(译注:
首先需要定义一个cacheentry:
1.typedef struct {
2.SEL name;
3.void *unused;
4.IMP imp;
5.} cache_entry;
相当简单。
别问我unused字段是干什么的,我也不知道它为什么在那。
这是cache的全部定义:
1.struct objc_cache {
2.uintptr_t mask;
3.uintptr_t occupied;
4.cache_entry *buckets[1];
5.};
缓存使用hashtable(哈希表)实现。
实现这个表是为了速度的考虑,其他无关的都简化了,所以它有点不一样。
表的大小永远都是2的幂。
表格使用selector做索引,bucket是直接使用selector的值做索引,可能会通过移位去除不相关的低位(lowbits),并与mask执行一个逻辑与(logicaland)。
下面是一些宏,用于给定selector和mask时计算bucket的索引:
1.#ifndef __LP64__
2.# define CACHE_HASH(sel, mask) (((uintptr_t)(sel)>>2) & (mask))
3.#else
4.# define CACHE_HASH(sel, mask) (((unsigned int)((uintptr_t)(sel)>>0)) & (mask))
5.#endif
最后是类的结构体。
这是Class指向的类型:
1.struct class_t {
2.struct class_t *isa;
3.struct class_t *superclass;
4.struct objc_cache *cache;
5.IMP *vtable;
6.};
需要的结构体都已经有了,现在开始实现GetImplementation吧:
1.IMP GetImplementation(id self, SEL _cmd)
2.{
首先要做的是获取对象的类。
真正的objc_msgSend通过类似self->isa的方式获取,但是它会使用官方的API实现:
1.Class c = object_getClass(self);
因为我想访问最原始的形式,我会为指向class_t结构体的指针执行类型转换:
1.struct class_t *classInternals = (struct class_t *)c;
现在该查找IMP了。
首先我们把它初始为NULL。
如果我们在缓存中找到,我们会赋值为它。
如果查找缓存后仍为NULL,我们会回退到速度较慢的方法:
1.IMP imp = NULL;
接着,获取指向cache的指针:
1.struct objc_cache *cache = classInternals->cache;
计算bucket的索引,获取指向buckets数组的指针:
1.uintptr_t index = CACHE_HASH(_cmd, cache->mask);
2.cache_entry **buckets = cache->buckets;
然后,我们使用要找的selector查找缓存。
runtime使用的是线性链(linearchaining),之后只是遍历buckets子集直到找到需要的entry或者NULLentry:
1.for(; buckets[index] !
= NULL; index = (index + 1) & cache->mask)
2.{
3.if(buckets[index]->name == _cmd)
4.{
5.imp = buckets[index]->imp;
6.break;
7.}
8.}
如果没有找到entry,我们会调用runtime使用一种较慢的方法。
在真正的objc_msgSend中,上面的所有代码都是使用汇编实现的,这时候就该离开汇编代码调用runtime自己的方法了。
一旦查找缓存后没有找到需要的entry,期望快速发送消息的希望就要落空了。
这时候获取更快的速度就没那么重要了,因为已经注定会变慢,在一定程度上也极少的需要这么调用。
因为这点,放弃汇编代码转而使用更可维护的C也是可以接受的:
1.if(imp == NULL)
2.imp = class_getMethodImplementation(c, _cmd);
不管怎样,IMP现在已经获取到了。
如果它在缓存中,就会在那里找到它,否则它会通过runtime查找到。
class_getMethodImplementation调用同样会使用缓存,所以下次调用会更快。
剩下的就是返回IMP:
1.return imp;
2.}
测试
为了确保它能工作,我写了一个快速的测试程序:
1.@interface Test :
NSObject
2.- (void)none;
3.- (void)param:
(int)x;
4.- (void)params:
(int)a :
(int)b :
(int)c :
(int)d :
(int)e :
(int)f :
(int)g;
5.- (int)retval;
6.@end
7.@implementation Test
8.- (id)init
9.{
10.fprintf(stderr, "in init method, self is %p\n", self);
11.return self;
12.}
13.- (void)none
14.{
15.fprintf(stderr, "in none method\n");
16.}
17.- (void)param:
(int)x
18.{
19.fprintf(stderr, "got parameter %d\n", x);
20.}
21.- (void)params:
(int)a :
(int)b :
(int)c :
(int)d :
(int)e :
(int)f :
(int)g
22.{
23.fprintf(stderr, "got params %d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);
24.}
25.- (int)retval
26.{
27.fprintf(stderr, "in retval method\n");
28.return 42;
29.}
30.@end
31.int main(int argc, char **argv)
32.{
33.for(int i = 0; i < 20; i++)
34.{
35.Test *t = [[Test alloc] init];
36.[t none];
37.[t param:
9999];
38.[t params:
1 :
2 :
3 :
4 :
5 :
6 :
7];
39.fprintf(stderr, "retval gave us %d\n", [t retval]);
40.NSMutableArray *a = [[NSMutableArray alloc] init];
41.[a addObject:
@1];
42.[a addObject:
@{ @"foo" :
@"bar" }];
43.[a addObject:
@("blah")];
44.a[0] = @2;
45.NSLog(@"%@", a);
46.}
47.}
以防因为一些意外调用的是runtime的实现。
我在GetImplementation中加了一些调试的日志确保它被调用了。
一切都正常,即使是literalsandsubscripting也都调用的是替换的实现。
结论
objc_msgSend的核心部分相当的简单。
但它的实现需要一些汇编代码,这让它比它应该的样子更难理解。
但是为了性能的优化还是得使用一些汇编代码。
但是通过构建了一个简单的汇编蹦床,然后使用C实现了它的逻辑,我们可以看到它是如何工作的,它真的没有什么高深的。
很显然,你不应该在自己的app中使用替换的objc_msgSend实现。
你会后悔这么做的。
这么做只为了学习目的。