Jak komputer uruchamia program C#

Klikasz Uruchom albo wpisujesz dotnet run — i po ułamku sekundy widzisz wynik na ekranie. Ten artykuł pokazuje, co dokładnie dzieje się w tym czasie: nie na poziomie składni C#, ale na poziomie systemu operacyjnego i procesora, które fizycznie wykonują Twój program.
Plik wykonywalny — co naprawdę tworzy kompilacja
dotnet build nie zamienia Twojego kodu bezpośrednio na instrukcje procesora. Tworzy plik (np. .dll z metadanymi uruchomieniowymi) zawierający trzy rzeczy: kod pośredni (IL — Intermediate Language, coś pomiędzy kodem źródłowym a instrukcjami procesora), metadane (opisy wszystkich typów, metod i ich sygnatur — dzięki nim CLR wie, jak wywołać Main czy dopasować przeciążenia metod) oraz informację, gdzie znajduje się punkt startowy programu — metoda Main. Żaden fragment tego pliku nie jest jeszcze bezpośrednio zrozumiały dla procesora — to zadanie dla kolejnego etapu.
System operacyjny tworzy proces
Gdy uruchamiasz program, system operacyjny tworzy dla niego proces — izolowaną przestrzeń pamięci, którą tylko ten program może czytać i zapisywać. Dzięki tej izolacji dwa różne programy mogą jednocześnie “widzieć” ten sam adres pamięci (np. 0x00007FF6) i odwoływać się do zupełnie różnych, niepowiązanych danych — system operacyjny tłumaczy adresy wirtualne każdego procesu na osobne, fizyczne miejsca w pamięci RAM. Błąd w jednym programie nie może więc nadpisać pamięci innego.
Dla aplikacji .NET system operacyjny najpierw uruchamia hosta środowiska uruchomieniowego, który z kolei ładuje CLR (Common Language Runtime) do tej świeżo utworzonej przestrzeni procesu.
Procesor wykonuje instrukcje — cykl pobierz, zdekoduj, wykonaj
CLR tłumaczy kod IL na natywne instrukcje procesora (to robi JIT — Just-In-Time compiler) w momencie pierwszego wywołania danej metody, nie z góry dla całego programu — dlatego pierwsze wywołanie metody bywa odrobinę wolniejsze niż kolejne (wynik JIT-a zostaje w pamięci na potrzeby dalszych wywołań).
Od tego momentu procesor działa w nieustannej pętli: pobiera kolejną instrukcję z pamięci, dekoduje ją, wykonuje, zapisuje wynik — i pobiera następną. Miliardy razy na sekundę.
int a = 5;
int b = 10;
int suma = a + b;
Console.WriteLine(suma);Te cztery linijki to dla procesora coś w rodzaju:
MOV EAX, 5 ; załaduj 5 do rejestru EAX (zmienna 'a')
MOV EBX, 10 ; załaduj 10 do rejestru EBX (zmienna 'b')
ADD EAX, EBX ; dodaj EBX do EAX -> wynik w EAX (zmienna 'suma')
MOV [pamiec], EAX ; zapisz wynik z rejestru do pamięci
CALL WriteLine ; wywołanie systemowe do wypisania tekstuJedna linijka C# to zwykle kilka do kilkunastu takich pojedynczych instrukcji — procesor nie “widzi” int suma = a + b; jako jednej operacji, tylko jako sekwencję ładowania rejestrów, dodawania i zapisu.
Pamięć procesu — stos i sterta
W obrębie swojej izolowanej przestrzeni każdy proces dzieli pamięć na dwa główne obszary. Stos (stack) przechowuje zmienne lokalne o znanym z góry rozmiarze (jak int a, int b powyżej) — działa jak stos talerzy: dokładanie i zdejmowanie z wierzchu, bardzo szybkie, automatycznie sprzątane, gdy metoda kończy działanie. Sterta (heap) przechowuje obiekty, których rozmiar może się zmieniać albo które muszą przeżyć dłużej niż pojedyncze wywołanie metody (jak instancje klas) — wolniejsza w obsłudze, sprzątana przez Garbage Collector, nie automatycznie po wyjściu z metody. To rozróżnienie stoi za różnicą między typami wartościowymi (struct, int) a referencyjnymi (class) w C# — wartościowe zwykle żyją na stosie, referencyjne na stercie.
Wejście i wyjście — jak Console.WriteLine trafia na ekran
Twój program nie rysuje tekstu na ekranie bezpośrednio — nie ma do tego uprawnień. Zamiast tego prosi system operacyjny (przez wywołanie systemowe, syscall) o zrobienie tego za niego — dokładnie jak w pseudo-asemblerze powyżej (CALL WriteLine ostatecznie prowadzi do syscalla). To samo dotyczy odczytu klawiatury, zapisu pliku czy połączenia sieciowego — zawsze pośredniczy system operacyjny, który ma bezpośredni dostęp do sprzętu.
Kiedy program się kończy
Gdy metoda Main zakończy działanie, proces zwraca systemowi operacyjnemu kod wyjścia (0 zwykle oznacza sukces, dowolna inna wartość — błąd konkretnego typu, który proces wywołujący może sprawdzić) i cała przydzielona mu pamięć zostaje odzyskana — niezależnie od tego, czy program “posprzątał” po sobie (zamknął pliki, zwolnił zasoby), czy nie. To dlatego try/finally gwarantuje sprzątanie tylko przy normalnym zakończeniu — gwałtowne zabicie procesu (np. kill -9 w Linuksie) nie daje programowi szansy na wykonanie kodu sprzątającego w ogóle.
Ten sam podział — dane wejściowe, przetwarzanie, wynik — opisujemy z perspektywy samego kodu C# (nie systemu operacyjnego) w artykule Input i Output w C#. A skąd biorą się CLR i BCL wspomniane wyżej, wyjaśniamy w Czym jest .NET?.
Co dalej
Ten artykuł pokazał drogę od pliku wykonywalnego do działającego procesu na poziomie systemu i procesora. Jeśli interesuje Cię, co dzieje się o warstwę wyżej — jak dokładnie kompilator C# i CLR przetwarzają Twój kod — pełny rozkład znajdziesz w artykule Jak działa program C#.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
2 comments