.. _z1-elf: ===================================== Zadanie 1: Konwerter plików binarnych ===================================== Data ogłoszenia: 02.03.2021 Termin oddania: 30.03.2021 (ostateczny 13.04.2021) .. contents:: .. toctree:: :hidden: Materiały dodatkowe =================== - przykład działania: :download:`zso1-example.tar` - :download:`z1_test.tar` .. - :ref:`z1-elf-en` Wprowadzenie ============ Choć praktycznie wszystkie współczesne maszyny x86 są już 64-bitowe i odchodzi się już od 32-bitowego oprogramowania, wciąż zdarza się spotkać kod binarny, który nigdy nie został przekompilowany na 64 bity i nie mamy do niego kodu źródłowego. W takim wypadku możemy skorzystać z jednego z wielu mechanizmów pozwalających na uruchomienie 32-bitowego kodu w 64-bitowym środowisku. Zazwyczaj granicą 32-bitowego kodu jest granica procesu, ale czasem przydaje się możliwość mieszania koda 32-bitowego z 64-bitowym w ramach jednego procesu — na przykład, by używać pojedynczych funkcji z 32-bitowego pliku ``.o`` w 64-bitowym pliku ``.o``. Zadanie ======= Napisać konwerter 32-bitowych plików ``ET_REL`` w 64-bitowe pliki ``ET_REL``, pozwalający wywoływać między nimi funkcje, dodający odpowiednie sekwencje kodu przechodzące pomiędzy trybami wykonania i dopasowujące konwencje przekazywania parametrów do funkcji. Założenia ========= 1. Wejściami do procesu konwersji są 1 (jeden) 32-bitowy plik binarny w formacie ``ET_REL`` o architekturze ``EM_386`` oraz 1 (jeden) plik tekstowy z listą funkcji i ich typów (wyspecyfikowany niżej). 2. Wyjściem z procesu konwersji powinien być 1 (jeden) 64-bitowy plik binarny w formacie ``ET_REL`` o architekturze ``EM_X86_64``. 3. Plik ``ET_REL`` może zawierać: - dowolne sekcje z flagą ``SHF_ALLOC`` (powinny zostać skopiowane do skonwertowanego pliku bez zmian) - własne symbole typu ``STT_NOTYPE``, ``STT_FUNC``, ``STT_OBJECT``, ``STT_SECTION`` (mogą też istnieć symbole ``STT_FILE``, ale można je zignorować) - dla globalnych symboli typu ``STT_FUNC`` należy wygenerować kawałek kodu (stub) pozwalający wywołać odpowiednią funkcję z 64-bitowego kodu na podstawie typu funkcji z listy, zmienić oryginalny symbol na lokalny, i stworzyć nowy symbol globalny wskazujący na stuba - odwołania do symboli zewnętrznych; jeśli symbol zewnętrzny jest wymieniony w pliku z listą funkcji, należy wygenerować dla niego odpowiedni kawałek kodu (stub) pozwalający wywołać zewnętrzną 64-bitową funkcję z 32-bitowego kodu, skonwertować odwołania tak by odwoływały się do wygenerowanego stuba, a właściwą 64-bitową funkcję wywołać z samego stuba; jeśli symbol zewnętrzny nie jest wymieniony w tym pliku, należy go traktować jak symbol danych i nie konwertować go w ten sposób - relokacje następujących typów: ``R_386_32``, ``R_386_PC32``, ``R_386_PLT32`` (które można traktować tak samo jak ``R_386_PC32``) — należy je skonwertować na relokacje ``R_X86_64_32``, ``R_X86_64_PC32`` 4. *Nie* trzeba obsługiwać następujących funkcjonalności: - symboli w "sekcji" ``SHN_COMMON`` bądź innych specjalnych sekcjach - innych typów symboli i relokacji niż podane powyżej 5. Wygenerowany kod może zakładać, że wszystkie przekazywane wskaźniki i wartości ``long`` / ``unsigned long`` mieszczą się w 32 bitach. W szczególności, można założyć, że wskaźnik stosu zawiera się w niskich 4GB. 6. Można założyć, że funkcje do których będziemy generować stuby mają co najwyżej 6 parametrów. Plik z funkcjami ---------------- Plik z listą funkcji jest plikiem tekstowym, w którym każda niepusta linijka opisuje jedną funkcję. Każda definicja funkcji zawiera następujące pola, oddzielone sekwencjami białych znaków (spacji bądź tabów): - nazwa funkcji - typ wartości zwracanej z funkcji, jeden z: - ``void`` - ``int`` (32-bitowa liczba ze znakiem) - ``uint`` (32-bitowa liczba bez znaku) - ``long`` (32-bitowa liczba ze znakiem po stronie 32-bitowej, 64-bitowa po stronie 64-bitowej) - ``ulong`` (32-bitowa liczba bez znaku po stronie 32-bitowej, 64-bitowa po stronie 64-bitowej) - ``longlong`` (64-bitowa liczba ze znakiem) - ``ulonglong`` (64-bitowa liczba bez znaku) - ``ptr`` (wskaźnik — 32-bitowy po stronie 32-bitowej, 64-bitowy po stronie 64-bitowej) - typy parametrów do funkcji (0 lub więcej razy): takie same jak typy wartości zwracanej, poza ``void`` Przykładowo:: fopen ptr ptr ptr fclose int ptr fputc int int ptr fputs int ptr ptr Odpowiadające:: FILE *fopen(const char *, const char *) void fclose(FILE *) int fputc(int, FILE *) int fputs(const char *, FILE *) Forma rozwiązania ================= Jako rozwiązanie należy dostarczyć paczkę zawierającą: - dowolną liczbę plików źródłowych z kodem rozwiązania - jeśli rozwiązanie jest napisane w języku kompilowanym, 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ęć Rozwiązanie (po ew. kompilacji) powinno znajdować się w pliku wykonywalnym o nazwie ``converter``. Program powinien mieć następujący interfejs:: ./converter 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ę dwie części: - wynik automatycznych testów (od 0 do 10 punktów) - ocena kodu rozwiązania (od 0 do -10 punktów) Najczęstsze błędy - spis oznaczeń w USOSwebie ============================================= 1. Nieprecyzyjne instrukcje budowania / zła nazwa pakietu w instrukcji (-0.0) 2. converter nie jest plikiem wykonywalnym (-0.1) 3. Problem z uruchomieniem z innego katalogu (/path/to/converter) (-0.1) 4. Niepoprawna obsługa ścieżek zawierających spację (-0.3) 5. Brak możliwości uruchomienia 2 instancji (ze względu na pliki tymczasowe) (-0.5) 6. Brak weryfikacji wejściowych typów ELF / tworzenie plików wynikowych przy nieprawidłowym ELF na wejściu (-0.1) 7. Dodatkowe symbole globalne (per funkcja) (-0.3) 8. Rearrange tablicy symboli bez zmiany identyfikatorów odwołań (-0.0; ujęte w pkt. za testy) 9. Błąd przy generowaniu wstawek: użycie movl zamiast movq (-0.0; ujęte w pkt. za testy) 10. Problemy z konwersją części argumentów zwracanych (-0.0; ujęte w pkt. za testy) 11. Brak sanityzacji parametrów przekazywanych do podprogramów (np. --xxxxx) (-0.1) 12. Wywoływanie zewnętrzych narzędzi do budowania każdorazowo przy konwersji (-0.0) 13. Nadmiarowe wyrównanie / alokacja stosu (-0.0) 14. Stuby stałych rozmiarów (-0.0) 15. Ignorowanie ``sh_addralign`` ze źródłowego pliku ELF (-0.1) 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 pamiętać, że tryby 32-bitowy i 64-bitowy mają różne konwencje wywołań funkcji, w tym zbiór zachowanych rejestrów. Należy pamietać o zapewnieniu odpowiedniego wyrównania stosu. Wskazówki ========= Sekwencje przechodzenia pomiędzy trybami wykonania -------------------------------------------------- Stub wywołujący 32-bitową funkcję z 64-bitowego kodu może wyglądać np. następująco:: # long long fun(void *ptr, int x, long long y) .code64 fun_stub: # zapis rejestrów pushq %rbx pushq %rbp pushq %r12 pushq %r13 pushq %r14 pushq %r15 # zapisujemy argumenty na stosie subq $0x18, %rsp movq %rdx, 8(%rsp) movl %esi, 4(%rsp) movl %edi, (%rsp) # zmiana trybu ljmpl *fun_addr_64to32 # część 32-bitowa .code32 fun_stub_32: # segmenty pushl $0x2b popl %ds pushl $0x2b popl %es # wywołanie właściwej funkcji call fun # powrót ljmpl *fun_addr_32to64 # znowu część 64-bitowa .code64 fun_stub_64: # konwertujemy wartość zwracaną mov %eax, %eax shlq $32, %rdx orq %rdx, %rax # zrzucamy rzeczy ze stosu i wracamy addq $0x18, %rsp popq %r15 popq %r14 popq %r13 popq %r12 popq %rbp popq %rbx retq fun_addr_64to32: .long fun_stub_32 .long 0x23 fun_addr_32to64: .long fun_stub_64 .long 0x33 Stub wywołujący 64-bitową funkcję z 32-bitowego kodu może wyglądać np. następujaco:: # long long fun(void *ptr, int x, long long y) .code32 fun_stub: # zapis rejestrów pushl %edi pushl %esi # wyrównanie stosu subl $4, %esp # zmiana trybu ljmpl *fun_addr_32to64 # część 64-bitowa .code64 fun_stub_64: # bierzemy argumenty ze stosu movl 0x10(%rsp), %edi movslq 0x14(%rsp), %rsi movq 0x18(%rsp), $rdx # wołamy właściwą funkcję call fun # konwersja wartości zwracanej movq %rax, %rdx shrq $32, %rdx # powrót ljmpl *fun_addr_64to32 .code32 fun_stub_32: addl $4, %esp popl %esi popl %edi retl fun_addr_64to32: .long fun_stub_32 .long 0x23 fun_addr_32to64: .long fun_stub_64 .long 0x33