Android N混合编译与对热补丁影响深度解析
大約在六月底,Tinker在微信全量上線了一個補丁版本,隨即華為反饋在Android N上微信無法啟動。冷汗冒一地,Android N又搞了什么東東?為什么與instant run保持一致的補丁方式也跪了?talk is cheap,show me the code。趁著臺風妮妲肆虐廣東,終于有時間總結一把。在此非常感謝華為工程師謝小靈與胡海亮的幫助,事實上微信與各大廠商都保持著非常緊密的聯系。
無法啟動的原因
我們遵循從問題出發的思路,針對華為提供的日志,我們看到微信在Android N上啟動時會報IllegalAccessError。可以從/data/user/0/com.tencent.mm/tinker/patch-a002c56d/dex/classes2.dex看到,的確跟補丁是有關系的。
java.lang.IllegalAccessError: Illegal class access: 'com.tencent.mm.ui.conversation.ConversationOverscrollListView' attempting to access 'com.tencent.mm.ui.conversation.ConversationOverscrollListView$c' (declaration of 'com.tencent.mm.ui.conversation.ConversationOverscrollListView' appears in /data/user/0/com.tencent.mm/tinker/patch-a002c56d/dex/classes2.dex)但是在我們手上Android N卻無法復現,同時跟華為的進一步溝通中,他們也明確只有一少部分N的用戶會出現問題。這就很難辦了,但是根據之前在art地址錯亂的經驗(似乎這里我還欠大家一篇分析文章),跟這里似乎有點相似。
但是Tinker已經做了全量替換,所以我懷疑由于Android N的某種機制這里只有部分用了補丁中的類,但是部分類導致使用了原來的dex中的。接下來就跟著我一起去研究Android N在編譯運行究竟做了什么改變吧?
Android N的混合編譯運行模式
網上關于Android N混合編譯的文章并不多,infoq上有一篇翻譯文章:Android N混合使用AOT編譯,解釋和JIT三種運行時。混合編譯運行主要指AOT編譯,解釋執行與JIT編譯,它主要解決的問題有以下幾個:
Android N為了解決這些問題,通過管理解釋,AOT與JIT三種模式,以達到一種運行效率、內存與耗電的折中。簡單來說,在應用運行時分析運行過的代碼以及“熱代碼”,并將配置存儲下來。在設備空閑與充電時,ART僅僅編譯這份配置中的“熱代碼”。我們先來看看Android N上有哪些編譯方法:
Android N的編譯模式
在compiler_filter.h,我們可以看到dex2oat一共有12種編譯模式:
enum Filter { VerifyNone, // Skip verification but mark all classes as verified anyway.kVerifyAtRuntime, // Delay verication to runtime, do not compile anything.kVerifyProfile, // Verify only the classes in the profile, compile only JNI stubs.kInterpretOnly, // Verify everything, compile only JNI stubs.kTime, // Compile methods, but minimize compilation time.kSpaceProfile, // Maximize space savings based on profile.kSpace, // Maximize space savings.kBalanced, // Good performance return on compilation investment.kSpeedProfile, // Maximize runtime performance based on profile.kSpeed, // Maximize runtime performance.kEverythingProfile, // Compile everything capable of being compiled based on profile.kEverything, // Compile everything capable of being compiled. };以上12種編譯模式按照排列次序逐漸增強,那系統默認采用了哪些編譯模式呢?我們可以在在手機上執行getprop | grep pm查看:
pm.dexopt.ab-ota: [speed-profile] pm.dexopt.bg-dexopt: [speed-profile] pm.dexopt.boot: [verify-profile] pm.dexopt.core-app: [speed] pm.dexopt.first-boot: [interpret-only] pm.dexopt.forced-dexopt: [speed] pm.dexopt.install: [interpret-only] pm.dexopt.nsys-library: [speed] pm.dexopt.shared-apk: [speed]其中有幾個我們是特別關心的,
總的來說,程序使用loaddex動態加載的代碼是無法享受混合編譯帶來的好處,我們應當盡量采用ClassN.dex方式來符合Google的規范。這不僅在ota還是混合編譯上,都會帶來很大的提升。
Android N的Profile文件
在講[speed-profile]是怎樣編譯之前,這里先簡單描述一下profile文件。profile相關的核心代碼都在art/runtime/jit中。簡單來說,profile_saver.cc會開啟線程去專門收集已經resolved的類與函數,達到一定條件即會持久化存儲在/data/misc/profiles文件夾中。具體的條件可以在profile\_saver\_options.h中查看,在收集過程會出現類似以下的日志:
tinker.sample.android I/art: Collecting resolved classes tinker.sample.android I/art: Collecting class profile for dex file /data/app/tinker.sample.android-1/base.apk types=2406 class_defs=1719 tinker.sample.android I/art: Dex location /data/app/tinker.sample.android-1/base.apk has 232 / 1719 resolved classesprofile的存儲格式在offline\_profiling\_info.h中定義,我們也可以通過profman命令查看profile文件中的數據,命令如下:
profman --profile-file=/data/misc/profiles/cur/0/tinker.sample.android/primary.prof --dump-only具體輸出如下:
=== profile === ProfileInfo: base.apk methods: 297,302,303,424,427,665,668,669,700,756,757,759,760,761,765,766,768,772,774, classes: 52,124,456,其中base.apk代表dex的位置,這里代表的是ClassN中的第一個dex。其他dex會使用類似base.apk:classes2.dex方式命名。后面的methods與classes代表的是它們在dex格式中的index,只有這些類與方法是我們需要在[speed-profile]模式中需要編譯。
Android N的dex2oat編譯
在這里我們比較關心系統究竟是什么時候會去對應用做類似增量的編譯,還有具體的編譯流程是怎么樣的?
dex2oat編譯的時機
首先我們來看系統在什么時候會對各個應用做這種漸進式編譯呢?手機在充電+空閑+四個小時間隔等多個條件下,通過BackgroundDexOptService.java中的JobSchedule下觸發編譯優化。
new JobInfo.Builder(BACKGROUND_DEXOPT_JOB, sDexoptServiceName).setRequiresDeviceIdle(true).setRequiresCharging(true).setMinimumLatency(minLatency).build();dex2oat編譯的流程
對于[speed-profile]模式,dex2oat編譯命令的核心參數如下:
dex2oat --dex-file=./base.apk --oat-file=./base.odex --compiler-filter=speed-profile --app-image-file=./base.art--profile-file=./primary.prof ...入口文件位于dex2oat.cc中,在這里并不想貼具體的調用函數,簡單的描述一下流程:若dex2oat參數中有輸入profile文件,會讀取profile中的數據。與以往不同的是,這里不僅會根據profile文件來生成base.odex文件,同時還會生成稱為app_image的base.art文件。與boot.art類似,base.art文件主要為了加快應用的對“熱代碼”的加載與緩存。
我們可以通過oatdump命令來看到art文件的內容,具體命令如下:
oatdump --app-image=base.art --app-oat=base.odex --image=/system/framework/boot.art --instruction-set=arm64我們可以dump到art文件中的所有信息,這里我只將它的頭部信息輸出如下:
IMAGE LOCATION: base.art IMAGE BEGIN: 0x77ea1000 IMAGE SIZE: 1597200 IMAGE SECTION SectionObjects: size=2040 range=0-2040 IMAGE SECTION SectionArtFields: size=0 range=2040-2040 IMAGE SECTION SectionArtMethods: size=0 range=2040-2040 IMAGE SECTION SectionRuntimeMethods: size=0 range=2040-2040 IMAGE SECTION SectionIMTConflictTables: size=0 range=2040-2040 IMAGE SECTION SectionDexCacheArrays: size=1591080 range=2040-1593120 IMAGE SECTION SectionInternedStrings: size=4040 range=1593120-1597160 IMAGE SECTION SectionClassTable: size=40 range=1597160-1597200 IMAGE SECTION SectionImageBitmap: size=4096 range=1597440-1601536base.art文件主要記錄已經編譯好的類的具體信息以及函數在oat文件的位置,一個class的輸出格式如下:
0x78c8f768: java.lang.Class "com.tencent.mm.ui.d.a" (StatusInitialized)shadow$_klass_: 0x6fc76488 Class: java.lang.Classshadow$_monitor_: 0 (0x0)accessFlags: 524305 (0x80011)annotationType: null sun.reflect.annotation.AnnotationTypeclassFlags: 0 (0x0)classLoader: 0x787b5140 java.lang.ClassLoaderclassSize: 460 (0x1cc)clinitThreadId: 0 (0x0)componentType: null java.lang.ClasscopiedMethodsOffset: 3 (0x3)dexCache: 0x782290c8 java.lang.DexCachedexCacheStrings: 2036372056 (0x79609258)dexClassDefIndex: 12138 (0x2f6a)dexTypeIndex: 11797 (0x2e15)iFields: 2031076964 (0x790fc664)ifTable: 0x78836500 java.lang.Object[]methods: 2032787876 (0x7929e1a4)name: null java.lang.StringnumReferenceInstanceFields: 4 (0x4)numReferenceStaticFields: 0 (0x0)objectSize: 36 (0x24)primitiveType: 131072 (0x20000)referenceInstanceOffsets: 63 (0x3f)sFields: 0 (0x0)status: 10 (0xa)superClass: 0x78bcc968 Class: com.tencent.mm.pluginsdk.ui.b.bverifyError: null java.lang.ObjectvirtualMethodsOffset: 1 (0x1)vtable: null java.lang.Objectmethod的輸出格式如下:
0x792b639c ArtMethod: void com.tencent.mm.e.a.je.<init>() OAT CODE: 0x471dae14-0x471daece SIZE: Dex Instructions=10 StackMaps=0 AccessFlags=0x90001 0x792b63c0 ArtMethod: void com.tencent.mm.e.a.je.<init>(byte) OAT CODE: 0x471daee4-0x471daf52 SIZE: Dex Instructions=48 StackMaps=0 AccessFlags=0x90002 0x792b63e8 ArtMethod: void com.tencent.mm.e.a.jo.<init>() OAT CODE: 0x463d5f44-0x463d5f50 SIZE: Dex Instructions=10 StackMaps=0 AccessFlags=0x90001那么我們就剩下最后一個問題,app image文件是什么時候被加載,并且為什么它會影響熱補丁的機制?
App image文件的加載
在apk啟動時我們需要加載應用的oat文件以及可能存在的app image文件,它的大致流程如下:
非常簡單的說,app image的作用是記錄已經編譯好的“熱代碼”,并且在啟動時一次性把它們加載到緩存。預先加載代替用時查找以提升應用的性能,到這里我們終于明白為什么base.art會影響熱補丁的機制。
無論是使用插入pathlist還是parent classloader的方式,若補丁修改的class已經存在與app image,它們都是無法通過熱補丁更新的。它們在啟動app時已經加入到PathClassLoader的ClassTable中,系統在查找類時會直接使用base.apk中的class。
instant run為什么沒有影響
對于instant run來說,它的目標是快速debug。從上面的編譯條件看來,它是不太可能可以觸發[speed-profile]編譯的。事實上,它在dex2oat上面傳入了--debugable參數, 不過dex2oat并沒有單獨處理這個參數。感興趣的同學,可以再詳細研究這一塊。
最后我們再來總結一下Android N混合編譯運行的整個流程,它就像一個小型生態系統那樣和諧。
Android N上熱補丁的出路
假設base.art文件在補丁前已經存在,這里存在三種情況:
如何解決這個問題呢?下面根據當時我的一些思路分別說明:
插樁?
當時第一反應想到是通過插樁是否能阻止類被編譯到app image中,從而規避了這個問題。事實上,在生成profile時,使用的是ClassLinker::GetResolvedClasses函數,插樁并沒有任何作用。
我這邊也專門單獨看了插樁后編譯的機器碼,僅僅是通過Trampoline模式跳回虛擬機查找而已。
DEX CODE:...0x0018: 0e00 | return-void0x45f0dda2: f8d9e29c ldr.w lr, [r9, #668] ; pInvokeStaticTrampolineWithAccessCheck...miniloader方案
假設我們實現一個最小化的loader,這部分代碼我們補丁時是不會去改變。然后其他代碼都通過動態方式加載,這套方案的確是可行的,但是并不會被采用,因為它會帶來以下幾個代價:
在微信中,補丁方案的原則應該是不能影響運行時的性能,所以這套方案也是不可取的。
運行時替換PathClassLoader方案
事實上,App image中的class是插入到PathClassloader中的ClassTable中。假設我們完全廢棄掉PathClassloader,而采用一個新建Classloader來加載后續的所有類,即可達到將cache無用化的效果。
需要注意的問題是我們的Application類是一定會通過PathClassloader加載的,所以我們需要將Application類與我們的邏輯解耦,這里方式有兩種:
我想說明的是許多號稱毫無兼容性問題的反射框架,在微信Android 數億用戶面前往往都是經不起考驗的。這也是為什么我們盡管采用增加接入成本方式也不愿意再多的使用反射的原因。總的來說,這種方式不會影響沒有補丁時的性能,但在加載補丁后,由于廢棄了App image帶來一定的性能損耗。具體數據如下:
事實上,在Android N上我們不會出現完整編譯一個應用的base.odex與base.art的情況。base.art的作用是加快類與方法的第一次查找速度,所以在啟動時這個數據是影響最大的。在這種情況,廢棄base.art大約帶來15%左右的性能損耗。在其他情況下,這個數字應該是遠遠小于這個數字。
Tinker的后續計劃
在Android N上,Tinker全量合成方案帶來了一個較為嚴重的問題。即將Android N的混合編譯退化了,因為動態編譯的代碼采用的是[speed]方式完整編譯,它會占用比較多Rom空間。所以未來我們計劃根據平臺區分合成的方式,在Dalvik平臺我們合成一個完整的dex,但在Art平臺只合成需要的類,它的規則如下:
規則看起來很復雜,同一個diff文件,根據不同平臺合成不同文件看起來也很復雜。更困難的是,dex格式是存在大量的互相引用,除了index區域,還有使用絕對地址引用的區域,大量的變長結構,4字節對齊......
所以Tinker最終期望的結構圖應該如下,在art上面僅僅合成mini.dex即可:?
結語
建議大家通過"閱讀全文"查看,以獲得更好的閱讀體驗。盡管當前Tinker還沒有開啟內測,我們會盡力在開源前做的更好。讓Tinker無論在Dalvik還是Art上,都有著最好的表現,同時也懇請大家繼續耐心等候我們。
原文地址: https://github.com/WeMobileDev/article/blob/master/Android_N%E6%B7%B7%E5%90%88%E7%BC%96%E8%AF%91%E4%B8%8E%E5%AF%B9%E7%83%AD%E8%A1%A5%E4%B8%81%E5%BD%B1%E5%93%8D%E8%A7%A3%E6%9E%90.md
總結
以上是生活随笔為你收集整理的Android N混合编译与对热补丁影响深度解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CVE-2015-8966/Androi
- 下一篇: Binder fuzz安全研究