0987 - Interpreter, Kompilator, Assembler Cz.2

Dziś kontynuujemy nasze rozważania o translacji na język maszynowy programów napisanych w językach wysokiego poziomu. Z samej zasady interpretacji wynika, że poprawność każdej instrukcji programu źródłowego jest badana dopiero gdy przyszła kolej na wykonywanie tej instrukcji.

Tak więc może się zdarzyć, że dopiero po wykonaniu znacznej części programu dowiemy się, że mamy błąd w jednej z ostatnich instrukcji. (Będzie to szczególnie niemiłe gdy program wykonuje się naprawdę długo.) Natomiast dobry kompilator nie przerywa tłumaczenia po napotkaniu błędu w programie źródłowym, tylko stara się za jednym przebiegiem wykryć również wszystkie pozostałe błędy.

Czasami zaletą kompilatora jest również to, że otrzymany kod wynikowy jest prawie zupełnie nieczytelny dla człowieka, a więc wprowadzanie w nim zmian może być bardzo trudne. Tak więc gotowy do pracy, w pełni działający program możemy rozpowszechniać bez obawy, że ktoś niepowołany w ciągu godziny może zmienić w nim to co zechce, jak to ma miejsce w przypadku programów źródłowych *).

Teraz wróćmy jeszcze do opisu kompilatora. Wiele działających systemów pozwala wyłączyć z programu głównego używane w nim procedury (podprogramy) i kompilować je oddzielnie. Jest to poważna zaleta, gdyż w przypadku dużych programów, składających się z wielu podprogramów, i zawierających w sumie tysiące instrukcji nie musimy po wprowadzeniu jednej poprawki kompilować ponownie całości - wystarczy kompilacja tej jednej, zmienionej procedury. Bardzo to miłe i wygodne, ale w tej chwili widać już, że pisząc krótko "po zakończeniu kompilacji można wykonać otrzymany kod wynikowy" popełniłem drobne nadużycie, polegające na zbytnim uproszczeniu. Otóż kod wynikowy otrzymany po kompilacji na ogół nie jest jeszcze gotowy do wykonania, nie jest ostatecznym produktem. (Gdyby tak było, to kiedy byśmy dołączali niezależnie skompilowane procedury?) Niezbędny jest jeszcze jeden etap, który scali wszystkie utworzone niezależne od siebie kawałki kodu w jedną całość - gotów do wykonania program. Jest to potrzebne nawet jeśli nie tworzyliśmy podprogramów, gdyż zawsze trzeba dołączać standardowe, dostarczane przez kompilator, procedury, np. wejścia, wyjścia.

Omówiony etap nazywany bywa różnie: scalanie, ładowanie**), linkowanie, konsolidacja, i jak nietrudno zagadnąć, wykonywany jest przez specjalny program. Oczywiście, utworzony po tym etapie program maszynowy (tym razem już na szczęście ostateczny) można także zapisać w pamięci zewnętrznej i wielokrotnie go używać bez potrzeby ponownej konsolidacji.

Skoro mamy możliwość dołączenia kodu ze skompilowanych wcześniej procedur, to nieważne, czy te procedury były napisane wczoraj, czy rok temu, czy przez nas, czy przez kogoś innego. Krótko mówiąc, doszliśmy do szeroko stosowanej koncepcji bibliotek podprogramów (ang. library). Taka biblioteka to nic innego jak specjalnie zorganizowany zbiór, zawierający skompilowaną postać pewnej liczby procedur, np. do rozwiązywania jednej klasy zadań - matematycznych, ekonomicznych, statystycznych itd. Biblioteka matematyczna może np. zawierać gotowe procedury rozwiązywania układów równań, czy znajdowania miejsc zerowych funkcji. Taki zbiór procedur, wraz ze szczegółową specyfikacją tego co robią poszczególne procedury i jak wywołać je w programie, może być dostarczany wraz z kompilatorem lub sprzedawany jako dodatkowe udogodnienie. Korzystanie z bibliotek to po prostu umieszczanie w programie wywołań procedur, których treści nie musimy pisać - na etapie scalania właściwe moduły zostaną wybrane z bibliotek i dołączone do ostatecznego programu.

Następne rozszerzenie możliwości programisty wynika z faktu, że skoro scalimy moduły wynikowe, pochodzące z kompilacji, to przecież nie musi być ważne w jakim języku poszczególne programy były napisane przed skompilowaniem. Jeśli tylko zachowana będzie standardowa postać kodu wynikowego, to możemy łączyć ze sobą podprogramy pisane w różnych językach programowania, a to czasami bywa bardzo wygodne. Niestety nie wszystkie języki programowania wysokiego poziomu pozwalają na realizację tej możliwości.

To co powiedzieliśmy do tej pory o translacji można potraktować jako pewien ogólny schemat, którego fazy występują w różnych translatorach w sposób nie zawsze wyraźnie widoczny dla użytkownika. W produktach różnych firm można spotkać wiele szczegółowych rozwiązań technologicznych różniących się między sobą, ale zasada jest zawsze taka sama. Przy tej okazji jeszcze trochę terminologii związanej z kompilacją. Niektóre kompilatory wykonują więcej niż jeden przebieg (ang. pass). Oznacza to, że najpierw czytany jest cały program i przetwarzany do postaci pośredniej. Następnie, (w drugim przebiegu) przetwarzana jest ta postać pośrednia itd. W niektórych podręcznikach określa się proces scalania jako kolejny (ostatni) przebieg kompilatora.

Spróbujmy teraz porównać obie metody translacji. Zastępując interpreter kompilatorem zyskujemy nie tylko większą prędkość programów ale i wiele innych dodatkowych możliwości. Niemniej jednak, w sytuacjach, w których przebiegi programów nie są długie, za to często wprowadzamy zmiany i program musi być tłumaczony na nowo, łatwość korzystania z interpretera może być przeważającą zaletą. Taką własnie sytuację mamy podczas pisania i uruchamiania programów, i wtedy użycie interpretera na ogół jest opłacalne. Natomiast program, który został ukończony i będzie już wprowadzony do eksploatacji warto skompilować***). Oczywiście, oprócz kodu wynikowego należy przechowywać zawsze wersję źródłową, co umożliwi wprowadzanie poprawek lub modyfikacji w przyszłości.

Języków programowania jest bardzo wiele, wiele z nich doczekało się realizacji na kilku różnych komputerach. Bywa nawet tak, że ten sam język ma kilka różnych translatorów na tej samej maszynie. W tej sytuacji nasuwa się pytanie, który z nich wybrać (jeśli oczywiście mamy wybór, a do tego należy dążyć)? Niestety nie ma tu jednoznacznej odpowiedzi, gdyż w zależności od problemu różne cechy dostępnych języków będą miały decydujące znaczenie. W informatyce profesjonalnej dla każdego poważnego zadania dobiera się język, w którym można je najskuteczniej zaprogramować - wybór języka jest po prostu jednym z elementów procesu rozwiązywania zadania. Istnieje nawet wiele języków tzw. problemowo zorientowanych, to znaczy zaprojektowanych z myślą o programowaniu tylko pewnej szczególnej klasy zadań, np. do symulacji lub zastosowań ekonomicznych.

Amator jest tu w gorszej sytuacji niż informatycy zawodowi. Na mikrokomputerach domowych nie ma do dyspozycji tylu translatorów co na sprzęcie profesjonalnym, jego możliwości uczenia się nowych języków są także ograniczone. Co radzić w tej sytuacji? Po pierwsze pamiętajmy, że BASIC jest językiem bardzo przestarzałym i zdecydowanie nie zalecanym jako język do nauki programowania. Był on już prawie zapomniany, gdy nagle okazało się, że jest on na tyle prosty (czytaj prymitywny), że da się dla niego zrobić interpreter, który zmieści się w ROM-ie domowego mikrokomputera. W tej chwili walczyć z BASIC-em nie ma sensu - akceptujemy go jako zło konieczne. Natomiast językiem, który warto polecić jest niewątpliwie PASCAL. Język ten (opisywany szczegółowo w "Bajtku") daje programiście bardzo duże możliwości, a równocześnie jego struktury ułatwiają wyrobienie sobie właściwego stylu programowania - bez nadużywania instrukcji skoku.

*) nie jest to na pewno zabezpieczenie absolutne, kod wynikowy też można rozszyfrować i zmienić, ale wymaga to naprawdę dużego nakładu pracy, więc większość potencjalnych piratów to odstrasza.

**) termin ładowania używany bywa w najróżniejszych znaczeniach, zwykle gdy wpisujemy cokolwiek do pamięci operacyjnej. Użycie tutaj wywodzi się z faktu, że często scalanie modułów wynikowych odbywa się podczas wpisywania (właśnie "ładowania") tych modułów do PaO.

***) w tym celu musimy dysponować interpreterem i kompilatorem rozpoznającym dokładnie tę samą wersję języka źródłowego, gdyż poszczególne realizacje tego samego języka programowania, np. przygotowane przez różne firmy, zwykle różnią się trochę między sobą.

Andrzej Pilaszek, Bajtek 09/1987r.