从C的伪代码到汇编动手实现objcmsgSend.docx

上传人:b****8 文档编号:30019121 上传时间:2023-08-04 格式:DOCX 页数:12 大小:55.87KB
下载 相关 举报
从C的伪代码到汇编动手实现objcmsgSend.docx_第1页
第1页 / 共12页
从C的伪代码到汇编动手实现objcmsgSend.docx_第2页
第2页 / 共12页
从C的伪代码到汇编动手实现objcmsgSend.docx_第3页
第3页 / 共12页
从C的伪代码到汇编动手实现objcmsgSend.docx_第4页
第4页 / 共12页
从C的伪代码到汇编动手实现objcmsgSend.docx_第5页
第5页 / 共12页
点击查看更多>>
下载资源
资源描述

从C的伪代码到汇编动手实现objcmsgSend.docx

《从C的伪代码到汇编动手实现objcmsgSend.docx》由会员分享,可在线阅读,更多相关《从C的伪代码到汇编动手实现objcmsgSend.docx(12页珍藏版)》请在冰豆网上搜索。

从C的伪代码到汇编动手实现objcmsgSend.docx

从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实现。

你会后悔这么做的。

这么做只为了学习目的。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高中教育 > 小学教育

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1