Zadanie 1: Kalkulator Wyborczy¶
Data ogłoszenia: 21.03.2019
Termin oddania: 11.04.2019 (ostateczny 25.04.2019)
Materiały dodatkowe¶
z1_tests.tar.gz(aktualizacja: poprawione budowanie)
Kalkulator Wyborczy¶
W ostatnim czasie wygraliśmy przetarg na organizację wyborów. Aby nie było konieczne ręczne liczenie głosów, postanowiliśmy utworzyć stosowny program – Kalkulator Wyborczy. Jego realizacją zajęli się oczywiście studenci w ramach Zespołowego Projektu Programistycznego.
Studentom udało się z powodzeniem zaprojektować i napisać program. Niestety, przy wdrożeniu nastąpił powien problem – program został nam dostarczony jako 32-bitowy program binarny, używający profesjonalnego systemu baz danych Wróżbita® (Enterprise Edition). Nasza licencja nie pozwala natomiast na użycie 32-bitowych bibliotek klienckich do tego systemu – mamy tylko i wyłącznie biblioteki 64-bitowe. Nie byliśmy też w stanie zdobyć 64-bitowej wersji programu ani jego źródeł (autorzy z jakiegoś powodu nie odpisują na nasze maile od kiedy obronili pracę licencjacką).
Oczywistym rozwiązaniem jest więc napisanie biblioteki, która pozwoli na łączenie 32-bitowego i 64-bitowego kodu w jednym procesie. Jesteśmy przekonani, że biblioteka ta pozwoli na bezproblemowe uruchomienie naszego programu i płynne przeprowadzenie uczcziwych wyborów.
Zadanie¶
Napisać bibliotekę pozwalającą na załadowanie 32-bitowego programu w formacie ELF do 64-bitowego procesu i wywoływanie 64-bitowych funkcji z 32-bitowego kodu.
Interfejs biblioteki¶
Biblioteka powinna dostarczać jedną funkcję, która powoduje załadowanie i uruchomienie 32-bitowego programu w osobnym procesie:
enum type {
TYPE_VOID,
TYPE_INT,
TYPE_LONG,
TYPE_LONG_LONG,
TYPE_UNSIGNED_INT,
TYPE_UNSIGNED_LONG,
TYPE_UNSIGNED_LONG_LONG,
TYPE_PTR,
};
struct function {
const char *name;
const enum type *args;
int nargs;
enum type result;
void *code;
};
int crossld_start(const char *fname, const struct function *funcs, int nfuncs);
Pierwszym parametrem do funkcji crossld_start jest nazwa pliku zawierającego 32-bitowy
program, który powinien zostać załadowany. Drugi parametr jest tablicą (64-bitowych) funkcji,
które załadowany program powinien móc wywołać. Trzeci parametr jest rozmiarem tej tablicy.
Funkcja powinna wykonać następujące czynności:
Otworzyć podany plik i zweryfikować, że rzeczywiście jest 32-bitowym programem na architekturę i386 w formacie ELF.
Wczytać nagłówki programu i załadować program do pamięci używając stosownych wywołań
mmap.Dla każdej funkcji w liście przekazanej jako drugi parametr stworzyć odpowiadający fragment 32-bitowego kodu (trampolinę), który:
przełączy się w tryb 64-bitowy
przekonwertuje przekazane argumenty na 64-bitową konwencję wywołań
wywoła 64-bitową wersję funkcji (podaną w strukturze
function)przejdzie z powrotem do trybu 32-bitowego
przekonwertuje wynik funkcji na 32-bitową konwencję wywołań
wróci do wywołującego 32-bitowego kodu
Przeczytać strukturę
_DYNAMICprogramu, znaleźć tabelę relokacji i powstawiać w odpowiednie miejsca adresy wytworzonych przed chwilą funkcji.Zaalokować nowy stos w zakresie adresów dostępnym z trybu 32-bitowego i przełączyć się na niego.
Przełączyć się w tryb 32-bitowy.
Skoczyć do punktu wejścia zadanego programu.
Struktura function zawiera następujace pola:
name: nazwa funkcji (nazwa symbolu, której program 32-bitowy będzie mógł użyć)args: tablica typów argumentów funkcji:TYPE_INT: argument typuintTYPE_LONG: argument typulongTYPE_LONG_LONG: argument typulong longTYPE_UNSIGNED_INT: argument typuunsigned intTYPE_UNSIGNED_LONG: argument typuunsigned longTYPE_UNSIGNED_LONG_LONG: argument typuunsigned long longTYPE_PTR: argument typuvoid *bądź podobnego
nargs: liczba argumentów funkcjiresult: typ zwracany przez funkcję (takie typy jak przy argumentach, bądźTYPE_VOIDjeśli funkcja nie zwraca wyniku)code: wskaźnik na (64-bitową) funkcję, którą należy wywołać
Poza funkcjami użytkownika, opisanymi w drugim parametrze, biblioteka powinna również
udostępniać programowi funkcję exit (oczywście 32-bitową):
_Noreturn void exit(int status)
Wywołanie funkcji exit powinno:
Przełączyć się z powrotem w tryb 64-bitowy
Wrócić do oryginalnego stosu (z czasu wywołania
crossld_start)Zwolnić wszystkie zasoby zaalokowane przez
crossld_startWrócić do kodu, który wywołał
crossld_start, zwracającstatusjako wynik tej funkcji.
Funkcja exit jest opcjonalna – jej brak będzie kosztował 2 punkty.
Jeśli z jakiegoś powodu uruchomienie programu nie uda się (nie jest to plik ELF,
niezdefiniowany symbol, itp.), funkcja crossld_start powinna zwrócić -1.
W przypadku błędu podczas konwersji wartości zwracanych (wskaźnik 64-bitowy, który nie mieści się w 32 bitach), biblioteka powinna zakończyć wykonanie 32-bitowego kodu, posprzątać zasoby, po czym zwrócić -1 z funkcji crossld_start (tak jakby została wywołana funkcja exit). Jeśli nasze rozwiązanie nie wspiera funkcji exit, możemy zamiast tego zakończyć proces funkcją abort.
Założenia¶
Można założyć, że wykonywany program 32-bitowy:
jest typu
ET_EXECnie używa TLS (nie ma
PT_TLS)nie używa innych relokacji dynamicznych niż
R_386_JMP_SLOT
Można zignorować wszystkie wpisy w nagłówkach programów inne niż PT_LOAD
i PT_DYNAMIC oraz wszystkie wpisy w tabeli _DYNAMIC inne niż
DT_STRTAB, DT_SYMTAB, DT_PLTRELSZ, DT_JMPREL.
Forma rozwiązania¶
Jako rozwiązanie należy dostarczyć paczkę zawierającą:
dowolną liczbę plików źródłowych z kodem rozwiązania
plik Makefile kompilujący rozwiązanie, lub odpowiadający plik z innego sensownego systemu budowania (np. cmake)
plik readme z krótkim opisem rozwiązania i instrukcjami kompilacji na obrazie qemu z pierwszych zajęć
Kod rozwiązania powinien być napisany w całości w C (ew. C++) i może
używać sensownych bibliotek dostępnych w wykorzystywanym na zajęciach systemie Debian.
Rozwiązanie powinno kompilować się do biblioteki o nazwie libcrossld.so
implementującej interfejs z pliku nagłówkowego. Biblioteka nie powinna eksportować
żadnych symboli poza tymi w nagłówku.
Rozwiązania będą testowane wewnątrz qemu, uruchomionego z obrazem z pierwszych zajęć. Polecamy sprawdzenie, czy rozwiązania kompilują się w tym obrazie.
Rozwiązania prosimy nadsyłać na adres p.zuk@mimuw.edu.pl z kopią
do mwk@mimuw.edu.pl.
Zasady oceniania¶
Za zadanie można uzyskać do 10 punktów. Na ocenę zadania składają się trzy części:
działanie funkcji
crossld_start(od 0 do 8 punktów)działanie funkcji
exit(od 0 do 2 punktów)ocena kodu rozwiązania (od 0 do -10 punktów)
Najczęstsze błędy - spis oznaczeń w USOSwebie¶
Eksport dodatkowych symboli (-0.3)
Brak README (-0.2)
Odwołania do sekcji (-0.8)
Zmienne globalne (-0.8)
Kopiowanie segmentów do pamięci (-0.5)
Brak zamykania pliku (-0.5)
Exit zamiast zwrócenia -1 z crossld_start (-0.8)
Fragile code (-1.0)
Ładowanie całego ELF na stos (-0.5)
Obsługa błędów (-0.2 pkt za usterkę, max -1.5 pkt):
brak sprawdzenia wyniku malloc/mmap/etc
brak zamykania pliku przy błędzie (o ile nie występuje pkt 6)
wycieki pamięci
Nieodtworzenie wymaganego rejestru (-0.2)
Przechodzenie między trybem 32-bitowym a 64-bitowym¶
W architekturze x86, wybór trybu 32-bitowego bądź 64-bitowego jest dokonywany przez użycie odpowiedniego segmentu kodu. Aby przełączyć się między trybami, musimy użyć jednej z trzech instrukcji, które są w stanie zmienić aktywny segment kodu (i mogą być użyte w trybie użytkownika):
jmp far(ljmpw składni AT&T)call far(lcallw składni AT&T)ret far(lretw składni AT&T)
Aby przejść w tryb 32-bitowy, należy użyć jednej z powyższych instrukcji
z docelowym selektorem segmentu kodu równym 0x23. Aby przejść w tryb
64-bitowy, należy użyć selektora 0x33. Po przejściu w tryb 32-bitowy
należy ponadto ustawić segment danych na 0x2b zanim będziemy mogli odwołać się
do pamięci, na przykład w następujący sposób:
pushl $0x2b
popl %ds
pushl $0x2b
popl %es
Należy pamiętać, że przejście w tryb 32-bitowy zniszczy stan rejestrów
%r8-%r15 oraz górną część wszystkich rejestrów.
Należy zapewnić, że cały kod 32-bitowy (trampoliny itp.) oraz kod 64-bitowy wywoływany bezpośrednio z kodu 32-bitowego będzie umieszczony w pierwszych 4GB pamięci. To samo dotyczy stosu używanego przez kod 32-bitowy.
Należy pamiętać, że tryby 32-bitowy i 64-bitowy mają różne konwencje wywołań funkcji, w tym zbiór zachowanych rejestrów.
Przykład: switch_64_32.c.
Wskazówki¶
Wywołując funkcje napisane w C, należy zapewnić odpowiednie wyrównanie stosu.
Przed wykonaniem instrukcji call, stos musi być wyrównany do 16 bajtów.
Ponieważ rozwiązanie ma być 64-bitową biblioteką ładowaną dynamicznie, konieczna
będzie ręczna relokacja części kodu (trampolin wchodzących i wychodzących z trybu
32-bitowego) do niskich 4GB. Należy w tym celu stworzyć odpowiednie wykonywalne
mapowanie z MAP_32BIT.
Ponieważ wywołanie funkcji użytkownika wymaga przekazania parametrów, których liczba nie jest z góry znana, konieczne będzie napisanie funkcji w assemblerze, która dynamicznie przekazuje odpowiednią liczbę argumentów na stosie.
Należy pamiętać, że zbiór zachowanych rejestrów jest inny w trybie 64-bitowym
niż w trybie 32-bitowym – funkcja wołająca kod 64-bitowy z 32-bitowego powinna
zachować rejestry %esi i %edi.
Należy pamiętać, że wejście w tryb 32-bitowy zniszczy górną część wszystkich
rejestrów – implementując crossld_start + exit należy zachować i odtworzyć
stan rejestrów %rbx, %rbp, %r12-%r15.