🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

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 tekstu

Jedna 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#.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

czytanie to początek

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ę →