Systemy operacyjne (wiosna 2014) Laboratorium 9 - tutorial Podstawy programowania w Linux/UNIX dr inż. Wojciech Bieniecki Instytut Nauk Ekonomicznych i Informatyki http://wbieniec.kis.p.lodz.pl/pwsz
Wykorzystanie języka C i C++ System operacyjny Linux i UNIX standardowo wykorzystują języki C i C++ do: 1. Tworzenia własnych programów tekstowych i graficznych wykorzystujących możliwości systemu takie jak: Operacje na plikach Wykorzystanie funkcji systemowych Operacje na procesach, wątkach Tworzenie aplikacji sieciowych Tworzenie sterowników urządzeń 2. Instalowania i uruchamiania oprogramowania: Kompilacji jądra systemowego i programów systemowych Kompilacji sterowników urządzeń Kompilacji i instalacji oprogramowania freeware Przenoszenia oprogramowania pomiędzy różnymi platformami sprzętowymi i systemowymi – np. z Windows do Linux
Proces kompilacji Po utworzeniu plików źródłowych proces tworzenia programu składa się z dwóch etapów: - kompilacji (compile) - konsolidacji (linking) Każdy plik źródłowy np. foo.c jest kompilowany do pliku obiektowego foo.o. Każdy plik obiektowy zawiera zależną od systemu, skompilowaną reprezentację programu tak, jak została ona opisana w pliku źródłowym. Nazwa pliku obiektowego jest zazwyczaj taka sama, jak pliku źródłowego, ale z rozszerzeniem .o. Plik .o zawiera odniesienia, zwane symbolami, do funkcji, zmiennych itp., których potrzebuje kod. Osobne pliki obiektowe są konsolidowane razem tworząc jeden plik wykonywalny, który może zostać załadowany przez system operacyjny. Konsolidacja dołącza również obiekty biblioteczne zawierające definicje funkcji bibliotecznych.
Dlaczego stosujemy konsolidację Małe programy – jeden plik Większe programy: - Wiele linii kodu - Wiele komponentów - Więcej niż jeden programista Problemy: - Trudniej zarządzać długimi plikami (zarówno programistom, jak i komputerom) Każda zmiana wymaga długiej kompilacji Wielu programistów nie może modyfikować jednocześnie tego samego pliku - Pożądany jest podział na komponenty Podzielenie projektu na wiele plików - Dobry podział na komponenty - Mniej kompilacji przy zmianach w programie - Łatwiejsze zarządzanie strukturą i zależnościami w projekcie Język C posiada konstrukcje służące do odwołania do innych plików (extern) Podczas kompilacji można zlinkować wiele plików w celu stworzenia jednego programu. Można kompilować wiele plików źródłowych, linkować wiele prekompilowanych plików pośrednich lub połączyć te opcje
Program gcc gcc jest zestawem kompilatorów, m.in. języka C i C++, projektu GNU i jest to projekt o otwartym źródle. Jest to zarówno kompilator jak i konsolidator. Ogólnie gcc wywołujemy następująco: $ gcc [options] [files] Najczęściej stosowane opcje: -c [files] – tylko kompilacja (bez konsolidacji) -o [file] – plik wyjściowy będzie nazywać się [file]. Jeżeli nie ma tej opcji, to przy kompilacji pliku źródłowego .c tworzony jest plik o tej samej nazwie z rozszerzeniem .o, a przy konsolidacji tworzony jest plik wyjściowy a.out. -g – dołączane są dodatkowe informacje dla debugowania -Wall – daje ostrzeżenia o możliwych błędach w kodzie źródłowym -I[dir] – dodaje [dir] do listy katalogów przeszukiwanych przez dyrektywę #include -l[mylib] – przeszukuje bibliotekę [mylib] w celu znalezienia nierozwiązanych symboli w trakcie konsolidacji. Prawdziwą nazwą pliku jest lib[mylib].a -L[dir] – dodaje [dir] do listy katalogów przeszukiwanych w celu znalezienia plików bibliotecznych
Przykład programu wieloplikowego /* greetings.h */ void greetings(void); /* main.c */ #include <stdio.h> #include "greetings.h" int main() { greetings(); return 0; } /* greetings.c */ #include <stdio.h> #include "greetings.h" void greetings(void) { printf ("Hello user !\n"); } $ gcc -g -Wall -pedantic -c main.c -o main.o $ gcc -g -Wall -pedantic -c greetings.c -o greetings.o $ gcc -g main.o greetings.o -o hello
Przykład programu wieloplikowego /* greetings.h */ void greetings(void); Zmieniamy zawartość tego pliku /* main.c */ #include <stdio.h> #include "greetings.h" int main() { greetings(); return 0; } /* greetings.c */ #include <stdio.h> #include "greetings.h" void greetings(void) { printf ("Hello %s!\n", getenv("LOGNAME")); } $ gcc -g -Wall -pedantic -c greetings.c -o greetings.o $ gcc -g main.o greetings.o -o hello
Zależności między modułami Moduł A jest zależny na etapie kompilacji od modułu B, jeżeli A.c wymaga B.h w celu kompilacji Program A jest zależny na etapie konsolidacji od modułu B, jeżeli A wymaga B.o w celu konsolidacji Unikaj zależności cyklicznych (A -> B i B -> A) Zarządzanie złożonością oprogramowania wymaga m.in. minimalizacji zależności
Makefile i make Łatwo jest zmienić plik nagłówkowy, ale zapomnieć o rekompilacji wszystkich zależnych od niego plików źródłowych Pliki Makefile pomagają w zarządzaniu zależnościami make jest programem często używanym do kompilacji pakietów oprogramowania złożonych z wielu plików źródłowych określa automatycznie które pliki trzeba wygenerować ponownie używa pliku konfiguracyjnego (zazwyczaj zwanego makefile lub Makefile), określającego reguły i zależności dotyczące generacji każdego z plików Plik konfiguracyjny może mieć też inną nazwę, podaną jako argument polecenia make bash# make bash# make -f myfile.mk pierwsza wersja używa domyślnego pliku konfiguracyjnego (makefile lub Makefile), druga wersja używa myfile.mk jako pliku konfiguracyjnego.
Przykład pliku makefile hello: main.o greetings.o gcc -g main.o greetings.o -o hello main.o: main.c greetings.h gcc -g -Wall -pedantic -c main.c -o main.o greetings.o: greetings.c greetings.h gcc -g -Wall -pedantic -c greetings.c -o greetings.o Linia zależności Linia operacji Linia zależności + operacja = reguła Linia zależności musi zaczynać się w kolumnie 1 Linia operacji musi zaczynać się znakiem tabulacji $ make gcc -g -Wall -pedantic -c main.c -o main.o gcc -g -Wall -pedantic -c greetings.c -o greetings.o gcc -g main.o greetings.o -o hello make: `hello' is up to date.
Użycie zmiennych w makefile Makefile składa się z ciągu reguł zależności i definicji zmiennych. Zmienna w makefile jest nazwą reprezentującą łańcuch tekstowy, działającą podobnie do makr preprocesora języka C. Poprzez zmienne najczęściej są reprezentowane listy katalogów do przeszukania, opcje kompilatora, nazwy programów do uruchamiania. Zmienne nie są predeklarowane, ustawia się je poprzez ‘=’. CC = gcc tworzy zmienną CC i ustawia jej wartość na gcc. Pewne nazwy zmiennych uważane są za standardowe, ich użycie zgodnie z domyślnymi regułami upraszcza tworzenie plików makefile: CC – nazwa kompilatora C (domyślnie cc lub gcc w większości wersji make) CFLAGS – lista opcji przekazywana kompilatorowi C dla wszystkich plików źródłowych LDFLAGS – lista opcji przekazywana konsolidatorowi CFLAGS = -g -I/usr/12-unix/wyklad03/include $(CC) $(CFLAGS) -c foo.c
Uruchamianie programów (debugging) Często programy nie działają zgodnie z oczekiwaniami Potrzebujemy sposobu na przejście przez program krok po kroku innej, niż patrzenie na wydruk Znajomość przepływu sterowania i zmian wartości zmiennych podczas wykonywania programu pomaga w znalezieniu błędów Można wstawić do programu instrukcje printf wypisujące wartości zmiennych w strategicznych miejscach Użycie debugera jest zazwyczaj wygodniejsze. Debuger jest programem przy pomocy którego można uruchamiać inne programy, sterować nimi i drukować wartości zmiennych w celu znalezienia i poprawienia błędów Najpopularniejszym debugerem dla systemów UNIX jest GDB, the GNU debugger Debuger pozwala stwierdzić W której instrukcji lub wyrażeniu program się zatrzymał Jeżeli podczas wykonywania funkcji wystąpił błąd, która linia programu zawiera wywołanie tej funkcji i z jakimi parametrami Jakie są wartości zmiennych w danym miejscu podczas wykonywania programu Jaki jest wynik obliczenia dowolnego wyrażenia w danym momencie wykonywania programu
Użycie gdb Z poziomu powłoki gdb uruchamiamy następująco: $ gdb [program] [program] jest nazwą docelowego pliku wykonywalnego, który chcemy debugować. Po uruchomieniu debugger ładuje aplikację oraz jej tablicę symboli (nazwy zmiennych, plików źródłowych itp.). Można je uzyskać stosując opcję ‘-g’ kompilatora. Przykład: $ gcc -Wall -pedantic -g -c hello.c $ gcc -g hello.o -o hello $ gdb hello $ break main $ run
Polecenia gdb run [command-line-arguments] – Uruchamia program jak po napisaniu z linii poleceń czyli hello command-line-arguments Możemy napisać run <file1 >file2 aby przekierować standardowe wejście i wyjście do plików break place – Zastawia pułapkę; program zatrzyma się we wskazanym miejscu kiedy do niego dojdzie. Najczęściej pułapki ustawia się na początku funkcji, jak np. break main Można również zastawić pułapkę w konkretnej linii pliku źródłowego break 20 break hello.c:10 delete N – Usuwa pułapkę numer N. Pominięcie argumentu usuwa wszystkie pułapki. info break wypisuje informacje na temat zastawionych pułapek
Polecenia gdb help command – Opisuje pokrótce polecenie lub zagadnienie związane z GDB help wypisuje listę możliwych tematów step – wykonuje bieżącą linię programu i zatrzymuje się przed wykonaniem kolejnej linii next – jak step, z tym że jeżeli bieżąca linia programu zawiera wywołanie funkcji, przechodzi przez całe wywołanie funkcji i zatrzymuje się w następnej linii (nie wchodzi do wnętrza funkcji) finish – kontynuuje wykonanie programu do końca bieżącej funkcji continue – kontynuuje wykonywanie programu do jego normalnego zakończenia lub napotkania pułapki where – drukuje stos wywołań - łańcuch wywołań funkcji które przywiodły program do bieżącego miejsca. Równoważne polecenie to backtrace print E – drukuje wartość E w bieżącym miejscu programu, gdzie E jest poprawnym wyrażeniem w języku C (zwykle po prostu zmienną) display – działa podobnie, ale wyrażenie zamiast jednokrotnie jest drukowane po każdym zatrzymaniu programu w oparciu o uaktualnione wartości zmiennych quit – kończy działanie GDB
debugowanie pośmiertne Można spowodować, że program będzie zrzucał zawartość pamięci po wywołaniu funkcji abort Polega to na utworzeniu pliku core na dysku w katalogu programu, zawierającego obraz pamięci programu w momencie awaryjnego zakończenia Można wykorzystać ten plik w celu znalezienia przyczyny zatrzymania programu W celu włączenia tej opcji przy korzystaniu z powłoki bash, możemy napisać w linii poleceń ulimit -c unlimited Aby wykorzystać plik core w programie GDB używamy składni gdb progname corefilename