Android內核,Android 8.0 VDEX機制簡介

 2023-11-30 阅读 24 评论 0

摘要:背景 Android 8.0在odex的基礎上又引入了vdex機制,目的是為了降低dex2oat時間。 因為當系統ota后,用戶自己安裝的應用是不會發生任何變化的,但framework代碼已經發生了變化, 所以就需要重新對這些應用也做dex2oat,所以如果有vdex的話ÿ

背景

Android 8.0在odex的基礎上又引入了vdex機制,目的是為了降低dex2oat時間。
因為當系統ota后,用戶自己安裝的應用是不會發生任何變化的,但framework代碼已經發生了變化,
所以就需要重新對這些應用也做dex2oat,所以如果有vdex的話,就可以省去重新校驗apk里dex文件合法性的過程,節省一部分時間,所以本文分析下vdex的實現機制。

文件格式用途
.dex存儲java字節碼
.odex/.oatoptimized dex,ELF格式
.vdexverified dex,包含 raw dex +(quicken info)
.artimage文件,存儲熱點方法string, method, types等

首個VDEX實現相關commit:

1
2
3
commit 7b49e6cade09bc65b3b5f22d45fc9d0a7184e4f2
Author: David Brazdil <dbrazdil@google.com>
Date:   Thu Sep 1 11:06:18 2016 +0100

主要目的:降低dex2oat執行耗時
1、當系統OTA后,對于安裝在data分區下的app,因為它們的apk都沒有任何變化,那么在首次開機時,對于這部分app如果有vdex文件存在的話,執行dexopt時就可以直接跳過verify流程,進入compile dex的流程,從而加速首次開機速度;
2、當app的jit profile信息變化時,background dexopt會在后臺重新做dex2oat,因為有了vdex,這個時候也可以直接跳過

原理

應用首次安裝時,抽取出其中的dex文件,校驗成功后,存儲到一個獨立的文件中,后面由于jit profile改變,或OTA等原因,而重新進行dexopt時,可以跳過dex文件校驗流程

具體實現:

dex2oat關鍵路徑: main() → setup() → compileApp()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// art/dex2oat/dexoat.cc
int main(int argc, char** argv) {return static_cast<int>(art::Dex2oat(argc, argv));
}static dex2oat::ReturnCode Dex2oat(int argc, char** argv) {dex2oat->ParseArgs(argc, argv);dex2oat::ReturnCode setup_code = dex2oat->Setup();dex2oat::ReturnCode result = CompileApp(*dex2oat);return result;
}dex2oat::ReturnCode Setup() {// DoDexLayoutOptimizations()內部會校驗dex文件,所以當vdex存在或者DoDexLayoutOptimizations()也ok時// 后面就不用再次去檢查dex文件的合法性了const bool verify = !DoDexLayoutOptimizations() && (input_vdex_file_ == nullptr);if (!oat_writers_[i]->WriteAndOpenDexFiles(kIsVdexEnabled ? vdex_files_[i].get() : oat_files_[i].get(),rodata_.back(),instruction_set_,instruction_set_features_.get(),key_value_store_.get(),verify,update_input_vdex_,&opened_dex_files_map,&opened_dex_files)) {return dex2oat::ReturnCode::kOther;}
}

由于VDEX優化的是verfiy流程,即校驗dex文件的合法性,所以下面主要看一下 WriteAndOpenDexFiles() 函數的實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// art/compiler/oat_writer.cc
bool OatWriter::WriteAndOpenDexFiles(File* vdex_file,OutputStream* oat_rodata,InstructionSet instruction_set,const InstructionSetFeatures* instruction_set_features,SafeMap<std::string, std::string>* key_value_store,bool verify,bool update_input_vdex,/*out*/ std::unique_ptr<MemMap>* opened_dex_files_map,/*out*/ std::vector<std::unique_ptr<const DexFile>>* opened_dex_files) {...if (kIsVdexEnabled) {std::unique_ptr<BufferedOutputStream> vdex_out(MakeUnique<BufferedOutputStream>(MakeUnique<FileOutputStream>(vdex_file)));// 啟用了vdex,把dex文件寫到vdex文件里,并map到內存里,用于后續compile流程if (!WriteDexFiles(vdex_out.get(), vdex_file, update_input_vdex) ||!OpenDexFiles(vdex_file, verify, &dex_files_map, &dex_files)) {return false;}} else {// 沒有啟用vdex,就把dex文件寫到oat(odex)文件里if (!WriteDexFiles(oat_rodata, vdex_file, update_input_vdex) ||!OpenDexFiles(vdex_file, verify, &dex_files_map, &dex_files)) {return false;}}
...
}

第1步:打開apk文件,把其中的classes[N].dex文件合并寫入到vdex文件
WriteDexFiles流程:此處略過,見后文vdex的生成過程

第2步:map vdex文件到內存,用于后續對其中的dex文件做優化,并寫入到odex文件。
如果vdex是剛剛創建的,則還需要vdex里的dex文件部分是否合法,否則便可跳過校驗流程,直接進行后續的代碼優化流程
OpenDexFiles流程:OatWriter::OpenDexFiles -> DexFile::Open -> DexFile::OpenCommon -> DexFileVerifier::Verify

DexFileVerifier::Verify的主流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// art/runtime/dex_file_verifier.cc
bool DexFileVerifier::Verify() {// Check the header.if (!CheckHeader()) {return false;}// Check the map section.if (!CheckMap()) {return false;}// Check structure within remaining sections.if (!CheckIntraSection()) {return false;}// Check references from one section to another.return CheckInterSection();
}

結合一下dex文件的格式:
dex_format.pnguploading.4e448015.gif轉存失敗重新上傳取消
字段含義對照表:

名稱格式說明
headerheader_item標頭
string_idsstring_id_item[]字符串標識符列表。這些是此文件使用的所有字符串的標識符,用于內部命名(例如類型描述符)或用作代碼引用的常量對象。此列表必須使用 UTF-16 代碼點值按字符串內容進行排序(不采用語言區域敏感方式),且不得包含任何重復條目。
type_idstype_id_item[]類型標識符列表。這些是此文件引用的所有類型(類、數組或原始類型)的標識符(無論文件中是否已定義)。此列表必須按?string_id?索引進行排序,且不得包含任何重復條目。
proto_idsproto_id_item[]方法原型標識符列表。這些是此文件引用的所有原型的標識符。此列表必須按返回類型(按?type_id?索引排序)主要順序進行排序,然后按參數列表(按?type_id?索引排序的各個參數,采用字典排序方法)進行排序。該列表不得包含任何重復條目。
field_idsfield_id_item[]字段標識符列表。這些是此文件引用的所有字段的標識符(無論文件中是否已定義)。此列表必須進行排序,其中定義類型(按?type_id?索引排序)是主要順序,字段名稱(按?string_id?索引排序)是中間順序,而類型(按?type_id?索引排序)是次要順序。該列表不得包含任何重復條目。
method_idsmethod_id_item[]方法標識符列表。這些是此文件引用的所有方法的標識符(無論文件中是否已定義)。此列表必須進行排序,其中定義類型(按?type_id?索引排序)是主要順序,方法名稱(按?string_id?索引排序)是中間順序,而方法原型(按?proto_id?索引排序)是次要順序。該列表不得包含任何重復條目。
class_defsclass_def_item[]類定義列表。這些類必須進行排序,以便所指定類的超類和已實現的接口比引用類更早出現在該列表中。此外,對于在該列表中多次出現的同名類,其定義是無效的。
call_site_idscall_site_id_item[]調用站點標識符列表。這些是此文件引用的所有調用站點的標識符(無論文件中是否已定義)。此列表必須按?call_site_off?的升序進行排序。
method_handlesmethod_handle_item[]方法句柄列表。此文件引用的所有方法句柄的列表(無論文件中是否已定義)。此列表未進行排序,而且可能包含將在邏輯上對應于不同方法句柄實例的重復項。
dataubyte[]數據區,包含上面所列表格的所有支持數據。不同的項有不同的對齊要求;如有必要,則在每個項之前插入填充字節,以實現所需的對齊效果。
link_dataubyte[]靜態鏈接文件中使用的數據。本文檔尚未指定本區段中數據的格式。此區段在未鏈接文件中為空,而運行時實現可能會在適當的情況下使用這些數據。

CheckHeader():校驗dex頭信息(見上面dex文件結構圖左半部分)

校驗實際文件大小與dex頭文件里保存的file_size是否一致;
計算dex文件的checksum,然后與dex頭里保存的checksum對比,檢查是否一致;
比較dex文件的字節序(endian_tag)與當前機器的字節序是否一致,目前dex使用的都為小端序(little-endian);
檢測dex文件頭里保存的header_size是否存在異常;

檢查link_off, link_size, map_off, map_size…..等字段的是否合法,例如string_id偏移是否超出了dex文件本身長度,string_id偏移是否正確的對齊(align)了,定義的類/方法數是否超過了65536個

CheckMap():

當dex文件被映射(mmap)到內存后,map區域(mmap起始地址+ map_offset)就可以被看做一個list(MapList),這個list的每一個item(MapItem)分別表示了header, stringId, typeId, methodId, fieldId, …code等不同類別
MapList的結構如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct MapList {uint32_t size_; // 數組list_實際的長度 MapItem list_[1];
};
struct MapItem {// item類型,e.g. kDexTypeStringIdItem, kDexTypeStringDataItem... // 詳細見 DexFile::MapItemType(位于art/runtime/dex_file.h)uint16_t type_;uint16_t unused_; // 暫時僅用作4字節對齊用uint32_t size_;   // 該類item的數量// mmap后,該類型的第一個item相對與dex文件起始地址的偏移(即處于dex文件的第$offset_個字節)uint32_t offset_;
};

CheckIntraSection():

intra翻譯:同一事物內部各部分之間
checkPadding(): 前后兩個不同類型的item之前的是否正確的對齊了,例如用于對齊的每個字節的值必須為0,兩個mapitem的地址(MapItem.offset_)不能有相交(overlap)
若MapItem.type_是header, string_id, typeId, proto_id, field_id, method_id, class_def, callSizeId, methodHandle, typeList, anotationSet等類型的話:
1) 檢查其MapItem.offset_/MapItem.size_是否與dex頭部存放的相應xxx_off_/xxx_size_(若存在)相等,
2) 根據MapItem.offset_和MapItem.size_,就能夠遍歷該類型mapItem指向的所有item,檢查這些StringId/TypeId/ProtoId…里存放的offset是否合法(不超出系統可用內存大小,不超出dex文件末尾地址)
3) 若MapItem.type_是classData, codeData, stringData等 這幾個類型的話:
類似第2步,檢查各mapItem本身的地址偏移是否合法,實際指向的item數組里各項的offset是否合法;
對于classData類別,校驗每項item對應的class下所有的靜態/非靜態字段與方法的合法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Iterate and decode class_data_item
class ClassDataItemIterator {
private:// 為了壓縮dex文件大小,這部分數據是用的leb12b編碼,所以在運行時遍歷靜態/非靜態屬性和方法的時候,再依次decode出來struct ClassDataHeader {uint32_t static_fields_size_;  // the number of static fieldsuint32_t instance_fields_size_;  // the number of instance fieldsuint32_t direct_methods_size_;  // the number of direct methods,如私有方法,靜態方法,構造函數uint32_t virtual_methods_size_;  // the number of virtual methods} header_;// A decoded version of the field of a class_data_itemstruct ClassDataField {uint32_t field_idx_delta_;  // 表示在filed_ids鏈表中的索引uint32_t access_flags_;  // access flags for the field};ClassDataField field_;// A decoded version of the method of a class_data_itemstruct ClassDataMethod {uint32_t method_idx_delta_;  // 表示在method_ids鏈表中的索引uint32_t access_flags_;uint32_t code_off_;};ClassDataMethod method_;
};
struct FieldId {dex::TypeIndex class_idx_;   // index into type_ids_ array for defining classdex::TypeIndex type_idx_;    // index into type_ids_ array for field typedex::StringIndex name_idx_;  // index into string_ids_ array for field name
};
struct MethodId {dex::TypeIndex class_idx_;   // index into type_ids_ array for defining classuint16_t proto_idx_;         // index into proto_ids_ array for method prototypedex::StringIndex name_idx_;  // index into string_ids_ array for method name
};

?

校驗該類所有filed/method的**meber index**是有序,是否有越界;
根據filed/method的**member index**從filed_ids/method_ids鏈表找到對應的項,再利用filedId/methodId里的class_idx遍歷class_def 鏈表,找出對應的該field/method所屬class_def項,?并校驗該類下所有field/method的均含有一致的class_idx;
校驗該類下的所有field/method的訪問標志符的合法性: private/protected/public 僅能有其一,是否定義了未知的flag, 一個方法不能 同時聲明virtual和direct, 虛方法不能同時聲明有final/private/static/...;
校驗name_idx是否合法:能否根據其解析出方法的字符串;
檢查method的code_off_合法性:native/abstract 方法不能有code,所以其code_off_須為0;相反的,其他類型的方法則其code_off_不能為0,等等;
...

4) codeItem: TL;DR
5) stringData: TL;DR

CheckInterSection():

*inter:不同事物之間*

在checkIntraSection()函數執行過程中,每校驗完一個位于data section的item時,如stringData, classData, codeItem…等時,就會把他們的地址偏移與類型給記錄到一個hash表offset_to_type_map_中
1) 遍歷string_id區所有項:

1
2
3
struct StringId {uint32_t string_data_off_;  // offset in bytes from the base address
};

?

檢查string_id里存儲的string_data_off_是否合法,即能否在offset_to_type_map_查找到匹配的記錄,所有的string_id項已按照其指向的字符串字典序排好序了

2) 校驗type_id區所有項:

1
2
3
struct TypeId {dex::StringIndex descriptor_idx_;  // index into string_ids
};

?

檢查type_id項里存儲的descriptor_idx_是否合法,即能否在根據該idx解析成該type的字符串表示,并檢查所有項是否已按照descriptor_idx_從小到大排序
3) 校驗proto_id區所有項:

1
2
3
4
5
6
struct ProtoId {dex::StringIndex shorty_idx_;     // index into string_ids array for shorty descriptordex::TypeIndex return_type_idx_;  // index into type_ids array for return typeuint16_t pad_;                    // padding = 0uint32_t parameters_off_;         // file offset to type_list for parameter types
};

?

檢查能否根據 parameters_off_ 在offset_to_type_map_里查到相應記錄, 并檢查所有的參數是否合法
能否根據short_idx_正確decode出字符串表示
檢查return_type_idx_是否越界(65535),能否由其最終正確解析出它的字符串表示形式
檢查所有proto_id項是否已根據return_type_idx_, 參數的type_id排序

4) 校驗所有的filed_id:TL; DR
5) 校驗所有的method_id:TL; DR

vdex的生成過程

vdex文件結構

| magic: 4字節 | version: 4字節 | dex[0] | dex[1] | … | dex[N] |
定義見:art/runtime/vdex_file.h

生成過程

OatWriter:WriteAndOpenDexFiles()
-> WriteDexFiles() -> WriteDexFile() -> ZipEntry.extraTo(vdex_file)
-> WriteVdexHeader()

這一步主流程很簡單,就是把apk里的dex文件抽取抽取然后寫入到vdex文件里,最后寫入vdex版本號和校驗和,比較麻煩的是寫入文件的時候要做4字節對齊,所以每次寫入一個dex文件都要先設置好文件偏移,這樣后面map這個vdex里時候能夠提高效率。

主流app的dex文件校驗耗時:

App名稱版本dex文件總大小/包體積verify耗時dex2oat總耗時(4線程)
微信6.5.2341.8Mb / 54Mb2.681秒13.376秒
支付寶10.1.8.11230540.8Mb / 58Mb2.768秒13.764秒
淘寶7.2.311.6Mb / 76Mb692毫秒3.641秒
今日頭條極速版6.1.94.8Mb / 2.9Mb242毫秒1.899s

新版VDEX結構

到Android 9.0上,vdex結構變的豐富了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// VDEX files contain extracted DEX files. The VdexFile class maps the file to
// memory and provides tools for accessing its individual sections.
//
// File format:
//   VdexFile::VerifierDepsHeader    fixed-length header
//      Dex file checksums
//
//   Optionally:
//      VdexFile::DexSectionHeader   fixed-length header
//
//      quicken_table_off[0]  offset into QuickeningInfo section for offset table for DEX[0].
//      DEX[0]                array of the input DEX files, the bytecode may have been quickened.
//      quicken_table_off[1]
//      DEX[1]
//      ...
//      DEX[D]
//
//   VerifierDeps
//      uint8[D][]                 verification dependencies
//
//   Optionally:
//      QuickeningInfo
//        uint8[D][]                  quickening data
//        uint32[D][]                 quickening data offset tables

?

增加了VerifierDeps和QuickeningInfo,其中VerifierDeps是用于快速校驗dex里method合法性的,它是在第一次dex2oat的時候生成,
后面再做dex2oat的時候可以根據這個信息,就不用挨個分析字節碼了(虛擬機總是向前兼容的),只要確認依賴的類存在就ok了,詳細可以看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
commit ca3c8c33501bf199d6fd0a5db30a27d8e010cb23
Author: David Brazdil <dbrazdil@google.com>
Date:   Tue Sep 6 14:04:48 2016 +0100Collect verifier dependenciesMethodVerifier tests whether a DEX method is valid w.r.t. the classesin class path. Since the APK does not change across OTA updates, itis not necessary to analyze the bytecode again with MethodVerifier,as long as its dependencies on the class path (which may have changed)are satisfied.This patch introduces VerifierDeps, a class path dependency collector,and adds hooks into MethodVerifier where classes/methods/fields areresolved and where assignability of types is tested.

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/4/185568.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息