Looking for x86 Keywords? Try Ask4Keywords

Intel x86 Assembly Language & MicroarchitectureErste Schritte mit Intel x86 Assembly Language & Microarchitecture


Bemerkungen

In diesem Abschnitt erhalten Sie einen Überblick darüber, was x86 ist und warum ein Entwickler es verwenden möchte.

Es sollte auch alle großen Themen innerhalb von x86 erwähnen und auf die verwandten Themen verweisen. Da die Dokumentation für x86 neu ist, müssen Sie möglicherweise erste Versionen dieser verwandten Themen erstellen.

x86-Assemblysprache

Die Familie der x86-Assemblersprachen repräsentiert Jahrzehnte des Fortschritts der ursprünglichen Intel 8086-Architektur. Neben den verschiedenen Dialekten, die auf dem verwendeten Assembler basieren, wurden im Laufe der Jahre zusätzliche Prozessoranweisungen, Register und andere Funktionen hinzugefügt, während sie weiterhin abwärtskompatibel zu der in den 80er Jahren verwendeten 16-Bit-Assembly sind.

Der erste Schritt bei der Arbeit mit der x86-Assembly besteht darin, das Ziel zu bestimmen. Wenn Sie beispielsweise Code innerhalb eines Betriebssystems schreiben möchten, sollten Sie zusätzlich festlegen, ob Sie einen Stand-Alone-Assembler oder integrierte Inline-Assembly-Features einer übergeordneten Sprache wie C. verwenden möchten Wenn Sie ohne Betriebssystem auf dem "Bare Metal" kodieren möchten, müssen Sie lediglich den Assembler Ihrer Wahl installieren und verstehen, wie Sie Binärcode erstellen, der in Flash-Speicher, ein bootfähiges Image oder in einen anderen Speicher des Systems umgewandelt werden kann geeigneter Ort, um die Ausführung zu beginnen.

Ein sehr beliebter Assembler, der auf einer Reihe von Plattformen gut unterstützt wird, ist NASM (Netwide Assembler), der unter http://nasm.us/ erhältlich ist . Auf der NASM-Site können Sie den neuesten Release-Build für Ihre Plattform herunterladen.

Windows

Für Windows sind sowohl die 32-Bit- als auch die 64-Bit-Version von NASM verfügbar. NASM wird mit einem praktischen Installationsprogramm geliefert, das auf Ihrem Windows-Host verwendet werden kann, um den Assembler automatisch zu installieren.

Linux

Möglicherweise ist NASM bereits in Ihrer Linux-Version installiert. Um zu überprüfen, führen Sie Folgendes aus:

nasm -v
 

Wenn der Befehl nicht gefunden wird, müssen Sie eine Installation durchführen. Wenn Sie nichts tun, was die neuesten NASM-Funktionen erfordert, sollten Sie zur Installation von NASM das integrierte Paketverwaltungstool für Ihre Linux-Distribution verwenden. Führen Sie beispielsweise unter von Debian abgeleiteten Systemen wie Ubuntu und anderen Folgendes über eine Eingabeaufforderung aus:

sudo apt-get install nasm
 

Bei RPM-basierten Systemen können Sie Folgendes versuchen:

sudo yum install nasm
 

Mac OS X

Bei neueren Versionen von OS X (einschließlich Yosemite und El Capitan) ist eine ältere Version von NASM vorinstalliert. Beispielsweise hat El Capitan die Version 0.98.40 installiert. Während dies wahrscheinlich für fast alle normalen Zwecke funktionieren wird, ist es tatsächlich ziemlich alt. Derzeit wird NASM-Version 2.11 veröffentlicht, und 2.12 verfügt über eine Reihe von Veröffentlichungskandidaten.

Sie können den NASM-Quellcode über den obigen Link abrufen. Wenn Sie jedoch keine spezielle Installation von source benötigen, ist es wesentlich einfacher, das Binärpaket aus dem OS X-Versionsverzeichnis herunterzuladen und es zu entpacken.

Nach dem Entpacken wird dringend empfohlen, die vom System installierte Version von NASM nicht zu überschreiben. Stattdessen können Sie es in / usr / local installieren:

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

Zu diesem Zeitpunkt befindet sich NASM in /usr/local/bin , aber es befindet sich nicht in Ihrem Pfad. Sie sollten jetzt die folgende Zeile am Ende Ihres Profils hinzufügen:

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

Dadurch wird /usr/local/bin Ihrem Pfad vorangestellt. Beim Ausführen von nasm -v an der Eingabeaufforderung sollte jetzt die richtige, neuere Version angezeigt werden.

Beispiel für x86 Linux Hello World

Dies ist ein einfaches Hello World-Programm in der NASM-Assembly für 32-Bit-x86-Linux, bei dem Systemaufrufe direkt (ohne libc-Funktionsaufrufe) verwendet werden. Es ist viel zu verstehen, aber mit der Zeit wird es verständlich. Zeilen, die mit einem Semikolon ( ; ) beginnen, sind Kommentare.

Wenn Sie nicht wissen, wie Unix-Systeme auf niedriger Ebene programmiert werden, möchten Sie vielleicht einfach Funktionen in asm schreiben und sie aus C- oder C ++ - Programmen aufrufen. Dann müssen Sie sich nur mit dem Umgang mit Registern und Speicher befassen, ohne die POSIX-Systemaufruf-API und die ABI für deren Verwendung zu lernen.


Dies führt zu zwei Systemaufrufen: write(2) und _exit(2) (nicht der libc-Wrapper für exit(3) , der stdio-Puffer löscht usw.). (Technisch _exit() ruft _exit() sys_exit_group auf, nicht sys_exit, aber das ist nur in einem Multithread-Prozess von Bedeutung . Weitere syscalls(2) zu Systemaufrufen im Allgemeinen und zum Unterschied zwischen der direkten Verwendung und der Verwendung der libc finden Sie in syscalls(2) Wrapper-Funktionen.

Zusammenfassend werden Systemaufrufe durchgeführt, indem die Argumente in die entsprechenden Register und die eax in eax und dann eine Anweisung int 0x80 . Siehe auch Was sind die Rückgabewerte von Systemaufrufen in Assembly? Weitere Informationen dazu, wie die asm syscall-Schnittstelle hauptsächlich mit C-Syntax dokumentiert wird.

Die Systemrufnummern für die 32-Bit-ABI befinden sich in /usr/include/i386-linux-gnu/asm/unistd_32.h (gleiche Inhalte in /usr/include/x86_64-linux-gnu/asm/unistd_32.h ).

#include <sys/syscall.h> schließlich die richtige Datei. Sie können also echo '#include <sys/syscall.h>' | gcc -E - -dM | less , um die Makro-Defs anzuzeigen ( weitere Informationen zum Finden von Konstanten für ASM in C-Headern finden Sie in dieser Antwort ).


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
 

Unter Linux können Sie diese Datei als Hello.asm und daraus eine 32-Bit- Hello.asm mit den folgenden Befehlen Hello.asm :

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
 

In dieser Antwort finden Sie weitere Informationen zum Erstellen von Assemblys in statische oder dynamisch verknüpfte ausführbare 32- oder 64-Bit-Linux-Dateien für die NASM / YASM-Syntax oder die GNU AT & T-Syntax mit GNU as Anweisungen. ( -m32 : Stellen Sie sicher, dass Sie -m32 oder ein gleichwertiges -m32 wenn Sie 32-Bit-Code auf einem 64-Bit-Host -m32 zur Laufzeit verwirrende Probleme auf.)

Sie können die Ausführung mit strace , um die Systemaufrufe zu sehen:

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

Die Ablaufverfolgung auf stderr und die reguläre Ausgabe auf stdout gehen hier zum Terminal, sodass sie den write stören. Umleiten oder auf eine Datei zurückverfolgen, wenn es Ihnen wichtig ist. Beachten Sie, dass wir auf diese Weise die Rückgabewerte von syscall problemlos sehen können, ohne Code zum Drucken hinzufügen zu müssen. Dies ist sogar noch einfacher als die Verwendung eines regulären Debuggers (wie gdb).

Die x86-64-Version dieses Programms wäre sehr ähnlich und würde dieselben Argumente an die gleichen Systemaufrufe weiterleiten, nur in verschiedenen Registern. Und mit der syscall Anweisung anstelle von int 0x80 .