.. _z1-elf: ===================================== Zadanie 1: Konwerter plików binarnych ===================================== Data ogłoszenia: 01.03.2022 Termin oddania: 29.03.2022 (ostateczny 12.04.2022) .. contents:: .. toctree:: :hidden: Materiały dodatkowe =================== - przykład działania: :download:`example.tar.gz` - testy: :download:`z1_test.tar.gz` Wprowadzenie ============ Choć prawie wszystkie współczesne maszyny x86 są już 64-bitowe i odchodzi się już od 32-bitowego oprogramowania, wciąż zdarza się spotkać kod 32-bitowy kod binarny. Praktycznym narzędziem mogącym pozwolić na łączenie kodu 32-bitowego i 64-bitowego może okazać się konwerter zamieniające 64-bitowe pliki ELF na ich 32-bitowe odpowiedniki — na przykład, by używać pojedynczych funkcji z 64-bitowego pliku ``.o`` w 32-bitowym pliku ``.o``. Zadanie ======= Napisać konwerter 64-bitowych plików ``ET_REL`` w 32-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) 64-bitowy plik binarny w formacie ``ET_REL`` o architekturze ``EM_X86_64`` 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_386``. 3. Plik ``ET_REL`` może zawierać: - dowolne sekcje z flagą ``SHF_ALLOC`` (powinny zostać skopiowane do skonwertowanego pliku, a jedyne zmiany w tych sekcjach powinny dotyczyć relokacji) - 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ą 32-bitową funkcję z 64-bitowego kodu, skonwertować odwołania tak by odwoływały się do wygenerowanego stuba, a właściwą 32-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_X86_64_32``/``R_X86_64_32S``, ``R_X86_64_PC32``/``R_X86_64_PLT32`` — należy je skonwertować na relokacje ``R_386_32``, ``R_386_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 w języku C, C++ lub Python - 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 ``a.jackowski@mimuw.edu.pl`` z kopią do ``p.zuk@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) 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 ========= Kompilacja z -m32 -------------------------------------------------- Czasami przydatny może być pakiet gcc-multilib, który rozwiązuje problem niektórych brakujących bibliotek przy kompilacji z -m32. Pakiet ten można zainstalować (również w maszynie wirtualnej przygotowanej na laby) przy pomocy apt-geta. 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