在ART上用YAHFA、Legend以及一个java层实现的Andix:ART深度探索开篇:从Method Hook谈起对系统中不同的类进行hook,发现除了framework层的类(如telephonymanager)和应用中的类有效外,对于java核心库的类(如IOBridge和Class等)的hook都无效,所以我就以telephonymanager和IOBridge这两个类为例,试图从编译解析加载等角度分析这两者的区别以及造成hook结果不同的原因,如果有大牛能指点一二的话,不胜感激。。。

首先对于应用层的类,由于是标准的从apk经过dex2oat生成OAT文件(虽然后缀还是dex),然后加载到系统中进行类解析和方法链接等流程,这种流程在老罗博客和上述三种常见的ART hook上进行了详细分析,而上述三种hook方法正是针对这种流程设计的,所以毫无疑问有效,此处不再赘述。

接下来正式开始,首先看下源码路径

telephonymanager等框架层类位于android源码目录\framework\base路径下,经编译后生成的jar包位于android系统的/system/framework/framework.jar

IOBridge等java核心类位于android源码目录\libcore路径下,经编译后生成的jar包位于android系统的/system/framework/core.jar或者/system/framework/core-libart.jar

从这个角度看两者也没太大区别,都是从源码生成了jar包,位于了同一个路径下的不同jar包,但是这个jar包是怎么被加载到内存中去的呢?由于ART系统上执行APK时都被从DEX编译转换成了OAT文件,然后进行加载,所以不禁会问系统的JAR包被如何处理成OAT的呢?

这里就要引出boot.art和boot.oat这两个文件了,这两个文件都位于手机系统的/data/dalvik-cache/arm目录下。boot.art是一个img文件,而boot.oat文件可以将其理解为ART虚拟机的启动类,这两个文件是dex2oat命令将Android系统必须的的jar包编译生成的,这两个文件相互联系,缺一不可,boot.art这个img文件直接被映射到ART虚拟机的堆空间中,包含了boot.oat中的某些对象实例以及函数地址。老罗的文章中也讲了,我们后边再介绍,先看看这两个文件的来历。

这两个文件是从什么地方生成的呢?删除了这两个boot文件,那么在下次android启动的时候,系统就会重新生成这两个文件,通过查找手机log中的dex2oat关键字就可以查看到这两个文件的生成命令。注意删除了这两个文件后,重新启动也会重新解析apk,所以会花较长时间。

08-09 16:10:47.463: I/art(324): GenerateImage: /system/bin/dex2oat --image=/data/dalvik-cache/arm/system@framework@boot.art --dex-file=/system/framework/core-libart.jar --dex-file=/system/framework/conscrypt.jar --dex-file=/system/framework/okhttp.jar --dex-file=/system/framework/core-junit.jar --dex-file=/system/framework/bouncycastle.jar --dex-file=/system/framework/ext.jar --dex-file=/system/framework/framework.jar --dex-file=/system/framework/telephony-common.jar --dex-file=/system/framework/voip-common.jar --dex-file=/system/framework/ims-common.jar --dex-file=/system/framework/apache-xml.jar --dex-file=/system/framework/org.apache.http.legacy.boot.jar --oat-file=/data/dalvik-cache/arm/system@framework@boot.oat --instruction-set=arm --instruction-set-features=smp,div,atomic_ldrd_strd --base=0x6fc33000 --runtime-arg -Xms64m --runtime-arg -Xmx64m --image-classes=/system/etc/preloaded-classes --instruction-set-variant=krait --instruction-set-features=default
08-09 16:10:47.632: I/dex2oat(627): /system/bin/dex2oat --image=/data/dalvik-cache/arm/system@framework@boot.art --dex-file=/system/framework/core-libart.jar --dex-file=/system/framework/conscrypt.jar --dex-file=/system/framework/okhttp.jar --dex-file=/system/framework/core-junit.jar --dex-file=/system/framework/bouncycastle.jar --dex-file=/system/framework/ext.jar --dex-file=/system/framework/framework.jar --dex-file=/system/framework/telephony-common.jar --dex-file=/system/framework/voip-common.jar --dex-file=/system/framework/ims-common.jar --dex-file=/system/framework/apache-xml.jar --dex-file=/system/framework/org.apache.http.legacy.boot.jar --oat-file=/data/dalvik-cache/arm/system@framework@boot.oat --instruction-set=arm --instruction-set-features=smp,div,atomic_ldrd_strd --base=0x6fc33000 --runtime-arg -Xms64m --runtime-arg -Xmx64m --image-classes=/system/etc/preloaded-classes --instruction-set-variant=krait --instruction-set-features=default
08-09 16:10:47.639: I/dex2oat(627): setting boot class path to /system/framework/core-libart.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/apache-xml.jar:/system/framework/org.apache.http.legacy.boot.jar

从log中我们可以看到,生成boot.art和boot.oat文件主要依赖了core-libart.jar和framework.jar这两个文件,所以可以理解为telephonymanager和IOBridge所在的jar包经dex2oat打包到了boot.art和boot.oat文件中,所以下一步就可以直接分析boot.art和boot.oat文件是如何被加载到内存了。

由于这两个boot文件是由多个jar包生成的,而不是像apk那样通过一个classes.dex生成,所以情况可能会特殊,所以接下来的分析可能会回退到虚拟机启动流程分析入手,有兴趣可以看下老罗的几篇关于ART虚拟机的文章。

老罗的文章:Android运行时ART加载OAT文件的过程分析前半部分和网上其他一位大牛的文章:ART虚拟机启动之image空间都讲了boot镜像加载的流程,强烈推荐自己看下第二篇文章的讲解,比较简洁易懂。下面开始分析:

首先转载下大牛的流程图:

上述流程牵涉的代码位于以下三个文件中:

framework/base/cmds/app_process/app_main.cpp
frameworks/base/core/jni/AndroidRuntime.cpp
art/runtime/jni_internal.cc

首先AndroidRuntime::start函数中会进行jni的初始化,实际上就是加载虚拟机的so库,并从中导出三个函数,其中JNI_CreateJavaVM用来启动虚拟机。Android 5.0之后默认加载的就是libart.so。

JNI_CreateJavaVM主要负责创建ART虚拟机实例,并且启动ART虚拟机,然后给app_process返回JNIEnv和JavaVM。有了JNIEnv,app_process中才能使用JNI中的FindClass等函数。
创建虚拟机实例中,最主要的是Runtime::init函数,负责创建虚拟机堆空间,绑定Thread,创建和初始化classLinker。
利用gc::Heap创建堆空间时,主要有两件事情,加载boot.ar和boot.oat初始化imgae空间,另外就是与垃圾回收机制相关的东东(垃圾回收太复杂了,暂时略过,后面详谈)。

上述过程详前半部分细解析请查看我整理精简过的老罗的博客:Android ART运行时无缝替换Dalvik虚拟机的过程分析,为了省事我不再复制粘贴过来了,请自行跳转查看,然后再回来继续。

然后我们继续分析,从AndroidRuntime::startVm调用了JNI_CreateJavaVM,跳转到libart.so中执行的JNI_CreateJavaVM函数,函数JNI_CreateJavaVM的实现如下所示:

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {ATRACE_BEGIN(__FUNCTION__);const JavaVMInitArgs* args = static_cast<JavaVMInitArgs*>(vm_args);if (IsBadJniVersion(args->version)) {LOG(ERROR) << "Bad JNI version passed to CreateJavaVM: " << args->version;ATRACE_END();return JNI_EVERSION;}RuntimeOptions options;for (int i = 0; i < args->nOptions; ++i) {JavaVMOption* option = &args->options[i];options.push_back(std::make_pair(std::string(option->optionString), option->extraInfo));}bool ignore_unrecognized = args->ignoreUnrecognized;if (!Runtime::Create(options, ignore_unrecognized)) {ATRACE_END();return JNI_ERR;}Runtime* runtime = Runtime::Current();bool started = runtime->Start();if (!started) {delete Thread::Current()->GetJniEnv();delete runtime->GetJavaVM();LOG(WARNING) << "CreateJavaVM failed";ATRACE_END();return JNI_ERR;}*p_env = Thread::Current()->GetJniEnv();*p_vm = runtime->GetJavaVM();ATRACE_END();return JNI_OK;
}

这个函数定义在文件art/runtime/java_vm_ext.cc中,注意与老罗博客中的区别。

参数vm_args用作ART虚拟机的启动参数,它被转换为一个JavaVMInitArgs对象后,再按照Key-Value的组织形式保存一个Options向量中,并且以该向量作为参数传递给Runtime类的静态成员函数Create。
        Runtime类的静态成员函数Create负责在进程中创建一个ART虚拟机。创建成功后,就调用Runtime类的另外一个静态成员函数Start启动该ART虚拟机。注意,这个创建ART虚拟的动作只会在Zygote进程中执行,SystemServer系统进程以及Android应用程序进程的ART虚拟机都是直接从Zygote进程fork出来共享的。这与Dalvik虚拟机的创建方式是完全一样的。

接下来我们就重点分析Runtime类的静态成员函数Create,它的实现如下所示:

bool Runtime::Create(const RuntimeOptions& options, bool ignore_unrecognized) {// TODO: acquire a static mutex on Runtime to avoid racing.if (Runtime::instance_ != nullptr) {return false;}InitLogging(nullptr);  // Calls Locks::Init() as a side effect.instance_ = new Runtime;if (!instance_->Init(options, ignore_unrecognized)) {// TODO: Currently deleting the instance will abort the runtime on destruction. Now This will// leak memory, instead. Fix the destructor. b/19100793.// delete instance_;instance_ = nullptr;return false;}return true;
}

这个函数定义在文件art/runtime/runtime.cc中。
       instance_是Runtime类的静态成员变量,它指向进程中的一个Runtime单例。这个Runtime单例描述的就是当前进程的ART虚拟机实例。
       函数首先判断当前进程是否已经创建有一个ART虚拟机实例了。如果有的话,函数就立即返回。否则的话,就创建一个ART虚拟机实例,并且保存在Runtime类的静态成员变量instance_中,最后调用Runtime类的成员函数Init对该新创建的ART虚拟机进行初始化。

Runtime类的成员函数Init的实现如下所示:

bool Runtime::Init(const RuntimeOptions& raw_options, bool ignore_unrecognized) {........std::unique_ptr<ParsedOptions> parsed_options(ParsedOptions::Create(raw_options, ignore_unrecognized, &runtime_options));........heap_ = new gc::Heap(runtime_options.GetOrDefault(Opt::MemoryInitialSize),runtime_options.GetOrDefault(Opt::HeapGrowthLimit),runtime_options.GetOrDefault(Opt::HeapMinFree),runtime_options.GetOrDefault(Opt::HeapMaxFree),runtime_options.GetOrDefault(Opt::HeapTargetUtilization),runtime_options.GetOrDefault(Opt::ForegroundHeapGrowthMultiplier),runtime_options.GetOrDefault(Opt::MemoryMaximumSize),runtime_options.GetOrDefault(Opt::NonMovingSpaceCapacity),runtime_options.GetOrDefault(Opt::Image),runtime_options.GetOrDefault(Opt::ImageInstructionSet),........java_vm_ = new JavaVMExt(this, runtime_options);........Thread* self = Thread::Attach("main", false, nullptr, false);........class_linker_ = new ClassLinker(intern_table_);if (GetHeap()->HasImageSpace()) {ATRACE_BEGIN("InitFromImage");class_linker_->InitFromImage();........} else {........std::vector<std::unique_ptr<const DexFile>> boot_class_path;OpenDexFiles(dex_filenames,dex_locations,runtime_options.GetOrDefault(Opt::Image),&boot_class_path);instruction_set_ = runtime_options.GetOrDefault(Opt::ImageInstructionSet);class_linker_->InitWithoutImage(std::move(boot_class_path));........}........return true;
}

这个函数定义在文件art/runtime/runtime.cc中。

Runtime类的成员函数Init首先调用ParsedOptions类的静态成员函数Create对ART虚拟机的启动参数raw_options进行解析。解析后得到的参数保存在一个ParsedOptions对象中,接下来就根据这些参数一个ART虚拟机堆。ART虚拟机堆使用一个Heap对象来描述。
       创建好ART虚拟机堆后,Runtime类的成员函数Init接着又创建了一个JavaVMExt实例。这个JavaVMExt实例最终是要返回给调用者的,使得调用者可以通过该JavaVMExt实例来和ART虚拟机交互。再接下来,Runtime类的成员函数Init通过Thread类的成员函数Attach将当前线程作为ART虚拟机的主线程,使得当前线程可以调用ART虚拟机提供的JNI接口。
       Runtime类的成员函数GetHeap返回的便是当前ART虚拟机的堆,也就是前面创建的ART虚拟机堆。通过调用Heap类的成员函数HasImageSpace,判断如果虚拟机堆的第一个连续空间是一个Image空间,那么就调用ClassLinker类的成员函数InitFromImage来初始化创建的ClassLinker对象。否则的话,上述ClassLinker对象就要通过ClassLinker类的另外一个成员函数InitWithoutImage来初始化。初始化过的ClassLinker对象是后面ART虚拟机加载加载Java类时要用到的。
       后面我们分析ART虚拟机的垃圾收集机制时会看到,ART虚拟机的堆包含有三个连续空间和一个不连续空间。三个连续空间分别用来分配不同的对象。当第一个连续空间不是Image空间时,就表明当前进程不是Zygote进程,而是安装应用程序时启动的一个dex2oat进程。安装应用程序时启动的dex2oat进程也会在内部创建一个ART虚拟机,不过这个ART虚拟机是用来将DEX字节码编译成本地机器指令的,而Zygote进程创建的ART虚拟机是用来运行应用程序的。

接下来我们主要分析ParsedOptions类的静态成员函数Create和ART虚拟机堆Heap的构造函数,以便可以了解ART虚拟机的启动参数解析过程和ART虚拟机的堆创建过程。

ParsedOptions类的静态成员函数Create的实现如下所示:

ParsedOptions* ParsedOptions::Create(const RuntimeOptions& options, bool ignore_unrecognized,RuntimeArgumentMap* runtime_options) {CHECK(runtime_options != nullptr);std::unique_ptr<ParsedOptions> parsed(new ParsedOptions());if (parsed->Parse(options, ignore_unrecognized, runtime_options)) {return parsed.release();}return nullptr;
}bool ParsedOptions::Parse(const RuntimeOptions& options, bool ignore_unrecognized,RuntimeArgumentMap* runtime_options) {//  gLogVerbosity.class_linker = true;  // TODO: don't check this in!//  gLogVerbosity.compiler = true;  // TODO: don't check this in!//  gLogVerbosity.gc = true;  // TODO: don't check this in!//  gLogVerbosity.heap = true;  // TODO: don't check this in!//  gLogVerbosity.jdwp = true;  // TODO: don't check this in!//  gLogVerbosity.jit = true;  // TODO: don't check this in!//  gLogVerbosity.jni = true;  // TODO: don't check this in!//  gLogVerbosity.monitor = true;  // TODO: don't check this in!//  gLogVerbosity.profiler = true;  // TODO: don't check this in!//  gLogVerbosity.signals = true;  // TODO: don't check this in!//  gLogVerbosity.startup = true;  // TODO: don't check this in!//  gLogVerbosity.third_party_jni = true;  // TODO: don't check this in!//  gLogVerbosity.threads = true;  // TODO: don't check this in!//  gLogVerbosity.verifier = true;  // TODO: don't check this in!for (size_t i = 0; i < options.size(); ++i) {if (true && options[0].first == "-Xzygote") {LOG(INFO) << "option[" << i << "]=" << options[i].first;}}auto parser = MakeParser(ignore_unrecognized);// Convert to a simple string list (without the magic pointer options)std::vectorargv_list;if (!ProcessSpecialOptions(options, nullptr, &argv_list)) {return false;}CmdlineResult parse_result = parser->Parse(argv_list);// Handle parse errors by displaying the usage and potentially exiting.if (parse_result.IsError()) {if (parse_result.GetStatus() == CmdlineResult::kUsage) {UsageMessage(stdout, "%s\n", parse_result.GetMessage().c_str());Exit(0);} else if (parse_result.GetStatus() == CmdlineResult::kUnknown && !ignore_unrecognized) {Usage("%s\n", parse_result.GetMessage().c_str());return false;} else {Usage("%s\n", parse_result.GetMessage().c_str());Exit(0);}UNREACHABLE();}using M = RuntimeArgumentMap;RuntimeArgumentMap args = parser->ReleaseArgumentsMap();// -help, -showversion, etc.if (args.Exists(M::Help)) {Usage(nullptr);return false;} else if (args.Exists(M::ShowVersion)) {UsageMessage(stdout, "ART version %s\n", Runtime::GetVersion());Exit(0);} else if (args.Exists(M::BootClassPath)) {LOG(INFO) << "setting boot class path to " << *args.Get(M::BootClassPath);}// Set a default boot class path if we didn't get an explicit one via command line.if (getenv("BOOTCLASSPATH") != nullptr) {args.SetIfMissing(M::BootClassPath, std::string(getenv("BOOTCLASSPATH")));}// Set a default class path if we didn't get an explicit one via command line.if (getenv("CLASSPATH") != nullptr) {args.SetIfMissing(M::ClassPath, std::string(getenv("CLASSPATH")));}// Default to number of processors minus one since the main GC thread also does work.args.SetIfMissing(M::ParallelGCThreads, gc::Heap::kDefaultEnableParallelGC ?static_cast(sysconf(_SC_NPROCESSORS_CONF) - 1u) : 0u);// -Xverbose:{LogVerbosity *log_verbosity = args.Get(M::Verbose);if (log_verbosity != nullptr) {gLogVerbosity = *log_verbosity;}}// -Xprofile:Trace::SetDefaultClockSource(args.GetOrDefault(M::ProfileClock));if (!ProcessSpecialOptions(options, &args, nullptr)) {return false;}{// If not set, background collector type defaults to homogeneous compaction.// If foreground is GSS, use GSS as background collector.// If not low memory mode, semispace otherwise.gc::CollectorType background_collector_type_;gc::CollectorType collector_type_ = (XGcOption{}).collector_type_;  // NOLINT [whitespace/braces] [5]bool low_memory_mode_ = args.Exists(M::LowMemoryMode);background_collector_type_ = args.GetOrDefault(M::BackgroundGc);{XGcOption* xgc = args.Get(M::GcOption);if (xgc != nullptr && xgc->collector_type_ != gc::kCollectorTypeNone) {collector_type_ = xgc->collector_type_;}}if (background_collector_type_ == gc::kCollectorTypeNone) {if (collector_type_ != gc::kCollectorTypeGSS) {background_collector_type_ = low_memory_mode_ ?gc::kCollectorTypeSS : gc::kCollectorTypeHomogeneousSpaceCompact;} else {background_collector_type_ = collector_type_;}}args.Set(M::BackgroundGc, BackgroundGcOption { background_collector_type_ });}// If a reference to the dalvik core.jar snuck in, replace it with// the art specific version. This can happen with on device// boot.art/boot.oat generation by GenerateImage which relies on the// value of BOOTCLASSPATH.
#if defined(ART_TARGET)std::string core_jar("/core.jar");std::string core_libart_jar("/core-libart.jar");
#else// The host uses hostdex files.std::string core_jar("/core-hostdex.jar");std::string core_libart_jar("/core-libart-hostdex.jar");
#endifauto boot_class_path_string = args.GetOrDefault(M::BootClassPath);size_t core_jar_pos = boot_class_path_string.find(core_jar);if (core_jar_pos != std::string::npos) {boot_class_path_string.replace(core_jar_pos, core_jar.size(), core_libart_jar);args.Set(M::BootClassPath, boot_class_path_string);}{auto&& boot_class_path = args.GetOrDefault(M::BootClassPath);auto&& boot_class_path_locations = args.GetOrDefault(M::BootClassPathLocations);if (args.Exists(M::BootClassPathLocations)) {size_t boot_class_path_count = ParseStringList<':'>::Split(boot_class_path).Size();if (boot_class_path_count != boot_class_path_locations.Size()) {Usage("The number of boot class path files does not match"" the number of boot class path locations given\n""  boot class path files     (%zu): %s\n""  boot class path locations (%zu): %s\n",boot_class_path.size(), boot_class_path_string.c_str(),boot_class_path_locations.Size(), boot_class_path_locations.Join().c_str());return false;}}}if (!args.Exists(M::CompilerCallbacksPtr) && !args.Exists(M::Image)) {std::string image = GetAndroidRoot();image += "/framework/boot.art";args.Set(M::Image, image);}if (args.GetOrDefault(M::HeapGrowthLimit) == 0u) {  // 0 means no growth limitargs.Set(M::HeapGrowthLimit, args.GetOrDefault(M::MemoryMaximumSize));}*runtime_options = std::move(args);return true;
}

注意这里着重把core.jar替换为core-libart.jar,不知道是什么原因。后续再研究。

这个函数定义在文件art/runtime/parsed_options.cc中,注意此处和老罗博客的区别。
       ART虚拟机的启动参数比较多,这里我们只关注两个:-Xbootclasspath、-Ximage和compiler。
       参数-Xbootclasspath用来指定启动类路径。如果没有指定启动类路径,那么默认的启动类路径就通过环境变量BOOTCLASSPATH来获得。
       参数-Ximage用来指定ART虚拟机所使用的Image文件。这个Image是用来启动ART虚拟机的。
       参数compiler用来指定当前要创建的ART虚拟机是用来将DEX字节码编译成本地机器指令的。
       如果没有指定Image文件,并且当前创建的ART虚拟机又不是用来编译DEX字节码的,那么就将该Image文件指定为设备上的/system/framework/boot.art文件。我们知道,system分区的文件都是在制作ROM时打包进去的。这样上述代码的逻辑就是说,如果没有指定Image文件,那么将system分区预先准备好的framework/boot.art文件作为Image文件来启动ART虚拟机。不过,/system/framework/boot.art文件可能是不存在的。在这种情况下,就需要生成一个新的Image文件。这个Image文件就是一个包含了多个DEX文件的OAT文件。接下来通过分析ART虚拟机堆的创建过程就会清楚地看到这一点。

Heap类的构造函数的实现如下所示:

Heap::Heap(size_t initial_size, size_t growth_limit, size_t min_free, size_t max_free,double target_utilization, double foreground_heap_growth_multiplier,size_t capacity, size_t non_moving_space_capacity, const std::string& image_file_name,const InstructionSet image_instruction_set, CollectorType foreground_collector_type,CollectorType background_collector_type,space::LargeObjectSpaceType large_object_space_type, size_t large_object_threshold,size_t parallel_gc_threads, size_t conc_gc_threads, bool low_memory_mode,size_t long_pause_log_threshold, size_t long_gc_log_threshold,bool ignore_max_footprint, bool use_tlab,bool verify_pre_gc_heap, bool verify_pre_sweeping_heap, bool verify_post_gc_heap,bool verify_pre_gc_rosalloc, bool verify_pre_sweeping_rosalloc,bool verify_post_gc_rosalloc, bool gc_stress_mode,bool use_homogeneous_space_compaction_for_oom,uint64_t min_interval_homogeneous_space_compaction_by_oom)......{......if (!image_file_name.empty()) {ATRACE_BEGIN("ImageSpace::Create");std::string error_msg;auto* image_space = space::ImageSpace::Create(image_file_name.c_str(), image_instruction_set,&error_msg);ATRACE_END();if (image_space != nullptr) {AddSpace(image_space);// Oat files referenced by image files immediately follow them in memory, ensure alloc space// isn't going to get in the middleuint8_t* oat_file_end_addr = image_space->GetImageHeader().GetOatFileEnd();CHECK_GT(oat_file_end_addr, image_space->End());requested_alloc_space_begin = AlignUp(oat_file_end_addr, kPageSize);} else {LOG(ERROR) << "Could not create image space with image file '" << image_file_name << "'. "<< "Attempting to fall back to imageless running. Error was: " << error_msg;}}......}

这个函数定义在文件art/runtime/gc/heap.cc中。
        ART虚拟机堆的详细创建过程我们在后面分析ART虚拟机的垃圾收集机制时再分析,这里只关注与Image文件相关的逻辑。
        参数image_file_name描述的就是前面提到的Image文件的路径。如果它的值不等于空的话,那么就以它为参数,调用ImageSpace类的静态成员函数Create创建一个Image空间,并且调用Heap类的成员函数AddContinuousSpace将该Image空间作为本进程的ART虚拟机堆的第一个连续空间。

接下来我们继续分析ImageSpace类的静态成员函数Create,它的实现如下所示:

ImageSpace* ImageSpace::Create(const char* image_location,const InstructionSet image_isa,std::string* error_msg) {std::string system_filename;bool has_system = false;std::string cache_filename;bool has_cache = false;bool dalvik_cache_exists = false;bool is_global_cache = true;const bool found_image = FindImageFilename(image_location, image_isa, &system_filename,&has_system, &cache_filename, &dalvik_cache_exists,&has_cache, &is_global_cache);if (Runtime::Current()->IsZygote()) {MarkZygoteStart(image_isa, Runtime::Current()->GetZygoteMaxFailedBoots());}ImageSpace* space;bool relocate = Runtime::Current()->ShouldRelocate();bool can_compile = Runtime::Current()->IsImageDex2OatEnabled();if (found_image) {const std::string* image_filename;bool is_system = false;bool relocated_version_used = false;if (relocate) {if (!dalvik_cache_exists) {*error_msg = StringPrintf("Requiring relocation for image '%s' at '%s' but we do not have ""any dalvik_cache to find/place it in.",image_location, system_filename.c_str());return nullptr;}if (has_system) {if (has_cache && ChecksumsMatch(system_filename.c_str(), cache_filename.c_str())) {// We already have a relocated versionimage_filename = &cache_filename;relocated_version_used = true;} else {// We cannot have a relocated version, Relocate the system one and use it.std::string reason;bool success;// Check whether we are allowed to relocate.if (!can_compile) {reason = "Image dex2oat disabled by -Xnoimage-dex2oat.";success = false;} else if (!ImageCreationAllowed(is_global_cache, &reason)) {// Whether we can write to the cache.success = false;} else {// Try to relocate.success = RelocateImage(image_location, cache_filename.c_str(), image_isa, &reason);}if (success) {relocated_version_used = true;image_filename = &cache_filename;} else {*error_msg = StringPrintf("Unable to relocate image '%s' from '%s' to '%s': %s",image_location, system_filename.c_str(),cache_filename.c_str(), reason.c_str());// We failed to create files, remove any possibly garbage output.// Since ImageCreationAllowed was true above, we are the zygote// and therefore the only process expected to generate these for// the device.PruneDalvikCache(image_isa);return nullptr;}}} else {CHECK(has_cache);// We can just use cache's since it should be fine. This might or might not be relocated.image_filename = &cache_filename;}} else {if (has_system && has_cache) {// Check they have the same cksum. If they do use the cache. Otherwise system.if (ChecksumsMatch(system_filename.c_str(), cache_filename.c_str())) {image_filename = &cache_filename;relocated_version_used = true;} else {image_filename = &system_filename;is_system = true;}} else if (has_system) {image_filename = &system_filename;is_system = true;} else {CHECK(has_cache);image_filename = &cache_filename;}}{// Note that we must not use the file descriptor associated with// ScopedFlock::GetFile to Init the image file. We want the file// descriptor (and the associated exclusive lock) to be released when// we leave Create.ScopedFlock image_lock;image_lock.Init(image_filename->c_str(), error_msg);VLOG(startup) << "Using image file " << image_filename->c_str() << " for image location "<< image_location;// If we are in /system we can assume the image is good. We can also// assume this if we are using a relocated image (i.e. image checksum// matches) since this is only different by the offset. We need this to// make sure that host tests continue to work.space = ImageSpace::Init(image_filename->c_str(), image_location,!(is_system || relocated_version_used), error_msg);}if (space != nullptr) {return space;}if (relocated_version_used) {// Something is wrong with the relocated copy (even though checksums match). Cleanup.// This can happen if the .oat is corrupt, since the above only checks the .art checksums.// TODO: Check the oat file validity earlier.*error_msg = StringPrintf("Attempted to use relocated version of %s at %s generated from %s ""but image failed to load: %s",image_location, cache_filename.c_str(), system_filename.c_str(),error_msg->c_str());PruneDalvikCache(image_isa);return nullptr;} else if (is_system) {// If the /system file exists, it should be up-to-date, don't try to generate it.*error_msg = StringPrintf("Failed to load /system image '%s': %s",image_filename->c_str(), error_msg->c_str());return nullptr;} else {// Otherwise, log a warning and fall through to GenerateImage.LOG(WARNING) << *error_msg;}}if (!can_compile) {*error_msg = "Not attempting to compile image because -Xnoimage-dex2oat";return nullptr;} else if (!dalvik_cache_exists) {*error_msg = StringPrintf("No place to put generated image.");return nullptr;} else if (!ImageCreationAllowed(is_global_cache, error_msg)) {return nullptr;} else if (!GenerateImage(cache_filename, image_isa, error_msg)) {*error_msg = StringPrintf("Failed to generate image '%s': %s",cache_filename.c_str(), error_msg->c_str());// We failed to create files, remove any possibly garbage output.// Since ImageCreationAllowed was true above, we are the zygote// and therefore the only process expected to generate these for// the device.PruneDalvikCache(image_isa);return nullptr;} else {// Check whether there is enough space left over after we have generated the image.if (!CheckSpace(cache_filename, error_msg)) {// No. Delete the generated image and try to run out of the dex files.PruneDalvikCache(image_isa);return nullptr;}// Note that we must not use the file descriptor associated with// ScopedFlock::GetFile to Init the image file. We want the file// descriptor (and the associated exclusive lock) to be released when// we leave Create.ScopedFlock image_lock;image_lock.Init(cache_filename.c_str(), error_msg);space = ImageSpace::Init(cache_filename.c_str(), image_location, true, error_msg);if (space == nullptr) {*error_msg = StringPrintf("Failed to load generated image '%s': %s",cache_filename.c_str(), error_msg->c_str());}return space;}
}

这个函数定义在文件art/runtime/gc/space/image_space.cc中。

ImageSpace::create逻辑很简单,就是先FindImageFilename找boot.art和boot.oat.看他们存在与否。如果存在的话,是在/system/framework中呢,还是在/data/dalvik-cache中。
1). 如果找到了boot.art
接着判断是否需要对boot.art在内存中的位置重定位,这个是有relocate来判断的。默认情况下,ART虚拟机启动的时候是必须要重定位boot.art的,但是可以通过在ART虚拟机启动的时候,添加在参数“-Xnorelocate”来禁止重定位。
先看需要重定位的情况:
此情况下,如果boot.art是在/data/dalvik-cache里的话,那就说明是已经重定位过了,如果/data/dalvik-cache没有,而是在/system/framework里的话,那么就需要对其进行重定位。
接着就调用RelocateImage对其重定位。该函数实际上是调用patchoat可执行程序对boot.oat和boot.art进行重定位。重定位之后新的boot.art和boot.art会存放在/data/dalvik-cache中。
不需要重定位:
此情况下,如果system和data中都有boot.art和boot.oat,倘若两者cksum一致,就使用/data/dalvik-cache中的,否则就使用/system/framework中的。如果system和data中只有其一有,那么谁有,就用谁的。
2). 如果没找到boot.art
没找到的话,那就利用GenerateImage函数自己创建,实际上该函数就是调用dex2oat命令将BOOTCLASSPATH中中的jar编译为boot.art和boot.oat。这里要注意的话,该函数生成的是重定位过后的boot.art和boot.oat.

下面我们详细介绍上述流程的FindImageFilename、GenerateImage和Init三个函数。

先看看FindImageFilename怎么查找boot.art和boot.oat:

bool ImageSpace::FindImageFilename(const char* image_location,const InstructionSet image_isa,std::string* system_filename,bool* has_system,std::string* cache_filename,bool* dalvik_cache_exists,bool* has_cache,bool* is_global_cache) {*has_system = false;*has_cache = false;// image_location = /system/framework/boot.art// system_image_location = /system/framework/<image_isa>/boot.artstd::string system_image_filename(GetSystemImageFilename(image_location, image_isa));if (OS::FileExists(system_image_filename.c_str())) {*system_filename = system_image_filename;*has_system = true;}bool have_android_data = false;*dalvik_cache_exists = false;std::string dalvik_cache;GetDalvikCache(GetInstructionSetString(image_isa), true, &dalvik_cache,&have_android_data, dalvik_cache_exists, is_global_cache);if (have_android_data && *dalvik_cache_exists) {// Always set output location even if it does not exist,// so that the caller knows where to create the image.//// image_location = /system/framework/boot.art// *image_filename = /data/dalvik-cache/<image_isa>/boot.artstd::string error_msg;if (!GetDalvikCacheFilename(image_location, dalvik_cache.c_str(), cache_filename, &error_msg)) {LOG(WARNING) << error_msg;return *has_system;}*has_cache = OS::FileExists(cache_filename->c_str());}return *has_system || *has_cache;
}

该函数会首先在/system/framework/<image_isa>/boot.art
找,然后再在/data/dalvik-cache/<image_isa>/boot.art
找,最后将结果通过返回值和传进来的参数返回。

如果没有找到,就会调用GenerateImage来生成:

static bool GenerateImage(const std::string& image_filename, InstructionSet image_isa,std::string* error_msg) {const std::string boot_class_path_string(Runtime::Current()->GetBootClassPathString());std::vector<std::string> boot_class_path;Split(boot_class_path_string, ':', &boot_class_path);if (boot_class_path.empty()) {*error_msg = "Failed to generate image because no boot class path specified";return false;}// We should clean up so we are more likely to have room for the image.if (Runtime::Current()->IsZygote()) {LOG(INFO) << "Pruning dalvik-cache since we are generating an image and will need to recompile";PruneDalvikCache(image_isa);}std::vector<std::string> arg_vector;std::string dex2oat(Runtime::Current()->GetCompilerExecutable());arg_vector.push_back(dex2oat);std::string image_option_string("--image=");image_option_string += image_filename;arg_vector.push_back(image_option_string);for (size_t i = 0; i < boot_class_path.size(); i++) {arg_vector.push_back(std::string("--dex-file=") + boot_class_path[i]);}std::string oat_file_option_string("--oat-file=");oat_file_option_string += ImageHeader::GetOatLocationFromImageLocation(image_filename);arg_vector.push_back(oat_file_option_string);// Note: we do not generate a fully debuggable boot image so we do not pass the// compiler flag --debuggable here.Runtime::Current()->AddCurrentRuntimeFeaturesAsDex2OatArguments(&arg_vector);CHECK_EQ(image_isa, kRuntimeISA)<< "We should always be generating an image for the current isa.";int32_t base_offset = ChooseRelocationOffsetDelta(ART_BASE_ADDRESS_MIN_DELTA,ART_BASE_ADDRESS_MAX_DELTA);LOG(INFO) << "Using an offset of 0x" << std::hex << base_offset << " from default "<< "art base address of 0x" << std::hex << ART_BASE_ADDRESS;arg_vector.push_back(StringPrintf("--base=0x%x", ART_BASE_ADDRESS + base_offset));if (!kIsTargetBuild) {arg_vector.push_back("--host");}const std::vector<std::string>& compiler_options = Runtime::Current()->GetImageCompilerOptions();for (size_t i = 0; i < compiler_options.size(); ++i) {arg_vector.push_back(compiler_options[i].c_str());}std::string command_line(Join(arg_vector, ' '));LOG(INFO) << "GenerateImage: " << command_line;return Exec(arg_vector, error_msg);
}

ImageSpace类的静态成员函数GenerateImage实际上就调用dex2oat工具在/data/dalvik-cache目录下生成两个文件:system@framework@boot.art@classes.dex和system@framework@boot.art@classes.oat。
       system@framework@boot.art@classes.dex是一个Image文件,通过--image选项传递给dex2oat工具,里面包含了一些需要在Zygote进程启动时预加载的类。这些需要预加载的类由/system/framework/framework.jar文件里面的preloaded-classes文件指定。
       system@framework@boot.art@classes.oat是一个OAT文件,通过--oat-file选项传递给dex2oat工具,它是由系统启动路径中指定的jar文件生成的。每一个jar文件都通过一个--dex-file选项传递给dex2oat工具。这样dex2oat工具就可以将它们所包含的classes.dex文件里面的DEX字节码翻译成本地机器指令。
       这样,我们就得到了一个包含有多个DEX文件的OAT文件system@framework@boot.art@classes.oat。

有了boot.art和boot.oat之后,就调用 ImageSpace::Init初始化image空间:

ImageSpace* ImageSpace::Init(const char* image_filename, const char* image_location,bool validate_oat_file, std::string* error_msg) {................std::unique_ptr space(new ImageSpace(image_filename, image_location,map.release(), bitmap.release(), image_end));// VerifyImageAllocations() will be called later in Runtime::Init()// as some class roots like ArtMethod::java_lang_reflect_ArtMethod_// and ArtField::java_lang_reflect_ArtField_, which are used from// Object::SizeOf() which VerifyImageAllocations() calls, are not// set yet at this point.space->oat_file_.reset(space->OpenOatFile(image_filename, error_msg));if (space->oat_file_.get() == nullptr) {DCHECK(!error_msg->empty());return nullptr;}space->oat_file_non_owned_ = space->oat_file_.get();if (validate_oat_file && !space->ValidateOatFile(error_msg)) {DCHECK(!error_msg->empty());return nullptr;}Runtime* runtime = Runtime::Current();runtime->SetInstructionSet(space->oat_file_->GetOatHeader().GetInstructionSet());runtime->SetResolutionMethod(image_header.GetImageMethod(ImageHeader::kResolutionMethod));runtime->SetImtConflictMethod(image_header.GetImageMethod(ImageHeader::kImtConflictMethod));runtime->SetImtUnimplementedMethod(image_header.GetImageMethod(ImageHeader::kImtUnimplementedMethod));runtime->SetCalleeSaveMethod(image_header.GetImageMethod(ImageHeader::kCalleeSaveMethod), Runtime::kSaveAll);runtime->SetCalleeSaveMethod(image_header.GetImageMethod(ImageHeader::kRefsOnlySaveMethod), Runtime::kRefsOnly);runtime->SetCalleeSaveMethod(image_header.GetImageMethod(ImageHeader::kRefsAndArgsSaveMethod), Runtime::kRefsAndArgs);if (VLOG_IS_ON(heap) || VLOG_IS_ON(startup)) {LOG(INFO) << "ImageSpace::Init exiting (" << PrettyDuration(NanoTime() - start_time)<< ") " << *space.get();}return space.release();
}

在这里调用了new ImageSpace用来加载boot.art,而OpenOatFile方法再调用OatFile::Open方法打开boot.oat文件。

在Image文件头的成员变量image_roots_描述的对象数组中,有四个特殊的ArtMethod对象,用来描述四种特殊的ART运行时方法。ART运行时方法是一种用来描述其它ART方法的ART方法。它们具有特殊的用途,如下所示:
       1. ImageHeader::kResolutionMethod: 用来描述一个还未进行解析和链接的ART方法。
       2. ImageHeader::kCalleeSaveMethod: 用来描述一个由被调用者保存的r4-r11、lr和s0-s31寄存器的ART方法,即由被调用者保存非参数使用的通用寄存器以及所有的浮点数寄存器。
       3. ImageHeader::kRefsOnlySaveMethod: 用来描述一个由被调用者保存的r5-r8、r10-r11和lr寄存器的ART方法,即由被调用者保存非参数使用的通用寄存器。
       4. ImageHeader::kRefsAndArgsSaveMethod: 用来描述一个由被调用者保存的r1-r3、r5-r8、r10-r11和lr寄存器的ART方法,即由被调用者保存参数和非参数使用的通用寄存器。
       注意,我们上面描述是针对ARM体系结构的ART方法调用约定的。其中,r0寄存器用来保存当前调用的ART方法,r1-r3寄存器用来传递前三个参数,其它参数通过栈来传递。栈顶由sp寄存器指定,r4寄存器用来保存一个在GC过程中使用到的线程挂起计数器,r5-r8和r10-r11寄存器用来分配给局部变量使用,r9寄存器用来保存当前调用线程对象,lr寄存器用来保存当前ART方法的返回地址,ip寄存器用来保存当前执行的指令地址。
       上面四个特殊的ArtMethod对象从Image Space取出来之后,会通过调用Runtime类的成员函数SetResolutionMethod和SetCalleeSaveMethod保存在用来描述ART运行时的一个Runtime对象的内部,其中,第2、3和4个ArtMethod对象在ART运行时内部对应的类型分别为Runtime::kSaveAll、Runtime::kRefsOnly和Runtime::kRefsAndArgs。

1. ImageHeader::kResolutionMethod: 用来描述一个还未进行解析和链接的ART方法。
       2. ImageHeader::kCalleeSaveMethod: 用来描述一个由被调用者保存的r4-r11、lr和s0-s31寄存器的ART方法,即由被调用者保存非参数使用的通用寄存器以及所有的浮点数寄存器。
       3. ImageHeader::kRefsOnlySaveMethod: 用来描述一个由被调用者保存的r5-r8、r10-r11和lr寄存器的ART方法,即由被调用者保存非参数使用的通用寄存器。
       4. ImageHeader::kRefsAndArgsSaveMethod: 用来描述一个由被调用者保存的r1-r3、r5-r8、r10-r11和lr寄存器的ART方法,即由被调用者保存参数和非参数使用的通用寄存器。
       注意,我们上面描述是针对ARM体系结构的ART方法调用约定的。其中,r0寄存器用来保存当前调用的ART方法,r1-r3寄存器用来传递前三个参数,其它参数通过栈来传递。栈顶由sp寄存器指定,r4寄存器用来保存一个在GC过程中使用到的线程挂起计数器,r5-r8和r10-r11寄存器用来分配给局部变量使用,r9寄存器用来保存当前调用线程对象,lr寄存器用来保存当前ART方法的返回地址,ip寄存器用来保存当前执行的指令地址。
       上面四个特殊的ArtMethod对象从Image Space取出来之后,会通过调用Runtime类的成员函数SetResolutionMethod和SetCalleeSaveMethod保存在用来描述ART运行时的一个Runtime对象的内部,其中,第2、3和4个ArtMethod对象在ART运行时内部对应的类型分别为Runtime::kSaveAll、Runtime::kRefsOnly和Runtime::kRefsAndArgs。

而OpenOatFile方法实现如下:

OatFile* ImageSpace::OpenOatFile(const char* image_path, std::string* error_msg) const {const ImageHeader& image_header = GetImageHeader();std::string oat_filename = ImageHeader::GetOatLocationFromImageLocation(image_path);CHECK(image_header.GetOatDataBegin() != nullptr);OatFile* oat_file = OatFile::Open(oat_filename, oat_filename, image_header.GetOatDataBegin(),image_header.GetOatFileBegin(),!Runtime::Current()->IsAotCompiler(),nullptr, error_msg);if (oat_file == nullptr) {*error_msg = StringPrintf("Failed to open oat file '%s' referenced from image %s: %s",oat_filename.c_str(), GetName(), error_msg->c_str());return nullptr;}uint32_t oat_checksum = oat_file->GetOatHeader().GetChecksum();uint32_t image_oat_checksum = image_header.GetOatChecksum();if (oat_checksum != image_oat_checksum) {*error_msg = StringPrintf("Failed to match oat file checksum 0x%x to expected oat checksum 0x%x"" in image %s", oat_checksum, image_oat_checksum, GetName());return nullptr;}int32_t image_patch_delta = image_header.GetPatchDelta();int32_t oat_patch_delta = oat_file->GetOatHeader().GetImagePatchDelta();if (oat_patch_delta != image_patch_delta && !image_header.CompilePic()) {// We should have already relocated by this point. Bail out.*error_msg = StringPrintf("Failed to match oat file patch delta %d to expected patch delta %d ""in image %s", oat_patch_delta, image_patch_delta, GetName());return nullptr;}return oat_file;
}

至此,boot.art和boot.oat加载就暂时分析完了,由于这个过程中IOBridge和Telephonyanager这两个类都位于boot.oat中,所以此处未分析出原因,接下来看boot.oat的解析,即OatFile::Open方法,我会在下一篇文章中解析。

参考资料:

Android ART虚拟机中 boot.art 和 boot.oat 之间什么关系?

初探boot.art与boot.oat

Android运行时ART加载OAT文件的过程分析

【个人笔记一】ART系统类的编译解析加载探究相关推荐

  1. java 类编译_Java类编译、加载、和执行机制

    Java类编译.加载.和执行机制 标签: java 类加载 类编译 类执行 机制 0.前言 个人认为,对于JVM的理解,主要是两大方面内容: Java类的编译.加载和执行. JVM的内存管理和垃圾回收 ...

  2. cfile清空文件内容_编译-链接-加载 :ELF文件格式解析

    摘要:对于C++的初学者,经常在程序的编译或者加载过程中遇到很多错误,类似undefined reference to ... 和 GLIBCXX_3.4.20 not found 等.这些错误都涉及 ...

  3. 你知道 Java 类是如何被加载的吗?

    一:前言 最近给一个非Java方向的朋友讲了下双亲委派模型,朋友让我写篇文章深度研究下JVM的ClassLoader,我确实也好久没写JVM相关的文章了,有点手痒痒,涂了皮炎平也抑制不住. 我在向朋友 ...

  4. 面试官:你真的知道 Java 类是如何被加载的吗?

    来自:https://yq.aliyun.com/articles/710407 一:前言 最近给一个非Java方向的朋友讲了下双亲委派模型,朋友让我写篇文章深度研究下JVM的ClassLoader, ...

  5. Linux下C/C++程序编译链接加载过程中的常见问题及解决方法

    Linux下C/C++程序编译链接加载过程中的常见问题及解决方法 1 头文件包含的问题 报错信息 该错误通常发生在编译时,常见报错信息如下: run.cpp:2:10: fatal error: dl ...

  6. 懒加载和预加载的区别_类的动态创建(ro,rw)amp; 懒加载类和非懒加载类底层加载的区别 amp; 类和分类的搭配分析...

    黑客技术点击右侧关注,了解黑客的世界! Java开发进阶点击右侧关注,掌握进阶之路! Python开发点击右侧关注,探讨技术话题!作者丨OSMin链接:https://juejin.im/post/5 ...

  7. [JAVA冷知识]动态加载不适合数组类?那如何动态加载一个数组类?

    写在前面 今天和小伙伴分享一些java小知识点,主要围绕下面几点: 既然数组是一个类, 那么编译后类名是什么?类路径呢? 为什么说动态加载不适合数组? 那应该如何动态加载一个数组? 部分内容参考 &l ...

  8. vm虚拟服务器添加网卡,win7系统下vmware虚拟机添加加载无线网卡的方法

    vmware虚拟机想必大家都很熟悉吧,很多win7系统用户想要在vmware虚拟机添加加载无线网卡的,然而它本身是无法识别添加加载无线网卡的,那么要怎么办呢,下面给大家分享一下win7系统下vmwar ...

  9. 在Linux系统中实现一个可加载的内核模块

    Intro 坐标成都电讯大专, 某操作系统课老师在PPT上草草写下3个内核线程API后就要求编程, 感受一下: include/linux/kthread.h,你就看到了它全部的API,一共三个函数. ...

  10. dvm,art模式下的dex文件加载流程

    dvm,art模式下的dex文件加载流程 dex加载是学习android的重中之重,刚看完几篇参考博客,对应android源码,收益匪浅,用一篇博客总结一下自己学到的东西. 1.dvm模式下的dex加 ...

最新文章

  1. 日期时间类,按特定格式显示日期时间
  2. python中del语句
  3. 修改图片src_【学习园地】企业SRC搭建
  4. 蓝桥杯第七届省赛JAVA真题----剪邮票
  5. GCC帧指针的开启与关闭以及反汇编测试
  6. php和html开发工具,常用的php开发工具有哪些?
  7. sql查询初学者指南_适用于初学者SQL Server查询优化技巧与实际示例
  8. BZOJ.3262.陌上花开([模板]CDQ分治 三维偏序)
  9. 微信修改运动步数卡密源码 每日自助修改
  10. php 读取微信对账单,扣丁学堂PHP培训简述PHP如何实现微信对账单处理
  11. 个人最喜爱产品分析:大众点评app
  12. 如何提高下载速度(校园网怎么提高下载速度)
  13. python hist函数_Python pandas.DataFrame.hist函数方法的使用
  14. 光猫的ip地址段和路由器ip地址段互换
  15. 雪花屏幕保护程序(VB.ENT)
  16. intel无线网卡日志服务器,Intel的无线网卡总掉线,慎入
  17. 一、基于workflow-core强势开发审批流【已成功流转50W笔单据】
  18. 《计算机达人成长之路——憧憬与迷茫篇》有钱的捧个预订场,有人的捧个评价场...
  19. 规划求解 python_使用Python/PuLp解决线性规划问题
  20. 给内网映射增加安全防护

热门文章

  1. Python中如何输入一个整数实例
  2. Qt拖拽实现绘制流程图
  3. 服务器p盘cpu占用率低,硬盘问题导致的CPU占用率100%解决实例
  4. 服务器cpu占用过高一般是什么原因,常见云服务器CPU占用100%问题原因及解决办法...
  5. SaltStack ----(五)Jinja模板的使用
  6. excel表格怎么求时间差值_怎么用excel的函数计算日期差值
  7. 基于JAVA社团管理系统计算机毕业设计源码+数据库+lw文档+系统+部署
  8. 如何快速成为数据分析师
  9. Windows10系统迁移-同一PC硬盘之间
  10. 黑盒测试AND白盒测试