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ę ADD
oraz tablicę operandów (odniesienie do rejestru EAX
i 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:
mi
Jest descriptiuon z operandów: onu modr/m
(rejestr lub pamięć) operand (co oznacza, że musimy dołączyć modr/m
bajt 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/m
Bajt 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 reg32
zgadza się z wymaganym rozmiarem instrukcji z oryginalnej bazy danych (tak jest). Jest 0
to numer rejestru. modr/m
Bajt 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, mod
pole jest 0b11
.
reg
Pole jest numer rejestru używamy,0b000
- Ponieważ w tej instrukcji jest tylko jeden rejestr, musimy
rm
coś wypełnić . To właśnie te dodatkowe dane określone w /0
był za, więc stawiamy, że w rm
polu 0b000
.
modr/m
Bajt jest zatem 0b11000000
albo 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.
$ 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ę. :)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
execve
dział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.
źródło