Learn · concepts
Czym jest bytecode JVM? Wyjaśnienie plików klas Java
Kod bajtowy JVM to zwięzły zestaw instrukcji, na którym działa Java Virtual Machine. Dowiedz się, jak działają pliki .class, dlaczego mody przepisują kod bajtowy i jaką rolę pełni ASM.
Co to jest bajtkod JVM?
Bajtkod JVM to zestaw instrukcji, które wykonuje Wirtualna Maszyna Java. Podczas kompilacji kodu źródłowego Java, kompilator nie generuje kodu maszynowego dla Twojego procesora. Generuje bajtkod – kompaktowy, przenośny format przechowywany w plikach .class. JVM odczytuje te instrukcje w czasie działania i przekształca je w kod natywny w miarę potrzeb.
To warstwowe podejście – od kodu źródłowego do bajtkodu, a następnie do kodu maszynowego – sprawia, że Java jest przenośna. Ten sam plik .class działa na Windows, macOS, Linux lub dowolnym innym systemie z kompatybilną JVM, ponieważ to JVM jest warstwą, która wie, jak wykonywać instrukcje na lokalnej maszynie. Napisz raz, uruchom gdziekolwiek.
Jak kod źródłowy staje się bajtkodem
Plik .java to tekst, który pisze człowiek. Plik .class to skompilowany wynik, który uruchamia JVM. Kompilator odczytuje Twój kod źródłowy, sprawdza go i zapisuje jeden plik .class na każdą klasę, zawierający instrukcje bajtkodu, pulę stałych, tabele metod oraz inne metadane.
Bajtkod nie jest powiązany z żadnym konkretnym procesorem. Ten sam plik .class działa na Windows, macOS, Linux lub czymkolwiek z uruchomionym Java, ponieważ to JVM jest warstwą, która wie, jak wykonywać instrukcje na lokalnej maszynie. To przenośność jest głównym celem tego formatu.
Jak wyglądają instrukcje
Kod bajtowy to zestaw instrukcji oparty na stosie. Większość operacji umieszcza wartości na stosie operandów, pobiera je z niego i umieszcza wynik. Nie ma alokacji rejestrów, nad czym trzeba by się zastanawiać; model jest celowo uproszczony, aby JVM mogła go szybko zweryfikować i wykonać.
Kilka przykładów kodów operacji daje wyobrażenie o tym:
| Kod operacji | Co robi |
|---|---|
iload | Umieszcza lokalną zmienną typu int na stosie |
iadd | Pobiera dwa inty ze stosu, umieszcza ich sumę |
invokevirtual | Wywołuje metodę instancyjną |
getfield | Odczytuje pole instancyjne |
return | Zwraca z metody |
Rzadko czytasz lub zapisujesz je ręcznie. Narzędzia je generują i konsumują, a biblioteka, taka jak ASM, w tym pomaga.
Dlaczego kod bajtowy ma znaczenie dla modyfikacji
Kod bajtowy ma znaczenie, ponieważ pozwala na zmianę zachowania bez oryginalnego kodu źródłowego. Modyfikacja Minecrafta prawie nigdy nie ma kodu źródłowego gry. Ma ona skompilowaną grę na dysku. Aby dodać lub zmienić funkcję, mod odczytuje istniejący kod bajtowy, go przepisuje i przekazuje wynik z powrotem do JVM w momencie ładowania klas.
Właśnie tak działają frameworki patchowania, wstrzykując haki do metod, których nie posiadają. Znajdują odpowiednie miejsce w skompilowanej metodzie i wplatają wywołanie kodu modyfikacji. Praca na poziomie kodu bajtowego oznacza, że mod może się podłączyć do gry, nawet jeśli nigdy nie widzi linii oryginalnej Javy.
W praktyce, tak modyfikowane są Terminus oraz inne klienty oparte na Fabric w grze Minecraft. Zamiast posiadać kod źródłowy Minecraft, operują skompilowanymi plikami .class i przepisują bytecode, dodając funkcje takie jak moduły, ekrany ustawień i niestandardowe renderowanie.
Gdzie wchodzi ASM
ASM to biblioteka Java do odczytu, zapisu i transformacji bytecode. Parsuje plik .class na sekwencje zdarzeń lub drzewo, umożliwia modyfikację instrukcji i generuje poprawny plik .class. Jest mały, szybki i działa blisko surowego formatu, dlatego narzędzia do modowania wyższego poziomu budują na nim, zamiast samodzielnie parsować pliki klas.
Większość modderów nigdy nie wywołuje ASM bezpośrednio. Używają warstwy wyższego poziomu, która ukrywa opkody i udostępnia bardziej przyjazny sposób wyrażenia „uruchom mój kod na początku tej metody”. Pod spodem ta warstwa edytuje bytecode, często za pomocą ASM.
Dekompilacja kodu bajtowego z powrotem do kodu źródłowego
Dekompilatory takie jak Fernflower i CFR pobierają skompilowane pliki .class i rekonstruują z nich czytelny kod Java z kodu bajtowego. Proces ten nie jest doskonały: niektóre nazwy i komentarze są tracone, niektóre konstrukcje trudniej odtworzyć niż inne, a zaciemniony kod staje się nieczytelny. Jednak w przypadku zwykłego, niezaciemnionego kodu, dekompilatory zazwyczaj generują wynik wystarczająco zbliżony do oryginału, aby go zrozumieć i z nim pracować.
Jest to przydatne do poznawania działania gry, ale nie zastępuje posiadania rzeczywistego kodu źródłowego. Dekompilowany kod to nadal skompilowana logika bez komentarzy, a zrekonstruowane nazwy zmiennych mogą być mylące. Jest to narzędzie do czytania, a nie do pisania.
FAQ
Nie. Kod maszynowy to instrukcje natywne dla konkretnego procesora. Kod bajtowy to własny zestaw instrukcji JVM, który JVM tłumaczy na kod maszynowy w czasie działania. To dodatkowa warstwa sprawia, że plik .class może być uruchamiany na dowolnej platformie.
Częściowo. Dekompilatory rekonstruują czytelny kod Java z kodu bajtowego i w przypadku zwykłego kodu robią to całkiem dobrze. Wynik nie zawsze jest identyczny z oryginałem, a komentarze i niektóre lokalne nazwy znikają, ale zazwyczaj wystarcza to do śledzenia.
Ponieważ modderzy zazwyczaj nie posiadają kodu źródłowego. Kompilują modyfikacje na podstawie skompilowanej gry. Przepisywanie kodu bajtowego pozwala modyfikacji zmieniać zachowanie bez dotykania, a nawet posiadania, oryginalnych plików Java.
Zazwyczaj nie. Frameworki modyfikacji i warstwy skryptowe zajmują się pracą z bytecode'em za Ciebie. Znajomość tego, jak to działa, pomaga, gdy debugujesz skomplikowany patch lub tworzysz narzędzia.