Kod źródłowy, maszynowy i pośredni oraz dwie drogi tłumaczenia: kompilacja całości przed uruchomieniem i interpretacja linia po linii.
Ten sam program może istnieć w kilku postaciach, coraz bardziej oddalonych od człowieka, a coraz bliższych procesorowi.
Kod źródłowy to tekst napisany przez programistę w języku wysokiego poziomu. Jest czytelny, można go poprawiać i to on podlega ochronie jako utwór. Dostęp do niego decyduje o tym, czy program jest otwarty.
Kod maszynowy to ciąg liczb binarnych, które procesor wykonuje bezpośrednio. Jest nieczytelny dla człowieka i związany z konkretną architekturą procesora. Blisko niego stoi asembler – zapis tych samych instrukcji przy pomocy skrótów słownych, czytelny na tyle, by dało się w nim pracować.
Kod pośredni leży pomiędzy. Nie jest już tekstem źródłowym, ale też nie jest instrukcjami konkretnego procesora – to uniwersalna forma wykonywana przez maszynę wirtualną. Dzięki temu jeden skompilowany plik działa na różnych systemach. Tak działa Java.
Procesor rozumie wyłącznie kod maszynowy, a człowiek pisze w języku programowania. Ktoś musi to przetłumaczyć – i tu pojawiają się dwa różne podejścia: kompilator i interpreter.
Cztery zasady wystarczą, żeby odpowiedzieć na większość pytań o drogę kodu:
To hasło mówi o tłumaczeniu kodu, a nie o stylu jego pisania – podział na programowanie proceduralne, obiektowe i funkcyjne opisuje hasło o paradygmatach programowania.
Kompilator tłumaczy cały program naraz, jeszcze przed uruchomieniem, i tworzy plik wykonywalny. Od tego momentu program działa samodzielnie, bez kompilatora. Jest szybki, bo tłumaczenie już się odbyło, a błędy składniowe wychodzą przed uruchomieniem. Wadą jest to, że plik wykonywalny działa tylko na tej platformie, dla której powstał, a po każdej zmianie trzeba kompilować od nowa.
Interpreter tłumaczy i wykonuje kod linia po linii w trakcie działania. Nie powstaje plik wykonywalny – do uruchomienia programu potrzebny jest interpreter. Jest wolniejszy, bo tłumaczenie odbywa się na bieżąco, ale za to ten sam kod działa wszędzie, gdzie zainstalowano interpreter, a efekt zmiany widać natychmiast.
Języki takie jak C i C++ są kompilowane, a Python jest interpretowany – dlatego program w Pythonie uruchamia się bez osobnego kroku kompilacji. Rozwiązaniem pośrednim jest kompilacja do kodu pośredniego: program kompiluje się do postaci uniwersalnej, a maszyna wirtualna wykonuje ją na docelowym sprzęcie.
Kompilacja większego programu ma jeszcze jeden krok, o którym łatwo zapomnieć, a który bywa w zadaniach czwartą odpowiedzią do odrzucenia. Kompilator przerabia każdy plik źródłowy osobno i zostawia po nim plik z kodem maszynowym, ale jeszcze nie gotowy do uruchomienia. Dopiero konsolidator zbiera te pliki razem z bibliotekami – gotowym kodem, którego nikt nie pisał na nowo, na przykład z procedurami wypisywania na ekran – i skleja je w jeden plik wykonywalny. Konsolidator niczego nie tłumaczy: łączy to, co kompilator już przetłumaczył. Dlatego w zadaniu, w którym pyta się o tłumaczenie, konsolidator jest odpowiedzią błędną.
Warto też widzieć różnicę między tłumaczeniem a uruchomieniem. Gdy plik wykonywalny jest już gotowy, tłumacz nie jest do niczego potrzebny – wczytaniem pliku do pamięci i oddaniem procesorowi czasu na jego rozkazy zajmuje się system operacyjny. To dlatego skompilowany program jest przywiązany nie tylko do rodziny procesorów, ale i do systemu: w środku ma wywołania funkcji tego konkretnego systemu, a inny system ich nie rozpozna, nawet jeśli procesor jest identyczny.
Moment, w którym wychodzi błąd, jest praktycznie najważniejszą różnicą między obiema drogami. Kompilator czyta cały tekst programu, zanim cokolwiek powstanie, więc literówka w rzadko odwiedzanej linii zatrzyma budowanie pliku wykonywalnego – program ani razu się nie uruchomi. Interpreter dochodzi do tej samej linii dopiero wtedy, gdy wykonanie tam trafi. Wcześniejsze instrukcje wykonają się normalnie i zdążą zrobić swoje: wypisać tekst, zapisać plik, zmienić dane. Program przerwie się w połowie pracy, zostawiając po sobie skutki – i to jest sytuacja, której przy kompilacji po prostu nie ma.
| Plik | Język i sposób tłumaczenia | Długość | Gdzie jest błąd |
|---|---|---|---|
| program.cpp | C++, kompilowany | 60 linii | linia 40: literówka w nazwie zmiennej |
| program.py | Python, interpretowany | 60 linii | linia 40: literówka w nazwie zmiennej |
| Etap | Kompilacja: program.cpp | Interpretacja: program.py |
|---|---|---|
| Kiedy zaczyna się tłumaczenie | przed uruchomieniem, na komputerze programisty | dopiero w chwili uruchomienia, linia po linii |
| Co powstaje na dysku | osobny plik wykonywalny, na przykład program.exe | nic nowego, zostaje sam plik program.py |
| Kiedy wychodzi błąd z linii 40 | przed uruchomieniem: kompilator przerywa pracę i nie tworzy pliku wykonywalnego | w trakcie działania, po poprawnym wykonaniu 39 wcześniejszych linii |
| Co użytkownik zdąży zobaczyć | nic, bo program nie uruchomił się ani razu | wyniki 39 linii: wypisany tekst, zapisane pliki, zmienione dane |
| Co dostaje odbiorca programu | sam plik wykonywalny, bez kodu źródłowego | kod źródłowy, bo bez niego nie ma czego uruchomić |
| Co musi mieć docelowy komputer | system i procesor zgodne z tymi, dla których skompilowano | zainstalowany interpreter tego języka |
| Co trzeba zrobić po poprawce | skompilować od nowa i rozesłać nowy plik wykonywalny | zapisać plik i uruchomić program ponownie |
| Etap | Co wchodzi | Co wychodzi | Czym się to robi |
|---|---|---|---|
| Napisanie programu | pomysł i specyfikacja zadania | kod źródłowy w pliku tekstowym | edytor kodu lub środowisko programistyczne |
| Sprawdzenie składni | kod źródłowy | lista błędów albo zgoda na dalsze etapy | kompilator, jego pierwsza faza |
| Tłumaczenie całości z góry | poprawny składniowo kod źródłowy | kod maszynowy w plikach pośrednich | kompilator |
| Dołączenie bibliotek | pliki z kodem maszynowym i gotowe biblioteki | jeden plik wykonywalny | konsolidator |
| Uruchomienie | plik wykonywalny na dysku | program wczytany do pamięci operacyjnej | system operacyjny |
| Wykonanie rozkazów | kod maszynowy w pamięci | wyniki działania programu | procesor |
| Tłumaczenie na bieżąco | kod źródłowy czytany linia po linii | rozkazy wykonywane od razu, bez zapisu na dysk | interpreter |
| Kompilacja do kodu pośredniego | kod źródłowy | kod pośredni, niezależny od rodziny procesorów | kompilator danego języka |
| Wykonanie kodu pośredniego | kod pośredni | wyniki działania programu | maszyna wirtualna |
| Tłumaczenie zapisu bliskiego sprzętowi | program w asemblerze | kod maszynowy | asembler, czyli program tłumaczący |
| Próba odwrócenia drogi | plik wykonywalny | kod przybliżony, bez nazw zmiennych i komentarzy | dekompilator |
| Cecha | Kompilator | Interpreter |
|---|---|---|
| Moment tłumaczenia | przed uruchomieniem | w trakcie działania |
| Ile razy tłumaczy | raz, przy budowaniu programu | przy każdym uruchomieniu od nowa |
| Wynik na dysku | plik wykonywalny | żaden, zostaje kod źródłowy |
| Szybkość działania | większa | mniejsza |
| Wykrywanie błędów składni | przed uruchomieniem, w całym pliku | dopiero po dojściu wykonania do błędu |
| Co widać po błędzie w środku programu | nic, program się nie uruchomił | skutki wszystkich wcześniejszych linii |
| Co musi mieć docelowy komputer | zgodny system i procesor | zainstalowany interpreter |
| Co dostaje odbiorca | program bez kodu źródłowego | kod źródłowy, bo on jest programem |
| Przenośność | ograniczona do jednej platformy | wysoka |
| Przykłady | C, C++ | Python |
| Nazwa angielska | compiler | interpreter |
Zmiana rozszerzenia pliku na exe. To najbardziej pouczający błąd w całym haśle, bo CKE sprawdziła go wprost: w kluczu z 2011 roku zdanie „wystarczy zmienić rozszerzenie głównego pliku tego programu na exe" jest fałszywe. Rozszerzenie to tylko końcówka nazwy pliku, informacja dla systemu, czym plik ma być otwierany. Nie zmienia ani jednego bajtu zawartości. Plik tekstowy nazwany program.exe nadal zawiera litery, a nie rozkazy procesora, więc próba uruchomienia skończy się komunikatem o błędzie. Kod maszynowy powstaje wyłącznie przez tłumaczenie.
Kompilator pomylony z edytorem, BIOS-em albo innym narzędziem. Klucze z 2017 i 2009 roku zbudowane są dokładnie na tym nieporozumieniu i odrzucają kolejno walidator, edytor tekstu, defragmentator, BIOS i konsolidator. Edytor służy do pisania kodu i niczego nie tłumaczy – w środowisku programistycznym siedzi obok kompilatora w jednym oknie, przez co łatwo je utożsamić. BIOS jest oprogramowaniem uruchamiającym komputer i należy do hasła o budowie komputera; z językami programowania nie ma nic wspólnego. Konsolidator, jak wyżej, łączy, a nie tłumaczy. Tłumaczy tylko kompilator, interpreter i asembler.
Przekonanie, że plik wykonywalny z Windowsa uruchomi się na Linuksie. Skoro procesor jest ten sam, wydaje się, że plik powinien zadziałać. Nie zadziała, bo skompilowany program to nie tylko rozkazy procesora – to także wywołania funkcji konkretnego systemu operacyjnego: otwórz plik, narysuj okno, przydziel pamięć. Każdy system nazywa te funkcje po swojemu. Przenieść można kod źródłowy i skompilować go osobno na drugim systemie; gotowego pliku wykonywalnego przenieść się nie da. Odwrotnie jest z programem interpretowanym: on jest kodem źródłowym, więc wystarczy interpreter po drugiej stronie.
Przekonanie, że z pliku wykonywalnego odzyska się kod źródłowy. Tłumaczenie jest stratne w jednym konkretnym sensie: nazwy zmiennych, nazwy funkcji, komentarze i puste linie nie są procesorowi do niczego potrzebne, więc w pliku wynikowym po prostu ich nie ma. Dekompilator odtworzy strukturę obliczeń, ale nazwie zmienne po swojemu i nie wymyśli komentarzy. Dostaje się coś, co robi to samo, a czyta się nieporównanie gorzej niż oryginał. To dlatego program rozpowszechniany w postaci skompilowanej chroni pomysł autora, a licencja typu GPL wymaga udostępnienia właśnie kodu źródłowego, nie samego pliku wykonywalnego.
Interpreter rozumiany jak tłumacz języka naturalnego. Polska nazwa „tłumaczenie" podpowiada obraz translatora, który dostaje cały tekst i oddaje cały przekład. Interpreter tak nie pracuje: bierze jedną instrukcję, od razu ją wykonuje, dopiero potem sięga po następną. Stąd wynikają wszystkie jego właściwości – brak pliku wynikowego, mniejsza szybkość i to, że o błędzie w czterdziestej linii dowiadujemy się po wykonaniu trzydziestu dziewięciu poprzednich. Kompilator jest bliższy potocznemu „tłumaczeniu": czyta całość, zanim cokolwiek odda.
W jednym katalogu leżą cztery pliki tego samego projektu:
| Plik | Co zawiera | Jak trafia do procesora |
|---|---|---|
| projekt.cpp | kod źródłowy w C++ | przez kompilację przed uruchomieniem |
| projekt.py | kod źródłowy w Pythonie | przez interpretację w trakcie działania |
| projekt.exe | kod maszynowy dla Windowsa | jest wykonywany bezpośrednio |
| projekt.asm | program zapisany w asemblerze | po przetłumaczeniu na kod maszynowy |
Uzupełnij odpowiedzi:
| Pytanie | Odpowiedź |
|---|---|
| Która postać programu jest wykonywana bezpośrednio przez procesor? | |
| Program tłumaczący cały kod przed uruchomieniem to | |
| Program tłumaczący i wykonujący kod linia po linii to | |
| W pliku projekt.py błąd składni jest w 40. linii. Ile wcześniejszych linii wykona się poprawnie? | |
| Ile linii wykona się w projekt.cpp, zanim kompilator zgłosi ten sam błąd? | |
| Który z czterech plików uruchomi się bez zainstalowanego interpretera? | |
| Program łączący pliki z kodem maszynowym i biblioteki w jeden plik wykonywalny to | |
| Co wykonuje kod pośredni? |
Po kompilacji odbiorca dostaje i nie potrzebuje już kompilatora, natomiast program interpretowany rozsyła się w postaci , bo bez niego nie ma czego uruchomić.
Kod skompilowany, bo tłumaczenie odbyło się wcześniej. Interpreter musi analizować instrukcje na bieżąco, co kosztuje czas przy każdym uruchomieniu.
Bo kompilacja tworzy plik dla konkretnego systemu i architektury. Ten sam kod źródłowy trzeba skompilować osobno dla każdej platformy.
Bo każda architektura ma własny zestaw rozkazów. Instrukcje zapisane dla jednej rodziny procesorów nie mają sensu dla innej.
Dekompilacja pozwala odtworzyć coś zbliżonego, ale bez nazw zmiennych i komentarzy. Efekt bywa trudny do zrozumienia i nie jest oryginałem.
Do zadań wymagających pełnej kontroli nad sprzętem i maksymalnej wydajności – sterowników i systemów wbudowanych. W typowym programowaniu jest zbędny.
Zbiera pliki z kodem maszynowym, które wyprodukował kompilator, dokłada do nich biblioteki i skleja wszystko w jeden plik wykonywalny. Sam niczego nie tłumaczy, dlatego w zadaniu o tłumaczeniu kodu jest odpowiedzią błędną – tak właśnie użyto go w kluczu z 2009 roku.
Standardowy Python tłumaczy kod źródłowy do własnego kodu pośredniego i wykonuje go maszyną wirtualną – z punktu widzenia użytkownika działa jak interpreter: bez osobnego kroku kompilacji i bez pliku wykonywalnego. Na maturze obowiązuje klasyfikacja „Python – interpretowany".
Do tego hasła nie mamy jeszcze przypisanych pojedynczych zadań. Cały spis zadań maturalnych z tego działu jest tutaj:
Zagadnienia z tego samego obszaru matury – warto je powtórzyć razem: