Компилировать и декомпилировать код Java

Java

GitHub 2.5k ЗвездаПуть к тому, чтобы стать Java-инженером, почему бы тебе не прийти и не узнать?

GitHub 2.5k ЗвездаПуть к тому, чтобы стать Java-инженером, ты правда не хочешь узнать?

GitHub 2.5k ЗвездаПуть к тому, чтобы стать Java-инженером, ты действительно уверен, что не хочешь узнать?

Язык программирования

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

Машинный язык (Machine Language) и язык ассемблера (Assembly Language) — это языки низкого уровня, которые напрямую используют компьютерные инструкции для написания программ.

К языкам высокого уровня относятся C, C++, Java, Python и т. д. Программы пишутся с операторами, которые являются абстрактными представлениями компьютерных инструкций.

Например, одно и то же выражение выражается на языке C, ассемблере и машинном языке следующим образом:

Компьютеры могут работать только с числами. Символы, звуки и изображения должны быть представлены числами внутри компьютера, и инструкции не являются исключением. Машинный язык в приведенной выше таблице полностью состоит из шестнадцатеричных чисел. Первые программисты использовали машинный язык для программирования напрямую, но это было очень хлопотно. Нужно было просмотреть большое количество таблиц, чтобы определить, что означает каждое число. Программы, которые они писали, были очень неинтуитивными и подвержены ошибкам, поэтому они использовали ассемблер. язык и поставить машину. Группа чисел в языке представлена ​​мнемоникой (мнемоника), непосредственно используйте эти мнемоники для написания ассемблера, а затем пусть ассемблер (ассемблер) ищет таблицу, чтобы заменить мнемонику числами, а затем assemble Language переводится на машинный язык.

Однако язык ассемблера также более сложен в использовании, и позже были созданы языки высокого уровня, такие как Java, C и C++.

что такое компиляция

Есть два языка, упомянутых выше, язык низкого уровня и язык высокого уровня. Упрощенно это можно понять так: язык низкого уровня — это язык, распознаваемый компьютером, а язык высокого уровня — это язык, распознаваемый программистом.

Так как же преобразовать язык высокого уровня в язык низкого уровня? Этот процесс фактически компилируется.

Из приведенного выше примера также видно, что между операторами языка C и инструкциями языка низкого уровня нет простого однозначного соответствия.a=b+1Оператор ; должен быть преобразован в три ассемблерных или машинных инструкции.Этот процесс называется компиляцией, который выполняется компилятором.Очевидно, что функция компилятора намного сложнее, чем функция ассемблера. Программы, написанные на языке C, должны быть скомпилированы в машинные инструкции, прежде чем они смогут быть выполнены компьютером.Компиляция занимает некоторое время.Это недостаток программирования на языках высокого уровня, но это скорее преимущество. Во-первых, на C проще программировать, а код получается более компактным, читабельным и его легче исправить, если что-то пойдет не так.

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

Теперь мы знаем, что такое компиляция и что такое компилятор. Для разных языков есть свои компиляторы.Компилятором, отвечающим за компиляцию в языке Java, является команда:javac

javac — это компилятор языка Java, включенный в JDK. Этот инструмент может скомпилировать исходный файл с суффиксом .java в байт-код с суффиксом .class, который может работать на виртуальной машине Java.

когда мы закончим писатьHelloWorld.javaфайл, мы можем использоватьjavac HelloWorld.javaкоманда для генерацииHelloWorld.classфайл, этоclassТип файла — это тот, который JVM может распознать. Обычно мы думаем об этом процессе как о компиляции языка Java. фактически,classФайл по-прежнему не является языком, который машина может распознать, потому что машина может распознавать только машинный язык, а JVM нужноclassБайт-код типа файла преобразуется в машинный язык, понятный машине.

что такое декомпиляция

Процесс декомпиляции прямо противоположен компиляции, то есть восстановить скомпилированный язык программирования в нескомпилированное состояние, то есть найти исходный код языка программирования. Это преобразование языка, понятного машине, в язык, понятный программисту. Декомпиляция в языке Java обычно относится к преобразованиюclassфайл преобразован вjavaдокумент.

С помощью инструментов декомпиляции мы можем делать много вещей, основная функция которых заключается в том, что с помощью инструментов декомпиляции мы можем читать и понимать байт-код, сгенерированный компилятором Java. Если вы хотите спросить, какая польза от чтения байт-кода, то могу вам ответственно сказать, польза велика. Например, несколько типичных принципиальных статей в моем блоге получены путем анализа декомпилированного кода с помощью инструментов декомпиляции. Например, глубокое понимание многопоточности (1) — принцип реализации Synchronized, углубленный анализ типов перечисления Java — безопасность потоков и сериализация перечисления, переключение ответа Java на целочисленные, символьные и строковые типы. детали реализации, стирание типа Java и т. д. Недавно я написал статью о синтаксическом сахаре Java на GitChat, и большая часть ее использует инструменты декомпиляции, чтобы понять принципы, лежащие в основе синтаксического сахара.

Инструмент декомпиляции Java

В этой статье в основном представлены три инструмента декомпиляции Java:javap,jadиcfr

javap

javapЭто инструмент, поставляемый с jdk, который может декомпилировать код и просматривать байт-код, сгенерированный компилятором java.javapСамое большое отличие от двух других инструментов декомпиляции заключается в том, что файлы, которые он генерирует, неjavaфайл, и его не легче понять, чем два других инструмента, генерирующих код. Возьмем в качестве примера простой фрагмент кода, если мы хотим проанализировать код на Java 7.switchкак поддержатьStringДа, сначала у нас есть следующий исходный код, который можно скомпилировать:

public class switchDemoString {
    public static void main(String[] args) {
        String str = "world";
        switch (str) {
            case "hello":
                System.out.println("hello");
                break;
            case "world":
                System.out.println("world");
                break;
            default:
                break;
        }
    }
}

Выполните следующие две команды:

javac switchDemoString.java
javap -c switchDemoString.class

Сгенерированный код выглядит следующим образом:

public class com.hollis.suguar.switchDemoString {
  public com.hollis.suguar.switchDemoString();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // String world
       2: astore_1
       3: aload_1
       4: astore_2
       5: iconst_m1
       6: istore_3
       7: aload_2
       8: invokevirtual #3                  // Method java/lang/String.hashCode:()I
      11: lookupswitch  { // 2
              99162322: 36
             113318802: 50
               default: 61
          }
      36: aload_2
      37: ldc           #4                  // String hello
      39: invokevirtual #5                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      42: ifeq          61
      45: iconst_0
      46: istore_3
      47: goto          61
      50: aload_2
      51: ldc           #2                  // String world
      53: invokevirtual #5                  // Method java/lang/String.equals:(Ljava/lang/Object;)Z
      56: ifeq          61
      59: iconst_1
      60: istore_3
      61: iload_3
      62: lookupswitch  { // 2
                     0: 88
                     1: 99
               default: 110
          }
      88: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      91: ldc           #4                  // String hello
      93: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      96: goto          110
      99: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     102: ldc           #2                  // String world
     104: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     107: goto          110
     110: return
}

Мое личное понимание,javapне декомпилирует байт-код вjavaфайл, но генерирует байт-код, который мы можем понять. На самом деле файлы, сгенерированные javap, все еще представляют собой байт-коды, но программисты могут немного в них разобраться. Если вы разбираетесь в байт-коде, вы все равно можете понять приведенный выше код. По сути, это преобразование String в хэш-код и последующее сравнение.

Лично думаю, что в целом мы будем использоватьjavapКоманд не так много, и обычно они используются только тогда, когда вам действительно нужно увидеть байт-код. А вот вещи, выставленные в середине байткода, наиболее полные, и у вас должна быть возможность их использовать.Например, я анализируюsynchronizedПринцип используется, когдаjavap. пройти черезjavapсгенерированный байт-код, я нашелsynchronizedНижний слой зависит отACC_SYNCHRONIZEDотметьте иmonitorenter,monitorexitДве инструкции для достижения синхронизации.

jad

jad — относительно хороший инструмент для декомпиляции, пока вы загружаете инструмент для выполнения, вы можетеclassДекомпилировать файл. Или приведенный выше исходный код, содержимое после декомпиляции с помощью jad выглядит следующим образом:

Заказ:jad switchDemoString.class

public class switchDemoString
{
    public switchDemoString()
    {
    }
    public static void main(String args[])
    {
        String str = "world";
        String s;
        switch((s = str).hashCode())
        {
        default:
            break;
        case 99162322:
            if(s.equals("hello"))
                System.out.println("hello");
            break;
        case 113318802:
            if(s.equals("world"))
                System.out.println("world");
            break;
        }
    }
}

Слушайте, вы должны понимать этот код, потому что это не стандартный исходный код Java. Это хорошо видноПереключение строки выполняетсяequals()иhashCode()метод достижения.

Однако jad давно не обновлялся, при декомпиляции байт-кода, сгенерированного Java7, периодически возникают неподдерживаемые проблемы, а при декомпиляции лямбда-выражений в Java 8 — полный сбой.

CFR

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

Например, сейчас мы используем cfr для декомпиляции кода. Выполните следующую команду:

java -jar cfr_0_125.jar switchDemoString.class --decodestringswitch false

получить следующий код:

public class switchDemoString {
    public static void main(String[] arrstring) {
        String string;
        String string2 = string = "world";
        int n = -1;
        switch (string2.hashCode()) {
            case 99162322: {
                if (!string2.equals("hello")) break;
                n = 0;
                break;
            }
            case 113318802: {
                if (!string2.equals("world")) break;
                n = 1;
            }
        }
        switch (n) {
            case 0: {
                System.out.println("hello");
                break;
            }
            case 1: {
                System.out.println("world");
                break;
            }
        }
    }
}

Переключатель, который также может получить строку через этот код, черезequals()иhashCode()метод получения заключения.

По сравнению с Jad, CFR имеет много параметров, и это все еще код.Если мы используем следующую команду, вывод будет другим:

java -jar cfr_0_125.jar switchDemoString.class

public class switchDemoString {
    public static void main(String[] arrstring) {
        String string;
        switch (string = "world") {
            case "hello": {
                System.out.println("hello");
                break;
            }
            case "world": {
                System.out.println("world");
                break;
            }
        }
    }
}

так--decodestringswitchУказывает, что детали строки поддержки коммутатора декодированы. Похожие также--decodeenumswitch,--decodefinally,--decodelambdasЖдать. В моей статье о синтаксическом сахаре я использую--decodelambdasДекомпилированные лямбда-выражения. Исходный код:

public static void main(String... args) {
    List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");

    strList.forEach( s -> { System.out.println(s); } );
}

java -jar cfr_0_125.jar lambdaDemo.class --decodelambdas falseДекомпилированный код:

public static /* varargs */ void main(String ... args) {
    ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");
    strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
}

private static /* synthetic */ void lambda$main$0(String s) {
    System.out.println(s);
}

Есть много других параметров CFR, которые используются в различных сценариях, читатели могут использоватьjava -jar cfr_0_125.jar --helpпонять. Он не вводится здесь один за другим.

Как предотвратить декомпиляцию

Поскольку у нас есть инструменты дляClassФайлы декомпилируются, поэтому для разработчиков защита Java-программ становится очень важной задачей. Однако рост дьявола составляет один фут, а рост Дао - один чжан. Конечно, для декомпиляции существуют соответствующие технологии. Однако здесь следует указать, что, как и в случае с защитой сетевой безопасности, сколько бы усилий ни прилагалось, это только увеличит затраты злоумышленников. нельзя полностью предотвратить.

Типичные стратегии выживания следующие:

  • Изолируйте Java-программы
    • Держите пользователей подальше от ваших файлов Class
  • Шифровать файлы класса
    • Упомяните сложность взлома
  • Обфускация кода
    • Преобразование кода в функционально эквивалентную, но трудную для чтения и понимания форму.