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ę
_DYNAMIC
programu, 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 typuint
TYPE_LONG
: argument typulong
TYPE_LONG_LONG
: argument typulong long
TYPE_UNSIGNED_INT
: argument typuunsigned int
TYPE_UNSIGNED_LONG
: argument typuunsigned long
TYPE_UNSIGNED_LONG_LONG
: argument typuunsigned long long
TYPE_PTR
: argument typuvoid *
bądź podobnego
nargs
: liczba argumentów funkcjiresult
: typ zwracany przez funkcję (takie typy jak przy argumentach, bądźTYPE_VOID
jeś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_start
Wrócić do kodu, który wywołał
crossld_start
, zwracającstatus
jako 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_EXEC
nie 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
(ljmp
w składni AT&T)call far
(lcall
w składni AT&T)ret far
(lret
w 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
.