不自觉的想起自己从业的这十几年,如白驹过隙。现在谈到上还熟悉的的语言以ASM/C/C++/OC/JS/Lua/Ruby/Shell等为主,其他的基本上都是用时拈来过时忘,语言这种东西变化是在太快了, 不过大体换汤不换药,我感觉近几年来所有的语言隐隐都有一种大统一的走势,一旦有个特性不错,你会在不同的语言中都找到这种技术的影子。所以我对使用哪种语言并不是很执着,不过C/C++是信仰罢了 : )
Lokie工作中大部分用OC和Ruby、Shell之类的东西,前段时间一直想找一款合适的iOS下能用的AOP框架。iOS业内比较被熟知的应该就是Aspect了。但是Aspect性能比较差,Aspect的trampoline函数借助了OC语言的消息转发流程,函数调用使用了NSInvocation,我们知道,这两样都是性能大户。有一份测试数据,基本上NSInvocation的调用效率是普通消息发送效率的100倍左右。事实上,Aspect只能适用于每秒中调用次数不超过1000次的场景。当然还有一些其他的库,虽然性能有所提升,但不支持多线程场景,一旦加锁,性能又有明显的损耗。
找来找去也没有什么趁手的库,于是想了想,自己写一个吧。于是Lokie便诞生了。
Lokie的设计基本原则只有两条,第一高效,第二线程安全。为了满足高效这一设计原则,Lokie一方面采用了高效的C++设计语言,标准使用C++14。C++14因引入了一些非常棒的特性比如MOV语义,完美转发,右值引用,多线程支持等使得与C++98相比,性能有了显著的提升。另一方面我们抛弃了对OC消息转发和NSInvocation的依赖,使用libffi进行核心trampoline函数的设计,从而直接从设计上就砍倒性能大户。此外,对于线程锁的实现也使用了轻量的CAS无锁同步的技术,对于线程同步开销也降低了不少。
通过一些真机的性能数据来看,以iPhone 7P为例, Aspect百万次调用消耗为6s左右,而相同场景Lokie开销仅有0.35s左右, 从测试数据上来看,性能提升还是非常显著的。
我是个急性子,看书的时候也是喜欢先看代码。所以我先帖lokie的开源地址:
https://github.com/alibaba/Lokie
喜欢翻代码的同学可以先去看看。
Lokie的头文件非常简单, 如下所示只有两个方法和一个LokieHookPolicy的枚举。
#import
typedef enum : NSUInteger {
LokieHookPolicyBefore = 1 method_name = sel;
smt->method_types = "";
smt->method_imp = &_objc_msgForward;
_cache_fill (cls, smt, sel);
methodPC = &_objc_msgForward;
}
...
}
消息转发机制这部分动态方法解析,备援接收者,消息重定向应该是很多面试官都喜欢问的环节 : ) ,我想大家肯定是比较熟悉这部分内容,这里就不再赘述了。
trampline函数的实现接下来的内容,我们简单介绍下,从汇编的视角出发,如何实现一个trampline函数,完成c函数级别的函数转发。以x86指令集为例,其他类型原理也相似。
从汇编的角度来看,函数的跳转,最直接的方式就是插入jmp指令。x86指令集中,每条指令都有自己的指令长度,比如说jmp指令, 长度为5,其中包含一个字节的指令码,4个字节的相对偏移量。假定我们手头有两个函数A和B, 如果想让B的调用转发到A上去, 毫无疑问,jmp指令是可以帮上忙的。接着我们要解决的问题是如何计算出这两个函数的相对偏移量。这个问题我们可以这样考虑, 但cpu碰到jmp的时候,它的执行动作为ip = ip + 5 + 相对偏移量。
为了更加直接的解释这个问题,我们看看下面的额汇编函数(不熟悉汇编的同学不用担心, 这个函数没有干任何事情,只是做一个跳转)。
你也可以跟我一起来做,先写一个jump_test.s,定义了一个什么事情都没做的函数。
先看看汇编代码文件:(jump_test.s)翻译成C函数的话,就是void jump_test(){ return ; }。
.global _jump_test
_jump_test:
jmp jlable #!为了测试jmp指令偏移量,人为的给加几个nop
nop
nop
nop
jlable:
rep;ret
接着,我们在创建一个C文件:在这个文件里,我们调用刚才创建的jump_test函数。
#include
extern void jump_test();
int main(){
jump_test();
}
最后就是编译链接了, 我们创建一个build.sh生成可执行文件portal 。
#! /bin/sh
cc -c -o main.o main.c
as -o jump_test.o jump_test.s
cc -o portal main.c jump_test.o
我们使用 lldb 加载调试刚才生成的prtal文件,并把断点打在函数 jump_test 上。
lldb ./portal
b jump_test
r
在我机器上,是如下的跳转地址, 你的地址可能和我的不太一样,不过没关系,这并不影响我们的分析。
Process 22830 launched: './portal' (x86_64)
Process 22830 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100000f9f portal`jump_test
portal`jump_test:
-> 0x100000f9f : jmp 0x100000fa7 ; jlable
0x100000fa4 : nop
0x100000fa5 : nop
0x100000fa6 : nop
演示到这里的时候,我们成功的从汇编的视角,看到了一些我们想要的东西。
首先看看当前的 ip 是 0x100000f9f, 我们汇编中使用的jlable此时已经被计算,变成了新的目标地址(0x100000fa7)。我们知道,新的 ip 是通过当前 ip 加偏移算出来的, jmp的指令长度是5,前面我们已经解释过了。所以我们可以知道下面的关系:
new_ip = old_ip + 5 + offset;
把从 lldb 中获取的地址放进来,就变成了:
0x100000fa7 = 0x100000f9f + 5 + offset ==> offset = 3.
回头看看汇编代码, 我们在代码中使用了三个nop, 每个nop指令为1个字节, 刚好就是跳转到三个nop指令之后。做了个简单的验证之后,我们把这个等式做个变形,于是得到 offset = new_ip - old_ip - 5; 当我们知道 A函数和B函数之后,就很容易算出jmp的操作数是多少了。
讲到这里,函数的跳转思路就非常清晰了,我们想在调用A的时候,实际跳转到B。比如我们有个C api, 我们希望每次调用这个api的时候,实际上跳转到我们自定义的函数里面, 我们需要把这个api的前几个字节修改下,直接jmp到我们自己定义的函数中。前5个字节第一个当然就是jmp的操作码了,后面四个字节是我们计算出的偏移量。
最后给出一个完整的例子。汇编分析以及C代码一并打包放上来。
#include
#include
int new_add(int a, int b){
return a+b;
}
int add(int a, int b){
printf("my_add org is called!\n");
return 0;
}
typedef struct{
uint8_t jmp;
uint32_t off;
} __attribute__((packed)) tramp_line_code;
void dohook(void *src, void *dst){
vm_protect(mach_task_self(), (vm_address_t)src, 5, 0, VM_PROT_ALL);
tramp_line_code jshort;
jshort.jmp = 0xe9;
jshort.off = (uint32_t)(long)dst - (uint32_t)(long)src - 0x5;
memcpy(my_add, (const void*)&jshort, sizeof(tramp_line_code));
vm_protect(mach_task_self(), (vm_address_t)src, 5, 0, VM_PROT_READ|VM_PROT_EXECUTE);
}
int main(){
dohook(add, new_add);
int c = add(10, 20); //! 该函数默认实现是返回 0, hook之后,返回 30
printf("res is %d\n", c);
return 0;
}
编译脚本(系统 macOS):
gcc -o portal ./main.c
执行: ./portal
输出: res is 30
至此, 函数调用已经被成功转发了。
原文链接:https://developer.aliyun.com/article/770518?
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。