Intel x86 Assembly Language & Microarchitecture Tutorial
Introduzione a Intel x86 Assembly Language e Microarchitettura
Ricerca…
Osservazioni
Questa sezione fornisce una panoramica di cosa sia x86 e perché uno sviluppatore potrebbe volerlo utilizzare.
Dovrebbe anche menzionare qualsiasi argomento di grandi dimensioni all'interno di x86 e collegarsi agli argomenti correlati. Poiché la Documentazione per x86 è nuova, potrebbe essere necessario creare versioni iniziali di tali argomenti correlati.
x86 Assembly Language
La famiglia dei linguaggi assembly x86 rappresenta decenni di progressi nell'architettura originale Intel 8086. Oltre a diversi dialetti basati sull'assembler utilizzato, nel corso degli anni sono state aggiunte ulteriori istruzioni per i processori, registri e altre funzionalità, rimanendo comunque retrocompatibili con l'assembly a 16 bit utilizzato negli anni '80.
Il primo passo per lavorare con l'assembly x86 è determinare quale sia l'obiettivo. Se si sta cercando di scrivere codice all'interno di un sistema operativo, ad esempio, si vorrà anche determinare se si sceglierà di utilizzare un assemblatore autonomo o funzionalità di assemblaggio inline incorporate di un linguaggio di livello superiore come C. Se si Desiderate programmare il "bare metal" senza un sistema operativo, è sufficiente installare l'assemblatore di vostra scelta e capire come creare codice binario che può essere trasformato in memoria flash, immagine avviabile o altrimenti caricato in memoria al posizione appropriata per iniziare l'esecuzione.
Un assemblatore molto popolare che è ben supportato su un numero di piattaforme è NASM (Netwide Assembler), che può essere ottenuto da http://nasm.us/ . Sul sito NASM è possibile continuare a scaricare l'ultima versione di build per la propria piattaforma.
finestre
Sono disponibili per Windows sia le versioni a 32 bit che a 64 bit di NASM. NASM è dotato di un comodo programma di installazione che può essere utilizzato sull'host Windows per installare automaticamente l'assemblatore.
Linux
Potrebbe essere che NASM sia già installato sulla tua versione di Linux. Per verificare, eseguire:
nasm -v
Se il comando non viene trovato, sarà necessario eseguire un'installazione. A meno che non si stia facendo qualcosa che richiede funzionalità NASM all'avanguardia, il modo migliore è utilizzare lo strumento di gestione dei pacchetti integrato per la propria distribuzione Linux per installare NASM. Ad esempio, in sistemi derivati da Debian come Ubuntu e altri, eseguire quanto segue da un prompt dei comandi:
sudo apt-get install nasm
Per i sistemi basati su RPM, potresti provare:
sudo yum install nasm
Mac OS X
Le versioni recenti di OS X (incluso Yosemite ed El Capitan) sono fornite con una versione precedente di NASM preinstallata. Ad esempio, El Capitan ha installato la versione 0.98.40. Mentre questo probabilmente funzionerà per quasi tutti gli scopi normali, in realtà è piuttosto vecchio. Al momento della pubblicazione, la versione 2.11 del NASM è stata rilasciata e 2.12 ha un numero di candidati disponibili.
È possibile ottenere il codice sorgente NASM dal link precedente, ma a meno che non si abbia una necessità specifica di installare dal sorgente, è molto più semplice scaricare il pacchetto binario dalla directory di rilascio di OS X e decomprimerlo.
Una volta decompresso, si consiglia vivamente di non sovrascrivere la versione di NASM installata sul sistema. Invece, potresti installarlo in / usr / local:
$ sudo su
<user's password entered to become root>
# cd /usr/local/bin
# cp <path/to/unzipped/nasm/files/nasm> ./
# exit
A questo punto, NASM è in /usr/local/bin , ma non è nel tuo percorso. Ora dovresti aggiungere la seguente riga alla fine del tuo profilo:
$ echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bash_profile
Questo anteporrà /usr/local/bin al tuo percorso. L'esecuzione di nasm -v al prompt dei comandi ora dovrebbe visualizzare la versione corretta, più recente.
Esempio x86 di Linux Hello World
Questo è un programma Hello World di base nell'assemblaggio NASM per Linux x86 a 32 bit, che utilizza le chiamate di sistema direttamente (senza alcuna chiamata di funzione libc). È molto da accettare, ma nel tempo diventerà comprensibile. Le righe che iniziano con un punto e virgola ( ; ) sono commenti.
Se non conosci già la programmazione dei sistemi Unix di basso livello, potresti semplicemente scrivere le funzioni in asm e chiamarle dai programmi C o C ++. Quindi puoi semplicemente preoccuparti di imparare come gestire registri e memoria, senza neanche imparare l'API di chiamata di sistema POSIX e l'ABI per usarlo.
Questo effettua due chiamate di sistema: write(2) e _exit(2) (non il wrapper libc exit(3) che svuota i buffer stdio e così via). (Tecnicamente, _exit() chiama sys_exit_group, non sys_exit, ma ciò conta solo in un processo multi-thread .) Vedi anche syscalls(2) per la documentazione sulle chiamate di sistema in generale, e la differenza tra renderli direttamente vs. usare la libc funzioni wrapper.
In breve, le chiamate di sistema vengono effettuate posizionando gli argomenti nei registri appropriati e il numero di chiamata di sistema in eax , quindi eseguendo un'istruzione int 0x80 . Vedi anche Quali sono i valori di ritorno delle chiamate di sistema in Assembly? per ulteriori spiegazioni su come l'interfaccia asm syscall è documentata principalmente con la sintassi C.
I numeri di chiamata syscall per l'ABI a 32 bit si trovano in /usr/include/i386-linux-gnu/asm/unistd_32.h (stesso contenuto in /usr/include/x86_64-linux-gnu/asm/unistd_32.h ).
#include <sys/syscall.h> alla fine includerà il file giusto, quindi potresti eseguire echo '#include <sys/syscall.h>' | gcc -E - -dM | less per vedere le macro def (vedi questa risposta per maggiori informazioni su come trovare le costanti per asm nelle intestazioni 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
Su Linux, puoi salvare questo file come Hello.asm e creare da esso un eseguibile a 32 bit con questi comandi:
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
Vedere questa risposta per ulteriori dettagli sulla creazione di assembly in eseguibili Linux a 32 o 64 bit statici o collegati dinamicamente, per sintassi NASM / YASM o sintassi GNU AT & T con GNU as direttive. (Punto chiave: assicurati di usare -m32 o equivalente quando si costruisce codice a 32 bit su un host a 64 bit, o si avranno problemi di confusione in fase di esecuzione.)
Puoi tracciare la sua esecuzione con strace per vedere le chiamate di sistema che effettua:
$ 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 +++
La traccia su stderr e l'output regolare su stdout sono entrambi diretti al terminale qui, quindi interferiscono nella linea con la chiamata di sistema di write . Reindirizzare o tracciare un file se ti interessa. Si noti come questo ci permette di vedere facilmente i valori di ritorno di syscall senza dover aggiungere codice per stamparli, ed è in realtà ancora più semplice dell'uso di un debugger regolare (come gdb) per questo.
La versione x86-64 di questo programma sarebbe estremamente simile, passando gli stessi argomenti alle stesse chiamate di sistema, solo in registri diversi. E usando l'istruzione syscall invece di int 0x80 .