Jak przechodzimy od montażu do kodu maszynowego (generowanie kodu)


16

Czy istnieje prosty sposób na wizualizację kroku między asemblowaniem 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 odpowiednim znakiem ascii dla jego wartości binarnej?

Ale jak przejść od montażu do binarnego, co dzieje się za kulisami?

Odpowiedzi:


28

Spójrz na dokumentację zestawu instrukcji, a znajdziesz wpisy takie jak ten z mikrokontrolera pic dla każdej instrukcji:

przykładowa instrukcja addlw

Wiersz „kodowania” informuje, jak ta instrukcja wygląda w formacie binarnym. W tym przypadku zawsze zaczyna się od 5, następnie nie obchodzi mnie bit (który może być jeden lub zero), a następnie „k” oznacza literał, który dodajesz.

Pierwsze bity nazywane są „kodem operacyjnym”, są unikalne dla każdej instrukcji. CPU w zasadzie patrzy na kod operacji, aby zobaczyć, jaka to instrukcja, a następnie wie, że dekoduje „k” jako liczbę, którą należy dodać.

To nużące, ale nie tak trudne do zakodowania i odkodowania. Miałem klasę licencjacką, gdzie musieliśmy to robić ręcznie na egzaminach.

Aby faktycznie utworzyć pełny plik wykonywalny, musisz także wykonać takie czynności, jak przydzielenie pamięci, obliczenie przesunięć gałęzi i ustawienie go w formacie ELF , w zależności od systemu operacyjnego.


10

Kody montażowe mają przeważnie korespondencję jeden-do-jednego z podstawowymi instrukcjami maszyny. Wszystko, co musisz zrobić, to zidentyfikować każdy kod operacji w języku asemblera, zamapować go na odpowiednią instrukcję maszyny i zapisać instrukcję maszyny w pliku, wraz z odpowiadającymi jej parametrami (jeśli takie istnieją). Następnie powtórz proces dla każdego dodatkowego kodu operacji w pliku źródłowym.

Oczywiście, stworzenie pliku wykonywalnego, który poprawnie załaduje się i uruchomi w systemie operacyjnym, wymaga więcej, a większość porządnych asemblerów ma pewne dodatkowe możliwości poza prostym mapowaniem kodów operacyjnych na instrukcje maszynowe (na przykład makra).


7

Pierwszą rzeczą, której potrzebujesz, jest coś takiego jak ten plik . Jest to baza danych instrukcji dla procesorów x86 używanych przez asembler NASM (który pomogłem napisać, choć 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, jest wariantem, który przyjmuje 32-bitowy rejestr lub adres pamięci i dodaje natychmiastową wartość 8-bitową (tj. Stałą bezpośrednio zawartą w instrukcji). Przykładowa instrukcja montażu, która użyłaby tej wersji, to:

add eax, 42

Teraz musisz wprowadzić tekst i parsować go w poszczególnych instrukcjach i operandach. W przypadku powyższej instrukcji prawdopodobnie spowodowałoby to strukturę zawierającą instrukcję ADDoraz tablicę operandów (odniesienie do rejestru EAXi wartości 42). Po zbudowaniu tej struktury, przeszukujesz 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, oznacza to błąd, który musi zostać przedstawiony użytkownikowi („niedozwolona kombinacja opkodu i operandów” lub podobnym jest zwykłym tekstem).

Kiedy mamy już wiersz 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, jak wygenerować wymaganą instrukcję kodu maszynowego:

  • miJest descriptiuon z operandów: onu modr/m(rejestr lub pamięć) operand (co oznacza, że musimy dołączyć modr/mbajt do końca instrukcji, która Przyjdziemy później) i jeden natychmiastowy instrukcji (która być użyte w opisie instrukcji).
  • Dalej jest hle. To określa, w jaki sposób obsługujemy prefiks „blokady”. Nie użyliśmy „blokady”, więc go ignorujemy.
  • Dalej jest o32. To mówi nam, że jeśli gromadzimy kod dla 16-bitowego formatu wyjściowego, instrukcja wymaga przedrostka wielkości operandu. Gdybyśmy produkowali 16-bitowe wyjście, wyprodukowalibyśmy teraz prefiks ( 0x66), ale zakładam, że nie jesteśmy i będziemy kontynuować.
  • Dalej jest 83. Jest to dosłowny bajt w systemie szesnastkowym. Wydajemy to.
  • Dalej jest /0. To określa dodatkowe bity, które będą nam potrzebne w bajcie modr / m, i spowoduje, że je wygenerujemy. modr/mBajt służy do rejestrów koduje lub pośrednie odniesienia pamięci. Mamy jeden taki operand, rejestr. Rejestr ma numer określony w innym pliku danych :

    eax     REG_EAX         reg32           0
  • Sprawdzamy, czy reg32zgadza się z wymaganym rozmiarem instrukcji z oryginalnej bazy danych (tak jest). Jest 0to numer rejestru. modr/mBajt jest strukturą danych określony przez procesor, który wygląda tak:

     (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, modpole jest 0b11.

  • regPole jest numer rejestru używamy,0b000
  • Ponieważ w tej instrukcji jest tylko jeden rejestr, musimy rmcoś wypełnić . To właśnie te dodatkowe dane określone w /0był za, więc stawiamy, że w rmpolu 0b000.
  • modr/mBajt jest zatem 0b11000000albo 0xC0. Wyprowadzamy to.
  • Dalej jest ib,s. Określa podpisany natychmiastowy bajt. Patrzymy na operandy i zauważamy, że mamy dostępną natychmiastową wartość. Konwertujemy go na bajt ze znakiem i wyprowadzamy ( 42=> 0x2A).

Pełna instrukcja montażu jest zatem: 0x83 0xC0 0x2A. Wyślij go do modułu wyjściowego wraz z uwagą, że żaden z bajtów nie stanowi odniesienia do pamięci (moduł wyjściowy może wymagać wiedzieć, czy tak jest).

Powtórz dla każdej instrukcji. Śledź etykiety, abyś wiedział, co wstawić, gdy są do nich odniesienia. Dodaj udogodnienia dla makr i dyrektyw, które są przekazywane do modułów wyjściowych plików obiektowych. I tak w zasadzie działa asembler.


1
Dziękuję Ci. Świetne wytłumaczenie, ale nie powinno to być „0x83 0xC0 0x2A” zamiast „0x83 0xB0 0x2A”, ponieważ 0b11000000 = 0xC0
Kamran

@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 całkowitą rację. :)
Jules

2

W praktyce asembler zwykle nie tworzy bezpośrednio binarnego pliku wykonywalnego , ale jakiś plik obiektowy (do późniejszego dostarczenia do linkera ). Istnieją jednak wyjątki (można użyć niektórych asemblerów do bezpośredniego utworzenia binarnego pliku wykonywalnego; są one rzadkie).

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

Plik obiektowy utworzony przez asembler zawiera w szczególności segment kodu i instrukcje relokacji . Jest zorganizowany w dobrze udokumentowanym formacie pliku, który zależy od systemu operacyjnego. W systemie Linux formatem tym (używanym do plików obiektowych, bibliotek współdzielonych, zrzutów pamięci i plików wykonywalnych) jest ELF . Ten plik obiektowy jest później wprowadzany do konsolidatora (który ostatecznie tworzy plik wykonywalny). Relokacje są określane przez ABI (np. X86-64 ABI ). Przeczytaj książkę Levine'a Łączniki i ładowarki, aby uzyskać więcej.

Segment kodu w takim pliku obiektowym zawiera kod maszynowy z otworami (do wypełnienia za pomocą informacji o relokacji przez linker). (Relokowalny) kod maszynowy generowany przez asembler jest oczywiście specyficzny dla architektury zestawu instrukcji . Do x86 lub x86-64 (stosowane w większości procesorów laptopa lub komputera stacjonarnego) ISA są strasznie skomplikowane w szczegółach. Ale uproszczony podzbiór, zwany y86 lub y86-64, został wynaleziony do celów dydaktycznych. Przeczytaj na nich slajdy . Inne odpowiedzi na to pytanie również trochę to wyjaśniają. Możesz przeczytać dobrą książkę na temat architektury komputera .

Większość asemblerów pracuje w dwóch przebiegach , drugi emituje relokację lub koryguje część wyniku pierwszego przejścia. Używają teraz zwykłych technik parsowania (więc może przeczytaj The Dragon Book ).

Sposób uruchamiania pliku wykonywalnego przez jądro systemu operacyjnego (np. Jak execvedziała wywołanie systemowe w systemie Linux) to inne (i złożone) pytanie. Zwykle konfiguruje wirtualną przestrzeń adresową (w procesie wykonującym to polecenie (2) ...), a następnie ponownie inicjuje wewnętrzny stan procesu (w tym rejestry trybu użytkownika ). Linker dynamiczny -such jak ld-linux.so (8) na Linux- może być zaangażowany w czasie wykonywania. Przeczytaj dobrą książkę, na przykład System operacyjny: trzy łatwe kawałki . OSDEV wiki daje również użyteczne informacje.

PS. Twoje pytanie jest tak ogólne, że musisz przeczytać o nim kilka książek. Podałem niektóre (bardzo niekompletne) referencje. Powinieneś znaleźć ich więcej.


1
Jeśli chodzi o formaty plików obiektowych, dla początkujących polecam przyjrzeć się formatowi RDOFF produkowanemu przez NASM. Zostało to celowo zaprojektowane tak, aby było tak proste, jak to realistycznie możliwe i nadal działało w różnych sytuacjach. Źródło NASM zawiera linker i moduł ładujący dla tego formatu. (Pełne ujawnienie - zaprojektowałem i napisałem je wszystkie)
Jules
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.