Сравнительный анализ некоторых Java-декомпиляторов

Сравнительный анализ некоторых Java-декомпиляторов


В этой статье будут рассмотрены четыре декомпилятора — Fernflower, CFR, Procyon и jadx — и произведено их сравнение по нескольким параметрам.


Дисклеймер: сравнение неформальное и не претендует на научность. Скорее, это просто обзор всех актуальных (на осень 2019) декомпиляторов Java-байткода.


Автор — Анна Явейн, разработчица Solar appScreener


Предыстория


Наш инструмент — Solar appScreener — предназначен для поиска уязвимостей в коде. Среди прочих языков он может анализировать и Java-байткод. Но самого по себе анализа мало: нужно показать результаты пользователю так, чтобы он мог интегрировать их в процесс разработки. Для этого недостаточно просто сказать "посмотрите на 147-ую байткод-инструкцию в методе таком-то". Чтобы эта информация была полезна программисту, нужно как-то сопоставить эти ошибки с исходным кодом.


Сразу же возникает проблема: а что делать, если исходники недоступны? Решение: можно декомпилировать байткод, найти в нем строки, соответствующие выявленным уязвимостям, и показывать пользователю сообщения об ошибках, привязанные к строкам в декомпилированном коде.


Итого, нам нужно уметь делать две вещи:


  • декомпилировать байткод;
  • строить соответствие между инструкциями в байткоде и строками исходного кода.

Спойлер: ни в одном из известных декомпиляторов нету инструментария для того, чтобы осуществить второе. Так что соответствие между ошибками и строками декомпилированного кода мы строим отдельным этапом, уже после декомпиляции. О том, как это делается, рассказано в статье моего коллеги .


А сейчас я расскажу вам о первом пункте: собственно, декомпиляции.


Что нам нужно от декомпилятора


Разные декомпиляторы заточены под разные задачи. Например, заявлено, что Fernflower — аналитический (analytical) декомпилятор. Что это значит, нигде толком не объясняется, но по идее этот компилятор акцентирует внимание на более глубоком анализе и деобфускации кода. Для нас эта функциональность не очень важна (во всяком случае, при отображении результатов анализа). В целом, приоритетом для нас является понятность и читаемость получающегося кода.


Так что основные требования к инструментам таковы:


  • читаемый и (по возможности) корректный код в результате;
  • поддержка синтаксического сахара (foreach, try-with-resources, etc).

Сравнение проводилось, исходя из этих соображений, и может быть неприменимо в случаях, когда к декомпиляторам предъявляются иные требования.


Инструменты


Для сравнения были выбраны четыре опенсорсных проекта (были взяты самые актуальные версии на момент написания этого текста — осень 2019 года). Остальные были забракованы еще на предварительном этапе и подробно не анализировались.



Полный список декомпиляторов, которые были рассмотрены, но не освещены подробно,


спрятан здесь.
  • JD-Core (он же JD Project ) — не опубликовано ни библиотеки, ни исходников (только плагины для сред разработки и gui). Единственная обертка, которая предоставляет cli, не обновляется и кидается эксепшнами.
  • Krakatau — написан на питоне, почти не обновляется, не поддерживает Java 8.
  • JAD — безнадежно устарел (не поддерживает даже Java 5).

Есть еще декомпиляторы, работающие только под Windows: Cavaj, DJ Java Decompiler, JBVD, AndroChef. Вероятно, где-то существуют еще какие-то декомпиляторы, но мне о них ничего не известно. И это, вероятно, к лучшему...


Краткая техническая информация о декомпиляторах:


Fernflower CFR Procyon jadx
Лицензия Apache 2.0 MIT Apache 2.0 Apache 2.0
Библиотека неофициальное зеркало на гитхабе Maven: org.benf.cfr Maven: org.bitbucket.mstrobel Bintray
Какие версии Java поддерживает не указано 8, частично 9 большая часть 8 частично 8
Написан на Java 8 Java 6 Java 7 Java 8
Документация нет есть! немножко README на гитхабе

Важно не забывать, что jadx в первую очередь предназначен для проектов под Android. И чтобы анализировать код, написанный под jvm, декомпилятор сначала конвертирует его с помощью инструмента dx. Поскольку эта конвертация сама по себе бывает некорректна, адекватное сравнение jadx с другими инструментами провести невозможно, поэтому в большинстве случаев функционал jadx рассматривается отдельно.
Также jadx поддерживает DEX только до 37 версии, из-за чего у него возникают проблемы, например, с обработкой лямбд.


Сравнение


В сравнении участвовали Fernflower ( версия с Гитхаба за 16.09.19 ), CFR (0.146), Procyon (0.5.36) и jadx (1.0.0). При этом сравнение с jadx проводилось не по всем параметрам.


Проект, на котором производилось сравнение, — сам Fernflower, так как у него относительно большая кодовая база, написанная целиком на Java 8. Причем в коде активно используются разные фичи языка. Код на более актуальной версии Java использовать было нельзя — Procyon не поддерживает Java 9 вообще, а CFR гарантирует поддержку только некоторых фич (про Fernflower ничего официально не сказано).


Строки запуска спрятаны тут.
java -jar fernflower.jar -dgs=1 -asc=1 -ind="    " <input-jar> <output-dir> java -jar cfr-0.146.jar <input-jar> --outputpath <output-dir> java -jar procyon-decompiler-0.5.36.jar -jar <input-jar> -o <output-dir> ./bin/jadx -d <output-dir> <input-jar> --show-bad-code

Если вам неинтересны детали и вы хотите сразу посмотреть результаты, это можно сделать в этом разделе .


Метрики


  • Поддержка и активность проекта.
  • Количество ошибок при сборке результата декомпиляции.
  • Скорость.
  • Обработка некоторых фич языка.

Поддержка и активность проекта


Fernflower


С одной стороны, этот декомпилятор используется в Intellij IDEA, что гарантирует жизнь и поддержку этого проекта.


С другой стороны, Fernflower — часть проекта Intellij IDEA. У самого декомпилятора нет даже отдельного репозитория на гитхабе (только упомянутое выше неофициальное зеркало , ссылка на которое — единственный способ подключить Fernflower к своему проекту как зависимость).


Если судить по репозиторию на гитхабе, активного добавления новых фич в этот проект не наблюдается. Последний коммит в master случился 3 месяца назад (состояние на осень 2019). Точнее понять, что происходит с этим проектом, трудно, так как кодовая база является частью репозитория Intellij IDEA.


CFR


Код пишется одним человеком, но релизы происходят регулярно (по нескольку раз в год). На все замечания, отправленные мной автору на почту, он ответил в течение нескольких дней и исправил ошибки в течение недели-двух. Только за время написания этой статьи вышел новый релиз (0.147), в котором починена одна из упомянутых ошибок.


Также этот проект относительно быстро развивается, и поддержка новых фич появляется в нем довольно оперативно.


Procyon


Проект поддерживается, этим летом (2019) даже был новый релиз. Но в этом релизе не было добавлено никаких новых фич, только починены старые баги. В общем, создается впечатление, что проект не забыт, но развиваться он больше не будет.


jadx


Этот декомпилятор постоянно развивается, репозиторий и ишью-трекер на гитхабе очень живые и активные. 20 июня 2019 произошел релиз версии 1.0.0. Новые фичи и поддержка более актуальных версий DVM добавляются.


Количество ошибок при сборке результата декомпиляции


Суть сравнения в том, что все декомпиляторы были запущены на одном проекте, а результаты декомпиляции собраны. Это позволило получить общее представление о том, какие ошибки может допускать каждый из декомпиляторов и насколько вообще адекватен получающийся при декомпиляции код.


В этой секции jadx не рассматривается, так как он бросает 39 исключений при декомпиляции fernflower.jar и, следовательно, в принципе не декомпилирует большое количество кода.


Для начала заметим, что есть три класса ошибок: синтаксические (их не выявлено ни одной, хотя еще несколько версий назад в CFR их было несколько); семантические ошибки, связанные с типами (неправильно выведенные параметры у дженериков, ненайденные методы, некорректные приведения типов), и все остальные семантические ошибки.


Причины, по которым ошибки, связанные с выведением типов, выделены в отдельную категорию:


  • декомпиляторы теоретически не способны полностью корректно восстановить типы;
  • эти ошибки встречаются чаще, чем все остальные вместе взятые;
  • они относительно мало влияют на читаемость кода.

К тому же, количество ошибок, связанных с выводом типов, примерно одинаково (хотя CFR все-таки проигрывает соперникам).


Из всего этого можно сделать вывод, что нам гораздо интереснее не связанные с типами ошибки.


Синтаксические Все семантические Связанные с типами Остальные
Fernflower 0 101 65 36
CFR 0 82 80 2
Procyon 0 79 61 16

В коде, сгенерированном с помощью Fernflower, таких ошибок больше всего, причем 34 из 36 — это ошибки вида variable <var> is already defined. Две ошибки у CFR тоже связаны с переопределением переменных. В случае Procyon'а большинство (10 из 16) ошибок происходят из-за того, что переменная типа boolean используется в качестве индекса массива. Это происходит из-за некорректной обработки тернарных операторов (подробнее этот случай рассмотрен в секции ниже ).


Отдельно стоит заметить, что CFR — единственный из трех декомпиляторов, улучшивший свои показатели за последние 4 месяца. Раньше у него было 10 ошибок, не связанных с типами и 72 — про типы. Из этого можно предположить, что большое количество "типовых" ошибок у CFR связано с тем, что остальных ошибок у него меньше и, следовательно, больше пространства для неправильного вывода типов.


Скорость


Дисклеймер: еще раз замечаю, что это исследование не претендует на какую-либо научность.


Сравнение скорости работы было проведено достаточно топорно: проекты запускались на среднего размера джарниках по несколько раз, после чего выяснялось минимальное время работы.


Здесь приведены результаты для 100 итераций на JAR-файле размером 5.2M (JAR-файл, естественно, состоит только из .class файлов).


Время в секундах
Fernflower 74
CFR 43
Procyon 74

В следующей таблице — результаты для 15 запусков на JAR-файле в 14M.


Время в секундах
Fernflower 939
CFR 128
Procyon 573

По результатам можно предположить, что в этих декомпиляторах используются алгоритмы с разной асимптотикой. При этом CFR работает стабильно быстрее конкурентов, а на больших входных файлах Fernflower начинает довольно сильно тормозить. Впрочем, 14M — это очень много памяти для архива .class файлов и в реальности такие проекты попадаются довольно редко.


Обработка конкретных фич языка


Здесь я просто рассмотрела несколько важных конструкций языка и сравнила то, насколько качественно они обрабатываются разными декомпиляторами.


Краткое резюме этого параграфа показано в таблице ниже. При этом надо не забывать, что результаты, показанные jadx, не вполне релевантны. Для jadx в следующей секции проведен отдельный разбор, в котором в качестве подопытного взят Android прект.



Для начала рассмотрим конструкции, с обработкой которых не справился только один из инструментов.


name="for-each"> for-each


FullInstructionSequence.java


for (ExceptionHandler handler : handlers) {     handler.from_instr = this.getPointerByAbsOffset(handler.from);     handler.to_instr = this.getPointerByAbsOffset(handler.to);     handler.handler_instr = this.getPointerByAbsOffset(handler.handler); }

Fernflower всегда раскрывает for-each конструкции через итераторы. Причем делает он это не вполне корректно.


Например, здесь handler засоряет внешнюю область видимости, из-за чего может происходить переопределение переменной. К тому же, у итератора var3 не указан параметр типа, что приводит к unchecked cast в четвертой строке:


ExceptionHandler handler; for (Iterator var3 = handlers.iterator(); var3.hasNext(); handler.handler_instr = this.getPointerByAbsOffset(handler.handler)) {     handler = (ExceptionHandler)var3.next();     handler.from_instr = this.getPointerByAbsOffset(handler.from);     handler.to_instr = this.getPointerByAbsOffset(handler.to); }

name="ternary"> Тернарный оператор при индексации массива


SSAConstructorSparseEx.java


varmaparr[varmaparr[1] == null ? 0 : 1]

Стандартная и очень неприятная ошибка Procyon'а. Разобраться, что хотел сказать автор, не имея исходного кода под рукой, — задача не очень тривиальная, особенно в более сложных случаях:


varmaparr[varmaparr[1] != null];

name="static"> Статическое поле в интерфейсе


IFernflowerPreferences.java


public interface IFernflowerPreferences {     Map<String, Object> DEFAULTS = getDefaults();      static Map<String, Object> getDefaults() { ... } }

Загадочная ошибка, воспроизводящаяся только при использовании Procyon. Атрибут default, указанный вместо static в определении getDefaults(), порождает ошибку:


public interface IFernflowerPreferences {     public static final Map<String, Object> DEFAULTS = getDefaults();      // Error: non-static method getDefaults()      // cannot be referenced from a static context.         default Map<String, Object> getDefaults() { return ... } }

Остальные ошибки


Дальше рассмотрено некоторое количество более сложных случаев, с которыми не справляется уже большее количество инструментов.


Чтобы не пугать народ, они спрятаны под спойлером.

Явный unboxing


VarVersionPair.java


VarVersionsProcessor.java


public class VarVersionPair {        public final int var;     public final int version;      public VarVersionPair(int var, int version) {         this.var = var;         this.version = version;     }      public VarVersionPair(Integer var, Integer version) {         this.var = var;         this.version = version;     } }  //////////////////   new VarVersionPair(ent.getKey().var /* int */, version.intValue() /* int */);

Fernflower


Неоднозначность при вызове конструктора.


public class VarVersionPair {     public final int var;     public final int version;      public VarVersionPair(int var, int version) {         this.var = var;         this.version = version;     }      public VarVersionPair(Integer var, Integer version) {         this.var = var;         this.version = version;     } }  ////////////////////////  new VarVersionPair(((VarVersionPair)ent.getKey()).var/* int */, version/* Integer */);

Procyon


public class VarVersionPair {     public final int var;     public final int version;      public VarVersionPair(final int var, final int version) {         this.var = var;         this.version = version;     }      public VarVersionPair(final Integer var, final Integer version) {         this.var = var;         this.version = version;     } }  ////////////////////////  new VarVersionPair(ent.getKey().var /* int */, (int)version /* int */);

CFR


public class VarVersionPair {     public final int var;     public final int version;      public VarVersionPair(int var, int version) {         this.var = var;         this.version = version;     }      public VarVersionPair(Integer var, Integer version) {         this.var = var;         this.version = version;     } }  ////////////////////////  new VarVersionPair(ent.getKey().var /* int */, (int)version /* int */);

dx + jadx


Работает корректно, но есть лишние приведения типов.


public class VarVersionPair {     public final int var;     public final int version;      public VarVersionPair(int var, int version) {         this.var = var2;         this.version = version2;     }      public VarVersionPair(Integer var, Integer version) {         this.var = var.intValue();         this.version = version.intValue();     } }  ////////////////////////  new VarVersionPair(((VarVersionPair) ent.getKey()).var /* int */, ((Integer) it.next()).intValue() /* int */);

Try-with-resources


ConsoleDecompiler.java


try (Writer out = new OutputStreamWriter(...)) {     <try-body> } catch (IOException ex) {    <catch-body> }

Fernflower


Очевидно, не поддерживает try-with-resources совсем. Зато можно оценить, насколько сложно переписать его через обычный try-catch. Результат неоднозначный (:


try {     Writer out = new OutputStreamWriter(...);     Throwable var8 = null;     try {         <try-body>     } catch (Throwable var18) {         var8 = var18;         throw var18;     } finally {         if (out != null) {             if (var8 != null) {                 try {                     out.close();                 } catch (Throwable var17) {                     var8.addSuppressed(var17);                 }             } else {                 out.close();             }         }     } } catch (IOException var20) {     <catch-body> }

CFR


У этого декомпилятора наблюдается регрессия: в версии 0.142 try-with-resources обрабатывался нормально, а в версии 0.146 начал появляться лишний try.


UPD: эта ошибка исправлена в версии 0.147.


try {     try (OutputStreamWriter out = new OutputStreamWriter(...);){         out.write(content);     } } catch (IOException ex) {     <catch-body> }

Procyon


try (final Writer out = new OutputStreamWriter(...)) {     out.write(content); } catch (IOException ex) {     <catch-body> }

dx + jadx (with --show-bad-code option)


jadx не справляется и честно сообщает об этом.


/* JADX WARNING: Code restructure failed: missing block: B:20:0x0048, code lost:     r3 = move-exception;  */ /* JADX WARNING: Code restructure failed: missing block: B:21:0x0049, code lost:     if (r2 != null) goto L_0x004b;  */ /* JADX WARNING: Code restructure failed: missing block: B:22:0x004b, code lost:     if (r4 != null) goto L_0x004d;  */ /* JADX WARNING: Code restructure failed: missing block: B:24:?, code lost:     r2.close();  */ /* JADX WARNING: Code restructure failed: missing block: B:26:?, code lost:     throw r3;  */ /* JADX WARNING: Code restructure failed: missing block: B:29:0x0056, code lost:     r2.close();  */ ... try {     Writer out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);     Throwable th = null;     <try-body>     if (out == null) {         return;     }     if (th != null) {         try {             out.close();         } catch (Throwable th2) {             th.addSuppressed(th2);         }     } else {         out.close();     } } catch (IOException ex) {     <catch-body> } catch (Throwable th3) {     r4.addSuppressed(th3); }

Лямбды


ClassReference14Processor.java


graph.iterateExprents(exprent -> {     for (Entry<ClassWrapper, MethodWrapper> ent : mapClassMeths.entrySet()) {         <body>     }     return 0; });

Fernflower


Лямбда обрабатывается корректно. Проблемы с for-each не связаны с лямбдой и воспроизводятся без нее (см секцию про for-each ).


graph.iterateExprents((exprentx) -> {     Iterator var3 = mapClassMeths.entrySet().iterator();     while(var3.hasNext()) {         Entry<ClassWrapper, MethodWrapper> ent = (Entry)var3.next();         <body>     }     return 0; });

Procyon


for-each внутри лямбды обработан некорректно (при этом с самой конструкцией for-each Procyon обычно нормально справляется). При этом объявления переменных ent и iterator2 оказались вынесенными из лямбды, что привело к ошибке сборки, так как ent не является effectively final переменной.


final Iterator<Map.Entry<ClassWrapper, MethodWrapper>> iterator2; Map.Entry<ClassWrapper, MethodWrapper> ent;  graph.iterateExprents(exprent -> {     // it probably tried to initialize iterator here but it failed miserably...     mapClassMeths.entrySet().iterator();            while (iterator2.hasNext()) {         ent = iterator2.next();         <body>     }     return 0; });

CFR


graph.iterateExprents(exprent -> {     for (Map.Entry ent : mapClassMeths.entrySet()) {         <body>     }     return 0; });

dx + jadx


jadx пока не поддерживает некоторые новые инструкции. На Гитхабе есть соответствуюшая задача .


/* // Can't load method instructions: Load method exception: Unknown instruction: 'invoke-custom/range' in method:      org.jetbrains.java.decompiler.main.ClassReference14Processor.processClassRec(org.jetbrains.java.decompiler.main.ClassesProcessor$ClassNode, java.util.Map, java.util.Set):void, dex: classes.dex */ throw new UnsupportedOperationException("Method not decompiled: org.jetbrains.java.decompiler.main.ClassReference14Processor.processClassRec(org.jetbrains.java.decompiler.main.ClassesProcessor$ClassNode, java.util.Map, java.util.Set):void"); 

Две инициализации в одном for


SwitchInstruction.java


for (int i = 0, k = 0; i < len; i++, k++) {     if (<condition>) {         ...         k++;     }     ... }

Fernflower


Переменная i вынесена во внешний скоуп и вызывает переопределение.


int i = 0; for(int k = 0; i < len; ++k) {     if (<condition>) {         ...         ++k;     }     ...     ++i; }

Procyon


for (int i = 0, k = 0; i < len; ++i, ++k) {     if (<condition>) {         ...         ++k;     }     ... }

CFR


Здесь во внешнюю область видимости выносятся уже две переменные (но при этом переопределения не происходит).


int i = 0; int k = 0; while (i < len) {     if (<condition>) {         ...         ++k;     }     ...     ++i;     ++k; }

dx + jadx


Так же, как и с CFR.


int i = 0; int k = 0; while (i < len) {     if (<condition>) {         ...         k++;     }     ...     i++;     k++; }

Generics


Пара простых примеров из великого множества ошибок с выведением типа.




ConcatenationHelper.java


List<Exprent> lstOperands = new ArrayList<>();

Fernflower


ArrayList lstOperands = new ArrayList();

Procyon


final List<Exprent> lstOperands = new ArrayList<Exprent>();

CFR


ArrayList<Exprent> lstOperands = new ArrayList<Exprent>();

dx + jadx


List<Exprent> lstOperands = new ArrayList<>();



VarTypeProcessor.java


LinkedList<Statement> stack = new LinkedList<>(); stack.add(root);                // root : RootStatement stack.addAll(stat.getStats());  // stat.getStats() : Collection<Statements>

Fernflower


unchecked assignment в первой строке.


LinkedList<Statement> stack = new LinkedList(); stack.add(root); stack.addAll(stat.getStats())

Procyon


Работает, хотя и ценой лишнего и бессмысленного приведения типов.


final LinkedList<Statement> stack = new LinkedList<Statement>(); stack.add(root); stack.addAll((Collection<? extends Statement>)stat.getStats());

CFR


Последняя строка не компилируется потому, что Statement нельзя добавить в список объектов типа <RootStatement>.


LinkedList<RootStatement> stack = new LinkedList<RootStatement>(); stack.add(root); stack.addAll(stat.getStats()); 

dx + jadx


LinkedList<Statement> stack = new LinkedList<>(); stack.add(root); stack.addAll(stat.getStats()); 



Statement.java


protected HashSet<Statement> continueSet = new HashSet<>(); ... continueSet.addAll(st.buildContinueSet());

Fernflower


protected HashSet<Statement> continueSet = new HashSet<>(); ... this.continueSet.addAll(st.buildContinueSet());

Procyon


Ошибка компиляции на последней строке.


protected HashSet<Statement> continueSet; ... public Statement() {     this.continueSet = new HashSet<Statement>();     ... } ... this.continueSet.addAll((Collection<?>)st.buildContinueSet());

CFR


protected HashSet<Statement> continueSet = new HashSet<>();; ... this.continueSet.addAll(st.buildContinueSet());

dx + jadx


protected HashSet<Statement> continueSet; ... this.continueSet.addAll(st.buildContinueSet());

Работа jadx на dex файле


Дополнительно я посмотрела, как работает jadx на настоящем Android проекте — AntennaPod (приложение для прослушивания подкастов).


Самые одиозные ошибки и странности тут.

Статические поля


У jadx регулярно возникают проблемы с доступом к статическим полям. Например, байткод, полученный из таких исходников:


private static Context context;  public static void init(Context context) {   UpdateManager.context = context;   ... }

он превращает в такое:


private static Context context;  public static void init(Context context) {   context = context;   ... }

Лямбды и анонимные классы


На каждую лямбду или анонимный класс jadx генерирует отдельный именованный класс, например, из такой безобидной лямбды:


(item1, item2) -> compareLong(item1.timePlayed, item2.timePlayed)

получается такое:


/* compiled from: lambda */ /* renamed from: de.danoeh.antennapod.core.storage.-$$Lambda$DBReader$J14FiokVfxZ2H5XUZEtHQOEEq_0 */ public final /* synthetic */ class $$Lambda$DBReader$J14FiokVfxZ2H5XUZEtHQOEEq_0 implements Comparator {   public static final /* synthetic */    $$Lambda$DBReader$J14FiokVfxZ2H5XUZEtHQOEEq_0 INSTANCE              = new $$Lambda$DBReader$J14FiokVfxZ2H5XUZEtHQOEEq_0();    private /* synthetic */ $$Lambda$DBReader$J14FiokVfxZ2H5XUZEtHQOEEq_0() { }    public final int compare(Object obj, Object obj2) {     return DBReader.compareLong(((StatisticsItem)obj).timePlayed,                                  ((StatisticsItem)obj2).timePlayed);   } }

Хтонический ужас с добавлениями и удалениями переменных.


Иногда количество переменных резко увеличивается после декомпиляции. Было:


public Feed(...) {     this(id, lastUpdate, title, null, link,           description, paymentLink, author,           language, type, feedIdentifier, imageUrl,          fileUrl, downloadUrl, downloaded,           new FlattrStatus(), false, null, null, false); }

Стало:


public Feed(...) {     long j = id;     String str = lastUpdate;     String str2 = title;     String str3 = link;     String str4 = description;     String str5 = paymentLink;     String str6 = author;     String str7 = language;     String str8 = type;     String str9 = feedIdentifier;     String str10 = imageUrl;     String str11 = fileUrl;     String str12 = downloadUrl;     boolean z = downloaded;     FlattrStatus flattrStatus = r5;     FlattrStatus flattrStatus2 = new FlattrStatus();     this(j, str, str2, null, str3,           str4, str5, str6,           str7, str8, str9, str10,           str11, str12, z,           flattrStatus, false, null, null, false); }



А иногда, наоборот, jadx решает выкинуть парочку неугодных ему переменных. Были переменные:


URL url = new URI(BASE_SCHEME, BASE_HOST,                    String.format("/api/2/tags/%d.json", count), null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); JSONArray jsonTagList = new JSONArray(response);

и не стало переменных:


JSONArray jsonTagList      = new JSONArray(executeRequest(new Builder().url(         new URI(BASE_SCHEME,                  this.BASE_HOST,                 String.format("/api/2/tags/%d.json",                  new Object[]{Integer.valueOf(count)}), null).toURL())));

Очень странный случай


Непонятно как, но jadx из этого:


final String action = intent.getStringExtra(ARG_ACTION); if (action != null) {   switch(action) {     case ACTION_SYNC:       <code1>     case ACTION_SYNC_SUBSCRIPTIONS:       <code2>     case ACTION_SYNC_ACTIONS:       <code3>     default:       <code4>   } }

умудрился получить это:


String action = intent.getStringExtra(ARG_ACTION);         if (action != null) {             Object obj = -1;             int hashCode = action.hashCode();             if (hashCode != -1744995379) {                 if (hashCode != 29421060) {                     if (hashCode == 1497029227 && action.equals(ACTION_SYNC_ACTIONS)) {                         obj = 2;                     }                 } else if (action.equals(ACTION_SYNC_SUBSCRIPTIONS)) {                     obj = 1;                 }             } else if (action.equals(ACTION_SYNC)) {                 obj = null;             }   switch (obj) {     case null:       <code1>     case 1:       <code2>     case 2:       <code3>     default:       <code4>   } }

И еще раз


if(item != null) {     return item.getId() == id; }

превращается в...


FeedItem feedItem = this.item; boolean z = true; if (feedItem != null) {     if (feedItem.getId() != id) {         z = false;     }     return z; }

В общем, можно подытожить, что код, декомпилированный jadx, не очень стабилен в плане читаемости, хотя при этом довольно неплох со стороны корректности и разнообразия обрабатываемых конструкций. При этом редкие, но кошмарные ситуации, когда jadx добавляет в код 15 ненужных переменных или раскрывает простейший switch-case через if-else с тремя уровнями вложенности, очень портят впечатление от получающегося в результате кода.


name="results"> Результаты


По результатам сравнения можно сказать следующее:


CFR


Обгоняет конкурентов и по читаемости кода (лучше обрабатывает синтаксический сахар типа for-each, try-with-resources и другие, при этом результат содержит меньшее количество семантических ошибок), и по скорости (особенно это заметно на файлах большого размера). Также CFR стабильно развивается и поддерживается разработчиком.


Из минусов — проект относительно молодой, разрабатывается одним человеком и, предположительно, довольно сырой (в одном из релизов произошла небольшая регрессия, которую, правда, быстро исправили; еще полгода назад результирующий код мог содержать синтаксические ошибки).


Procyon


Более надежный и стабильный, но почти не развивается. Из-за этого начал отставать от CFR в смысле поддержки фич Java 9 и старше. Также Procyon до сих пор содержит довольно маргинальные баги (обработка некоторых тернарных операторов и статических полей в интерфейсах ).


Fernflower


Не очень подходит для наших задач. Проигрывает конкурентам по скорости и качеству результата (во всяком случае на необфусцированных данных). С другой стороны, Fernflower используется в Intellij IDEA, что дает некоторые гарантии того, что проект не умрет в ближайшем будущем.


jadx


Единственный достойный (если вообще не единственный) декомпилятор, предназначенный для Android. Дает неплохие результаты, но работает нестабильно (иногда декомпилирует байткод в корректный, но абсолютно нечитаемый код). Не поддерживает некоторые фичи языка (например, try-with-resources) и некоторые инструкции DVM старше 37 версии. Для декомпиляции JAR файлов не подходит в принципе.


P.S. уже после написания этого текста, нашлась вот такая статья: очень подробное сравнение декомпиляторов . Статья формальная, научная, но оценивает декомпиляторы в основном со стороны корректности получающегося кода, не рассматривая такие метрики, как читаемость кода и скорость работы декомпилятора.

Alt text

Где кванты и ИИ становятся искусством?

На перекрестке науки и фантазии — наш канал

Подписаться