Статьи

Увидеть мир в песчинке: еще раз Hello World

«Чтобы увидеть мир в песчинке», и мы, вероятно, увидим мир в самом простом «Привет мире», так что мы здесь, еще раз мы скажем Привет миру.

Я предполагаю, что все курсы Java, учебные пособия начинаются с этой знаменитой программы Hello World, и это одна из тех очень редких программ, которые я могу написать без помощи IDE 🙂

Песчинка
1
2
3
4
5
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

1. Знаете ли вы эти варианты Javac?

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

1
javac HelloWorld.java

Возможно, вы обнаружите, что нет необходимости называть файл «HelloWorld.java», «Hello.java» также работает. И public class HelloWorld также может быть понижен до class HelloWorld .

Если вам достаточно любопытно нажать javac --help , вы увидите много опций, касающихся компилятора Java, например, мы хотим напечатать китайское издание «Hello World» и ожидать, что оно будет применено именно к уровню языка JDK8 с метаданными имена параметров, включенные в, это будет выглядеть так:

1
javac -encoding UTF-8 -source 8 -target 8 -parameters Hello.java

У вас установлен JDK11, но с помощью указанной выше команды вы выпускаете файлы классов, используя только функции 1.8. Если вы написали некоторые материалы, доступные только из JDK9, вы обнаружите, что они не могут быть скомпилированы должным образом.

2. Основы файла класса

В спецификации виртуальной машины Java есть целая глава, посвященная формату файлов классов. Хотите его немного изучить?

Песчинка

Вы видите, что байт-коды (скомпилированные с JDK11) начинаются с волшебного, загадочного «детки из кафе» и следуют со значением 55, и многие вещи повредят ваш мозг. Среди них «детка в кафе» — волшебство, 55 баллов за минорную версию, которая сопоставлена ​​с JDK11. По сравнению с чтением удивительного формата файла класса, вы также можете использовать javap для извлечения информации для этого файла класса:

1
2
# You would use javap -h to see how many options you have
javap -p -l -c -s -constants HelloWorld

Вы получите такие вещи:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
class HelloWorld {
  HelloWorld();                                                                                       
    descriptor: ()V                                                                                   
    Code:                                                                                             
       0: aload_0                                                                                     
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V                   
       4: return                                                                                      
    LineNumberTable:                                                                                  
      line 1: 0                                                                                       
                                                                                                        
  public static void main(java.lang.String[]);                                                        
    descriptor: ([Ljava/lang/String;)V                                                                
    Code:                                                                                             
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;       
       3: ldc           #3                  // String Hello World                                     
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return                                                                                      
    LineNumberTable:                                                                                  
      line 4: 0                                                                                       
      line 5: 8                                                                                       
}

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

3. Де-Компиляторы

Да, ты можешь. Существует много декомпиляторов, но некоторые из них устарели для использования в настоящее время, такие как JD-GUI , JAD и т. Д., Они не будут хорошо работать с файлом классов, скомпилированным с последней версией JDK. Вы можете все еще использовать их, но CFR был бы более подходящим.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
# java -jar cfr-0.139.jar HelloWorld.class
/*                                              
 * Decompiled with CFR 0.139.
 */                                             
import java.io.PrintStream;                     
                                                  
class HelloWorld {                              
    HelloWorld() {                              
    }                                           
                                                  
    public static void main(String[] arrstring) {
        System.out.println("Hello World");      
    }                                           
}

Возможно, вы обнаружили, что есть небольшая разница с исходным кодом и декомпилированным кодом (добавлен метод конструктора), на самом деле, вы можете быть удивлены, увидев, что иногда сгенерированный код кажется измененным в исходном коде. Тем не менее, многие из них являются оптимизацией от JVM и, как правило, получают улучшение производительности, сравнение различий на самом деле интересно и даст вам много идей.

4. Как можно снова инициализировать конечную переменную с нулевым значением?

System.out.println("Hello World") , System является классом и out является одним из его статических атрибутов с модификатором final:

1
public final static PrintStream out = null;

Тогда возникает проблема, почему взломать System.out.println("Hello World") не выбросит известную NullPointerException , в соответствии со спецификацией языка, кажется, что окончательная статическая переменная из нельзя присвоить действительному значению опять верно?

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

Если вы просто хотите поиграть, вы бы сделали так:

1
2
3
4
Field f = clazz.getDeclaredField("out");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);

Однако это не сработает для System , фактический секрет скрыт в следующих строках кода в System.java :

1
2
3
4
private static native void registerNatives();
static {
    registerNatives();
}

Согласно комментариям, написанным выше, метод «VM вызовет метод initializeSystemClass для завершения инициализации для этого класса», перейдите к методу initializeSystemClass и вы увидите следующие строки:

1
2
3
4
5
6
FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
setIn0(new BufferedInputStream(fdIn));
setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));

И вы также увидите эти 3 нативных метода для установки и out :

1
2
3
private static native void setIn0(InputStream in);
private static native void setOut0(PrintStream out);
private static native void setErr0(PrintStream err);

Итак, теперь вы знаете, что JVM выполняет эту работу на уровне ОС и «обходит» final ограничение, вы, вероятно, спросите, где взломать код уровня ОС, с которым JVM будет адаптироваться?

Итак, это System.c (версия JDK11) .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
JNIEXPORT void JNICALL
Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}
/*
 * The following three functions implement setter methods for
 * java.lang.System.{in, out, err}. They are natively implemented
 * because they violate the semantics of the language (i.e. set final
 * variable).
 */
JNIEXPORT void JNICALL
Java_java_lang_System_setIn0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"in","Ljava/io/InputStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

Здесь вы найдете заднюю дверь в комментариях: «Они изначально реализованы, потому что они нарушают семантику языка (т.е. устанавливают конечную переменную)» .

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

Конец: остановись на время

«Чтобы увидеть мир в песчинке
И рай в полевом цветке
Держите бесконечность в ладони
И вечность через час »

Если самый простой HelloWorld — это просто песчинка, конечно, в нем есть мир, может быть, вы много раз говорили ему «Hello», но это не значит, что вы немного изучили мир, может быть, теперь это время и исследовать мир, в то время как песок испачкает руки, а цветок — нет.

Опубликовано на Java Code Geeks с разрешения Натанаэля Янга, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Чтобы увидеть мир в песчинке: еще раз Hello World

Мнения, высказанные участниками Java Code Geeks, являются их собственными.