jadx源码阅读(一)

2,145 阅读4分钟

这里只分析主线的源码

入口:MainWindow

GUI层级选择完文件之后便跳转到MainWindow的open方法中,打开文件分两种类型,一种是后缀为.jadx的,另一种是其它的类型,比如:apk、aar、jar等,其中apk是最复杂的。

private final transient JadxWrapper wrapper;

void open(Path path, Runnable onFinish) {
      backgroundExecutor.execute(NLS.str("progress.load"),
            () -> wrapper.openFile(path.toFile()),
            () -> {
             ......
            });
}

打开文件使用的是JadxWrapper类中的openFile方法,具体如下:

public void openFile(File file) {
   close();
   this.openFile = file;
   try {
      JadxArgs jadxArgs = settings.toJadxArgs();
      jadxArgs.setInputFile(file);

      this.decompiler = new JadxDecompiler(jadxArgs);
      this.decompiler.load();
   } catch (Exception e) {
      LOG.error("Jadx init error", e);
      close();
   }
}

JadxArgs是一个实体类,里面除了存放输入和输出的文件、输出的文件夹、反编译线程池的大小等,存放;设置完输入的文件后,使用JadxDecompiler执行反编译操作,我们接着看下它的load方法:sfsad

private JadxPluginManager pluginManager = new JadxPluginManager();

public void load() {
   ......
   loadInputFiles();

   root = new RootNode(args);
   root.loadClasses(loadedInputs);
   root.initClassPath();
   root.loadResources(getResources());
   root.initPasses();
   root.runPreDecompileStage();
}

private void loadInputFiles() {
   loadedInputs.clear();
   List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
   for (JadxInputPlugin inputPlugin : pluginManager.getInputPlugins()) {
      loadedInputs.add(inputPlugin.loadFiles(inputPaths));
   }
}

JadxDecompiler是一个非常核心的管理类,里面维护了一个反编译的线程池,用ConcurrentHashMap结构存储类、方法、变量的信息,Root节点信息,资源列表,java类的列表(这个和前面ConcurrentHashMap存储的方法有关联),利用多线程可以提高反编译的效率;

下面具体看下代码相关的,里面很重要的一个方法是loadInputFiles,默认的输入文件是1个,然后JadxPluginManager会根据项目中加载的插件去加载文件的内容。

JadxPluginManager负责加载和管理所有的JadxInputPlugin插件,这些插件是使用ServiceLoader进行加载的,JadxInputPlugin是一个接口,具体在jadx-plugins-api模块中,它属于jadx-plugins下的子模块,整个项目中继承JadxInputPlugin的类有两个,分别是:JavaConvertPlugin(在jadx-java-convert下)和DexInputPlugin(在jadx-dex-input下)。SericeLoader属于java SPI(Service Provider Interfaces)范畴,能够很好的实现服务提供和使用解耦。

然后具体看loadInputFiles方法,核心的点在于JadxPluginManager使用不同的plugin加载文件的内容,jadx中主要使用的就是前面讲到的两个插件:JavaConvertPlugin和DexInputPlugin

JavaConvertPlugin详解

@Override
public JadxPluginInfo getPluginInfo() {
   return new JadxPluginInfo("java-convert", "JavaConvert", "Convert .jar and .class files to dex");
}

根据getPluginInfo的第三个参数的描述,JavaConvertPlugin的主要功能是将.jar、.class转为dex

@Override
public ILoadResult loadFiles(List<Path> input) {
   ConvertResult result = JavaConvertLoader.process(input);
   if (result.isEmpty()) {
      result.deleteTemp();
      return EmptyLoadResult.INSTANCE;
   }
   List<DexReader> dexReaders = DexFileLoader.collectDexFiles(result.getConverted());
   return new DexLoadResult(dexReaders) {
      @Override
      public void close() throws IOException {
         super.close();
         result.deleteTemp();
      }
   };
}

接下来具体看下JavaConvertPlugin的loadFiles方法,JavaConvertLoader会将文件处理一下,主要的功能是判断是否是.jar或者.class文件,如果文件是apk,result的结果为空。

DexInputPlugin详解

@Override
public JadxPluginInfo getPluginInfo() {
   return new JadxPluginInfo("dex-input", "DexInput", "Load .dex and .apk files");
}

DexInputPlugin的主要功能是加载.dex和apk文件

@Override
public ILoadResult loadFiles(List<Path> input) {
   return new DexLoadResult(DexFileLoader.collectDexFiles(input));
}

DexInputPlugin的 loadFiles核心是用DexFileLoader加载的,DexLoadResult只不过是将结果进行了转换,接下来看DexFileLoader核心部分:

private static List<DexReader> loadDexFromPath(Path path, int depth) {
   try (InputStream inputStream = Files.newInputStream(path, StandardOpenOption.READ)) {
      byte[] magic = new byte[DexConsts.MAX_MAGIC_SIZE];
      if (inputStream.read(magic) != magic.length) {
         return Collections.emptyList();
      }
      if (isStartWithBytes(magic, DexConsts.DEX_FILE_MAGIC)) {
         return Collections.singletonList(new DexReader(path));
      }
      if (depth == 0 && isStartWithBytes(magic, DexConsts.ZIP_FILE_MAGIC)) {
         return collectDexFromZip(path, depth);
      }
   } catch (Exception e) {
      LOG.error("File open error: {}", path, e);
   }
   return Collections.emptyList();
}
//Collections.singletonList()返回的是不可变的集合,但是这个长度的集合只有1

这里使用读取4个字节的方式,来判断是不是我们需要的文件,然后根据文件hex头部判断是dex或者zip,其中dex的文件头魔法值为:0x64, 0x65, 0x78, 0x0a ,zip文件的文件头魔法值为:0x50, 0x4B, 0x03, 0x04,大家可以使用notepad++或者sublime的hex插件对比下这两种文件的文件头。这两种方式本质上都会执行DexReader的方法,所以我们这边直接看DexReader的内容。

public DexReader(Path path) throws IOException {
   this.path = path;
   this.buf = ByteBuffer.wrap(Files.readAllBytes(path));
   this.header = new DexHeader(new SectionReader(this, 0));
}

DexReader只负责将dex的所有的内容读取到ByteBuffer中,然后使用SectionReader将buffer复制一份,DexHeader负责读取属性的大小和偏移量,下面是截取的一小部分:

public DexHeader(SectionReader buf) {
   ......
   fieldIdsSize = buf.readInt();
   fieldIdsOff = buf.readInt();
   methodIdsSize = buf.readInt();
   methodIdsOff = buf.readInt();
   classDefsSize = buf.readInt();
   classDefsOff = buf.readInt();
   ......
}

核心的部分在SectionReader中,里面负责了变量、方法、类等信息的读取。至此,DexInputPlugin加载dex的大体流程已经介绍完了。

这整个的流程只是将apk当做一个zip文件进行读取,但并没有涉及到具体的读取操作,比如怎样将一个类完整的读取出来。