Intel x86 Assembly Language & Microarchitecture Samouczek
Rozpoczęcie pracy z językiem asemblera i mikroarchitekturą Intel x86
Szukaj…
Uwagi
Ta sekcja zawiera przegląd tego, czym jest x86 i dlaczego deweloper może chcieć go użyć.
Powinien również wymieniać wszelkie duże tematy w obrębie x86 i zawierać linki do powiązanych tematów. Ponieważ Dokumentacja dla x86 jest nowa, może być konieczne utworzenie początkowych wersji tych pokrewnych tematów.
Język asemblera x86
Rodzina języków asemblera x86 reprezentuje dziesięciolecia rozwoju oryginalnej architektury Intel 8086. Oprócz kilku różnych dialektów opartych na używanym asemblerze, przez lata dodawano dodatkowe instrukcje procesora, rejestry i inne funkcje, wciąż zachowując zgodność z 16-bitowym zestawem używanym w latach 80.
Pierwszym krokiem do pracy ze złożeniem x86 jest określenie celu. Na przykład, jeśli chcesz napisać kod w systemie operacyjnym, musisz dodatkowo ustalić, czy zdecydujesz się użyć autonomicznego asemblera, czy wbudowanych funkcji asemblera języka wyższego poziomu, takiego jak C. Jeśli chcesz zakodować na „bare metal” bez systemu operacyjnego, wystarczy zainstalować wybrany asembler i zrozumieć, jak utworzyć kod binarny, który można przekształcić w pamięć flash, obraz rozruchowy lub w inny sposób załadować do pamięci na odpowiednia lokalizacja, aby rozpocząć wykonywanie.
Bardzo popularnym asemblerem, który jest dobrze obsługiwany na wielu platformach, jest NASM (Netwide Assembler), który można uzyskać pod adresem http://nasm.us/ . Na stronie NASM możesz pobrać najnowszą wersję kompilacji dla swojej platformy.
Windows
Zarówno 32-bitowa, jak i 64-bitowa wersja NASM są dostępne dla systemu Windows. NASM jest wyposażony w wygodny instalator, którego można użyć na hoście Windows, aby automatycznie zainstalować asembler.
Linux
Możliwe, że NASM jest już zainstalowany w twojej wersji Linuksa. Aby sprawdzić, wykonaj:
nasm -v
Jeśli polecenie nie zostanie znalezione, konieczne będzie przeprowadzenie instalacji. Jeśli nie robisz czegoś, co wymaga najnowocześniejszych funkcji NASM, najlepszą ścieżką jest skorzystanie z wbudowanego narzędzia do zarządzania pakietami dla dystrybucji Linuksa, aby zainstalować NASM. Na przykład w systemach opartych na Debianie, takich jak Ubuntu i inne, wykonaj następujące polecenie w wierszu polecenia:
sudo apt-get install nasm
W przypadku systemów opartych na RPM możesz spróbować:
sudo yum install nasm
Mac OS X
Najnowsze wersje systemu OS X (w tym Yosemite i El Capitan) są wyposażone w preinstalowaną starszą wersję NASM. Na przykład El Capitan ma zainstalowaną wersję 0.98.40. Chociaż będzie to prawdopodobnie działać prawie dla wszystkich normalnych celów, jest tak naprawdę dość stare. W tym piśmie NASM wersja 2.11 zostaje wydana, a 2.12 ma wielu kandydatów do wydania.
Kod źródłowy NASM można uzyskać z powyższego łącza, ale jeśli nie ma konkretnej potrzeby instalacji ze źródła, o wiele łatwiej jest pobrać pakiet binarny z katalogu wydania OS X i rozpakować go.
Po rozpakowaniu zdecydowanie zaleca się, aby nie zastępować zainstalowanej przez system wersji NASM. Zamiast tego możesz zainstalować go w / usr / local:
$ sudo su
<user's password entered to become root>
# cd /usr/local/bin
# cp <path/to/unzipped/nasm/files/nasm> ./
# exit
W tym momencie NASM znajduje się w /usr/local/bin , ale nie znajduje się na twojej ścieżce. Teraz powinieneś dodać następujący wiersz na końcu swojego profilu:
$ echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bash_profile
Spowoduje to dodanie /usr/local/bin do twojej ścieżki. Wykonanie nasm -v w wierszu polecenia powinno teraz wyświetlać poprawną, nowszą wersję.
x86 Linux Hello World Przykład
Jest to podstawowy program Hello World w asemblerze NASM dla 32-bitowego systemu Linux x86, wykorzystujący bezpośrednio wywołania systemowe (bez wywołań funkcji libc). To dużo do przyjęcia, ale z czasem stanie się zrozumiałe. Linie zaczynające się od średnika ( ; ) są komentarzami.
Jeśli nie znasz jeszcze programowania niskiego poziomu w systemach uniksowych, możesz po prostu pisać funkcje w asm i wywoływać je z programów C lub C ++. Następnie możesz się martwić, jak nauczyć się obsługi rejestrów i pamięci, bez uczenia API API wywołania systemowego POSIX i ABI do korzystania z niego.
Powoduje to dwa wywołania systemowe: write(2) i _exit(2) (nie wrapper exit(3) libc, który opróżnia bufory stdio i tak dalej). (Technicznie, _exit() wywołuje sys_exit_group, nie sys_exit, ale to ma znaczenie tylko w procesie wielowątkowym .) Zobacz także syscalls(2) aby uzyskać dokumentację dotyczącą wywołań systemowych i różnicę między tworzeniem ich bezpośrednio a używaniem libc funkcje owijania.
Podsumowując, wywołania systemowe są wykonywane przez umieszczenie argumentów w odpowiednich rejestrach, a numer wywołania systemowego w eax , a następnie uruchomienie instrukcji int 0x80 . Zobacz także Jakie są zwracane wartości wywołań systemowych w asemblerze? aby uzyskać dodatkowe wyjaśnienie, w jaki sposób interfejs syscall asm jest dokumentowany głównie składnią C.
Numery wywołań syscall dla 32-bitowego ABI znajdują się w /usr/include/i386-linux-gnu/asm/unistd_32.h (ta sama zawartość w /usr/include/x86_64-linux-gnu/asm/unistd_32.h ).
#include <sys/syscall.h> ostatecznie uwzględni właściwy plik, abyś mógł uruchomić echo '#include <sys/syscall.h>' | gcc -E - -dM | less aby zobaczyć definicje makr (zobacz tę odpowiedź, aby dowiedzieć się więcej o znajdowaniu stałych dla asm w nagłówkach C )
section .text ; Executable code goes in the .text section
global _start ; The linker looks for this symbol to set the process entry point, so execution start here
;;;a name followed by a colon defines a symbol. The global _start directive modifies it so it's a global symbol, not just one that we can CALL or JMP to from inside the asm.
;;; note that _start isn't really a "function". You can't return from it, and the kernel passes argc, argv, and env differently than main() would expect.
_start:
;;; write(1, msg, len);
; Start by moving the arguments into registers, where the kernel will look for them
mov edx,len ; 3rd arg goes in edx: buffer length
mov ecx,msg ; 2nd arg goes in ecx: pointer to the buffer
;Set output to stdout (goes to your terminal, or wherever you redirect or pipe)
mov ebx,1 ; 1st arg goes in ebx: Unix file descriptor. 1 = stdout, which is normally connected to the terminal.
mov eax,4 ; system call number (from SYS_write / __NR_write from unistd_32.h).
int 0x80 ; generate an interrupt, activating the kernel's system-call handling code. 64-bit code uses a different instruction, different registers, and different call numbers.
;; eax = return value, all other registers unchanged.
;;;Second, exit the process. There's nothing to return to, so we can't use a ret instruction (like we could if this was main() or any function with a caller)
;;; If we don't exit, execution continues into whatever bytes are next in the memory page,
;;; typically leading to a segmentation fault because the padding 00 00 decodes to add [eax],al.
;;; _exit(0);
xor ebx,ebx ; first arg = exit status = 0. (will be truncated to 8 bits). Zeroing registers is a special case on x86, and mov ebx,0 would be less efficient.
;; leaving out the zeroing of ebx would mean we exit(1), i.e. with an error status, since ebx still holds 1 from earlier.
mov eax,1 ; put __NR_exit into eax
int 0x80 ;Execute the Linux function
section .rodata ; Section for read-only constants
;; msg is a label, and in this context doesn't need to be msg:. It could be on a separate line.
;; db = Data Bytes: assemble some literal bytes into the output file.
msg db 'Hello, world!',0xa ; ASCII string constant plus a newline (0x10)
;; No terminating zero byte is needed, because we're using write(), which takes a buffer + length instead of an implicit-length string.
;; To make this a C string that we could pass to puts or strlen, we'd need a terminating 0 byte. (e.g. "...", 0x10, 0)
len equ $ - msg ; Define an assemble-time constant (not stored by itself in the output file, but will appear as an immediate operand in insns that use it)
; Calculate len = string length. subtract the address of the start
; of the string from the current position ($)
;; equivalently, we could have put a str_end: label after the string and done len equ str_end - str
W systemie Linux możesz zapisać ten plik jako Hello.asm i zbudować z niego 32-bitowy plik wykonywalny za pomocą następujących poleceń:
nasm -felf32 Hello.asm # assemble as 32-bit code. Add -Worphan-labels -g -Fdwarf for debug symbols and warnings
gcc -nostdlib -m32 Hello.o -o Hello # link without CRT startup code or libc, making a static binary
Zobacz tę odpowiedź, aby uzyskać więcej informacji na temat wbudowywania asemblera w 32 lub 64-bitowe statyczne lub dynamicznie połączone pliki wykonywalne Linuksa, dla składni NASM / YASM lub GNU AT&T z GNU as dyrektywami. (Kluczowa kwestia: pamiętaj o użyciu opcji -m32 lub równoważnej podczas budowania 32-bitowego kodu na 64-bitowym hoście, w przeciwnym razie wystąpią mylące problemy w czasie wykonywania).
Możesz śledzić jego wykonanie za pomocą strace aby zobaczyć wywołania systemowe:
$ strace ./Hello
execve("./Hello", ["./Hello"], [/* 72 vars */]) = 0
[ Process PID=4019 runs in 32 bit mode. ]
write(1, "Hello, world!\n", 14Hello, world!
) = 14
_exit(0) = ?
+++ exited with 0 +++
Śledzenie na stderr i regularne wyjście na stdout idą tutaj do terminala, więc zakłócają linię przy wywołaniu systemowym write . Przekieruj lub śledź do pliku, jeśli cię to obchodzi. Zauważ, jak to pozwala nam łatwo zobaczyć zwracane wartości syscall bez konieczności dodawania kodu, aby je wydrukować, i jest nawet łatwiejsze niż użycie zwykłego debuggera (takiego jak gdb).
Wersja tego programu x86-64 byłaby bardzo podobna, przekazując te same argumenty do tych samych wywołań systemowych, tylko w różnych rejestrach. I używając instrukcji syscall zamiast int 0x80 .