Od kodu do programu: kompilator, IL i JIT
Napisałeś tekst w edytorze, nacisnąłeś klawisz i coś się uruchomiło. Ale procesor nie rozumie słowa Console — rozumie tylko liczby. Ta lekcja pokazuje, co dzieje się po drodze i dlaczego dzięki temu ten sam program działa na Windowsie i na Linuksie.
Czego się dziś nauczysz
- Wyjaśnisz, czym różni się kompilator od interpretera
- Opiszesz dwa etapy, przez które przechodzi kod C#, zanim się wykona
- Wyjaśnisz, po co istnieje kod pośredni IL i co robi kompilator JIT
- Rozpoznasz, który plik w folderze
binzawiera twój program - Skompilujesz i uruchomisz projekt z wiersza poleceń
- Odczytasz komunikat o błędzie kompilacji i poprawisz kod
Problem: procesor nie zna C#
Kod, który piszesz, jest zrozumiały dla człowieka. Procesor rozumie coś zupełnie innego — kod maszynowy, czyli ciąg liczb oznaczających elementarne operacje: „weź liczbę z pamięci”, „dodaj”, „skocz do innego miejsca”. Ktoś musi jedno przetłumaczyć na drugie. Robi to kompilator.
Kompilator a interpreter
To dwa różne podejścia do tego samego zadania.
Kompilator
Tłumaczy cały program naraz, przed uruchomieniem. Powstaje osobny plik, który potem uruchamiasz. Błędy wychodzą podczas tłumaczenia — program w ogóle nie powstanie, dopóki ich nie poprawisz.
Interpreter
Wykonuje instrukcje programu podczas działania; nie musi robić tego dosłownie linia po linii, w trakcie działania. Implementacja może tworzyć kod pośredni lub pliki pamięci podręcznej. Błąd w dziesiątej linii wyjdzie dopiero wtedy, gdy program do niej dojdzie.
Analogia: tłumacz książki a tłumacz na konferencji
Kompilator jest jak tłumacz książki: dostaje cały tekst, tłumaczy go w całości i oddaje gotowy przekład. Jeśli w oryginale jest zdanie bez sensu, zauważy to podczas pracy.
Interpreter jest jak tłumacz na żywo: tłumaczy zdanie po zdaniu, w miarę jak mówca je wypowiada. Nie wie z góry, co padnie za pięć minut, więc o błędzie dowie się dopiero, gdy do niego dojdzie.
Implementacje języków mogą łączyć interpretację i kompilację. Edytor potrafi analizować kod i zaznaczać błędy także w językach interpretowanych. W typowym projekcie C# kompilator sprawdza kod przed uruchomieniem.
Dwa etapy zamiast jednego
W C# tłumaczenie nie odbywa się jednym skokiem. Dzieje się w dwóch krokach, w różnych momentach.
Program.cs plik .dll kod maszynowy
(kod w C#) (kod pośredni IL) (rozumie procesor)
│ │ │
│ ETAP 1: kompilator │ ETAP 2: kompilator JIT │
│ Roslyn │ w środowisku .NET │
└─────────────────────────────►└─────────────────────────────►
gdy klikasz „Zbuduj" gdy program działa
Etap 1 — kompilator Roslyn: C# → kod pośredni
Gdy budujesz projekt, do pracy zabiera się Roslyn — kompilator języka C#. Sprawdza kod pod względem poprawności i tłumaczy go na kod pośredni, nazywany IL (ang. Intermediate Language).
IL to język pośredni między C# a kodem maszynowym. Nie jest przeznaczony dla człowieka, ale też nie jest jeszcze zrozumiały dla procesora. Ważne: IL nie zależy od tego, na jakim procesorze i systemie ostatecznie uruchomisz program.
Etap 2 — kompilator JIT: kod pośredni → kod maszynowy
Gdy uruchamiasz program, wkracza środowisko uruchomieniowe .NET. Zawiera ono kompilator JIT (ang. Just-In-Time — „w samą porę”), który tłumaczy IL na kod maszynowy dopiero w trakcie działania programu i dokładnie dla tego procesora, na którym program właśnie działa.
Po co ta komplikacja? Dla przenośności
Gdyby C# kompilował się od razu do kodu maszynowego, trzeba by tworzyć osobną wersję programu dla Windowsa, osobną dla Linuksa, osobną dla procesorów ARM. Dzięki kodowi pośredniemu powstaje jedna wersja, a dopasowaniem do konkretnego sprzętu zajmuje się JIT już na miejscu.
Ta sama idea działa w Javie — tam kod pośredni nazywa się bytecode, a środowisko to maszyna wirtualna JVM.
Czy JIT nie spowalnia programu?
Trochę tak — przy pierwszym wywołaniu metody JIT musi ją najpierw przetłumaczyć. W uproszczeniu wynik kompilacji jest zachowywany w pamięci; przy kompilacji warstwowej metoda może zostać później skompilowana ponownie. Zwykle wynik zostaje w pamięci i kolejne wywołania idą już z pełną prędkością. Co więcej, JIT wie, na jakim procesorze działa, więc może wykorzystać jego konkretne możliwości — czego kompilator na twoim komputerze nie mógłby przewidzieć.
Kod zarządzany i kompilacja podczas działania
C# jest językiem kompilowanym. JIT również jest kompilatorem, a nie interpreterem. Kod zarządzany korzysta z usług środowiska .NET, np. odśmiecania pamięci. Kod maszynowy wykonuje procesor. Istnieje też publikowanie Native AOT: kompilacja do kodu maszynowego przed uruchomieniem, bez JIT podczas działania.
Co naprawdę powstaje na dysku
Zbuduj projekt z poprzedniej lekcji i zajrzyj do folderu bin\Debug\. Znajdziesz tam kilka plików — warto wiedzieć, który jest który.
PierwszyProgram.dll ← TUTAJ jest twój kod (w postaci IL)
PierwszyProgram.exe ← mały program uruchamiający
PierwszyProgram.pdb ← informacje dla debugera
PierwszyProgram.runtimeconfig.json ← która wersja .NET ma to uruchomić
PierwszyProgram.deps.json ← lista zależności projektu
To nie plik .exe zawiera twój program
Wbrew intuicji twój kod znajduje się w pliku .dll. Plik .exe to tylko kilkadziesiąt kilobajtów programu uruchamiającego (ang. apphost), którego jedynym zadaniem jest odnalezienie środowiska .NET i podanie mu pliku .dll do wykonania.
Na Linuksie i macOS nie ma żadnego .exe — jest plik bez rozszerzenia, pełniący tę samą rolę. Plik .dll jest natomiast identyczny na każdym systemie.
Omówienie
| Plik | Do czego służy |
|---|---|
.dll | Zestaw (ang. assembly) — zawiera twój kod przetłumaczony na IL oraz opis wszystkich typów. To jest właściwy efekt kompilacji. |
.exe | Program uruchamiający. Nie ma go, gdy budujesz bibliotekę zamiast aplikacji. |
.pdb | Mapa łącząca IL z konkretnymi liniami twojego pliku .cs. Dzięki niej debuger wie, przy której linii się zatrzymać, a komunikat o błędzie potrafi podać numer wiersza. |
.runtimeconfig.json | Zwykły plik tekstowy — możesz go otworzyć w Notatniku. Mówi, jakiej wersji środowiska .NET wymaga program. |
Debug czy Release?
| Konfiguracja | Kiedy | Różnica |
|---|---|---|
| Debug | podczas pisania | Kod nie jest optymalizowany, za to zawiera pełne informacje dla debugera. Program działa wolniej, ale można go zatrzymać w dowolnym miejscu i podejrzeć zmienne. |
| Release | gdy program jest gotowy | Kompilator optymalizuje kod — usuwa zbędne operacje, upraszcza obliczenia. Program jest szybszy, ale debugowanie staje się utrudnione. |
Konfigurację zmienia się na pasku narzędzi Visual Studio, w rozwijanym polu obok przycisku uruchamiania. Na czas nauki zostaw Debug — potrzebujesz dokładnych komunikatów o błędach bardziej niż szybkości.
To samo z wiersza poleceń
Visual Studio wykonuje kompilację za ciebie, ale wszystko to samo można zrobić poleceniem dotnet. Warto je znać: na egzaminie zawodowym bywa szybsze, a na Linuksie często jest jedyną drogą.
dotnet new console -o MojProgram # utwórz nowy projekt w folderze MojProgram
cd MojProgram # wejdź do tego folderu
dotnet build # skompiluj (C# → IL)
dotnet run # skompiluj i od razu uruchom
dotnet clean # usuń wyniki kompilacji
| Polecenie | Co robi |
|---|---|
dotnet new console | Tworzy nowy projekt konsolowy: plik Program.cs i plik projektu .csproj. Przełącznik -o podaje nazwę folderu. |
dotnet build | Uruchamia kompilator Roslyn. Wynik ląduje w bin\Debug\. Program się nie uruchamia. |
dotnet run | Buduje, jeśli trzeba, i od razu uruchamia program. To odpowiednik klawisza F5. |
dotnet clean | Kasuje zawartość folderów bin i obj. Przydatne, gdy projekt zachowuje się dziwnie po wielu zmianach. |
Plik projektu
Obok Program.cs leży plik z rozszerzeniem .csproj. To zwykły plik XML z ustawieniami projektu:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
| Wpis | Znaczenie |
|---|---|
OutputType | Exe — program do uruchomienia. Library — biblioteka, z której korzystają inne projekty. |
TargetFramework | Wersja .NET, pod którą budujemy. net10.0 oznacza .NET 10. |
ImplicitUsings | Automatyczne dołączanie najpopularniejszych przestrzeni nazw — dlatego Console działa bez using System;. |
Nullable | Włącza ostrzeżenia o zmiennych, które mogą nie zawierać żadnej wartości. Wrócimy do tego w bloku o C# nowoczesnym. |
Jak czytać komunikaty kompilatora
Komunikat kompilatora ma zawsze ten sam układ. Warto raz nauczyć się go czytać — potem oszczędzi ci to godziny.
Program.cs(7,42): error CS1002: Oczekiwano ;
│ │ │ │
│ │ │ └── treść: co jest nie tak
│ │ └── kod błędu — wpisz go w wyszukiwarkę, to działa
│ └── wiersz 7, znak 42
└── plik, w którym wystąpił problem
Zawsze poprawiaj od pierwszego błędu
Kompilator, gdy się pogubi, potrafi zgłosić kilkanaście błędów wynikających z jednej literówki. Popraw pierwszy z listy i zbuduj projekt ponownie — bardzo często reszta zniknie sama. Poprawianie od dołu to strata czasu.
Błąd czy ostrzeżenie?
| Rodzaj | Co oznacza |
|---|---|
| error (czerwony) | Kompilator nie potrafi zbudować programu. Musisz to poprawić — nic się nie uruchomi. |
| warning (żółty) | Kod się skompiluje i uruchomi, ale kompilator podejrzewa, że coś jest nie tak. Nie ignoruj ostrzeżeń — bardzo często to zapowiedź błędu, który wyjdzie dopiero u użytkownika. |
Najczęstsze błędy na początku
| Kod | Komunikat | Zwykła przyczyna |
|---|---|---|
CS1002 | Oczekiwano ; | Brak średnika na końcu instrukcji. |
CS0103 | Nazwa x nie istnieje w bieżącym kontekście | Literówka w nazwie zmiennej albo użycie jej przed zadeklarowaniem. Pamiętaj: wielkość liter ma znaczenie. |
CS1061 | Typ nie zawiera definicji x | Literówka w nazwie metody — np. Writeline zamiast WriteLine. |
CS1525 | Nieprawidłowy token | Zwykle niedomknięty nawias albo cudzysłów. |
CS1513 | Oczekiwano } | Brak klamry zamykającej. Skrót Ctrl + K, Ctrl + D formatuje dokument — po nim źle domknięte klamry rzucają się w oczy. |
CS5001 | Program nie zawiera statycznej metody Main | Projekt jest typu Exe, ale nie ma punktu wejścia. O tym w następnej lekcji. |
Zadania
Poniższy program się nie kompiluje. Przepisz go do Visual Studio, odczytaj komunikaty i popraw wszystkie błędy. Jest ich cztery.
using System
namespace MojaAplikacja
{
class Program
{
static void Main(string[] args)
{
string imie = "Jan"
Console.Writeline("Cześć " + imie)
}
}
Bez używania Visual Studio: utwórz projekt konsolowy poleceniem dotnet, zmień treść programu w Notatniku, skompiluj i uruchom. Zanotuj, co wypisuje polecenie dotnet build.
Zbuduj ten sam projekt w obu konfiguracjach. Porównaj zawartość folderów bin\Debug\ i bin\Release\ oraz rozmiary powstałych plików. Co się zmieniło?
Pobierz darmowy program ILSpy i otwórz w nim plik .dll swojego projektu. Odszukaj metodę Main i przełącz widok na IL. Porównaj to, co widzisz, ze swoim kodem w C#.
Odpowiedz własnymi słowami, nie zaglądając do lekcji:
- Dlaczego kod C# nie jest tłumaczony od razu na kod maszynowy?
- Który plik w folderze
binzawiera twój program i dlaczego nie jest to.exe? - Kiedy działa kompilator Roslyn, a kiedy kompilator JIT?
- Czym różni się błąd od ostrzeżenia?
- Czym różni się kompilacja JIT od interpretacji?
Co trzeba zapamiętać
- Kompilator tłumaczy kod na inną postać. Interpreter wykonuje instrukcje; nie musi dosłownie czytać źródła linia po linii.
- W typowym projekcie Roslyn kompiluje C# do IL, a JIT w czasie działania tworzy kod maszynowy.
- JIT zachowuje wygenerowany kod i może później skompilować metodę ponownie w celu optymalizacji.
- Przenośny kod IL nadal wymaga zgodnego środowiska oraz użytych zależności.
- W zwykłym projekcie konsolowym DLL zawiera kod, a EXE może być programem uruchamiającym. Inne tryby publikowania, np. Native AOT, działają inaczej.
- Komunikat kompilatora zawiera kod błędu i lokalizację. Popraw przyczynę, a potem zbuduj program ponownie.
- Debug służy pracy z debuggerem; Release włącza ustawienia odpowiednie do wydania.