Java >> Programma Java >  >> Java

Per vedere un mondo in un granello di sabbia:ancora una volta Hello World

"Per vedere un mondo in un granello di sabbia", e probabilmente vedremmo un mondo nel più semplice "Hello World", quindi eccoci qui, ancora una volta diremo Hello to the World.

Immagino che tutti i corsi Java, i tutorial inizino da questo famoso programma Hello World, e questo è uno di quei programmi molto rari che posso scrivere senza l'aiuto dell'IDE :)

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

1. Conosci queste opzioni di javac?

Dopo che il tuo primo programma è stato scritto, esegui prima il comando seguente per compilarlo, altrimenti non puoi eseguirlo.

javac HelloWorld.java

Probabilmente scopriresti che non è necessario nominare il file "HelloWorld.java", anche "Hello.java" funziona. E public class HelloWorld può anche essere declassato a class HelloWorld .

Se sei abbastanza curioso, premi javac --help , vedrai molte opzioni relative al compilatore Java, ad esempio, vogliamo stampare l'edizione cinese "Hello World" e ci aspettiamo che si applichi esattamente al livello di lingua JDK8, con i metadati dei nomi dei parametri inclusi, sarà simile a questo:

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

Hai installato JDK11, ma usando il comando sopra stai rilasciando file di classe usando solo le funzionalità 1.8. Se hai scritto alcune cose disponibili solo da JDK9 in poi, potresti scoprire che non può essere compilato come previsto.

2. Nozioni di base sul file di classe

C'è un intero capitolo relativo al formato del file di classe nelle specifiche di Java Virtual Machine, vuoi esplorarlo un po'?

Vedi i bytecode (compilati con JDK11) iniziano con un magico e misterioso "cafe babe" e seguono con un valore di 55 e molte cose danneggerebbero il tuo cervello. Tra questi, "cafe babe" è la magia, 55 punti alla versione minore, che è mappata su JDK11. Rispetto alla lettura del fantastico formato di file di classe, puoi anche utilizzare javap per recuperare le informazioni per quel file di classe:

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

Otterrai cose come questa:

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                                                                                        
}

Puoi vedere che le istruzioni qui sono in qualche modo simili al codice sorgente, con le mappature del numero di riga del codice sorgente e dei numeri di istruzione, potresti chiederti, posso ripristinare il sorgente da questi grappoli di cose?

3. Decompilatori

Si, puoi. Esistono molti decompilatori, ma alcuni di essi sono obsoleti per l'uso al giorno d'oggi, come JD-GUI, JAD e così via, non funzionerebbero bene su file di classe compilati con l'ultimo JDK. Puoi ancora usarli, ma CFR sarebbe più adatto.

# 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");       
    }                                            
}

Potresti aver scoperto che c'è una leggera differenza con il codice sorgente e il codice decompilato (metodo costruttore aggiunto), in realtà potresti essere sorpreso di vedere che a volte il codice generato sembra essere modificato sul codice sorgente. Tuttavia, molti di questi sono ottimizzazioni da JVM e di solito ottengono un miglioramento delle prestazioni, confrontare la differenza è effettivamente interessante e ti darebbe molti spunti.

4. Come è possibile inizializzare nuovamente una variabile finale con valore nullo?

System.out.println("Hello World") , System è una classe e out è uno dei suoi attributi statici con il modificatore finale:

public final static PrintStream out = null;

Poi arriva il problema, perché l'hack System.out.println("Hello World") non lancerà il famoso NullPointerException , secondo le specifiche del linguaggio, sembra che la variabile statica finale out sia impossibile da assegnare nuovamente a un valore valido, giusto?

Sì, è giusto nella maggior parte dei casi se non usi i trucchi del riflesso sporco e non introduci il native compagno.

Se vuoi solo giocare, faresti così:

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

Tuttavia, questo non funzionerà per System , il vero segreto è nascosto in queste righe di codice in System.java :

private static native void registerNatives();
static {
    registerNatives();
}

Come per i commenti scritti sopra il metodo, "VM invocherà il metodo initializeSystemClass per completare l'inizializzazione per questa classe", vai al metodo di initializeSystemClass e vedrai queste righe:

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")));

E vedrai anche questi 3 metodi nativi per impostare in e out :

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

Quindi ora sai che JVM fa queste cose a livello di sistema operativo e "bypassa" il final restrizione, probabilmente chiederesti, dov'è l'hacking il codice a livello di sistema operativo con cui JVM si adatterà?

Quindi eccolo System.c (versione JDK11).

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);
}

Qui trovi la backdoor nei commenti, "Sono implementati in modo nativo perché violano la semantica del linguaggio (cioè impostano la variabile finale)" .

E poi, scopriresti che è davvero una lunga, lunga strada. Il viaggio non si fermerà mai.

La fine:fermati per un po'

“Vedere un mondo in un granello di sabbia
E un paradiso in un fiore selvatico
Tieni Infinity nel palmo della tua mano
E l'eternità in un'ora"

Se il più semplice HelloWorld è solo un granello di sabbia, sicuramente c'è un mondo dentro, forse gli hai detto "Ciao" numerose volte, ma non significa che hai esplorato un po' il mondo, forse ora è il momento di esplorare nel mondo, mentre la sabbia ti sporcherebbe le mani, il fiore no.

Etichetta Java