Intel x86 Assembly Language & MicroarchitectureAan de slag met Intel x86 Assembly Language & Microarchitecture


Opmerkingen

Deze sectie geeft een overzicht van wat x86 is en waarom een ontwikkelaar het misschien wil gebruiken.

Het moet ook alle grote onderwerpen binnen x86 vermelden en een link naar de gerelateerde onderwerpen bevatten. Omdat de documentatie voor x86 nieuw is, moet u mogelijk eerste versies van die gerelateerde onderwerpen maken.

x86 Montagetaal

De familie van x86-assemblagetalen vertegenwoordigt tientallen jaren vooruitgang op de originele Intel 8086-architectuur. Naast het feit dat er verschillende dialecten zijn op basis van de gebruikte assembler, zijn in de loop van de jaren extra processorinstructies, registers en andere functies toegevoegd, maar nog steeds achterwaarts compatibel met de 16-bits assemblage die in de jaren 1980 werd gebruikt.

De eerste stap om te werken met x86-assemblage is om te bepalen wat het doel is. Als u bijvoorbeeld code wilt schrijven binnen een besturingssysteem, wilt u bovendien bepalen of u ervoor kiest om een zelfstandige assembler of ingebouwde inline-assemblagefuncties van een hogere taal zoals C te gebruiken. Als u wilt coderen op de "bare metal" zonder een besturingssysteem, hoeft u alleen de assembler van uw keuze te installeren en te begrijpen hoe u binaire code kunt maken die kan worden omgezet in flash-geheugen, opstartbaar image of anders in het geheugen kan worden geladen op de geschikte locatie om met de uitvoering te beginnen.

Een zeer populaire assembler die goed wordt ondersteund op een aantal platforms is NASM (Netwide Assembler), die verkrijgbaar is via http://nasm.us/ . Op de NASM-site kunt u doorgaan met het downloaden van de nieuwste release-build voor uw platform.

ramen

Zowel 32-bit als 64-bit versies van NASM zijn beschikbaar voor Windows. NASM wordt geleverd met een handig installatieprogramma dat op uw Windows-host kan worden gebruikt om de assembler automatisch te installeren.

Linux

Het is goed mogelijk dat NASM al is geïnstalleerd op uw versie van Linux. Voer het volgende uit om dit te controleren:

nasm -v
 

Als de opdracht niet wordt gevonden, moet u een installatie uitvoeren. Tenzij u iets doet dat geavanceerde NASM-functies vereist, is het beste pad om uw ingebouwde pakketbeheertool te gebruiken voor uw Linux-distributie om NASM te installeren. Voer onder Debian-afgeleide systemen zoals Ubuntu en anderen bijvoorbeeld het volgende uit vanaf een opdrachtprompt:

sudo apt-get install nasm
 

Voor op RPM gebaseerde systemen kunt u het volgende proberen:

sudo yum install nasm
 

Mac OS X

Recente versies van OS X (inclusief Yosemite en El Capitan) worden geleverd met een oudere versie van NASM vooraf geïnstalleerd. El Capitan heeft bijvoorbeeld versie 0.98.40 geïnstalleerd. Hoewel dit waarschijnlijk voor bijna alle normale doeleinden zal werken, is het eigenlijk vrij oud. Op dit moment is NASM versie 2.11 vrijgegeven en heeft 2.12 een aantal release-kandidaten beschikbaar.

U kunt de NASM-broncode verkrijgen via de bovenstaande link, maar tenzij u specifiek vanuit de bron moet installeren, is het veel eenvoudiger om het binaire pakket te downloaden vanuit de OS X-releasemap en het uit te pakken.

Eenmaal uitgepakt, wordt het ten zeerste aanbevolen dat u de door het systeem geïnstalleerde versie van NASM niet overschrijft. In plaats daarvan zou je het kunnen installeren in / usr / local:

 $ sudo su
 <user's password entered to become root>
 # cd /usr/local/bin
 # cp <path/to/unzipped/nasm/files/nasm> ./
 # exit
 

Op dit moment bevindt NASM zich in /usr/local/bin , maar bevindt het zich niet op uw pad. Voeg nu de volgende regel toe aan het einde van uw profiel:

 $ echo 'export PATH=/usr/local/bin:$PATH' >> ~/.bash_profile
 

Dit zal /usr/local/bin op je pad zetten. Als nasm -v vanaf de opdrachtprompt, wordt nu de juiste, nieuwere versie weergegeven.

x86 Linux Hello World-voorbeeld

Dit is een basis Hello World-programma in NASM-assemblage voor 32-bits x86 Linux, waarbij systeemoproepen rechtstreeks worden gebruikt (zonder enige libc-functieoproepen). Het is veel om in te nemen, maar na verloop van tijd wordt het begrijpelijk. Regels die beginnen met een puntkomma ( ; ) zijn opmerkingen.

Als je het programmeren van Unix-systemen op laag niveau nog niet kent, wil je misschien gewoon functies in asm schrijven en ze vanuit C- of C ++ -programma's aanroepen. Dan kunt u zich gewoon zorgen maken over het leren omgaan met registers en geheugen, zonder ook de POSIX system-call API en de ABI voor het gebruik ervan te leren.


Dit doet twee systeemaanroepen: write(2) en _exit(2) (niet de exit(3) libc-wrapper die stdio-buffers doorspoelt, enzovoort). (Technisch gezien roept _exit() sys_exit_group aan, niet sys_exit, maar dat is alleen van belang in een multi-threaded proces .) Zie ook syscalls(2) voor documentatie over systeemaanroepen in het algemeen en het verschil tussen ze rechtstreeks maken versus het gebruik van de libc wrapper-functies.

Samenvattend worden systeemoproepen gedaan door de args in de juiste registers te plaatsen en het systeemoproepnummer in eax en vervolgens een int 0x80 instructie uit te voeren. Zie ook Wat zijn de retourwaarden van systeemaanroepen in Assemblage? voor meer uitleg over hoe de asm syscall-interface is gedocumenteerd met voornamelijk C-syntaxis.

De syscall-oproepnummers voor de 32-bits ABI staan in /usr/include/i386-linux-gnu/asm/unistd_32.h (dezelfde inhoud in /usr/include/x86_64-linux-gnu/asm/unistd_32.h ).

#include <sys/syscall.h> bevat uiteindelijk het juiste bestand, zodat u echo '#include <sys/syscall.h>' | gcc -E - -dM | less om de macro-defs te zien (zie dit antwoord voor meer informatie over het vinden van constanten voor asm in C-headers )


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
 

In Linux kunt u dit bestand opslaan als Hello.asm en er een 32-bits uitvoerbaar bestand van maken met deze opdrachten:

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
 

Zie dit antwoord voor meer informatie over het samenstellen van 32 of 64-bits statische of dynamisch gekoppelde Linux-uitvoerbare bestanden, voor NASM / YASM-syntaxis of GNU AT&T syntaxis met GNU as richtlijnen. (Belangrijk punt: zorg ervoor dat u -m32 of equivalent gebruikt bij het bouwen van 32-bits code op een 64-bits host, anders hebt u tijdens de uitvoering verwarrende problemen.)

Je kunt de uitvoering traceren met strace om te zien welke systeemaanroepen het doet:

$ 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 +++
 

De trace op stderr en de reguliere uitvoer op stdout gaan hier allebei naar de terminal, dus ze interfereren in de lijn met de write . Redirect of traceer naar een bestand als u dat wilt. Merk op hoe we hiermee gemakkelijk de syscall-retourwaarden kunnen zien zonder code toe te voegen om ze af te drukken, en het is zelfs nog eenvoudiger dan hiervoor een reguliere debugger (zoals gdb) te gebruiken.

De x86-64-versie van dit programma zou zeer vergelijkbaar zijn en dezelfde args doorgeven aan dezelfde systeemaanroepen, alleen in verschillende registers. En met behulp van de syscall instructie in plaats van int 0x80 .