版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)
文檔簡(jiǎn)介
Android運(yùn)行時(shí)ART加載OAT文件的過(guò)程分析在前面一文中,我們介紹了Android運(yùn)行時(shí)ART,它的核心是OAT文件。OAT文件是一種Android私有ELF文件格式,它不僅包含有從DEX文件翻譯而來(lái)的本地機(jī)器指令,還包含有原來(lái)的DEX文件內(nèi)容。這使得我們無(wú)需重新編譯原有的APK就可以讓它正常地在ART里面運(yùn)行,也就是我們不需要改變?cè)瓉?lái)的APK編程接口。本文我們通過(guò)OAT文件的加載過(guò)程分析OAT文件的結(jié)構(gòu),為后面分析ART的工作原理打基礎(chǔ)。由于OAT文件本質(zhì)上是一個(gè)ELF文件,因此在最外層它具有一般ELF文件的結(jié)構(gòu),例如它有標(biāo)準(zhǔn)的ELF文件頭以及通過(guò)段(Section)來(lái)描述文件內(nèi)容。關(guān)于ELF文件的更多知識(shí),可以參考維基百科:。作為Android私有的一種ELF文件,OAT文件包含有兩個(gè)特殊的段oatdata和oatexec,前者包含有用來(lái)生成本地機(jī)器指令的dex文件內(nèi)容,后者包含有生成的本地機(jī)器指令,它們之間的關(guān)系通過(guò)儲(chǔ)存在oatdata段前面的oat頭部描述。此外,在OAT文件的dynamic段,導(dǎo)出了三個(gè)符號(hào)oatdata、oatexec和oatlastword,它們的值就是用來(lái)界定oatdata段和oatexec段的起止位置的。其中,[oatdata,oatexec-1]描述的是oatdata段的起止位置,而[oatexec,oatlastword+3]描述的是oatexec的起止位置。要完全理解OAT的文件格式,除了要理解本文即將要分析的OAT加載過(guò)程之外,還需要掌握接下來(lái)文章分析的類和方法查找過(guò)程。在分析OAT文件的加載過(guò)程之前,我們需要簡(jiǎn)單介紹一下OAT是如何產(chǎn)生的。如前面一文所示,APK在安裝的過(guò)程中,會(huì)通過(guò)dex2oat工具生成一個(gè)OAT文件:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片staticvoidrun_dex2oat(intzip_fd,intoat_fd,constchar*input_file_name,constchar*output_file_name,constchar*dexopt_flags){staticconstchar*DEX2OAT_BIN="/system/bin/dex2oat";staticconstintMAX_INT_LEN=12;//'-'+10dig+'\0'-OR-0x+8digcharzip_fd_arg[strlen("--zip-fd=")+MAX_INT_LEN];charzip_location_arg[strlen("--zip-location=")+PKG_PATH_MAX];charoat_fd_arg[strlen("--oat-fd=")+MAX_INT_LEN];charoat_location_arg[strlen("--oat-name=")+PKG_PATH_MAX];sprintf(zip_fd_arg,"--zip-fd=%d",zip_fd);sprintf(zip_location_arg,"--zip-location=%s",input_file_name);sprintf(oat_fd_arg,"--oat-fd=%d",oat_fd);sprintf(oat_location_arg,"--oat-location=%s",output_file_name);ALOGV("Running%sin=%sout=%s\n",DEX2OAT_BIN,input_file_name,output_file_name);execl(DEX2OAT_BIN,DEX2OAT_BIN,zip_fd_arg,zip_location_arg,oat_fd_arg,oat_location_arg,(char*)NULL);ALOGE("execl(%s)failed:%s\n",DEX2OAT_BIN,strerror(errno));}這個(gè)函數(shù)定義在文件frameworks/native/cmds/installd/commands.c中。其中,參數(shù)zip_fd和oat_fd都是打開(kāi)文件描述符,指向的分別是正在安裝的APK文件和要生成的OAT文件。OAT文件的生成過(guò)程主要就是涉及到將包含在APK里面的classes.dex文件的DEX字節(jié)碼翻譯成本地機(jī)器指令。這相當(dāng)于是編寫(xiě)一個(gè)輸入文件為DEX、輸出文件為OAT的編譯器。這個(gè)編譯器是基于LLVM編譯框架開(kāi)發(fā)的。編譯器的工作原理比較高大上,所幸的是它不會(huì)影響到我們接下來(lái)的分析,因此我們就略過(guò)DEX字節(jié)碼翻譯成本地機(jī)器指令的過(guò)程,假設(shè)它很愉快地完成了。APK安裝過(guò)程中生成的OAT文件的輸入只有一個(gè)DEX文件,也就是來(lái)自于打包在要安裝的APK文件里面的classes.dex文件。實(shí)際上,一個(gè)OAT文件是可以由若干個(gè)DEX生成的。這意味著在生成的OAT文件的oatdata段中,包含有多個(gè)DEX文件。那么,在什么情況下,會(huì)生成包含多個(gè)DEX文件的OAT文件呢?從前面一文可以知道,當(dāng)我們選擇了ART運(yùn)行時(shí)時(shí),Zygote進(jìn)程在啟動(dòng)的過(guò)程中,會(huì)調(diào)用libart.so里面的函數(shù)JNI_CreateJavaVM來(lái)創(chuàng)建一個(gè)ART虛擬機(jī)。函數(shù)JNI_CreateJavaVM的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片extern"C"jintJNI_CreateJavaVM(JavaVM**p_vm,JNIEnv**p_env,void*vm_args){constJavaVMInitArgs*args=static_cast<JavaVMInitArgs*>(vm_args);if(IsBadJniVersion(args->version)){LOG(ERROR)<<"BadJNIversionpassedtoCreateJavaVM:"<<args->version;returnJNI_EVERSION;}Runtime::Optionsoptions;for(inti=0;i<args->nOptions;++i){JavaVMOption*option=&args->options[i];options.push_back(std::make_pair(std::string(option->optionString),option->extraInfo));}boolignore_unrecognized=args->ignoreUnrecognized;if(!Runtime::Create(options,ignore_unrecognized)){returnJNI_ERR;}Runtime*runtime=Runtime::Current();boolstarted=runtime->Start();if(!started){deleteThread::Current()->GetJniEnv();deleteruntime->GetJavaVM();LOG(WARNING)<<"CreateJavaVMfailed";returnJNI_ERR;}*p_env=Thread::Current()->GetJniEnv();*p_vm=runtime->GetJavaVM();returnJNI_OK;}這個(gè)函數(shù)定義在文件art/runtime/jni_internal.cc中。參數(shù)vm_args用作ART虛擬機(jī)的啟動(dòng)參數(shù),它被轉(zhuǎn)換為一個(gè)JavaVMInitArgs對(duì)象后,再按照Key-Value的組織形式保存一個(gè)Options向量中,并且以該向量作為參數(shù)傳遞給Runtime類的靜態(tài)成員函數(shù)Create。Runtime類的靜態(tài)成員函數(shù)Create負(fù)責(zé)在進(jìn)程中創(chuàng)建一個(gè)ART虛擬機(jī)。創(chuàng)建成功后,就調(diào)用Runtime類的另外一個(gè)靜態(tài)成員函數(shù)Start啟動(dòng)該ART虛擬機(jī)。注意,這個(gè)創(chuàng)建ART虛擬的動(dòng)作只會(huì)在Zygote進(jìn)程中執(zhí)行,SystemServer系統(tǒng)進(jìn)程以及Android應(yīng)用程序進(jìn)程的ART虛擬機(jī)都是直接從Zygote進(jìn)程fork出來(lái)共享的。這與Dalvik虛擬機(jī)的創(chuàng)建方式是完全一樣的。接下來(lái)我們就重點(diǎn)分析Runtime類的靜態(tài)成員函數(shù)Create,它的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片boolRuntime::Create(constOptions&options,boolignore_unrecognized){//TODO:acquireastaticmutexonRuntimetoavoidracing.if(Runtime::instance_!=NULL){returnfalse;}InitLogging(NULL);//CallsLocks::Init()asasideeffect.instance_=newRuntime;if(!instance_->Init(options,ignore_unrecognized)){deleteinstance_;instance_=NULL;returnfalse;}returntrue;}這個(gè)函數(shù)定義在文件art/runtime/runtime.cc中。instance_是Runtime類的靜態(tài)成員變量,它指向進(jìn)程中的一個(gè)Runtime單例。這個(gè)Runtime單例描述的就是當(dāng)前進(jìn)程的ART虛擬機(jī)實(shí)例。函數(shù)首先判斷當(dāng)前進(jìn)程是否已經(jīng)創(chuàng)建有一個(gè)ART虛擬機(jī)實(shí)例了。如果有的話,函數(shù)就立即返回。否則的話,就創(chuàng)建一個(gè)ART虛擬機(jī)實(shí)例,并且保存在Runtime類的靜態(tài)成員變量instance_中,最后調(diào)用Runtime類的成員函數(shù)Init對(duì)該新創(chuàng)建的ART虛擬機(jī)進(jìn)行初始化。Runtime類的成員函數(shù)Init的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片boolRuntime::Init(constOptions&raw_options,boolignore_unrecognized){......UniquePtr<ParsedOptions>options(ParsedOptions::Create(raw_options,ignore_unrecognized));......heap_=newgc::Heap(options->heap_initial_size_,options->heap_growth_limit_,options->heap_min_free_,options->heap_max_free_,options->heap_target_utilization_,options->heap_maximum_size_,options->image_,options->is_concurrent_gc_enabled_,options->parallel_gc_threads_,options->conc_gc_threads_,options->low_memory_mode_,options->long_pause_log_threshold_,options->long_gc_log_threshold_,options->ignore_max_footprint_);......java_vm_=newJavaVMExt(this,options.get());......Thread*self=Thread::Attach("main",false,NULL,false);......if(GetHeap()->GetContinuousSpaces()[0]->IsImageSpace()){class_linker_=ClassLinker::CreateFromImage(intern_table_);}else{......class_linker_=ClassLinker::CreateFromCompiler(*options->boot_class_path_,intern_table_);}......returntrue;}這個(gè)函數(shù)定義在文件art/runtime/runtime.cc中。Runtime類的成員函數(shù)Init首先調(diào)用ParsedOptions類的靜態(tài)成員函數(shù)Create對(duì)ART虛擬機(jī)的啟動(dòng)參數(shù)raw_options進(jìn)行解析。解析后得到的參數(shù)保存在一個(gè)ParsedOptions對(duì)象中,接下來(lái)就根據(jù)這些參數(shù)一個(gè)ART虛擬機(jī)堆。ART虛擬機(jī)堆使用一個(gè)Heap對(duì)象來(lái)描述。創(chuàng)建好ART虛擬機(jī)堆后,Runtime類的成員函數(shù)Init接著又創(chuàng)建了一個(gè)JavaVMExt實(shí)例。這個(gè)JavaVMExt實(shí)例最終是要返回給調(diào)用者的,使得調(diào)用者可以通過(guò)該JavaVMExt實(shí)例來(lái)和ART虛擬機(jī)交互。再接下來(lái),Runtime類的成員函數(shù)Init通過(guò)Thread類的成員函數(shù)Attach將當(dāng)前線程作為ART虛擬機(jī)的主線程,使得當(dāng)前線程可以調(diào)用ART虛擬機(jī)提供的JNI接口。Runtime類的成員函數(shù)GetHeap返回的便是當(dāng)前ART虛擬機(jī)的堆,也就是前面創(chuàng)建的ART虛擬機(jī)堆。通過(guò)調(diào)用Heap類的成員函數(shù)GetContinuousSpaces可以獲得堆里面的連續(xù)空間列表。如果這個(gè)列表的第一個(gè)連續(xù)空間是一個(gè)Image空間,那么就調(diào)用ClassLinker類的靜態(tài)成員函數(shù)CreateFromImage來(lái)創(chuàng)建一個(gè)ClassLinker對(duì)象。否則的話,上述ClassLinker對(duì)象就要通過(guò)ClassLinker類的另外一個(gè)靜態(tài)成員函數(shù)CreateFromCompiler來(lái)創(chuàng)建。創(chuàng)建出來(lái)的ClassLinker對(duì)象是后面ART虛擬機(jī)加載加載Java類時(shí)要用到的。后面我們分析ART虛擬機(jī)的垃圾收集機(jī)制時(shí)會(huì)看到,ART虛擬機(jī)的堆包含有三個(gè)連續(xù)空間和一個(gè)不連續(xù)空間。三個(gè)連續(xù)空間分別用來(lái)分配不同的對(duì)象。當(dāng)?shù)谝粋€(gè)連續(xù)空間不是Image空間時(shí),就表明當(dāng)前進(jìn)程不是Zygote進(jìn)程,而是安裝應(yīng)用程序時(shí)啟動(dòng)的一個(gè)dex2oat進(jìn)程。安裝應(yīng)用程序時(shí)啟動(dòng)的dex2oat進(jìn)程也會(huì)在內(nèi)部創(chuàng)建一個(gè)ART虛擬機(jī),不過(guò)這個(gè)ART虛擬機(jī)是用來(lái)將DEX字節(jié)碼編譯成本地機(jī)器指令的,而Zygote進(jìn)程創(chuàng)建的ART虛擬機(jī)是用來(lái)運(yùn)行應(yīng)用程序的。接下來(lái)我們主要分析ParsedOptions類的靜態(tài)成員函數(shù)Create和ART虛擬機(jī)堆Heap的構(gòu)造函數(shù),以便可以了解ART虛擬機(jī)的啟動(dòng)參數(shù)解析過(guò)程和ART虛擬機(jī)的堆創(chuàng)建過(guò)程。ParsedOptions類的靜態(tài)成員函數(shù)Create的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片Runtime::ParsedOptions*Runtime::ParsedOptions::Create(constOptions&options,boolignore_unrecognized){UniquePtr<ParsedOptions>parsed(newParsedOptions());constchar*boot_class_path_string=getenv("BOOTCLASSPATH");if(boot_class_path_string!=NULL){parsed->boot_class_path_string_=boot_class_path_string;}......parsed->is_compiler_=false;......for(size_ti=0;i<options.size();++i){conststd::stringoption(options[i].first);......if(StartsWith(option,"-Xbootclasspath:")){parsed->boot_class_path_string_=option.substr(strlen("-Xbootclasspath:")).data();}elseif(option=="bootclasspath"){parsed->boot_class_path_=reinterpret_cast<conststd::vector<constDexFile*>*>(options[i].second);}elseif(StartsWith(option,"-Ximage:")){parsed->image_=option.substr(strlen("-Ximage:")).data();}elseif(......){......}elseif(option=="compiler"){parsed->is_compiler_=true;}else{......}}......if(!parsed->is_compiler_&&parsed->image_.empty()){parsed->image_+=GetAndroidRoot();parsed->image_+="/framework/boot.art";}......returnparsed.release();}這個(gè)函數(shù)定義在文件art/runtime/runtime.cc中。ART虛擬機(jī)的啟動(dòng)參數(shù)比較多,這里我們只關(guān)注兩個(gè):-Xbootclasspath、-Ximage和compiler。參數(shù)-Xbootclasspath用來(lái)指定啟動(dòng)類路徑。如果沒(méi)有指定啟動(dòng)類路徑,那么默認(rèn)的啟動(dòng)類路徑就通過(guò)環(huán)境變量BOOTCLASSPATH來(lái)獲得。參數(shù)-Ximage用來(lái)指定ART虛擬機(jī)所使用的Image文件。這個(gè)Image是用來(lái)啟動(dòng)ART虛擬機(jī)的。參數(shù)compiler用來(lái)指定當(dāng)前要?jiǎng)?chuàng)建的ART虛擬機(jī)是用來(lái)將DEX字節(jié)碼編譯成本地機(jī)器指令的。如果沒(méi)有指定Image文件,并且當(dāng)前創(chuàng)建的ART虛擬機(jī)又不是用來(lái)編譯DEX字節(jié)碼的,那么就將該Image文件指定為設(shè)備上的/system/framework/boot.art文件。我們知道,system分區(qū)的文件都是在制作ROM時(shí)打包進(jìn)去的。這樣上述代碼的邏輯就是說(shuō),如果沒(méi)有指定Image文件,那么將system分區(qū)預(yù)先準(zhǔn)備好的framework/boot.art文件作為Image文件來(lái)啟動(dòng)ART虛擬機(jī)。不過(guò),/system/framework/boot.art文件可能是不存在的。在這種情況下,就需要生成一個(gè)新的Image文件。這個(gè)Image文件就是一個(gè)包含了多個(gè)DEX文件的OAT文件。接下來(lái)通過(guò)分析ART虛擬機(jī)堆的創(chuàng)建過(guò)程就會(huì)清楚地看到這一點(diǎn)。Heap類的構(gòu)造函數(shù)的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片Heap::Heap(size_tinitial_size,size_tgrowth_limit,size_tmin_free,size_tmax_free,doubletarget_utilization,size_tcapacity,conststd::string&original_image_file_name,boolconcurrent_gc,size_tparallel_gc_threads,size_tconc_gc_threads,boollow_memory_mode,size_tlong_pause_log_threshold,size_tlong_gc_log_threshold,boolignore_max_footprint):......{......std::stringimage_file_name(original_image_file_name);if(!image_file_name.empty()){space::ImageSpace*image_space=space::ImageSpace::Create(image_file_name);......AddContinuousSpace(image_space);......}......}這個(gè)函數(shù)定義在文件art/runtime/gc/heap.cc中。ART虛擬機(jī)堆的詳細(xì)創(chuàng)建過(guò)程我們?cè)诤竺娣治鯝RT虛擬機(jī)的垃圾收集機(jī)制時(shí)再分析,這里只關(guān)注與Image文件相關(guān)的邏輯。參數(shù)original_image_file_name描述的就是前面提到的Image文件的路徑。如果它的值不等于空的話,那么就以它為參數(shù),調(diào)用ImageSpace類的靜態(tài)成員函數(shù)Create創(chuàng)建一個(gè)Image空間,并且調(diào)用Heap類的成員函數(shù)AddContinuousSpace將該Image空間作為本進(jìn)程的ART虛擬機(jī)堆的第一個(gè)連續(xù)空間。接下來(lái)我們繼續(xù)分析ImageSpace類的靜態(tài)成員函數(shù)Create,它的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片ImageSpace*ImageSpace::Create(conststd::string&original_image_file_name){if(OS::FileExists(original_image_file_name.c_str())){//Ifthe/systemfileexists,itshouldbeup-to-date,don'ttrytogeneratereturnspace::ImageSpace::Init(original_image_file_name,false);}//Ifthe/systemfiledidn'texist,weneedtouseonefromthedalvik-cache.//Ifthecachefileexists,trytoopen,butifitfails,regenerate.//Ifitdoesnotexist,generate.std::stringimage_file_name(GetDalvikCacheFilenameOrDie(original_image_file_name));if(OS::FileExists(image_file_name.c_str())){space::ImageSpace*image_space=space::ImageSpace::Init(image_file_name,true);if(image_space!=NULL){returnimage_space;}}CHECK(GenerateImage(image_file_name))<<"Failedtogenerateimage:"<<image_file_name;returnspace::ImageSpace::Init(image_file_name,true);}這個(gè)函數(shù)定義在文件art/runtime/gc/space/image_space.cc中。ImageSpace類的靜態(tài)成員函數(shù)Create首先是檢查參數(shù)original_image_file_name指定的Image文件是否存在。如果存在的話,就以它為參數(shù),調(diào)用ImageSpace類的另外一個(gè)靜態(tài)成員函數(shù)Init來(lái)創(chuàng)建一個(gè)Image空間。否則的話,再調(diào)用函數(shù)GetDalvikCacheFilenameOrDie根據(jù)參數(shù)original_image_file_name構(gòu)造另外一個(gè)在/data/dalvik-cache目錄下的文件路徑,然后再檢查這個(gè)文件是否存在。如果存在的話,就同樣是以它為參數(shù),調(diào)用ImageSpace類的靜態(tài)成員函數(shù)Init來(lái)創(chuàng)建一個(gè)Image空間。否則的話,就要調(diào)用ImageSpace類的另外一個(gè)靜態(tài)成員函數(shù)GenerateImage來(lái)生成一個(gè)新的Image文件,接著再調(diào)用ImageSpace類的靜態(tài)成員函數(shù)Init來(lái)創(chuàng)建一個(gè)Image空間了。我們假設(shè)參數(shù)original_image_file_name的值等于“/system/framework/boot.art”,那么ImageSpace類的靜態(tài)成員函數(shù)Create的執(zhí)行邏輯實(shí)際上就是:1.檢查文件/system/framework/boot.art是否存在。如果存在,那么就以它為參數(shù),創(chuàng)建一個(gè)Image空間。否則的話,執(zhí)行下一步。2.檢查文件/data/dalvik-cache/system@framework@boot.art@classes.dex是否存在。如果存在,那么就以它為參數(shù),創(chuàng)建一個(gè)Image空間。否則的話,執(zhí)行下一步。3.調(diào)用ImageSpace類的靜態(tài)成員函數(shù)GenerateImage在/data/dalvik-cache目錄下生成一個(gè)system@framework@boot.art@classes.dex,然后再以該文件為參數(shù),創(chuàng)建一個(gè)Image空間。接下來(lái)我們?cè)賮?lái)看看ImageSpace類的靜態(tài)成員函數(shù)GenerateImage的實(shí)現(xiàn),如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片staticboolGenerateImage(conststd::string&image_file_name){conststd::stringboot_class_path_string(Runtime::Current()->GetBootClassPathString());std::vector<std::string>boot_class_path;Split(boot_class_path_string,':',boot_class_path);......std::vector<std::string>arg_vector;std::stringdex2oat(GetAndroidRoot());dex2oat+=(kIsDebugBuild?"/bin/dex2oatd":"/bin/dex2oat");arg_vector.push_back(dex2oat);std::stringimage_option_string("--image=");image_option_string+=image_file_name;arg_vector.push_back(image_option_string);......for(size_ti=0;i<boot_class_path.size();i++){arg_vector.push_back(std::string("--dex-file=")+boot_class_path[i]);}std::stringoat_file_option_string("--oat-file=");oat_file_option_string+=image_file_name;oat_file_option_string.erase(oat_file_option_string.size()-3);oat_file_option_string+="oat";arg_vector.push_back(oat_file_option_string);......if(kIsTargetBuild){arg_vector.push_back("--image-classes-zip=/system/framework/framework.jar");arg_vector.push_back("--image-classes=preloaded-classes");}......//Converttheargstocharpointers.std::vector<char*>char_args;for(std::vector<std::string>::iteratorit=arg_vector.begin();it!=arg_vector.end();++it){char_args.push_back(const_cast<char*>(it->c_str()));}char_args.push_back(NULL);//forkandexecdex2oatpid_tpid=fork();if(pid==0){......execv(dex2oat.c_str(),&char_args[0]);......returnfalse;}else{......//waitfordex2oattofinishintstatus;pid_tgot_pid=TEMP_FAILURE_RETRY(waitpid(pid,&status,0));.......}returntrue;}這個(gè)函數(shù)定義在文件art/runtime/gc/space/image_space.cc中。ImageSpace類的靜態(tài)成員函數(shù)GenerateImage實(shí)際上就調(diào)用dex2oat工具在/data/dalvik-cache目錄下生成兩個(gè)文件:system@framework@boot.art@classes.dex和system@framework@boot.art@classes.oat。system@framework@boot.art@classes.dex是一個(gè)Image文件,通過(guò)--image選項(xiàng)傳遞給dex2oat工具,里面包含了一些需要在Zygote進(jìn)程啟動(dòng)時(shí)預(yù)加載的類。這些需要預(yù)加載的類由/system/framework/framework.jar文件里面的preloaded-classes文件指定。system@framework@boot.art@classes.oat是一個(gè)OAT文件,通過(guò)--oat-file選項(xiàng)傳遞給dex2oat工具,它是由系統(tǒng)啟動(dòng)路徑中指定的jar文件生成的。每一個(gè)jar文件都通過(guò)一個(gè)--dex-file選項(xiàng)傳遞給dex2oat工具。這樣dex2oat工具就可以將它們所包含的classes.dex文件里面的DEX字節(jié)碼翻譯成本地機(jī)器指令。這樣,我們就得到了一個(gè)包含有多個(gè)DEX文件的OAT文件system@framework@boot.art@classes.oat。通過(guò)上面的分析,我們就清楚地看到了ART運(yùn)行時(shí)所需要的OAT文件是如何產(chǎn)生的了。其中,由系統(tǒng)啟動(dòng)類路徑指定的DEX文件生成的OAT文件稱為類型為BOOT的OAT文件,即boot.art文件。有了這個(gè)背景知識(shí)之后,接下來(lái)我們就繼續(xù)分析ART運(yùn)行時(shí)是如何加載OAT文件的。ART運(yùn)行時(shí)提供了一個(gè)OatFile類,通過(guò)調(diào)用它的靜態(tài)成員函數(shù)Open可以在本進(jìn)程中加載OAT文件,它的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片OatFile*OatFile::Open(conststd::string&filename,conststd::string&location,byte*requested_base,boolexecutable){CHECK(!filename.empty())<<location;CheckLocation(filename);#ifdefART_USE_PORTABLE_COMPILER//IfweareusingPORTABLE,usedlopentodealwithrelocations.////WeuseourownELFloaderforQuicktodealwithlegacyappsthat//openagenerateddexfilebyname,removethefile,thenopen//anothergenerateddexfilewiththesamename.http://b/10614658if(executable){returnOpenDlopen(filename,location,requested_base);}#endif//Ifwearen'ttryingtoexecute,wejustuseourownElfFileloaderforacouplereasons:////Ontarget,dlopenmayfailwhencompilingduetoselinuxrestrictionsoninstalld.////Onhost,dlopenisexpectedtofailwhencrosscompiling,sofallbacktoOpenElfFile.//Thiswon'tworkforportableruntimeexecutionbecauseitdoesn'tprocessrelocations.UniquePtr<File>file(OS::OpenFileForReading(filename.c_str()));if(file.get()==NULL){returnNULL;}returnOpenElfFile(file.get(),location,requested_base,false,executable);}這個(gè)函數(shù)定義在文件art/runtime/oat_file.cc中。參數(shù)filename和location實(shí)際上是一樣的,指向要加載的OAT文件。參數(shù)requested_base是一個(gè)可選參數(shù),用來(lái)描述要加載的OAT文件里面的oatdata段要加載在的位置。參數(shù)executable表示要加載的OAT是不是應(yīng)用程序的主執(zhí)行文件。一般來(lái)說(shuō),一個(gè)應(yīng)用程序只有一個(gè)classes.dex文件,這個(gè)classes.dex文件經(jīng)過(guò)編譯后,就得到一個(gè)OAT主執(zhí)行文件。不過(guò),應(yīng)用程序也可以在運(yùn)行時(shí)動(dòng)態(tài)加載DEX文件。這些動(dòng)態(tài)加載的DEX文件在加載的時(shí)候同樣會(huì)被翻譯成OAT再運(yùn)行,它們相應(yīng)打包在應(yīng)用程序的classes.dex文件來(lái)說(shuō),就不屬于主執(zhí)行文件了。OatFile類的靜態(tài)成員函數(shù)Open的實(shí)現(xiàn)雖然只有寥寥幾行代碼,但是要理解它還得先理解宏ART_USE_PORTABLE_COMPILER的的作用。在前面一文中提到,ART運(yùn)行時(shí)利用LLVM編譯框架來(lái)將DEX字節(jié)碼翻譯成本地機(jī)器指令,其中要通過(guò)一個(gè)稱為Backend的模塊來(lái)生成本地機(jī)器指令。這些生成的機(jī)器指令就保存在ELF文件格式的OAT文件的oatexec段中。ART運(yùn)行時(shí)會(huì)為每一個(gè)類方法都生成一系列的本地機(jī)器指令。這些本地機(jī)器指令不是孤立存在的,因?yàn)樗鼈兛赡苄枰渌暮瘮?shù)來(lái)完成自己的功能。例如,它們可能需要調(diào)用ART運(yùn)行時(shí)的堆管理系統(tǒng)提供的接口來(lái)為對(duì)象分配內(nèi)存空間。這樣就會(huì)涉及到一個(gè)模塊依賴性問(wèn)題,就好像我們?cè)诰帉?xiě)程序時(shí),需要依賴C庫(kù)提供的接口一樣。這要求Backend為類方法生成本地機(jī)器指令時(shí),要處理調(diào)用其它模塊提供的函數(shù)的問(wèn)題。ART運(yùn)行時(shí)支持兩種類型的Backend:Portable和Quick。Portable類型的Backend通過(guò)集成在LLVM編譯框架里面的一個(gè)稱為MCLinker的鏈接器來(lái)生成本地機(jī)器指令。關(guān)于MCLinker的更多知識(shí),可以參考。簡(jiǎn)單來(lái)說(shuō),假設(shè)我們有一個(gè)模塊A,它依賴于模塊B、C和D,那么在為模塊A生成本地機(jī)器指令時(shí),指出它依賴于模塊B、C和D就行了。在生成的OAT文件中會(huì)記錄好這些依賴關(guān)系,這是ELF文件格式本來(lái)就支持的特性。這些OAT文件要通過(guò)系統(tǒng)的動(dòng)態(tài)鏈接器提供的dlopen函數(shù)來(lái)加載。函數(shù)dlopen在加載OAT文件的時(shí)候,會(huì)通過(guò)重定位技術(shù)來(lái)處理好它與其它模塊的依賴關(guān)系,使得它能夠調(diào)用其它模塊提供的接口。這個(gè)實(shí)際上就通用的編譯器、靜態(tài)連接器以及動(dòng)態(tài)鏈接器合作在一起干的事情,MCLinker扮演的就是靜態(tài)鏈接器的角色。既然是通用的技術(shù),因?yàn)榫头Q能產(chǎn)生這種OAT文件的Backend為Portable類型的。另一方面,Quick類型的Backend生成的本地機(jī)器指令用另外一種方式來(lái)處理依賴模塊之間的依賴關(guān)系。簡(jiǎn)單來(lái)說(shuō),就是ART運(yùn)行時(shí)會(huì)在每一個(gè)線程的TLS(線程本地區(qū)域)提供一個(gè)函數(shù)表。有了這個(gè)函數(shù)表之后,Quick類型的Backend生成的本地機(jī)器指令就可以通過(guò)它來(lái)調(diào)用其它模塊的函數(shù)。也就是說(shuō),Quick類型的Backend生成的本地機(jī)器指令要依賴于ART運(yùn)行時(shí)提供的函數(shù)表。這使得Quick類型的Backend生成的OAT文件在加載時(shí)不需要再處理模式之間的依賴關(guān)系。再通俗一點(diǎn)說(shuō)的就是Quick類型的Backend生成的OAT文件在加載時(shí)不需要重定位,因此就不需要通過(guò)系統(tǒng)的動(dòng)態(tài)鏈接器提供的dlopen函數(shù)來(lái)加載。由于省去重定位這個(gè)操作,Quick類型的Backend生成的OAT文件在加載時(shí)就會(huì)更快,這也是稱為Quick的緣由。關(guān)于ART運(yùn)行時(shí)類型為Portable和Quick兩種類型的Backend,我們就暫時(shí)講解到這里,后面分析ART運(yùn)行時(shí)執(zhí)行類方法的時(shí)候,我們?cè)僭敿?xì)分析。現(xiàn)在我們需要知道的就是,如果在編譯ART運(yùn)行時(shí)時(shí),定義了宏ART_USE_PORTABLE_COMPILER,那么就表示要使用Portable類型的Backend來(lái)生成OAT文件,否則就使用Quick類型的Backend來(lái)生成OAT文件。默認(rèn)情況下,使用的是Quick類型的Backend。接下就可以很好地理解OatFile類的靜態(tài)成員函數(shù)Open的實(shí)現(xiàn)了:1.如果編譯時(shí)指定了ART_USE_PORTABLE_COMPILER宏,并且參數(shù)executable為true,那么就通過(guò)OatFile類的靜態(tài)成員函數(shù)OpenDlopen來(lái)加載指定的OAT文件。OatFile類的靜態(tài)成員函數(shù)OpenDlopen直接通過(guò)動(dòng)態(tài)鏈接器提供的dlopen函數(shù)來(lái)加載OAT文件。2.其余情況下,通過(guò)OatFile類的靜態(tài)成員函數(shù)OpenElfFile來(lái)手動(dòng)加載指定的OAT文件。這種方式是按照ELF文件格式來(lái)解析要加載的OAT文件的,并且根據(jù)解析獲得的信息將OAT里面相應(yīng)的段加載到內(nèi)存中來(lái)。接下來(lái)我們就分別看看OatFile類的靜態(tài)成員函數(shù)OpenDlopen和OpenElfFile的實(shí)現(xiàn),以便可以對(duì)OAT文件有更清楚的認(rèn)識(shí)。OatFile類的靜態(tài)成員函數(shù)OpenDlopen的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片OatFile*OatFile::OpenDlopen(conststd::string&elf_filename,conststd::string&location,byte*requested_base){UniquePtr<OatFile>oat_file(newOatFile(location));boolsuccess=oat_file->Dlopen(elf_filename,requested_base);if(!success){returnNULL;}returnoat_file.release();}這個(gè)函數(shù)定義在文件art/runtime/oat_file.cc中。OatFile類的靜態(tài)成員函數(shù)OpenDlopen首先是創(chuàng)建一個(gè)OatFile對(duì)象,接著再調(diào)用該OatFile對(duì)象的成員函數(shù)Dlopen加載參數(shù)elf_filename指定的OAT文件。OatFile類的成員函數(shù)Dlopen的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片boolOatFile::Dlopen(conststd::string&elf_filename,byte*requested_base){char*absolute_path=realpath(elf_filename.c_str(),NULL);......dlopen_handle_=dlopen(absolute_path,RTLD_NOW);......begin_=reinterpret_cast<byte*>(dlsym(dlopen_handle_,"oatdata"));......if(requested_base!=NULL&&begin_!=requested_base){......returnfalse;}end_=reinterpret_cast<byte*>(dlsym(dlopen_handle_,"oatlastword"));......//Readjusttobenon-inclusiveupperbound.end_+=sizeof(uint32_t);returnSetup();}這個(gè)函數(shù)定義在文件art/runtime/oat_file.cc中。OatFile類的成員函數(shù)Dlopen首先是通過(guò)動(dòng)態(tài)鏈接器提供的dlopen函數(shù)將參數(shù)elf_filename指定的OAT文件加載到內(nèi)存中來(lái),接著同樣是通過(guò)動(dòng)態(tài)鏈接器提供的dlsym函數(shù)從加載進(jìn)來(lái)的OAT文件獲得兩個(gè)導(dǎo)出符號(hào)oatdata和oatlastword的地址,分別保存在當(dāng)前正在處理的OatFile對(duì)象的成員變量begin_和end_中。根據(jù)圖1所示,符號(hào)oatdata的地址即為OAT文件里面的oatdata段加載到內(nèi)存中的開(kāi)始地址,而符號(hào)oatlastword的地址即為OAT文件里面的oatexec加載到內(nèi)存中的結(jié)束地址。符號(hào)oatlastword本身也是屬于oatexec段的,它自己占用了一個(gè)地址,也就是sizeof(uint32_t)個(gè)字節(jié),于是將前面得到的end_值加上sizeof(uint32_t),得到的才是oatexec段的結(jié)束地址。實(shí)際上,上面得到的begin_值指向的是加載內(nèi)存中的oatdata段的頭部,即OAT頭。這個(gè)OAT頭描述了OAT文件所包含的DEX文件的信息,以及定義在這些DEX文件里面的類方法所對(duì)應(yīng)的本地機(jī)器指令在內(nèi)存的位置。另外,上面得到的end_是用來(lái)在解析OAT頭時(shí)驗(yàn)證數(shù)據(jù)的正確性的。此外,如果參數(shù)requested_base的值不等于0,那么就要求oatdata段必須要加載到requested_base指定的位置去,也就是上面得到的begin_值與requested_base值相等,否則的話就會(huì)出錯(cuò)返回。最后,OatFile類的成員函數(shù)Dlopen通過(guò)調(diào)用另外一個(gè)成員函數(shù)Setup來(lái)解析已經(jīng)加載內(nèi)存中的oatdata段,以獲得ART運(yùn)行時(shí)所需要的更多信息。我們分析完成OatFile類的靜態(tài)成員函數(shù)OpenElfFile之后,再來(lái)看OatFile類的成員函數(shù)Setup的實(shí)現(xiàn)。OatFile類的靜態(tài)成員函數(shù)OpenElfFile的實(shí)現(xiàn)如下所示:[cpp]viewplaincopyOatFile*OatFile::OpenElfFile(File*file,conststd::string&location,byte*requested_base,boolwritable,boolexecutable){UniquePtr<OatFile>oat_file(newOatFile(location));boolsuccess=oat_file->ElfFileOpen(file,requested_base,writable,executable);if(!success){returnNULL;}returnoat_file.release();}這個(gè)函數(shù)定義在文件art/runtime/oat_file.cc中。OatFile類的靜態(tài)成員函數(shù)OpenElfFile創(chuàng)建了一個(gè)OatFile對(duì)象后,就調(diào)用它的成員函數(shù)ElfFileOpen來(lái)執(zhí)行加載OAT文件的工作,它的實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片boolOatFile::ElfFileOpen(File*file,byte*requested_base,boolwritable,boolexecutable){elf_file_.reset(ElfFile::Oen(file,writable,true));......boolloaded=elf_file_->Load(executable);......begin_=elf_file_->FindDynamicSymbolAddress("oatdata");......if(requested_base!=NULL&&begin_!=requested_base){......returnfalse;}end_=elf_file_->FindDynamicSymbolAddress("oatlastword");......//Readjusttobenon-inclusiveupperbound.end_+=sizeof(uint32_t);returnSetup();}這個(gè)函數(shù)定義在文件art/runtime/oat_file.cc中。OatFile類的靜態(tài)成員函數(shù)OpenElfFile的實(shí)現(xiàn)與前面分析的成員函數(shù)Dlopen是很類似的,唯一不同的是前者通過(guò)ElfFile類來(lái)手動(dòng)加載參數(shù)file指定的OAT文件,實(shí)際上就是按照ELF文件格式來(lái)解析參數(shù)file指定的OAT文件,并且將文件里面的oatdata段和oatexec段加載到內(nèi)存中來(lái)。我們可以將ElfFile類看作是ART運(yùn)行時(shí)自己實(shí)現(xiàn)的OAT文件動(dòng)態(tài)鏈接器。一旦參數(shù)file指定的OAT文件指定的文件加載完成之后,我們同樣是通過(guò)兩個(gè)導(dǎo)出符號(hào)oatdata和oatlastword來(lái)獲得oatdata段和oatexec段的起止位置。同樣,如果參數(shù)requested_base的值不等于0,那么就要求oatdata段必須要加載到requested_base指定的位置去。將參數(shù)file指定的OAT文件加載到內(nèi)存之后,OatFile類的靜態(tài)成員函數(shù)OpenElfFile最后也是調(diào)用OatFile類的成員函數(shù)Setup來(lái)解析其中的oatdata段。OatFile類的成員函數(shù)Setup定義在文件art/runtime/oat_file.cc中,我們分三部分來(lái)閱讀,以便可以更好地理解OAT文件的格式。OatFile類的成員函數(shù)Setup的第一部分實(shí)現(xiàn)如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片boolOatFile::Setup(){if(!GetOatHeader().IsValid()){LOG(WARNING)<<"Invalidoatmagicfor"<<GetLocation();returnfalse;}constbyte*oat=Begin();oat+=sizeof(OatHeader);if(oat>End()){LOG(ERROR)<<"Inoatfile"<<GetLocation()<<"foundtruncatedOatHeader";returnfalse;}我們先來(lái)看OatFile類的三個(gè)成員函數(shù)GetOatHeader、Begin和End的實(shí)現(xiàn),如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片constOatHeader&OatFile::GetOatHeader()const{return*reinterpret_cast<constOatHeader*>(Begin());}constbyte*OatFile::Begin()const{CHECK(begin_!=NULL);returnbegin_;}constbyte*OatFile::End()const{CHECK(end_!=NULL);returnend_;}這三個(gè)函數(shù)主要是涉及到了OatFile類的兩個(gè)成員變量begin_和end_,它們分別是OAT文件里面的oatdata段開(kāi)始地址和oatexec段的結(jié)束地址。通過(guò)OatFile類的成員函數(shù)GetOatHeader可以清楚地看到,OAT文件里面的oatdata段的開(kāi)始儲(chǔ)存著一個(gè)OAT頭,這個(gè)OAT頭通過(guò)類OatHeader描述,定義在文件art/runtime/oat.h中,如下所示:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片classPACKED(4)OatHeader{public:......private:uint8_tmagic_[4];uint8_tversion_[4];uint32_tadler32_checksum_;InstructionSetinstruction_set_;uint32_tdex_file_count_;uint32_texecutable_offset_;uint32_tinterpreter_to_interpreter_bridge_offset_;uint32_tinterpreter_to_compiled_code_bridge_offset_;uint32_tjni_dlsym_lookup_offset_;uint32_tportable_resolution_trampoline_offset_;uint32_tportable_to_interpreter_bridge_offset_;uint32_tquick_resolution_trampoline_offset_;uint32_tquick_to_interpreter_bridge_offset_;uint32_timage_file_location_oat_checksum_;uint32_timage_file_location_oat_data_begin_;uint32_timage_file_location_size_;uint8_timage_file_location_data_[0];//notevariablewidthdataatend......};類OatHeader的各個(gè)成員變量的含義如下所示:magic:標(biāo)志OAT文件的一個(gè)魔數(shù),等于‘oat\n’。version:OAT文件版本號(hào),目前的值等于‘007、0’。adler32_checksum_:OAT頭部檢驗(yàn)和。instruction_set_:本地機(jī)指令集,有四種取值,分別為kArm(1)、kThumb2(2)、kX86(3)和kMips(4)。dex_file_count_:OAT文件包含的DEX文件個(gè)數(shù)。executable_offset_:oatexec段開(kāi)始位置與oatdata段開(kāi)始位置的偏移值。interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_:ART運(yùn)行時(shí)在啟動(dòng)的時(shí)候,可以通過(guò)-Xint選項(xiàng)指定所有類的方法都是解釋執(zhí)行的,這與傳統(tǒng)的虛擬機(jī)使用解釋器來(lái)執(zhí)行類方法差不多。同時(shí),有些類方法可能沒(méi)有被翻譯成本地機(jī)器指令,這時(shí)候也要求對(duì)它們進(jìn)行解釋執(zhí)行。這意味著解釋執(zhí)行的類方法在執(zhí)行的過(guò)程中,可能會(huì)調(diào)用到另外一個(gè)也是解釋執(zhí)行的類方法,也可能調(diào)用到另外一個(gè)按本地機(jī)器指令執(zhí)行的類方法中。OAT文件在內(nèi)部提供有兩段trampoline代碼,分別用來(lái)從解釋器調(diào)用另外一個(gè)也是通過(guò)解釋器來(lái)執(zhí)行的類方法和從解釋器調(diào)用另外一個(gè)按照本地機(jī)器執(zhí)行的類方法。這兩段trampoline代碼的偏移位置就保存在成員變量interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_。jni_dlsym_lookup_offset_:類方法在執(zhí)行的過(guò)程中,如果要調(diào)用另外一個(gè)方法是一個(gè)JNI函數(shù),那么就要通過(guò)存在放置jni_dlsym_lookup_offset_的一段trampoline代碼來(lái)調(diào)用。portable_resolution_trampoline_offset_和quick_resolution_trampoline_offset_:用來(lái)在運(yùn)行時(shí)解析還未鏈接的類方法的兩段trampoline代碼。其中,portable_resolution_trampoline_offset_指向的trampoline代碼用于Portable類型的Backend生成的本地機(jī)器指令,而quick_resolution_trampoline_offset_用于Quick類型的Backend生成的本地機(jī)器指令。portable_to_interpreter_bridge_offset_和quick_to_interpreter_bridge_offset_:與interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_的作用剛好相反,用來(lái)在按照本地機(jī)器指令執(zhí)行的類方法中調(diào)用解釋執(zhí)行的類方法的兩段trampoline代碼。其中,portable_to_interpreter_bridge_offset_用于Portable類型的Backend生成的本地機(jī)器指令,而quick_to_interpreter_bridge_offset_用于Quick類型的Backend生成的本地機(jī)器指令。由于每一個(gè)應(yīng)用程序都會(huì)依賴于boot.art文件,因此為了節(jié)省由打包在應(yīng)用程序里面的classes.dex生成的OAT文件的體積,上述interpreter_to_interpreter_bridge_offset_、interpreter_to_compiled_code_bridge_offset_、jni_dlsym_lookup_offset_、portable_resolution_trampoline_offset_、portable_to_interpreter_bridge_offset_、quick_resolution_trampoline_offset_和quick_to_interpreter_bridge_offset_七個(gè)成員變量指向的trampoline代碼段只存在于boot.art文件中。換句話說(shuō),在由打包在應(yīng)用程序里面的classes.dex生成的OAT文件的oatdata段頭部中,上述七個(gè)成員變量的值均等于0。image_file_location_data_:用來(lái)創(chuàng)建Image空間的文件的路徑的在內(nèi)存中的地址。image_file_location_size_:用來(lái)創(chuàng)建Image空間的文件的路徑的大小。image_file_location_oat_data_begin_:用來(lái)創(chuàng)建Image空間的OAT文件的oatdata段在內(nèi)存的位置。image_file_location_oat_checksum_:用來(lái)創(chuàng)建Image空間的OAT文件的檢驗(yàn)和。上述四個(gè)成員變量記錄了一個(gè)OAT文件所依賴的用來(lái)創(chuàng)建Image空間文件以及創(chuàng)建這個(gè)Image空間文件所使用的OAT文件的相關(guān)信息。通過(guò)OatFile類的成員函數(shù)Setup的第一部分代碼的分析,我們就知道了,OAT文件的oatdata段在最開(kāi)始保存著一個(gè)OAT頭,如圖2所示:我們接著再看OatFile類的成員函數(shù)Setup的第二部分代碼:[cpp]viewplaincopy在CODE上查看代碼片派生到我的代碼片oat+=GetOatHeader().GetImageFileLocationSize();if(oat>End()){LOG(ERROR)<<"Inoatfile"<<GetLocation()<<"foundtruncatedimagefilelocation:"<<reinterpret_cast<constvoid*>(Begin())<<"+"<<sizeof(OatHeader)<<"+"<<GetOatHeader().GetImageFileLocationSize()<<"<="<<reinterpret_cast<constvoid*>(End());returnfalse;}調(diào)用Oat
溫馨提示
- 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 2024年度陜西省公共營(yíng)養(yǎng)師之四級(jí)營(yíng)養(yǎng)師通關(guān)提分題庫(kù)(考點(diǎn)梳理)
- 2024年度陜西省公共營(yíng)養(yǎng)師之四級(jí)營(yíng)養(yǎng)師考試題庫(kù)
- 2024年度陜西省公共營(yíng)養(yǎng)師之二級(jí)營(yíng)養(yǎng)師題庫(kù)練習(xí)試卷A卷附答案
- 教育環(huán)境下學(xué)生膳食營(yíng)養(yǎng)規(guī)劃指南
- 2025年度廚房設(shè)備綠色環(huán)保認(rèn)證與推廣合同4篇
- 教育培訓(xùn)項(xiàng)目的師資力量與教學(xué)資源配置
- 二零二五版櫥柜定制與智能家居系統(tǒng)集成安裝合同3篇
- 2025版門窗工程承包合同書(shū)(智能門窗系統(tǒng)維護(hù))4篇
- 二零二五年度企業(yè)合同封面定制服務(wù)合同3篇
- 二零二五年度不銹鋼門窗加工安裝合同3篇
- 拆遷評(píng)估機(jī)構(gòu)選定方案
- 趣味知識(shí)問(wèn)答100道
- 鋼管豎向承載力表
- 2024年新北師大版八年級(jí)上冊(cè)物理全冊(cè)教學(xué)課件(新版教材)
- 人教版數(shù)學(xué)四年級(jí)下冊(cè)核心素養(yǎng)目標(biāo)全冊(cè)教學(xué)設(shè)計(jì)
- JJG 692-2010無(wú)創(chuàng)自動(dòng)測(cè)量血壓計(jì)
- 三年級(jí)下冊(cè)口算天天100題(A4打印版)
- 徐州市2023-2024學(xué)年八年級(jí)上學(xué)期期末地理試卷(含答案解析)
- CSSD職業(yè)暴露與防護(hù)
- 飲料對(duì)人體的危害1
- 移動(dòng)商務(wù)內(nèi)容運(yùn)營(yíng)(吳洪貴)項(xiàng)目三 移動(dòng)商務(wù)運(yùn)營(yíng)內(nèi)容的策劃和生產(chǎn)
評(píng)論
0/150
提交評(píng)論