Java – ByteCode

Schodzimy nisko, ale czy to coś złego? Według IBM developerWorks: „Znajomość kodu bajtowego Javy pomaga programiście tak, jak znajomość asemblera pomaga programistom języków C i C++” i ja z tym stwierdzeniem się zgadzam. W tym wpisie postaram się przybliżyć wam kod bajtowych.

Cykl życia

Na początku zacznijmy od przebiegu/cyklu życia naszej aplikacji. Jako pierwszy mamy kod źródłowy, czyli kod napisany w jednym z języków, który ma zbudowany dla siebie kompilator. Następnie nasz kod źródłowy jest kompilowany do postaci kodu bajtowego.

kod źródłowy(java, scala, kotlin i inne języki JVM) - > kompilator (np. javac) - > kod bajtowy

Zatrzymamy się na kodzie bajtowy, bo o tym jest ten wpis. Czyli kompilator dostarcza nam kod w postaci kodu bajtów. Nie jest to jeszcze kod natywny tylko postać pośrednia. Dzięki temu, że generowana jest postać pośrednia można w łatwy sposób napisać swój własny język. Musimy “jedynie” zaprojektować gramatykę i kompilator, który generuje kod bajtowy Javy. Dzięki powstaniu tej postaci pośredniej nie martwimy się tym co jest dalej, bo to już załatwia za nas JVM.

ByteCode

Kod bajtowy Javy to zbiór instrukcji, których aktualnie jest około 200 na 256 możliwych (jeden bajt to osiem bitów 2^8 = 256). Nowe instrukcje dodawane są bardzo rozważnie, zazwyczaj bardziej skomplikowane operacje zastępowane są szeregiem instrukcji już wbudowanych w kod bajtowy. Każda operacja ma długość jednego bajta. Przykład hello world w kodzie bajtowym:

public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: return

  public static void main(java.lang.String[]);
    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
}

Javap

Aby samemu móc skompilować kod do postaci kodu bajtowego należy skorzystać z narzędzia javap, które jest dostępne w pakiecie JDK. Należy wskazać klasę *.java (w Intellij, normalnie należy wskazać plik .class) i uruchomić javap. Fajnym rozwiązaniem w Intellij jest dodanie External Tool.

javap

Dzięki temu możemy od razu podejrzeć sobie wynik w konsoli:

javapResult

Od razu nasuwa się pytanie czy jest to rozwiązanie bezpiecznie, niestety nie. Można wziąć kod i zmienić dowolną instrukcję. Do czego można wykorzystać wiedzę o kodzie bajtowy? Możemy podejrzeć jak wykonywane są operacje “od środka“.

StringBuilder

Na przykład kompilator ma wbudowaną zmianę Stringów na StringBuildery, które są bardziej optymalne w operacji konkatenacji:

        String concatenation = "empty";
        concatenation+="not empty";
        concatenation+="not empty";
        concatenation+="not empty";
        concatenation+="not empty";
        concatenation+="not empty";

Wynik:

      8: ldc           #7                  // String empty
      10: astore_1
      11: new           #8                  // class java/lang/StringBuilder
      14: dup
      15: invokespecial #9                  // Method java/lang/StringBuilder."":()V
      18: aload_1
      19: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: ldc           #11                 // String not empty
      24: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      27: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;