前言

由于匹夫本人是做游戏开发工作的,所以平时也会加一些玩家的群。而一些困扰玩家的问题,同样也困扰着我们这些手机游戏开发者。这不最近匹夫看自己加的一些群,常常会有人问为啥这个游戏一更新就要重新下载,而不能游戏内更新呢?作为游戏开发者,或者说Unity3D程序猿,我们都清楚Unity3D不支持热更新,甚至于在IOS平台上生成新的代码都会导致游戏报错崩溃(匹夫之所以在此处强调生成新的代码这几个字,就是提醒各位不要混淆Reflection.Emit和反射)。但我们是否和普通的玩家一样,看到的仅仅是“不能”的现象,而不了解“不能”背后的原因呢?那今天小匹夫就抛砖引玉,写写自己对这个问题的想法~~聊聊到底是谁偷了玩家的热更新。

从一个常见的报错说起

不知道各位看官中的U3D程序猿在开发IOS版本的时候是否也曾经碰到过这样的报错:

ExecutionEngineException: Attempting to JIT compile method 'XXXX' while running with --aot-only.

这个报错的意思很明确,说的也很具体,翻译成中文的大意就是在使用--aot-only这个选项的前提下,又试图去使用JIT编译器编译XXX方法。

那么不知道是否会有看官觉得这个问题兴许是程序跑在IOS平台上时,不小心犯了IOS的忌讳,使用了JIT(假设此时我们还不知道为何使用JIT是IOS的忌讳)去动态编译代码导致的IOS的报错呢?

答案是否定的。

又或者更进一步,看到“ExecutionEngineException”,似乎和IOS平台的异常没什么太大的关联,那就把责任定位在Unity3D的引擎上好了。一定是游戏引擎此时不支持JIT编译了。

也不全对,不过离真相很近了。

各位想想,能涉及到编译的被怀疑的对象还能有谁呢?

好了,不卖关子了。这个异常其实是Mono的异常。换言之,Unity3D使用了Mono来编译,所以Unity3D的嫌疑被排除。而IOS并没有因为生成或者运行动态生成的代码而报错,换言之这个异常发生在触发IOS异常之前,所以说Mono在IOS平台上进行JIT编译之前就先一步让程序崩溃了。

说到这里,就绕不过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文件中是这样处理的:

  1. if (mono_aot_only) {
  2. char *fullname = mono_method_full_name (method, TRUE);
  3. 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);
  4. *jit_ex = mono_get_exception_execution_engine (msg);
  5. g_free (fullname);
  6. g_free (msg);
  7. return NULL;
  8. }

mono_aot_only?没错,只要我们设定mono的编译模式为full-aot(比如打IOS安装包的时候),则在运行时试图使用JIT编译时,mono自身的JIT编译器就会禁止这种行为进而报告这个异常。JIT编译的过程根本还没开始,就被自己扼杀了。

那么JIT究竟是什么洪水猛兽?为何IOS这么忌讳它呢?那就不得不聊聊JIT本尊了。

美丽的JIT

因何美丽

名如其特点,JIT——just in time,即时编译。

什么?这就是匹夫你要告诉大家伙的?这不是人人都知道的嘛?而且网上一搜也全都是JIT=just in time了事。好吧好吧,匹夫知错啦。那就认真的定义一下JIT:

一个程序在它运行的时候创建并且运行了全新的代码,而并非那些最初作为这个程序的一部分保存在硬盘上的固有的代码。就叫JIT。

几个点:

  1. 程序需要运行
  2. 生成的代码是新的代码,并非作为原始程序的一部分被存在磁盘上的那些代码
  3. 不光生成代码,还要运行。

需要提醒的是第三点,也就是JIT不光是生成新的代码,它还会运行新生成的代码。之后我们会就这个话题展开。不过在之前匹夫还是要解释一下,为何称JIT是美丽的。

举个例子:

比如你某一天突然穿越成为了一个优秀的学者(好吧好吧,这个貌似不是必须要穿越),现在要去一个语言不通的国家做一系列讲座。面对语言不通的窘境,如何才不出丑呢?

匹夫有三条方案:

  1. 在家的时候雇人把所有的讲稿全部翻译一遍。这是最省事的做法,但却缺乏灵活性。比如临时有更好的话题或者点子,也只能恨自己没有好好学外语了。
  2. 雇一个翻译和你一起出发,你说啥他就翻译成啥。这样就不存在灵活性的问题,因为完全是同步的。不过缺点同样明显,翻译要翻译很多话,包括你重复说的话。所以需要的时间要远远高于方案1。
  3. 雇一个翻译和你一起出发,但不是你说啥他就翻译啥,而是记录翻译过的话,遇到曾经翻译过的就不会再翻译了。你自己就可以根据之前的翻译记录和别人交流了。

看完这三条方案,各位看官心中更喜欢哪个呢?

匹夫个人的答案是方案3,因为这便是JIT的道。所以说JIT的美丽,就在于即保留了对代码优化的灵活性,也兼具对热点代码进行重复利用的功能。

模拟一下JIT的过程

JIT这么好,那它是如何实现既生成新代码,又能运行新代码的呢?

编译器如何生成代码很多文章都有涉及,匹夫就不多在此着墨了。下面我就着重和各位聊聊,如何运行新生成的代码。

首先我们要知道生成的所谓机器码到底是神马东西。一行看上去只是处理几个数字的代码,蕴含着的就是机器码。

  1. unsigned char[] macCode = {0x, 0x8b, 0x};

macCode对应的汇编指令就是:

  1. mov (%rdi),%rax

其实可以看出机器码就是比特流,所以将它加载进内存并不困难。而问题是应该如何执行。

好啦。下面我们就模拟一下执行新生成的机器码的过程。假设JIT已经为我们编译出了新的机器码,是一个求和函数的机器码:

  1. long add(long num) {
  2. return num + ;
  3. }
  4.  
  5. //对应的机器码
    0x48, 0x, 0xc0, 0x,
  6. 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  映射区域可被写入;

  1. #include<stdio.h>
    #include <stdlib.h>
  2. #include <string.h>
  3. #include <unistd.h>
  4. #include <sys/mman.h>
  5.  
  6. //分配内存
  7. void* create_space(size_t size) {
  8. void* ptr = mmap(, size,
  9. PROT_READ | PROT_WRITE | PROT_EXEC,
  10. MAP_PRIVATE | MAP_ANON,
  11. -, );
  12. return ptr;
  13. }

这样我们就获得了一块分配给我们存放代码的空间。下一步就是实现一个方法将机器码,也就是比特流拷贝到分配给我们的那块空间上去。使用memcpy即可。

  1. //在内存中创建函数
  2. void copy_code_2_space(unsigned char* m) {
  3. unsigned char macCode[] = {
  4. 0x, 0x, 0xc0, 0x,
  5. c3
  6. };
  7. memcpy(m, macCode, sizeof(macCode));
  8. }

然后我们在写一个main函数来处理整个逻辑:

  1. #include<stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <sys/mman.h>
  6.  
  7. //分配内存
  8. void* create_space(size_t size) {
  9. void* ptr = mmap(, size,
  10. PROT_READ | PROT_WRITE | PROT_EXEC,
  11. MAP_PRIVATE | MAP_ANON,
  12. -, );
  13. return ptr;
  14. }
  15.  
  16. //在内存中创建函数
  17. void copy_code_2_space(unsigned char* addr) {
  18. unsigned char macCode[] = {
  19. 0x, 0x, 0xc0, 0x,
  20. 0xc3
  21. };
  22. memcpy(addr, macCode, sizeof(macCode));
  23. }
  24.  
  25. //main 声明一个函数指针TestFun用来指向我们的求和函数在内存中的地址
  26. int main(int argc, char** argv) {
  27. const size_t SIZE = ;
  28. typedef long (*TestFun)(long);
  29. void* addr = create_space(SIZE);
  30. copy_code_2_space(addr);
  31. TestFun test = addr;
  32. int result = test();
  33. printf("result = %d\n", result);
  34. return ;
  35. }

编译并且运行看一下结果:

  1. //编译
  2. gcc testFun.c
  3. //运行
  4. ./a.out

留给我们的难题

OK,到此为止,一切都很顺利。这个例子模拟了动态代码在内存上的生成,和之后的运行。似乎没有什么问题呀?可不知道各位是否忽略了一个前提?那就是我们为这块区域设置的保护模式可是:可读,可写,可执行的啊!如果没有内存可读写可执行的权限,我们的实验还能成功吗?

让我们把create_space函数中的“可执行”PROT_EXEC权限去掉,看看结果会是怎样的一番景象。

修改代码,同时将刚才生成的可执行文件a.out删除重新生成运行。

  1. rm a.out
  2. vim testFun.c
  3. gcc testFun.c
  4. ./a.out

结果。。。报错了!

小结论

所以,IOS并非把JIT禁止了。或者换个句式讲,IOS封了内存(或者堆)的可执行权限,相当于变相的封锁了JIT这种编译方式。原因呢?且听下回分解~~~~~谁偷了我的热更新?IOS和安全漏洞的赌注

如果各位看官觉得文章写得还好,那么就容小匹夫跪求各位给点个“推荐”,谢啦~

装模作样的声明一下:本博文章若非特殊注明皆为原创,若需转载请保留原文链接http://www.cnblogs.com/murongxiaopifu/p/4278947.html )及作者信息慕容小匹夫

谁偷了我的热更新?Mono,JIT,iOS的更多相关文章

  1. 移动端热更新方案(iOS+Android)

    PPT资源包含iOS+Android 各种方案分析:https://github.com/qiyer/Share/blob/master/%E7%83%AD%E6%9B%B4%E6%96%B0%E5% ...

  2. Unity应用的iOS热更新

    Unity应用的iOS热更新 作者:丁治宇 Unity TechnologiesChina Agenda •  什么是热更新 •  为何要热更新 •  如何在iOS 上对Unity 应用进行热更新 • ...

  3. iOS第三方类库JSPatch(热更新)

    ---------------------------------------------------------------------------------------------------- ...

  4. Unity3D热更新之LuaFramework篇[08]--热更新原理及热更服务器搭建

    前言 前面铺垫了这么久,终于要开始写热更新了. Unity游戏热更新包含两个方面,一个是资源的更新,一个是脚本的更新. 资源更新是Unity本来就支持的,在各大平台也都能用.而脚本的热更新在iOS平台 ...

  5. Unity3D 热更新方案(集合各位专家的汇总)

    http://blog.csdn.net/guofeng526/article/details/52662994 热更新”这个词,在Unity3D的应用下,是有些语义错误的,但是作为大家都熟知的一项技 ...

  6. Unity官方发布热更新方案性能对照

    孙广东  2016.3.11 Unity应用的iOS热更新 作者:丁治宇 Unity TechnologiesChina Agenda •  什么是热更新 •  为何要热更新 •  怎样在iOS 上对 ...

  7. unity热更新方案对比

    Unity应用的iOS热更新 •  什么是热更新 •  为何要热更新 •  怎样在iOS 上对Unity 应用进行热更新 •  支持Unity iOS 热更新的各种Lua 插件的对照 什么是热更新 • ...

  8. Unity3D热更新方案网摘总结

    参考:http://blog.csdn.net/guofeng526/article/details/52662994 http://blog.csdn.net/u010019717/article/ ...

  9. Unity实现c#热更新方案探究(一)

    转载请标明出处:http://www.cnblogs.com/zblade/ 最近研究了一下如何在unity中实现c#的热更新,对于整个DLL热更新的过程和方案有一个初步的了解,这儿就写下来,便于后续 ...

随机推荐

  1. 细说前端自动化打包工具--webpack

    背景 记得2004年的时候,互联网开发就是做网页,那时也没有前端和后端的区分,有时一个网站就是一些纯静态的html,通过链接组织在一起.用过Dreamweaver的都知道,做网页就像用word编辑文档 ...

  2. 使用Zabbix监控Oracle数据库

    Orabbix介绍 监控Oracle数据库我们需要安装第三方提供的Zabbix插件,我们先测试比较有名的Orabbix,http://www.smartmarmot.com/product/orabb ...

  3. SQL Server-聚焦APPLY运算符(二十七)

    前言 其实有些新的特性在SQL Server早就已经出现过,但是若非系统的去学习数据库你会发现在实际项目中别人的SQL其实是比较复杂的,其实利用新的SQL Server语法会更加方便和简洁,从本节开始 ...

  4. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  5. 【热门技术】EventBus 3.0,让事件订阅更简单,从此告别组件消息传递烦恼~

    一.写在前面 还在为时间接收而烦恼吗?还在为各种组件间的消息传递烦恼吗?EventBus 3.0,专注于android的发布.订阅事件总线,让各组件间的消息传递更简单!完美替代Intent,Handl ...

  6. python 入门笔记

    1.pip包安装 pip install *** pip 中http和https代理设置(/etc/profile) 2.强制保存 :w !sudo tee % 3.cffi是python调用C的包 ...

  7. python中IndentationError: expected an indented block错误的解决方法

    IndentationError: expected an indented block 翻译为IndentationError:预期的缩进块 解决方法:有冒号的下一行要缩进,该缩进就缩进

  8. https 安全验证问题

    最近为了满足苹果的 https 要求, 经过努力终于写出了方法 验证 SSL 证书是否满足 ATS 要求 nscurl --ats-diagnostics --verbose https://你的域名 ...

  9. MySQL常用命令

    数据库登陆命令: mysql -uroot -p 2.提示输入密码: 3.登陆成功: 4.数据库修改相关命令: 修改数据库的编码格式: 语法格式为:ALTER {DATABASE|SCHEMA}  [ ...

  10. Linux常用命令

    命令格式与目录处理命令 ls 命令格式与目录处理命令 ls 命令格式:命令 [-选项][参数] 例:ls -la /etc 说明: 1)个别命令使用不遵循格式 2)当有多个选项时,可以写在一起 3)简 ...