Jak przejdziemy od asemblacji do kodu maszynowego (generowanie kodu)

Czy istnieje łatwy sposób na wizualizację kroku między asemblacją kodu do kodu maszynowego?

Na przykład, jeśli otworzysz plik binarny w notatniku, zobaczysz sformatowaną tekstowo reprezentację kodu maszynowego. Zakładam, że każdy bajt (symbol), który widzisz, jest odpowiadającym mu znakiem ascii dla jego wartości binarnej?

Ale jak przejść od asemblera do pliku binarnego, co się dzieje za kulisami?

Odpowiedź

Przejrzyj dokumentację zestawu instrukcji, a znajdziesz takie wpisy z mikrokontroler pic dla każdej instrukcji:

przykładowa instrukcja addlw

Linia „encoding” mówi jak ta instrukcja wygląda binarnie. W tym przypadku zawsze zaczyna się od 5 jedynek, potem nieważnego bitu (który może być jeden lub zero), a następnie „k” oznacza literał, który dodajesz.

kilka pierwszych bitów nazywanych jest „kodem operacji”, jest unikalnych dla każdej instrukcji. Procesor w zasadzie patrzy na kod operacji, aby zobaczyć, jaka to instrukcja, a następnie wie, jak zdekodować „k” jako liczbę do dodania.

To żmudne, ale nie takie trudne do kodowania i dekodowania. Miałem na studiach licencjackich, na których musieliśmy robić to ręcznie na egzaminach.

Aby faktycznie utworzyć pełny plik wykonywalny, musisz także zrobić takie rzeczy, jak przydzielenie pamięci, obliczenie przesunięć gałęzi i umieszczenie go w format taki jak ELF , w zależności od systemu operacyjnego.

Odpowiedź

Kody montażowe w większości odpowiadają jeden do jednego z podstawowymi instrukcjami maszyny. Więc wszystko, co musisz zrobić, to zidentyfikować każdy kod operacji w języku asemblera, zmapować go na odpowiednią instrukcję maszynową i zapisać instrukcję maszynową do pliku, wraz z odpowiednimi parametrami (jeśli istnieją). Następnie powtarzasz ten proces dla każdego dodatkowego kodu operacji w pliku źródłowym.

Oczywiście stworzenie pliku wykonywalnego, który będzie poprawnie ładowany i uruchamiany w systemie operacyjnym, wymaga oczywiście więcej, a większość przyzwoitych asemblerów to robi. mają pewne dodatkowe możliwości poza prostym mapowaniem kodów operacyjnych na instrukcje maszynowe (na przykład makra).

Odpowiedź

Pierwsza potrzebujesz czegoś takiego jak ten plik . To jest baza danych instrukcji dla procesorów x86 używana przez asembler NASM (którą pomogłem napisać, chociaż nie części, które faktycznie tłumaczą instrukcje). Wybierzmy dowolną linię z bazy danych:

 ADD rm32,imm8 [mi: hle o32 83 /0 ib,s] 386,LOCK  

Oznacza to, że opisuje instrukcję ADD. Istnieje wiele wariantów tej instrukcji, a konkretny, który jest tutaj opisany, to wariant, który przyjmuje albo rejestr 32-bitowy, albo adres pamięci i dodaje natychmiastową wartość 8-bitową (tj. Stałą bezpośrednio zawartą w instrukcji). Przykładowa instrukcja asemblera, która używałaby tej wersji, jest następująca:

 add eax, 42  

Teraz, musisz wziąć wpisany tekst i przeanalizować go na indywidualne instrukcje i operandy. W przypadku powyższej instrukcji skutkowałoby to prawdopodobnie strukturą zawierającą instrukcję ADD i tablicę operandów (odniesienie do rejestru EAX i wartość 42). Gdy masz już taką strukturę, przeglądasz bazę danych instrukcji i znajdujesz wiersz, który pasuje zarówno do nazwy instrukcji, jak i do typów operandów. Jeśli nie znajdziesz dopasowania, jest to błąd, który należy przedstawić użytkownikowi (zwykły tekst to „niedozwolona kombinacja kodu operacji i operandów” lub podobny).

Gdy już pobraliśmy linię z bazy danych, patrzymy na trzecią kolumnę, która dla tej instrukcji to:

 [mi: hle o32 83 /0 ib,s]  

To jest zestaw instrukcji opisujących sposób generowania instrukcji kodu maszynowego, która jest wymagana:

  • mi to opis operandów: jeden a modr/m (rejestr lub pamięć) operand (co oznacza, że „będziemy musieli dołączyć modr/m bajt do koniec instrukcji, do której przejdziemy później) i jedną bezpośrednią instrukcję (która zostanie użyta w opisie instrukcji).
  • Dalej jest hle. To określa, jak traktujemy przedrostek „blokada”. Nie używaliśmy „blokady”, więc ją ignorujemy.
  • Następnie o32. To mówi nam, że jeśli „ponownie złożymy kod dla 16- bitowy format wyjściowy, instrukcja wymaga przedrostka zastępującego rozmiar argumentu.Gdybyśmy tworzyli 16-bitowe wyjście, „d utworzylibyśmy teraz przedrostek (0x66), ale założę, że tak nie jest i kontynuujemy.
  • Dalej jest 83. To jest bajt literału w formacie szesnastkowym. Wypisujemy go.
  • Dalej jest /0. To określa dodatkowe bity, których będziemy potrzebować w bytem modr / m i powoduje, że go generujemy. Bajt modr/m jest używany do kodowania rejestrów lub pośrednich odniesień do pamięci. Mamy jeden taki operand, rejestr. Rejestr ma numer, który jest określony w innym pliku danych :

     eax REG_EAX reg32 0  
  • Sprawdzamy, czy reg32 zgadza się z wymagany rozmiar instrukcji z oryginalnej bazy danych (tak). 0 to numer rejestru. Bajt modr/m to struktura danych określona przez procesor, która wygląda następująco:

      (most significant bit) 2 bits mod - 00 => indirect, e.g. [eax] 01 => indirect plus byte offset 10 => indirect plus word offset 11 => register 3 bits reg - identifies register 3 bits rm - identifies second register or additional data (least significant bit)  
  • Ponieważ pracujemy z rejestrem, pole mod to 0b11.

  • Pole reg zawiera numer rejestru, którego” używamy „, 0b000
  • Ponieważ w tej instrukcji jest tylko jeden rejestr, musimy czymś wypełnić pole rm. Po to właśnie były dodatkowe dane określone w /0, więc umieściliśmy je w polu rm, 0b000.
  • modr/m bajt to zatem 0b11000000 lub 0xC0. Wypisujemy to.
  • Dalej jest ib,s. To określa podpisany bajt natychmiastowy. Patrzymy na operandy i zauważamy, że mamy natychmiastowy dostępną wartość. Konwertujemy ją na bajt ze znakiem i wyprowadzamy (42 => 0x2A).

Kompletna złożona instrukcja to zatem: 0x83 0xC0 0x2A. Wyślij ją do modułu wyjściowego wraz z informacją, że żaden z bajtów nie stanowi odniesienia do pamięci (moduł wyjściowy może potrzebować wiedzieć jeśli tak).

Powtórz te czynności dla każdej instrukcji. Śledź etykiety, aby wiedzieć, co wstawić, gdy są ponownie przywoływane. Dodaj ułatwienia dla makr i dyrektyw, które są przekazywane do modułów wyjściowych pliku obiektowego. I tak właśnie działa asembler.

Komentarze

  • Dziękuję. Świetne wyjaśnienie, ale nie powinno ' t to być ” 0x83 0xC0 0x2A ” zamiast ” 0x83 0xB0 0x2A ” ponieważ 0b11000000 = 0xC0
  • @Kamran – $ cat > test.asm bits 32 add eax,42 $ nasm -f bin test.asm -o test.bin $ od -t x1 test.bin 0000000 83 c0 2a 0000003 … tak, masz ' masz rację. 🙂

Odpowiedź

W praktyce asembler zazwyczaj „nie tworzy bezpośrednio niektórych plików binarnych wykonywalnych , ale niektóre plik obiektowy (do podania później do konsolidatora ). Są jednak wyjątki (można użyć niektórych asemblerów do bezpośredniego utworzenia binarnego pliku wykonywalnego ; są rzadkie).

Po pierwsze, zauważ, że wiele asemblerów jest obecnie darmowymi programami . Pobierz i skompiluj na swoim komputerze źródło kod GNU as (część binutils ) i nasm . Następnie przestudiuj ich kod źródłowy. Przy okazji, polecam używać do tego celu Linuksa (jest to bardzo przyjazny dla programistów i wolnego oprogramowania system operacyjny).

Plik obiektowy utworzony przez asemblera zawiera w szczególności segment kodu i instrukcje dotyczące relokacji . Jest zorganizowany w dobrze udokumentowany format pliku, który zależy od systemu operacyjnego. W systemie Linux ten format (używany dla plików obiektów, bibliotek współdzielonych, zrzutów pamięci i plików wykonywalnych) to ELF . Ten plik obiektowy jest później wprowadzany do konsolidatora (który ostatecznie tworzy plik wykonywalny). Przemieszczenia są określane przez ABI (np. x86-64 ABI ). Przeczytaj książkę Levinea Linkers and Loaders , aby uzyskać więcej informacji.

Segment kodu w takim pliku obiektowym zawiera kod maszynowy z dziurami (do wypełnienia przez linker za pomocą informacji o relokacji). (relokowalny) kod maszynowy wygenerowany przez asembler jest oczywiście specyficzny dla zestawu instrukcji architektura .ISA x86 lub x86-64 (używane w większości procesorów do laptopów i komputerów stacjonarnych) są straszne złożone w szczegółach. Ale do celów dydaktycznych wymyślono uproszczony podzbiór, zwany y86 lub y86-64. Przeczytaj slajdy na nich. Inne odpowiedzi na to pytanie również trochę to wyjaśniają. Możesz przeczytać dobrą książkę o architekturze komputerów .

Większość asemblerów pracuje w dwa przebiegi , drugi emituje relokację lub koryguje część wyników pierwszego przebiegu. Używają teraz zwykłych technik analizy (więc przeczytaj być może Smoczą Księgę ).

Jak plik wykonywalny jest uruchamiany przez system operacyjny jądro (np. jak działa wywołanie systemowe execve w systemie Linux ) to inne (i złożone) pytanie. Zwykle tworzy wirtualną przestrzeń adresową (w procesie robiąc to execve (2) …), a następnie ponownie zainicjuj stan wewnętrzny procesu (w tym rejestry tryb użytkownika ). dynamiczny linker – taki jak ld-linux.so (8) w systemie Linux – może być zaangażowanym w czasie wykonywania. Przeczytaj dobrą książkę, na przykład System operacyjny: trzy łatwe elementy . Witryna wiki OSDEV również zawiera przydatne informacje.

PS. Twoje pytanie jest tak szerokie, że musisz przeczytać kilka książek na jego temat. Podałem kilka (bardzo niekompletnych) odniesień. Powinieneś znaleźć ich więcej.

Komentarze

  • Odnośnie formatów plików obiektowych, dla początkujących I ' d zalecam przyjrzenie się formatowi RDOFF stworzonemu przez NASM. Zostało to celowo zaprojektowane tak, aby było tak proste, jak realistycznie możliwe i nadal działało w różnych sytuacjach. Źródło NASM zawiera konsolidator i moduł ładujący dla formatu. (Pełne ujawnienie – wszystko to zaprojektowałem i napisałem)

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *