dex2oat

dex2oat原理图

sequenceDiagram
installd->>dex2oat: main

activate dex2oat
dex2oat->>dex2oat: art:dex2oat()

activate dex2oat
dex2oat->>dex2oat: Setup()
Note right of dex2oat: MethodVerified回调时deVirtual
deactivate dex2oat

activate dex2oat
dex2oat->>dex2oat: CompileImage()
Note right of dex2oat: 三种编译处理模式:dex2dex,jni,dex2Native

activate dex2oat
dex2oat->>dex2oat: LoadClassProfileDescriptors()
deactivate dex2oat

activate dex2oat
dex2oat->>dex2oat: Compile()

dex2oat->>CompilerDriver: CompileAll

CompilerDriver->>CompilerDriver: PreCompile
activate CompilerDriver
Note right of CompilerDriver: Resolve,Verify,InitializeClasses
deactivate CompilerDriver

activate CompilerDriver
CompilerDriver->>CompilerDriver: Compile()

activate CompilerDriver
CompilerDriver->>CompilerDriver: CompileDexFile()
Note right of CompilerDriver: 多线程编译,传递dex中的class_ids索引
activate CompilerDriver
CompilerDriver->>CompilerDriver: CompileClassVisitor

activate CompilerDriver
CompilerDriver->>CompilerDriver: CompileMethod()
Note right of CompilerDriver: AddCompiledMethod to CompilerDriver
deactivate CompilerDriver
deactivate CompilerDriver
deactivate CompilerDriver
deactivate CompilerDriver

deactivate dex2oat
deactivate dex2oat
deactivate dex2oat

dex2oat->>dex2oat: WriteOatFiles()
Note right of dex2oat: 输出.oat文件

dex2oat->>dex2oat: HandleImage()
Note right of dex2oat: 输出.art文件

oat文件格式

graph LR
OatDexFile("OatDexFile[N]")-->DexFile("DexFile[N]")-->TypeLookup-Table("TypeLookup-Table[N]")-->ClassOffsets("ClassOffsets[N]")
ClassOffsets-->OatClass0
ClassOffsets-->OatClass1
ClassOffsets-->OatClassXxx
OatClass1-->CompliedMethod0
OatClass1-->CompliedMethod1
OatClass1-->CompliedMethodXxx

oat文件和art文件的关系

·ImageHeader中有几个成员变量关联到oat文件里的信息。其中,oat_file_begin_指向oat文件加载到内存的虚拟地址(图中是0x700ec000),oat_data_begin_指向符号oatdata的值(图中为0x700ed000),oat_data_end_指向符号oatlastword的值(图中为0x740d7e5b)。_

·art文件里的ArtMethod对象的成员变量ptr_sized_fields_结构体的entry_point_from_quick_compiled_code_指向位于oat文件里对应的code_数组。

简单来说,可以将art文件看作是很多对象通过类似序列化的方法保存到文件里而得来的。当art文件通过mmap加载到内存时,这些文件里的信息就能转换成对象直接使用。

image-20210610095954261

参考《深入理解Android:Java虚拟机ART》的9.6.2.3

dex2oat

dex2oat main函数
int main(int argc, char** argv) {
  int result = art::dex2oat(argc, argv);
...
}
static int dex2oat(int argc, char** argv) {
...
 dex2oat->ParseArgs(argc, argv);
  if (dex2oat->UseProfileGuidedCompilation()) {
    if (!dex2oat->LoadProfile()) {
      return EXIT_FAILURE;
    }
  }
  dex2oat->Setup();
  bool result;
  if (dex2oat->IsImage()) {
    result = CompileImage(*dex2oat);
  } else {
    result = CompileApp(*dex2oat);
  }
   ...

当使用profile-guide 编译app时,会先 LoadProfile(),这里就是 load /data/misc/profiles/cur/0/packagename/primary.prof,进行解析出 class index 和 method index,放到 ProfileCompilationinfo 中;

如果当前的编译要生成 image时,走CompileImage流程,否则走CompileApp流程;

bool IsImage() const {
    return IsAppImage() || IsBootImage();//不论是编译boot image(boot.art)或者时 app 要生成image.
}
CompileApp和CompileImage
static int CompileApp(Dex2Oat& dex2oat) { dex2oat.Compile(); if (!dex2oat.WriteOatFiles()) { dex2oat.EraseOatFiles(); return EXIT_FAILURE; } dex2oat.DumpTiming(); return EXIT_SUCCESS;} static int CompileImage(Dex2Oat& dex2oat) { dex2oat.LoadClassProfileDescriptors(); dex2oat.Compile(); if (!dex2oat.WriteOatFiles()) { dex2oat.EraseOatFiles(); return EXIT_FAILURE; } // Creates the boot.art and patches the oat files. if (!dex2oat.HandleImage()) { return EXIT_FAILURE; } dex2oat.DumpTiming(); return EXIT_SUCCESS;

区别是:

  1. 编译image时需要 LoadClassProfileDescriptors() 产生 image_classes_ 集合

  2. 生成 image(HandleImage());

在生成的app image中将会包含 image_classes_ 集合中类的对象,不在 image_classes_集合中的app的类的对象,将不会被生成到 app-image中。

LoadClassProfileDescriptors()在从 profile信息中获取 image_classes_集合时,将会把 app dex 中的类以外的类,都过滤掉,比如 classpath dex 对应的类将不会生成到 app-image;


 void LoadClassProfileDescriptors() {
    if (profile_compilation_info_ != nullptr && app_image_) {
      std::set<DexCacheResolvedClasses> resolved_classes(profile_compilation_info_->GetResolvedClasses()); // 获取 profile信息中记录的所有 class
      // Filter out class path classes since we don't want to include these in the image.
      std::unordered_set<std::string> dex_files_locations;
      for (const DexFile* dex_file : dex_files_) {
        dex_files_locations.insert(dex_file->GetLocation());  // 当前app的所有dex file
      }
      for (auto it = resolved_classes.begin(); it != resolved_classes.end(); ) {
        if (dex_files_locations.find(it->GetDexLocation()) == dex_files_locations.end()) {  // 如果这个类不在当前app 的dex file中,则过滤掉
          VLOG(compiler) << "Removed profile samples for non-app dex file " << it->GetDexLocation();
          it = resolved_classes.erase(it);
        } else {
          ++it;
        }
      }
      image_classes_.reset(new std::unordered_set<std::string>(runtime->GetClassLinker()->GetClassDescriptorsForProfileKeys(resolved_classes)));
    }

dex2oat流程总结
  1. 根据dex2oat接收到的参数,组织编译参数
  2. 如果是 profile-guide 编译,则先进行 load app对应的 profile
  3. 收集参数中包含的所有dex file,启动 Compiler 编译这些dex file(classpath中对应的dex file,即uses-library 引用的jar文件,不会被编译),编译生成的数据放在compiler-driver中
  4. 使用 compiler-driver 中的数据,依据 oat文件设计的格式,组织成oat文件,嵌入到 ELF文件中
  5. 如果指定需要生成 app-image,则使用 HandleImage(), 生成app-image, 即 ***.art 文件

Compile 流程

 // Create and invoke the compiler driver. This will compile all the dex files.
  void Compile() {
  ...
    driver_.reset(new CompilerDriver(compiler_options_.get(),
                                     verification_results_.get(),
                                     &method_inliner_map_,
                                     compiler_kind_,
                                     instruction_set_,
                                     instruction_set_features_.get(),
                                     IsBootImage(),
                                     IsAppImage(),
                                     image_classes_.release(),
                                     compiled_classes_.release(),
                                     /* compiled_methods */ nullptr,
                                     thread_count_,
                                     dump_stats_,
                                     dump_passes_,
                                     compiler_phases_timings_.get(),
                                     swap_fd_,
                                     profile_compilation_info_.get()));
    driver_->SetDexFilesForOatFile(dex_files_);
    driver_->CompileAll(class_loader_, dex_files_, timings_);

编译dex文件时在 CompilerDriver 中完成, 其中LoadProfile时构造的 ==profile_compilation_info_也会指导 将要编译哪些class和 methods==。

driver_->SetDexFilesForOatFile(dex_files_);//表示将要编译的所有 dex file,这个集合是 –dex-file=/data/app/com.facebook.katana-1/base.apk 这个文件中包含的所有dex文件,比如facebook的apk中有 12个 dex文件,则会依次编译这12个文件。

void CompilerDriver::CompileAll(jobject class_loader,
                                const std::vector<const DexFile*>& dex_files,
                                TimingLogger* timings) {
  InitializeThreadPools();
  // Precompile:
  // 1) Load image classes
  // 2) Resolve all classes
  // 3) Attempt to verify all classes
  // 4) Attempt to initialize image classes, and trivially initialized classes
  PreCompile(class_loader, dex_files, timings);
  // Compile:
  // 1) Compile all classes and methods enabled for compilation.
  if (!GetCompilerOptions().VerifyAtRuntime()) {
    Compile(class_loader, dex_files, timings);
  }
}
 
void CompilerDriver::PreCompile(jobject class_loader,
                                const std::vector<const DexFile*>& dex_files,
                                TimingLogger* timings) {
   LoadImageClasses(timings); //这里只针对 bootimage的编译
   Resolve(class_loader, dex_files, timings);
  Verify(class_loader, dex_files, timings);
  InitializeClasses(class_loader, dex_files, timings);
}
 
void CompilerDriver::Verify(jobject class_loader,
                            const std::vector<const DexFile*>& dex_files,
                            TimingLogger* timings) {
  for (const DexFile* dex_file : dex_files) {
    CHECK(dex_file != nullptr);
    VerifyDexFile(class_loader,
                  *dex_file,
                  dex_files,
                  parallel_thread_pool_.get(),
                  parallel_thread_count_,
                  timings);
  }
}
 
void CompilerDriver::VerifyDexFile(...){
  ...
  VerifyClassVisitor visitor(&context, log_level);
  context.ForAll(0, dex_file.NumClassDefs(), &visitor, thread_count);
}
 
class VerifyClassVisitor : public CompilationVisitor {
 public:
  VerifyClassVisitor(const ParallelCompilationManager* manager, LogSeverity log_level)
     : manager_(manager), log_level_(log_level) {}
  virtual void Visit(size_t class_def_index) REQUIRES(!Locks::mutator_lock_) OVERRIDE {
    if (!manager_->GetCompiler()->ShouldVerifyClassBasedOnProfile(dex_file, class_def_index)) {
      // Skip verification since the class is not in the profile.
      return;
    }
    ...
  }
}
 
bool CompilerDriver::ShouldVerifyClassBasedOnProfile(const DexFile& dex_file,
                                                     uint16_t class_idx) const {
  ...
  bool result = profile_compilation_info_->ContainsClass(dex_file, class_idx);
  return result;

在这里可以看到,==前面从 profile中load出来的信息,将会决定只有这些 class才会进行Verify==。

接下来看下真正的编译,实际上编译对应的是 dalvik bytecode到 native code的转换,主要针对的 method;

void CompilerDriver::Compile(jobject class_loader,
                             const std::vector<const DexFile*>& dex_files,
                             TimingLogger* timings) {
  for (const DexFile* dex_file : dex_files) {
    CHECK(dex_file != nullptr);
    CompileDexFile(class_loader, *dex_file, dex_files, parallel_thread_pool_.get(), parallel_thread_count_, timings); // 按照dexfile 依次编译
  }
  ...
}
void CompilerDriver::CompileDexFile(jobject class_loader,
                                    const DexFile& dex_file,
                                    const std::vector<const DexFile*>& dex_files,
                                    ThreadPool* thread_pool,
                                    size_t thread_count,
                                    TimingLogger* timings) {
  TimingLogger::ScopedTiming t("Compile Dex File", timings);
  ParallelCompilationManager context(Runtime::Current()->GetClassLinker(), class_loader, this,
                                     &dex_file, dex_files, thread_pool);
  CompileClassVisitor visitor(&context);
  context.ForAll(0, dex_file.NumClassDefs(), &visitor, thread_count); //从dexfile的第一个class,直到最后一个class

编译的工作在 CompileClassVisitor 的Visit方法中进行;

class CompileClassVisitor : public CompilationVisitor {
 public:
  explicit CompileClassVisitor(const ParallelCompilationManager* manager) : manager_(manager) {}
  virtual void Visit(size_t class_def_index) REQUIRES(!Locks::mutator_lock_) OVERRIDE { // 传递的参数为 class在 dexfile中的 index,以此来查找class 数据
    const DexFile::ClassDef& class_def = dex_file.GetClassDef(class_def_index);
    const char* descriptor = dex_file.GetClassDescriptor(class_def);
    Handle<mirror::Class> klass(hs.NewHandle(class_linker->FindClass(soa.Self(), descriptor, class_loader)));
    const uint8_t* class_data = dex_file.GetClassData(class_def);
    ClassDataItemIterator it(dex_file, class_data);
 
    while (it.HasNextDirectMethod()) { // 编译direct mothod
      uint32_t method_idx = it.GetMemberIndex();
      CompileMethod(soa.Self(), driver, it.GetMethodCodeItem(), it.GetMethodAccessFlags(),
                    it.GetMethodInvokeType(class_def), class_def_index,
                    method_idx, jclass_loader, dex_file, dex_to_dex_compilation_level,
                    compilation_enabled, dex_cache);
      it.Next();
    }
    while (it.HasNextVirtualMethod()) { // 编译virtual methods
      uint32_t method_idx = it.GetMemberIndex();
      CompileMethod(soa.Self(), driver, it.GetMethodCodeItem(), it.GetMethodAccessFlags(),
                    it.GetMethodInvokeType(class_def), class_def_index,
                    method_idx, jclass_loader, dex_file, dex_to_dex_compilation_level,
                    compilation_enabled, dex_cache);
      it.Next();
    }
    ...
  }

从这一步中,我们可以看到,编译代码工作,主要的就是编译 method成为 native code;

CompileMethod
static void CompileMethod(Thread* self,
                          CompilerDriver* driver,
                          const DexFile::CodeItem* code_item,
                          uint32_t access_flags,
                          InvokeType invoke_type,
                          uint16_t class_def_idx,
                          uint32_t method_idx,
                          jobject class_loader,
                          const DexFile& dex_file,
                          optimizer::DexToDexCompilationLevel dex_to_dex_compilation_level,
                          bool compilation_enabled,
                          Handle<mirror::DexCache> dex_cache)
    REQUIRES(!driver->compiled_methods_lock_) {
    MethodReference method_ref(&dex_file, method_idx);
    if ((access_flags & kAccNative) != 0) { // 编译 JNI 函数
      compiled_method = driver->GetCompiler()->JniCompile(access_flags, method_idx, dex_file);
    } else if((access_flags & kAccAbstract) != 0) { // abstract 函数没有代码,不需要编译
    } else { //编译其他函数
    const VerifiedMethod* verified_method =
        driver->GetVerificationResults()->GetVerifiedMethod(method_ref);
    bool compile = compilation_enabled &&
        driver->GetVerificationResults()
            ->IsCandidateForCompilation(method_ref, access_flags) &&
        verified_method != nullptr &&
        !verified_method->HasRuntimeThrow() &&
        (verified_method->GetEncounteredVerificationFailures() &
            (verifier::VERIFY_ERROR_FORCE_INTERPRETER | verifier::VERIFY_ERROR_LOCKING)) == 0 &&
        driver->IsMethodToCompile(method_ref) &&
        driver->ShouldCompileBasedOnProfile(method_ref);// 如果是profile-guide编译,需要检查是否是 profile中指定的函数,如果不是,则不编译该函数
    if (compile) {
      // NOTE: if compiler declines to compile this method, it will return null.
      compiled_method = driver->GetCompiler()->Compile(code_item, access_flags, invoke_type,
                                                       class_def_idx, method_idx, class_loader,
                                                       dex_file, dex_cache);
    }
    ...
    driver->AddCompiledMethod(method_ref, compiled_method, non_relative_linker_patch_count);//把编译得到的 compiled-method 添加到 compiler-driver中,以便后面生成oat文件时使用
    }



bool CompilerDriver::ShouldCompileBasedOnProfile(const MethodReference& method_ref) const {
  if (profile_compilation_info_ == nullptr) {
    // If we miss profile information it means that we don't do a profile guided compilation.
    // Return true, and let the other filters decide if the method should be compiled.
    return true;
  }
  bool result = profile_compilation_info_->ContainsMethod(method_ref);// 判断当前method是不是在前面 load到的 profile 中
  return result;

compiled-method 的生成过程,是真正ART编译器工作的过程,使用了图等算法进行编译,非常复杂,这里不再详述,总之,这个过程中,完成了dalvik bytecode 到 native code的转化以及一定的优化,到这一步,我们得到了产出: compiled-method,ART运行过程中,执行函数时,如果这个函数被编译过,那么就会执行其对应的 compiled-method,否则继续解释执行其对应的 dalvik bytecode。

Compile流程总结
  1. PreCompile 做一些准备工作,ResolveClass(可以认为是从dex文件中构造class到内存中),VerifyClass(验证错误),InitializeClass(初始化)等动作,做一些过滤动作,比如把verify失败的class过滤掉
  2. Compile过程,多线成编译,线程数目是 CPU count -1, 最小编译单位是 method,依次按照method所在 dex,所在class进行编译
  3. ==如果存在profile的情况下,Verify过程只对profile中存在的Class进行verify,CompileMethod过程,只对profile中存在的method进行编译==
  4. 编译后生成的compiled-method 放到 compiler-driver中,以备在dex2oat中,准备写入OAT文件时使用

OAT 文件写入流程

在Compile流程结束后,会进行OAT文件的写入操作。

enum class WriteState {
    kAddingDexFileSources, // 添加dex文件到 oat文件中
    kPrepareLayout, //准备文件布局
    kWriteRoData, //写入RoData
    kWriteText, //写入代码段
    kWriteHeader, // 写入 oat header
    kDone // 写入完成
  }

从OatWriteState可以看到,其写入oat文件的流程。

  1. AddDexFileSource,在dex2oat Setup时,就已经将将要编译的dex file 写入到 OatWriter 中,并设置 write_state_ = WriteState::kPrepareLayout;
  2. 后续的步骤都在编译完成后,由 WriteOatFiles 完成
  3. kPrepareLayout,初始化 OatClass,OatMaps,OatCode, 准备OatMethod信息 和 bss段的DexCacheArray
  4. kWriteRoData,写入 readOnly 数据,依次写入 ClassOffset,写入 OatClass,写入函数的vmap Table,写入 padding
  5. kWriteText,对于要生成 bootimage时,写入trampoline,对与app只写入quick code
  6. kWriteHeader,填充 Oat Header信息,写入到oat文件
bool Setup() {
    CreateOatWriters();
    if (!AddDexFileSources()) {
      return false;
    }
 }
 
  bool WriteOatFiles() {
    if (IsImage()) { // 如果本次dex2oat要生成 image,则会在写入 oat文件时,做准备工作
      image_writer_.reset(new ImageWriter(*driver_, image_base_, compiler_options_->GetCompilePic(),IsAppImage(), image_storage_mode_, oat_filenames_, dex_file_oat_index_map_));
      if (!image_writer_->PrepareImageAddressSpace()) {
        LOG(ERROR) << "Failed to prepare image address space.";
        return false;
      }
    }
        oat_writer->PrepareLayout(driver_.get(), image_writer_.get(), dex_files, &patcher);
        size_t rodata_size = oat_writer->GetOatHeader().GetExecutableOffset();
        size_t text_size = oat_writer->GetSize() - rodata_size;
        elf_writer->SetLoadedSectionSizes(rodata_size, text_size, oat_writer->GetBssSize());
        if (!oat_writer->WriteRodata(rodata)) {
          LOG(ERROR) << "Failed to write .rodata section to the ELF file " << oat_file->GetPath();
          return false;
        }
        OutputStream* text = elf_writer->StartText();
        if (!oat_writer->WriteCode(text)) {
          LOG(ERROR) << "Failed to write .text section to the ELF file " << oat_file->GetPath();
          return false;
        }
        if (!oat_writer->WriteHeader(elf_writer->GetStream(),
                                     image_file_location_oat_checksum_,
                                     image_file_location_oat_data_begin_,
                                     image_patch_delta_)) {
          LOG(ERROR) << "Failed to write oat header to the ELF file " << oat_file->GetPath();
          return false;
        }

OAT文件的写入流程就是按照这几个步骤完成,可以参照oat文件的加载完成OAT文件格式的详细了解。

Toc of my detailed dex2oat.vsdx below

ParseArgs(argc, argv)–>创建CompilerOptions

OpenFile()创建目标oat文件

Setup()

创建VerificationResults

  verification_results_.reset(new VerificationResults(compiler_options_.get()));

配置QuickCompilerCallbacks

在做类校验时,外界可以传递一个回调接口对象。

·当类校验失败时,该接口对象的ClassRejected函数将被调用。

·当类的Java方法校验通过时,该接口对象的MethodVerified函数将被调用。

  callbacks_.reset(new QuickCompilerCallbacks( verification_results_.get(),&method_inliner_map_,
   IsBootImage() ?   CompilerCallbacks::CallbackMode::kCompileBootImage : CompilerCallbacks::CallbackMode::kCompileApp));

ClassRejected()

MethodVerified()

去虚拟化de virtual得到concrete_method
const VerifiedMethod* VerifiedMethod::Create(
                    verifier::MethodVerifier* method_verifier, bool compile) {
     if (compile) {//compile为true时表示这个方法将会被编译。
        //如果这个Java方法中有invoke-virtual或invoke-interface相关的指令,则下面if的条
        //件满足
        if (method_verifier->HasVirtualOrInterfaceInvokes()) {
            //去虚拟化de virtual。下面将介绍这个函数
            verified_method->GenerateDevirtMap(method_verifier);
        }

创建elf_writers_和oat_writers_

配置oat_writers_[0]中的oat_dex_files_

WriteAndOpenDexFiles_WriteTypeLookupTables

CompileImage

在dex2oat中,一个Java方法根据其具体情况有三种编译处理模式

1: dex到dex的编译

2: jni方法的编译

3: dex字节码到机器码的编译

dex2oat.Compile

compilerDriver.CompileAll

ClassLinker.ResolveType,ResolveField和ResolveMethod
void CompilerDriver::PreCompile(jobject class_loader,
                    const std::vector<const DexFile*>& dex_files,....) {
  ......
    if ((never_verify || verification_enabled) && !verify_only_profile) {
        /*下面的Resolve函数主要工作为遍历dex文件,然后:
          (1)解析其中的类型,即遍历dex文件里的type_ids数组。内部将调用ClassLinker的ResolveType函数。
          (2)解析dex里的类、成员变量、成员函数。内部将调用ClassLinker的ResolveType、ResolveField和ResolveMethod等函数。读者可回顾8.7.8.1节的内容。 */
        Resolve(class_loader, dex_files, timings);
    }
   /*下面两个函数的作用:
      (1)Verify:遍历dex文件,校验其中的类。校验结果通过QuickCompilationCallback存储在
       CompilerDriver的verification_results_中。
      (2)InitializeClasses:遍历dex文件,确保类的初始化。*/
    Verify(class_loader, dex_files, timings);
    InitializeClasses(class_loader, dex_files, timings);
}

image-20210208142938825

CompileDexFile
void CompilerDriver::Compile(jobject class_loader,
            const std::vector<const DexFile*>& dex_files, TimingLogger* timings) {
    for (const DexFile* dex_file : dex_files) {
        CompileDexFile(class_loader,*dex_file,dex_files,......);
        ......
    }

void CompilerDriver::CompileDexFile(jobject class_loader,
        const DexFile& dex_file, const std::vector<const DexFile*>& dex_files,
        ThreadPool* thread_pool, size_t thread_count, TimingLogger* timings) {
    CompileClassVisitor visitor(&context);
    /*context.ForAll将触发线程池进行编译工作。注意,编译是以类为单位进行处理的,每一个待编译
      的类都会交由CompileClassVisitor的Visit函数进行处理。*/
    context.ForAll(0, dex_file.NumClassDefs(), &visitor, thread_count);
}
 //编译时,编译线程将调用下面的这个Visit函数,参数为待处理类在dex文件里class_ids数组中的索引
    virtual void Visit(size_t class_def_index) ..... {
           //遍历direct的Java方法
       int64_t previous_direct_method_idx = -1;
        while (it.HasNextDirectMethod()) {
            uint32_t method_idx = it.GetMemberIndex();
        .....
            previous_direct_method_idx = method_idx;
            CompileMethod(soa.Self(), driver, it.GetMethodCodeItem(),
                    it.GetMethodAccessFlags(),it.GetMethodInvokeType(class_def),
                    class_def_index, method_idx, jclass_loader, dex_file,
                    dex_to_dex_compilation_level,compilation_enabled,
                    dex_cache);
            it.Next();
        }
        //编译虚函数,也是调用CompileMethod函数
    }

CompileMethod()
driver->AddCompiledMethod()

WriteOatFiles输出.oat文件

HandleImage处理.art文件

Detail in dex2oat.vsdx