誰偷了個人熱更新?Mono,JIT,iOS

前言

因爲匹夫本人是作遊戲開發工做的,因此平時也會加一些玩家的羣。而一些困擾玩家的問題,一樣也困擾着咱們這些手機遊戲開發者。這不最近匹夫看本身加的一些羣,經常會有人問爲啥這個遊戲一更新就要從新下載,而不能遊戲內更新呢?做爲遊戲開發者,或者說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

因何美麗

名如其特色,JIT——just in time,即時編譯。

什麼?這就是匹夫你要告訴你們夥的?這不是人人都知道的嘛?並且網上一搜也全都是JIT=just in time了事。好吧好吧,匹夫知錯啦。那就認真的定義一下JIT:

一個程序在它運行的時候建立而且運行了全新的代碼,而並不是那些最初做爲這個程序的一部分保存在硬盤上的固有的代碼。就叫JIT。

幾個點:

  1. 程序須要運行
  2. 生成的代碼是新的代碼,並不是做爲原始程序的一部分被存在磁盤上的那些代碼
  3. 不光生成代碼,還要運行。

須要提醒的是第三點,也就是JIT不光是生成新的代碼,它還會運行新生成的代碼。以後咱們會就這個話題展開。不過在以前匹夫仍是要解釋一下,爲什麼稱JIT是美麗的。

舉個例子:

好比你某一天忽然穿越成爲了一個優秀的學者(好吧好吧,這個貌似不是必需要穿越),如今要去一個語言不通的國家作一系列講座。面對語言不通的窘境,如何纔不出醜呢?

匹夫有三條方案:

  1. 在家的時候僱人把全部的講稿所有翻譯一遍。這是最省事的作法,但卻缺少靈活性。好比臨時有更好的話題或者點子,也只能恨本身沒有好好學外語了。
  2. 僱一個翻譯和你一塊兒出發,你說啥他就翻譯成啥。這樣就不存在靈活性的問題,由於徹底是同步的。不過缺點一樣明顯,翻譯要翻譯不少話,包括你重複說的話。因此須要的時間要遠遠高於方案1。
  3. 僱一個翻譯和你一塊兒出發,但不是你說啥他就翻譯啥,而是記錄翻譯過的話,遇到曾經翻譯過的就不會再翻譯了。你本身就能夠根據以前的翻譯記錄和別人交流了。

看完這三條方案,各位看官心中更喜歡哪一個呢?

匹夫我的的答案是方案3,由於這即是JIT的道。因此說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和安全漏洞的賭注

 

若是各位看官以爲文章寫得還好,那麼就容小匹夫跪求各位給點個「推薦」,謝啦~

裝模做樣的聲明一下:本博文章若非特殊註明皆爲原創,若需轉載請保留原文連接http://www.cnblogs.com/murongxiaopifu/p/4278947.html )及做者信息慕容小匹夫

相關文章
相關標籤/搜索