因为匹夫本人是作游戏开发工做的,因此平时也会加一些玩家的群。而一些困扰玩家的问题,一样也困扰着咱们这些手机游戏开发者。这不最近匹夫看本身加的一些群,经常会有人问为啥这个游戏一更新就要从新下载,而不能游戏内更新呢?做为游戏开发者,或者说Unity3D程序猿,咱们都清楚Unity3D不支持热更新,甚至于在IOS平台上生成新的代码都会致使游戏报错崩溃(匹夫之因此在此处强调生成新的代码这几个字,就是提醒各位不要混淆Reflection.Emit和反射)。但咱们是否和普通的玩家同样,看到的仅仅是“不能”的现象,而不了解“不能”背后的缘由呢?那今天小匹夫就抛砖引玉,写写本身对这个问题的想法~~聊聊究竟是谁偷了玩家的热更新。html
不知道各位看官中的U3D程序猿在开发IOS版本的时候是否也曾经碰到过这样的报错:ios
ExecutionEngineException: Attempting to JIT compile method 'XXXX' while running with --aot-only.vim
这个报错的意思很明确,说的也很具体,翻译成中文的大意就是在使用--aot-only这个选项的前提下,又试图去使用JIT编译器编译XXX方法。安全
那么不知道是否会有看官以为这个问题兴许是程序跑在IOS平台上时,不当心犯了IOS的忌讳,使用了JIT(假设此时咱们还不知道为什么使用JIT是IOS的忌讳)去动态编译代码致使的IOS的报错呢?函数
答案是否认的。优化
又或者更进一步,看到“ExecutionEngineException”,彷佛和IOS平台的异常没什么太大的关联,那就把责任定位在Unity3D的引擎上好了。必定是游戏引擎此时不支持JIT编译了。spa
也不全对,不过离真相很近了。翻译
各位想一想,能涉及到编译的被怀疑的对象还能有谁呢?指针
好了,不卖关子了。这个异常实际上是Mono的异常。换言之,Unity3D使用了Mono来编译,因此Unity3D的嫌疑被排除。而IOS并无由于生成或者运行动态生成的代码而报错,换言之这个异常发生在触发IOS异常以前,因此说Mono在IOS平台上进行JIT编译以前就先一步让程序崩溃了。code
说到这里,就绕不过Mono是如何编译代码这个话题了。若是咱们去Mono的托管页面看它的源码,就能够简单对它的目录结构作一个简单的分析,匹夫就简单总结一下Mono编译部分的目录结构:
docs | 关于mono运行时的文档,在这里你能够看到例如编译的说明文档,还有小匹夫很看重的Mono运行时的API列表 | ||
data | 一些Mono运行时的配置文件 | ||
mono | Mono运行时的核心,也是本文关于Mono部分的焦点,简单介绍一下它的几个比较重要的子目录 | ||
metadata | 实现了处理metadata的逻辑 | ||
mini | JIT编译器(重点) | ||
dis | 可执行CIL代码的反编译器 | ||
cil | CIL指令的XML配置,在这里你能够看到CIL的指令都是什么 | ||
arch | 不一样体系结构的特定部分。 | ||
mcs | C#源码编译器(C#---->CIL) | ||
mcs | |||
mcs | 源码编译器 | ||
jay | 分析程序的生成程序 |
好啦,具体到我们要聊的JIT编译,咱们须要看的就是mono目录下的mini文件夹中的文件了,这个文件夹中的.c文件们实现了JIT编译。
这个目录的结构截个图都截不全,由于文件太多:
不过这里小匹夫想来一个倒叙,也就是先直接定位这个报错“ExecutionEngineException: Attempting to JIT compile method 'XXXX' while running with --aot-only.”的位置,而后再探明它到底是如何被触发的。
这样,咱们就来到了mono的JIT编译器目录mini下的mini.c文件。这里就是JIT的逻辑实现。而那段报错呢?在mini.c文件中是这样处理的:
if (mono_aot_only) { char *fullname = mono_method_full_name (method, TRUE); char *msg = g_strdup_printf ("Attempting to JIT compile method '%s' while running with --aot-only. See http://docs.xamarin.com/ios/about/limitations for more information.\n", fullname); *jit_ex = mono_get_exception_execution_engine (msg); g_free (fullname); g_free (msg); return NULL; }
mono_aot_only?没错,只要咱们设定mono的编译模式为full-aot(好比打IOS安装包的时候),则在运行时试图使用JIT编译时,mono自身的JIT编译器就会禁止这种行为进而报告这个异常。JIT编译的过程根本还没开始,就被本身扼杀了。
那么JIT到底是什么洪水猛兽?为什么IOS这么忌讳它呢?那就不得不聊聊JIT本尊了。
名如其特色,JIT——just in time,即时编译。
什么?这就是匹夫你要告诉你们伙的?这不是人人都知道的嘛?并且网上一搜也全都是JIT=just in time了事。好吧好吧,匹夫知错啦。那就认真的定义一下JIT:
一个程序在它运行的时候建立而且运行了全新的代码,而并不是那些最初做为这个程序的一部分保存在硬盘上的固有的代码。就叫JIT。
几个点:
须要提醒的是第三点,也就是JIT不光是生成新的代码,它还会运行新生成的代码。以后咱们会就这个话题展开。不过在以前匹夫仍是要解释一下,为什么称JIT是美丽的。
举个例子:
好比你某一天忽然穿越成为了一个优秀的学者(好吧好吧,这个貌似不是必需要穿越),如今要去一个语言不通的国家作一系列讲座。面对语言不通的窘境,如何才不出丑呢?
匹夫有三条方案:
看完这三条方案,各位看官心中更喜欢哪一个呢?
匹夫我的的答案是方案3,由于这即是JIT的道。因此说JIT的美丽,就在于即保留了对代码优化的灵活性,也兼具对热点代码进行重复利用的功能。
JIT这么好,那它是如何实现既生成新代码,又能运行新代码的呢?
编译器如何生成代码不少文章都有涉及,匹夫就很少在此着墨了。下面我就着重和各位聊聊,如何运行新生成的代码。
首先咱们要知道生成的所谓机器码究竟是神马东西。一行看上去只是处理几个数字的代码,蕴含着的就是机器码。
unsigned char[] macCode = {0x48, 0x8b, 0x07};
macCode对应的汇编指令就是:
mov (%rdi),%rax
其实能够看出机器码就是比特流,因此将它加载进内存并不困难。而问题是应该如何执行。
好啦。下面咱们就模拟一下执行新生成的机器码的过程。假设JIT已经为咱们编译出了新的机器码,是一个求和函数的机器码:
long add(long num) { return num + 1; } //对应的机器码
0x48, 0x83, 0xc0, 0x01, 0xc3
首先,动态的在内存上建立函数以前,咱们须要在内存上分配空间。具体到模拟动态建立函数,其实就是将对应的机器码映射到内存空间中。这里咱们使用c语言作实验,利用mmap函数来实现这一点。
头文件 | #include <unistd.h> #include <sys/mman.h> |
定义函数 | void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offsize) |
函数说明 | mmap()用来将某个文件内容映射到内存中,对该内存区域的存取便是直接对该文件内容的读写。 |
由于咱们想要把已是比特流的“求和函数”在内存中建立出来,同时还要运行它。因此mmap有几个参数须要注意一下。
表明映射区域的保护方式,有下列组合:
PROT_EXEC 映射区域可被执行;
PROT_READ 映射区域可被读取;
PROT_WRITE 映射区域可被写入;
#include<stdio.h>
#include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> //分配内存 void* create_space(size_t size) { void* ptr = mmap(0, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0); return ptr; }
这样咱们就得到了一块分配给咱们存放代码的空间。下一步就是实现一个方法将机器码,也就是比特流拷贝到分配给咱们的那块空间上去。使用memcpy便可。
//在内存中建立函数 void copy_code_2_space(unsigned char* m) { unsigned char macCode[] = { 0x48, 0x83, 0xc0, 0x01, c3 }; memcpy(m, macCode, sizeof(macCode)); }
而后咱们在写一个main函数来处理整个逻辑:
#include<stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> //分配内存 void* create_space(size_t size) { void* ptr = mmap(0, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON, -1, 0); return ptr; } //在内存中建立函数 void copy_code_2_space(unsigned char* addr) { unsigned char macCode[] = { 0x48, 0x83, 0xc0, 0x01, 0xc3 }; memcpy(addr, macCode, sizeof(macCode)); } //main 声明一个函数指针TestFun用来指向咱们的求和函数在内存中的地址 int main(int argc, char** argv) { const size_t SIZE = 1024; typedef long (*TestFun)(long); void* addr = create_space(SIZE); copy_code_2_space(addr); TestFun test = addr; int result = test(1); printf("result = %d\n", result); return 0; }
编译而且运行看一下结果:
//编译 gcc testFun.c //运行 ./a.out 1
OK,到此为止,一切都很顺利。这个例子模拟了动态代码在内存上的生成,和以后的运行。彷佛没有什么问题呀?可不知道各位是否忽略了一个前提?那就是咱们为这块区域设置的保护模式但是:可读,可写,可执行的啊!若是没有内存可读写可执行的权限,咱们的实验还能成功吗?
让咱们把create_space函数中的“可执行”PROT_EXEC权限去掉,看看结果会是怎样的一番景象。
修改代码,同时将刚才生成的可执行文件a.out删除从新生成运行。
rm a.out vim testFun.c gcc testFun.c ./a.out 1
结果。。。报错了!
因此,IOS并不是把JIT禁止了。或者换个句式讲,IOS封了内存(或者堆)的可执行权限,至关于变相的封锁了JIT这种编译方式。缘由呢?且听下回分解~~~~~谁偷了个人热更新?IOS和安全漏洞的赌注
若是各位看官以为文章写得还好,那么就容小匹夫跪求各位给点个“推荐”,谢啦~