Zadanie 1: Konwerter plików binarnych¶
Data ogłoszenia: 02.03.2021
Termin oddania: 30.03.2021 (ostateczny 13.04.2021)
Materiały dodatkowe¶
przykład działania:
zso1-example.tar
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¶
Wejściami do procesu konwersji są 1 (jeden) 32-bitowy plik binarny w formacie
ET_REL
o architekturzeEM_386
oraz 1 (jeden) plik tekstowy z listą funkcji i ich typów (wyspecyfikowany niżej).Wyjściem z procesu konwersji powinien być 1 (jeden) 64-bitowy plik binarny w formacie
ET_REL
o architekturzeEM_X86_64
.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ć symboleSTT_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 jakR_386_PC32
) — należy je skonwertować na relokacjeR_X86_64_32
,R_X86_64_PC32
Nie trzeba obsługiwać następujących funkcjonalności:
symboli w “sekcji”
SHN_COMMON
bądź innych specjalnych sekcjachinnych typów symboli i relokacji niż podane powyżej
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.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 <plik ET_REL> <plik z listą funkcji> <docelowy plik ET_REL>
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¶
Nieprecyzyjne instrukcje budowania / zła nazwa pakietu w instrukcji (-0.0)
converter nie jest plikiem wykonywalnym (-0.1)
Problem z uruchomieniem z innego katalogu (/path/to/converter) (-0.1)
Niepoprawna obsługa ścieżek zawierających spację (-0.3)
Brak możliwości uruchomienia 2 instancji (ze względu na pliki tymczasowe) (-0.5)
Brak weryfikacji wejściowych typów ELF / tworzenie plików wynikowych przy nieprawidłowym ELF na wejściu (-0.1)
Dodatkowe symbole globalne (per funkcja) (-0.3)
Rearrange tablicy symboli bez zmiany identyfikatorów odwołań (-0.0; ujęte w pkt. za testy)
Błąd przy generowaniu wstawek: użycie movl zamiast movq (-0.0; ujęte w pkt. za testy)
Problemy z konwersją części argumentów zwracanych (-0.0; ujęte w pkt. za testy)
Brak sanityzacji parametrów przekazywanych do podprogramów (np. –xxxxx) (-0.1)
Wywoływanie zewnętrzych narzędzi do budowania każdorazowo przy konwersji (-0.0)
Nadmiarowe wyrównanie / alokacja stosu (-0.0)
Stuby stałych rozmiarów (-0.0)
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