Premiers pas avec Intel x86 Assembly Language & Microarchitecture

x86 Linux Hello World Example

Il s’agit d’un programme de base Hello World en assemblage NASM pour Linux x86 32 bits, utilisant directement les appels système (sans aucun appel de fonction libc). C’est beaucoup à assimiler, mais avec le temps, cela deviendra compréhensible. Les lignes commençant par un point-virgule (;) sont des commentaires.

Si vous ne connaissez pas déjà la programmation des systèmes Unix de bas niveau, vous pouvez simplement écrire des fonctions dans asm et les appeler à partir de programmes C ou C++. Ensuite, vous pouvez simplement vous soucier d’apprendre à gérer les registres et la mémoire, sans apprendre également l’API d’appel système POSIX et l’ABI pour l’utiliser.


Cela fait deux appels système : [write(2)][1] et [_exit(2)][2] (pas le [exit(3)][3] wrapper libc qui vide les tampons stdio et bientôt). (Techniquement, _exit() appelle sys_exit_group, pas sys_exit, mais cela n’a [d’importance que dans un processus multithread][4].) Voir aussi [syscalls(2)][5] pour la documentation sur les appels système dans général, et la différence entre les créer directement et utiliser les fonctions wrapper de la libc.

En résumé, les appels système sont effectués en plaçant les arguments dans les registres appropriés et le numéro d’appel système dans eax, puis en exécutant une instruction int 0x80. Voir aussi [What are the return values ​​of system calls in Assembly?][6] pour plus d’explications sur la façon dont l’interface asm syscall est documentée avec principalement la syntaxe C.

Les numéros d’appel système pour l’ABI 32 bits se trouvent dans /usr/include/i386-linux-gnu/asm/unistd_32.h (même contenu dans /usr/include/x86_64-linux-gnu/asm/unistd_32. h).

#include <sys/syscall.h> inclura finalement le bon fichier, vous pouvez donc exécuter echo '#include <sys/syscall.h>' | gcc -E - -dM | less pour voir la macro defs (voir [cette réponse pour en savoir plus sur la recherche de constantes pour asm dans les en-têtes C][7])


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

Sous Linux, vous pouvez enregistrer ce fichier sous Hello.asm et créer un exécutable 32 bits à partir de celui-ci avec ces commandes :

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

Voir [cette réponse][8] pour plus de détails sur la création d’assembly dans des exécutables Linux 32 ou 64 bits statiques ou liés dynamiquement, pour la syntaxe NASM/YASM ou la syntaxe GNU AT&T avec les directives GNU as. (Point clé : assurez-vous d’utiliser -m32 ou équivalent lors de la construction de code 32 bits sur un hôte 64 bits, sinon vous aurez des problèmes déroutants au moment de l’exécution.)

Vous pouvez tracer son exécution avec strace pour voir les appels système qu’il effectue :

$ 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 trace sur stderr et la sortie normale sur stdout vont toutes deux au terminal ici, elles interfèrent donc dans la ligne avec l’appel système write. Redirigez ou tracez vers un fichier si vous vous en souciez. Remarquez comment cela nous permet de voir facilement les valeurs de retour des appels système sans avoir à ajouter de code pour les imprimer, et c’est en fait encore plus facile que d’utiliser un débogueur normal (comme gdb) pour cela.

La version x86-64 de ce programme serait extrêmement similaire, passant les mêmes arguments aux mêmes appels système, juste dans des registres différents. Et en utilisant l’instruction syscall au lieu de int 0x80.

[1] : http://man7.org/linux/man-pages/man2/write.2.html [2] : http://man7.org/linux/man-pages/man2/_exit.2.html [3] : http://man7.org/linux/man-pages/man3/exit.3.html [4] : http://stackoverflow.com/questions/38434609/why-do-i-get-a-zombie-when-i-link-assembly-code-without-stdlib [5] : http://man7.org/linux/man-pages/man2/syscalls.2.html [6] : http://stackoverflow.com/q/38751614/224132 [7] : http://stackoverflow.com/q/38602525/224132 [8] : http://stackoverflow.com/questions/36861903/assembling-32-bit-binaries-on-a-64-bit-system-gnu-toolchain/36901649#36901649

Langage d’assemblage x86

La famille des langages d’assemblage x86 représente des décennies d’avancées par rapport à l’architecture Intel 8086 d’origine. En plus de l’existence de plusieurs dialectes différents basés sur l’assembleur utilisé, des instructions de processeur supplémentaires, des registres et d’autres fonctionnalités ont été ajoutés au fil des ans tout en restant rétrocompatibles avec l’assemblage 16 bits utilisé dans les années 1980.

La première étape pour travailler avec l’assemblage x86 consiste à déterminer quel est l’objectif. Si vous cherchez à écrire du code dans un système d’exploitation, par exemple, vous souhaiterez en outre déterminer si vous choisirez d’utiliser un assembleur autonome ou des fonctionnalités d’assemblage en ligne intégrées d’un langage de niveau supérieur tel que C. Si vous souhaitez coder sur le “bare metal” sans système d’exploitation, il vous suffit d’installer l’assembleur de votre choix et de comprendre comment créer du code binaire pouvant être transformé en mémoire flash, en image amorçable ou autrement chargé en mémoire à la endroit approprié pour commencer l’exécution.

Un assembleur très populaire qui est bien pris en charge sur un certain nombre de plates-formes est NASM (Netwide Assembler), qui peut être obtenu à partir de http://nasm.us/. Sur le site NASM, vous pouvez procéder au téléchargement de la dernière version de la version pour votre plate-forme.

Les fenêtres

Les versions 32 bits et 64 bits de NASM sont disponibles pour Windows. NASM est livré avec un programme d’installation pratique qui peut être utilisé sur votre hôte Windows pour installer automatiquement l’assembleur.

Linux

Il se peut que NASM soit déjà installé sur votre version de Linux. Pour vérifier, exécutez :

nasm -v

Si la commande n’est pas trouvée, vous devrez effectuer une installation. À moins que vous ne fassiez quelque chose qui nécessite des fonctionnalités NASM de pointe, le meilleur chemin consiste à utiliser votre outil de gestion de packages intégré pour votre distribution Linux pour installer NASM. Par exemple, sous des systèmes dérivés de Debian tels qu’Ubuntu et d’autres, exécutez ce qui suit à partir d’une invite de commande :

sudo apt-get install nasm

Pour les systèmes basés sur RPM, vous pouvez essayer :

sudo yum install nasm

Mac OS X

Les versions récentes d’OS X (y compris Yosemite et El Capitan) sont livrées avec une ancienne version de NASM préinstallée. Par exemple, El Capitan a la version 0.98.40 installée. Bien que cela fonctionnera probablement à presque toutes les fins normales, il est en fait assez ancien. Au moment d’écrire ces lignes, la version 2.11 de NASM est sortie et 2.12 a un certain nombre de versions candidates disponibles.

Vous pouvez obtenir le code source NASM à partir du lien ci-dessus, mais à moins que vous n’ayez un besoin spécifique d’installer à partir de la source, il est beaucoup plus simple de télécharger le package binaire à partir du répertoire de version OS X et de le décompresser.

Une fois décompressé, il est fortement recommandé de ne pas écraser la version installée sur le système de NASM. Au lieu de cela, vous pouvez l’installer dans /usr/local :

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

À ce stade, NASM est dans /usr/local/bin, mais il n’est pas dans votre chemin. Vous devez maintenant ajouter la ligne suivante à la fin de votre profil :

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

Cela ajoutera /usr/local/bin à votre chemin. L’exécution de nasm -v à l’invite de commande devrait maintenant afficher la version appropriée, la plus récente.