- PVSM.RU - https://www.pvsm.ru -
В этой статье будут рассмотрены четыре декомпилятора — Fernflower, CFR, Procyon и jadx — и произведено их сравнение по нескольким параметрам.
Дисклеймер: сравнение неформальное и не претендует на научность. Скорее, это просто обзор всех актуальных (на осень 2019) декомпиляторов Java-байткода.
Автор — Анна Явейн, разработчица Solar appScreener
Наш инструмент — Solar appScreener [1] — предназначен для поиска уязвимостей в коде. Среди прочих языков он может анализировать и Java-байткод. Но самого по себе анализа мало: нужно показать результаты пользователю так, чтобы он мог интегрировать их в процесс разработки. Для этого недостаточно просто сказать "посмотрите на 147-ую байткод-инструкцию в методе таком-то". Чтобы эта информация была полезна программисту, нужно как-то сопоставить эти ошибки с исходным кодом.
Сразу же возникает проблема: а что делать, если исходники недоступны? Решение: можно декомпилировать байткод, найти в нем строки, соответствующие выявленным уязвимостям, и показывать пользователю сообщения об ошибках, привязанные к строкам в декомпилированном коде.
Итого, нам нужно уметь делать две вещи:
Спойлер: ни в одном из известных декомпиляторов нету инструментария для того, чтобы осуществить второе. Так что соответствие между ошибками и строками декомпилированного кода мы строим отдельным этапом, уже после декомпиляции. О том, как это делается, рассказано в статье моего коллеги [2].
А сейчас я расскажу вам о первом пункте: собственно, декомпиляции.
Разные декомпиляторы заточены под разные задачи. Например, заявлено, что Fernflower — аналитический (analytical) декомпилятор. Что это значит, нигде толком не объясняется, но по идее этот компилятор акцентирует внимание на более глубоком анализе и деобфускации кода. Для нас эта функциональность не очень важна (во всяком случае, при отображении результатов анализа). В целом, приоритетом для нас является понятность и читаемость получающегося кода.
Так что основные требования к инструментам таковы:
foreach
, try-with-resources
, etc).Сравнение проводилось, исходя из этих соображений, и может быть неприменимо в случаях, когда к декомпиляторам предъявляются иные требования.
Для сравнения были выбраны четыре опенсорсных проекта (были взяты самые актуальные версии на момент написания этого текста — осень 2019 года). Остальные были забракованы еще на предварительном этапе и подробно не анализировались.
Полный список декомпиляторов, которые были рассмотрены, но не освещены подробно,
Есть еще декомпиляторы, работающие только под Windows: Cavaj, DJ Java Decompiler, JBVD, AndroChef. Вероятно, где-то существуют еще какие-то декомпиляторы, но мне о них ничего не известно. И это, вероятно, к лучшему...
Краткая техническая информация о декомпиляторах:
Fernflower | CFR | Procyon | jadx | |
---|---|---|---|---|
Лицензия | Apache 2.0 | MIT | Apache 2.0 | Apache 2.0 |
Библиотека | неофициальное зеркало на гитхабе [10] | Maven: org.benf.cfr | Maven: org.bitbucket.mstrobel | Bintray [11] |
Какие версии Java поддерживает | не указано | 8, частично 9 | большая часть 8 | частично 8 |
Написан на | Java 8 | Java 6 | Java 7 | Java 8 |
Документация | нет | есть! [12] | немножко [13] | README на гитхабе [14] |
Важно не забывать, что jadx в первую очередь предназначен для проектов под Android. И чтобы анализировать код, написанный под jvm, декомпилятор сначала конвертирует его с помощью инструмента dx. Поскольку эта конвертация сама по себе бывает некорректна, адекватное сравнение jadx с другими инструментами провести невозможно, поэтому в большинстве случаев функционал jadx рассматривается отдельно.
Также jadx поддерживает DEX только до 37 версии, из-за чего у него возникают проблемы, например, с обработкой лямбд.
В сравнении участвовали Fernflower (версия с Гитхаба за 16.09.19 [15]), 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
Если вам неинтересны детали и вы хотите сразу посмотреть результаты, это можно сделать в этом разделе [16].
С одной стороны, этот декомпилятор используется в Intellij IDEA, что гарантирует жизнь и поддержку этого проекта.
С другой стороны, Fernflower — часть проекта Intellij IDEA. У самого декомпилятора нет даже отдельного репозитория на гитхабе (только упомянутое выше неофициальное зеркало [10], ссылка на которое — единственный способ подключить Fernflower к своему проекту как зависимость).
Если судить по репозиторию на гитхабе, активного добавления новых фич в этот проект не наблюдается. Последний коммит в master
случился 3 месяца назад (состояние на осень 2019). Точнее понять, что происходит с этим проектом, трудно, так как кодовая база является частью репозитория Intellij IDEA.
Код пишется одним человеком, но релизы происходят регулярно (по нескольку раз в год). На все замечания, отправленные мной автору на почту, он ответил в течение нескольких дней и исправил ошибки в течение недели-двух. Только за время написания этой статьи вышел новый релиз (0.147), в котором починена одна из упомянутых ошибок.
Также этот проект относительно быстро развивается, и поддержка новых фич появляется в нем довольно оперативно.
Проект поддерживается, этим летом (2019) даже был новый релиз. Но в этом релизе не было добавлено никаких новых фич, только починены старые баги. В общем, создается впечатление, что проект не забыт, но развиваться он больше не будет.
Этот декомпилятор постоянно развивается, репозиторий и ишью-трекер на гитхабе очень живые и активные. 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
используется в качестве индекса массива. Это происходит из-за некорректной обработки тернарных операторов (подробнее этот случай рассмотрен в секции ниже [17]).
Отдельно стоит заметить, что 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 прект.
Для начала рассмотрим конструкции, с обработкой которых не справился только один из инструментов.
FullInstructionSequence.java [18]
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);
}
SSAConstructorSparseEx.java [19]
varmaparr[varmaparr[1] == null ? 0 : 1]
Стандартная и очень неприятная ошибка Procyon'а. Разобраться, что хотел сказать автор, не имея исходного кода под рукой, — задача не очень тривиальная, особенно в более сложных случаях:
varmaparr[varmaparr[1] != null];
IFernflowerPreferences.java [20]
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 ... }
}
Дальше рассмотрено некоторое количество более сложных случаев, с которыми не справляется уже большее количество инструментов.
VarVersionPair.java [21]
VarVersionsProcessor.java [22]
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 */);
Неоднозначность при вызове конструктора.
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 */);
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 */);
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 */);
Работает корректно, но есть лишние приведения типов.
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 (Writer out = new OutputStreamWriter(...)) {
<try-body>
}
catch (IOException ex) {
<catch-body>
}
Очевидно, не поддерживает 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>
}
У этого декомпилятора наблюдается регрессия: в версии 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>
}
try (final Writer out = new OutputStreamWriter(...)) {
out.write(content);
}
catch (IOException ex) {
<catch-body>
}
--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 [24]
graph.iterateExprents(exprent -> {
for (Entry<ClassWrapper, MethodWrapper> ent : mapClassMeths.entrySet()) {
<body>
}
return 0;
});
Лямбда обрабатывается корректно. Проблемы с for-each
не связаны с лямбдой и воспроизводятся без нее (см секцию про for-each
[25]).
graph.iterateExprents((exprentx) -> {
Iterator var3 = mapClassMeths.entrySet().iterator();
while(var3.hasNext()) {
Entry<ClassWrapper, MethodWrapper> ent = (Entry)var3.next();
<body>
}
return 0;
});
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;
});
graph.iterateExprents(exprent -> {
for (Map.Entry ent : mapClassMeths.entrySet()) {
<body>
}
return 0;
});
jadx пока не поддерживает некоторые новые инструкции. На Гитхабе есть соответствуюшая задача [26].
/*
// 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
for (int i = 0, k = 0; i < len; i++, k++) {
if (<condition>) {
...
k++;
}
...
}
Переменная i
вынесена во внешний скоуп и вызывает переопределение.
int i = 0;
for(int k = 0; i < len; ++k) {
if (<condition>) {
...
++k;
}
...
++i;
}
for (int i = 0, k = 0; i < len; ++i, ++k) {
if (<condition>) {
...
++k;
}
...
}
Здесь во внешнюю область видимости выносятся уже две переменные (но при этом переопределения не происходит).
int i = 0;
int k = 0;
while (i < len) {
if (<condition>) {
...
++k;
}
...
++i;
++k;
}
Так же, как и с CFR.
int i = 0;
int k = 0;
while (i < len) {
if (<condition>) {
...
k++;
}
...
i++;
k++;
}
Пара простых примеров из великого множества ошибок с выведением типа.
List<Exprent> lstOperands = new ArrayList<>();
ArrayList lstOperands = new ArrayList();
final List<Exprent> lstOperands = new ArrayList<Exprent>();
ArrayList<Exprent> lstOperands = new ArrayList<Exprent>();
List<Exprent> lstOperands = new ArrayList<>();
LinkedList<Statement> stack = new LinkedList<>();
stack.add(root); // root : RootStatement
stack.addAll(stat.getStats()); // stat.getStats() : Collection<Statements>
unchecked assignment
в первой строке.
LinkedList<Statement> stack = new LinkedList();
stack.add(root);
stack.addAll(stat.getStats())
Работает, хотя и ценой лишнего и бессмысленного приведения типов.
final LinkedList<Statement> stack = new LinkedList<Statement>();
stack.add(root);
stack.addAll((Collection<? extends Statement>)stat.getStats());
Последняя строка не компилируется потому, что Statement
нельзя добавить в список объектов типа <RootStatement>
.
LinkedList<RootStatement> stack = new LinkedList<RootStatement>();
stack.add(root);
stack.addAll(stat.getStats());
LinkedList<Statement> stack = new LinkedList<>();
stack.add(root);
stack.addAll(stat.getStats());
Statement.java [30]
protected HashSet<Statement> continueSet = new HashSet<>();
...
continueSet.addAll(st.buildContinueSet());
protected HashSet<Statement> continueSet = new HashSet<>();
...
this.continueSet.addAll(st.buildContinueSet());
Ошибка компиляции на последней строке.
protected HashSet<Statement> continueSet;
...
public Statement() {
this.continueSet = new HashSet<Statement>();
...
}
...
this.continueSet.addAll((Collection<?>)st.buildContinueSet());
protected HashSet<Statement> continueSet = new HashSet<>();;
...
this.continueSet.addAll(st.buildContinueSet());
protected HashSet<Statement> continueSet;
...
this.continueSet.addAll(st.buildContinueSet());
Дополнительно я посмотрела, как работает 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 с тремя уровнями вложенности, очень портят впечатление от получающегося в результате кода.
По результатам сравнения можно сказать следующее:
Обгоняет конкурентов и по читаемости кода (лучше обрабатывает синтаксический сахар типа for-each
, try-with-resources
и другие, при этом результат содержит меньшее количество семантических ошибок), и по скорости (особенно это заметно на файлах большого размера). Также CFR стабильно развивается и поддерживается разработчиком.
Из минусов — проект относительно молодой, разрабатывается одним человеком и, предположительно, довольно сырой (в одном из релизов произошла небольшая регрессия, которую, правда, быстро исправили; еще полгода назад результирующий код мог содержать синтаксические ошибки).
Более надежный и стабильный, но почти не развивается. Из-за этого начал отставать от CFR в смысле поддержки фич Java 9 и старше. Также Procyon до сих пор содержит довольно маргинальные баги (обработка некоторых тернарных операторов [17] и статических полей в интерфейсах [31]).
Не очень подходит для наших задач. Проигрывает конкурентам по скорости и качеству результата (во всяком случае на необфусцированных данных). С другой стороны, Fernflower используется в Intellij IDEA, что дает некоторые гарантии того, что проект не умрет в ближайшем будущем.
Единственный достойный (если вообще не единственный) декомпилятор, предназначенный для Android. Дает неплохие результаты, но работает нестабильно (иногда декомпилирует байткод в корректный, но абсолютно нечитаемый код). Не поддерживает некоторые фичи языка (например, try-with-resources
) и некоторые инструкции DVM старше 37 версии. Для декомпиляции JAR файлов не подходит в принципе.
P.S. уже после написания этого текста, нашлась вот такая статья: очень подробное сравнение декомпиляторов [32]. Статья формальная, научная, но оценивает декомпиляторы в основном со стороны корректности получающегося кода, не рассматривая такие метрики, как читаемость кода и скорость работы декомпилятора.
Автор: SolarSecurity
Источник [33]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/349881
Ссылки в тексте:
[1] Solar appScreener: https://rt-solar.ru/products/solar_appscreener/
[2] статье моего коллеги: https://habr.com/ru/company/solarsecurity/blog/312056/
[3] можно посмотреть на GitHub: https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine
[4] одним человеком: http://www.benf.org/
[5] Репозиторий на GitHub: https://github.com/leibnitz27/cfr
[6] Сайт с информацией о проекте: http://www.benf.org/other/cfr/index.html
[7] хостится на Bitbucket: https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler
[8] выложены на GitHub: https://github.com/skylot/jadx
[9] JD Project: http://java-decompiler.github.io/
[10] неофициальное зеркало на гитхабе: https://github.com/fesh0r/fernflower
[11] Bintray: https://bintray.com/skylot/jadx
[12] есть!: http://www.benf.org/other/cfr/api/index.html
[13] немножко: https://bitbucket.org/mstrobel/procyon/wiki/Decompiler%20API
[14] README на гитхабе: https://github.com/skylot/jadx/blob/master/README.md
[15] версия с Гитхаба за 16.09.19: https://github.com/JetBrains/intellij-community/commit/3e1257459560a5b3c7ba67096347eb67302eabf0#diff-20810b5ccdbdeab8b11cfecad864a39
[16] в этом разделе: #results
[17] секции ниже: #ternary
[18] FullInstructionSequence.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/code/FullInstructionSequence.java
[19] SSAConstructorSparseEx.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/modules/decompiler/sforms/SSAConstructorSparseEx.java
[20] IFernflowerPreferences.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/main/extern/IFernflowerPreferences.java
[21] VarVersionPair.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/modules/decompiler/vars/VarVersionPair.java
[22] VarVersionsProcessor.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/modules/decompiler/vars/VarVersionsProcessor.java
[23] ConsoleDecompiler.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/main/decompiler/ConsoleDecompiler.java
[24] ClassReference14Processor.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/main/ClassReference14Processor.java
[25] секцию про for-each
: #for-each
[26] задача: https://github.com/skylot/jadx/issues/384
[27] SwitchInstruction.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/code/SwitchInstruction.java
[28] ConcatenationHelper.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/modules/decompiler/ConcatenationHelper.java
[29] VarTypeProcessor.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/modules/decompiler/vars/VarTypeProcessor.java
[30] Statement.java: https://github.com/JetBrains/intellij-community/blob/master/plugins/java-decompiler/engine/src/org/jetbrains/java/decompiler/modules/decompiler/stats/Statement.java
[31] статических полей в интерфейсах: #static
[32] очень подробное сравнение декомпиляторов: https://arxiv.org/pdf/1908.06895.pdf
[33] Источник: https://habr.com/ru/post/489844/?utm_source=habrahabr&utm_medium=rss&utm_campaign=489844
Нажмите здесь для печати.