Instructions de montage et instructions de la machine. Instructions de montage et de machine Registres à usage général

J'écris un programme en assembleur (x86) pour le projecteur. L'essentiel est qu'il existe un mode de commutation automatique des diapositives. Il vous oblige à retarder le diaporama par incréments de 10 secondes. Ils m'ont aidé à faire un tel retard. Voici deux procédures (création de délai et décrémentation de délai)

MakeDelay Proc Near mov al,Timer ; valeur de retard dans la plage de 10 à 90 unités shr al,4 mov ah,al xor al,al ror ax,2 mov word ptr Delay+1,ax mov byte ptr Delay,0 mov byte ptr Delay + 3.0 ret MakeDelay Endp

mov ax,word ptr Delay ou ax,word ptr Delay + 2 cmp ax,0 ; je nxtslide ;Oui - aller au sous-mot de sélection de la diapositive ptr Delay,1 ;Non - aller à la diminution du mot sbb ptr Delay + 2,0 ;Retarde jmp ext5 ;Quitter le sous-programme

Dans la première procédure, on ne sait pas du tout comment ils sont arrivés à un tel algorithme de retard. Et dans la procédure de décrémentation du délai, on ne sait pas pourquoi faire une disjonction. Le problème est que la mise en œuvre du délai est très dépendante du système d'exploitation. Et, par exemple, définir la valeur sur 60 dans Virtual Windows XP entraîne un délai de 65 secondes et dans Windows 7 de 50 secondes. S'il vous plaît, aidez-moi à me débarrasser de ce gâchis.

Code de la tâche : "Assembleur x86"

textuel

Liste des programmes

Procédure ACP proche valeur correcte avec ADC push ax ; enregistrer les registres utilisés dans cette routine push dx ; mov al,01h ;active "Start" out 03h,al ; Attente : en al,02h ;attendre que "Rdy" s'allume test al,01h ; jz Attente ; mov al,0 ; réinitialise "Start" out 03h,al ; in al,01h ;lire la valeur sur le registre d'entrée mov ah,10d ; mov dl,0FFh ;charge dans dl le nombre maximum qui peut être appliqué au registre d'entrée mul ah ;multiplie le nombre du registre d'entrée par la limite supérieure de l'ADC (tension max.) div dl ;divise le résultat par le maximum valeur sur le registre d'entrée et obtenez le nombre dans la plage 1..10 mov skor_ACP,al pop dx pop ax ret ACP endp

Peut être considéré comme code automatique(voir ci-dessous), prolongé par des constructions . Il dépend essentiellement de la plate-forme. Les langages d'assemblage pour diverses plates-formes matérielles sont incompatibles, bien qu'ils puissent être généralement similaires.

En russe, on peut l'appeler simplement " assembleur" (des expressions comme "écrire un programme en assembleur" sont typiques), ce qui, à proprement parler, n'est pas vrai, puisque assembleur l'utilitaire pour traduire un programme avec langage d'assemblage en code informatique.

Définition générale

Le langage d'assemblage est une notation utilisée pour représenter des programmes écrits en code machine sous une forme lisible. Le langage d'assemblage permet au programmeur d'utiliser des codes d'opération mnémoniques alphabétiques, d'attribuer des noms symboliques aux registres et à la mémoire de l'ordinateur à sa discrétion, ainsi que de définir des schémas d'adressage qui lui conviennent (par exemple, index ou indirect). De plus, il vous permet d'utiliser divers systèmes calculus (par exemple, décimal ou hexadécimal) pour représenter des constantes numériques et permet de marquer des lignes de programme avec des étiquettes avec des noms symboliques afin qu'elles soient accessibles (par nom, pas par adresse) à partir d'autres parties du programme (par exemple, transférer le contrôle).

La traduction d'un programme en langage assembleur en code machine exécutable (calcul d'expressions, développement de macros, remplacement des mnémoniques par des codes machine proprement dits et des adresses symboliques par des adresses absolues ou relatives) est réalisée assembleur- un programme de traduction, qui a donné son nom au langage d'assemblage.

Les instructions en langage assembleur correspondent une à une aux instructions du processeur. En fait, ils représentent une forme de notation symbolique plus pratique pour une personne - mnémocodes- les commandes et leurs arguments. Dans ce cas, plusieurs variantes d'instructions processeur peuvent correspondre à une instruction en langage assembleur.

De plus, le langage d'assemblage permet l'utilisation d'étiquettes symboliques au lieu d'adresses de cellules mémoire, qui, lors de l'assemblage, sont remplacées par des adresses absolues ou relatives calculées par l'assembleur ou l'éditeur de liens, ainsi que les soi-disant directives(instructions assembleur qui ne sont pas traduites en instructions machine du processeur, mais exécutées par l'assembleur lui-même).

Les directives d'assemblage permettent notamment d'inclure des blocs de données, de définir l'assemblage d'un fragment de programme par condition, de définir des valeurs d'étiquette, d'utiliser des macros avec des paramètres.

Chaque modèle (ou famille) de processeurs possède son propre ensemble - système - d'instructions et le langage d'assemblage correspondant. Les syntaxes de langage d'assemblage les plus populaires sont la syntaxe Intel et la syntaxe AT&T.

Il existe des ordinateurs qui implémentent un langage de programmation de haut niveau (Fort, Lisp, El-76) en tant que langage machine. En fait, dans de tels ordinateurs, ils jouent le rôle de langages d'assemblage.

Opportunités

L'utilisation du langage d'assemblage fournit au programmeur un certain nombre de fonctionnalités qui ne sont généralement pas disponibles lors de la programmation dans des langages de haut niveau. La plupart d'entre eux sont liés à la proximité du langage avec la plate-forme matérielle.

  • La possibilité d'utiliser pleinement toutes les fonctionnalités de la plate-forme matérielle permet, théoriquement, d'écrire le code le plus rapide et le plus compact possible pour un processeur donné. Un programmeur qualifié, en règle générale, est capable d'optimiser de manière significative le programme par rapport à un traducteur d'un langage de haut niveau dans un ou plusieurs paramètres et de créer un code proche de Pareto optimal (en règle générale, la vitesse du programme est obtenu en allongeant le code et vice versa) :
    • grâce à une utilisation plus rationnelle des ressources du processeur, par exemple, le placement le plus efficace de toutes les données initiales dans les registres, il est possible d'éliminer les accès inutiles à la RAM;
    • grâce à l'optimisation manuelle des calculs, y compris une utilisation plus efficace des résultats intermédiaires, la quantité de code peut être réduite et la vitesse du programme peut être augmentée.
  • La capacité d'accéder directement au matériel, et, en particulier, aux ports d'E / S, aux adresses mémoire spécifiques, aux registres du processeur (cependant, cette capacité est considérablement limitée par le fait que dans de nombreux systèmes d'exploitation, l'accès direct depuis les programmes d'application pour écrire dans les registres du périphérique l'équipement est bloqué pour le fonctionnement du système de fiabilité).

L'utilisation de l'assembleur n'a presque pas d'alternative lors de la création :

  • les pilotes matériels et le noyau du système d'exploitation (au moins les sous-systèmes dépendant de la machine du noyau du système d'exploitation), lorsqu'il est important de coordonner le fonctionnement des périphériques avec le processeur central ;
  • les programmes qui doivent être stockés dans une ROM limitée et/ou exécutés sur des appareils aux performances limitées (« firmware » des ordinateurs et divers appareils électroniques)
  • composants spécifiques à la plate-forme des compilateurs et des interpréteurs de langages de haut niveau, des bibliothèques système et du code qui implémente la compatibilité de la plate-forme.

Séparément, on peut noter qu'à l'aide d'un programme de désassemblage, il est possible de convertir un programme compilé en un programme en langage assembleur. Dans la plupart des cas, c'est le seul moyen (quoique extrêmement chronophage) de désosser les algorithmes du programme si son code source dans un langage de haut niveau n'est pas disponible.

Restrictions

Application

Historiquement, si les codes machine sont considérés comme la première génération de langages de programmation, alors le langage d'assemblage peut être considéré comme la deuxième génération de langages de programmation. Les lacunes du langage d'assemblage, la complexité du développement de grands systèmes logiciels dessus ont conduit à l'émergence de langages de troisième génération - des langages de programmation de haut niveau (tels que Fortran, Lisp, Cobol, Pascal, C, etc. ). Ce sont les langages de programmation de haut niveau et leurs successeurs qui sont actuellement principalement utilisés dans l'industrie des technologies de l'information. Cependant, les langages d'assemblage conservent leur niche en raison de leurs avantages uniques en termes d'efficacité et de capacité à utiliser pleinement les fonctionnalités spécifiques d'une plate-forme particulière.

Les programmes ou leurs fragments sont écrits en langage assembleur dans les cas où ils sont critiques :

  • performances (pilotes, jeux) ;
  • la quantité de mémoire utilisée (secteurs de démarrage, logiciels embarqués (eng. embarqués), programmes pour microcontrôleurs et processeurs aux ressources limitées, virus, protection logicielle).

En utilisant la programmation en langage assembleur, les éléments suivants sont produits :

  • Optimisation des sections de programmes à vitesse critique dans des programmes en langages de haut niveau tels que C++ ou Pascal. Cela est particulièrement vrai pour les consoles de jeux, qui ont des performances fixes, et pour les codecs multimédias, qui ont tendance à être moins gourmands en ressources et plus rapides.
  • Création de systèmes d'exploitation (OS) ou de leurs composants. Actuellement, la grande majorité des systèmes d'exploitation sont écrits dans des langages de plus haut niveau (principalement C, un langage de haut niveau qui a été spécifiquement créé pour écrire l'une des premières versions d'UNIX). Les éléments de code dépendant du matériel, tels que le chargeur de système d'exploitation, la couche d'abstraction matérielle et le noyau, sont souvent écrits en langage d'assemblage. En fait, il y a très peu de code d'assemblage dans les noyaux Windows ou Linux, car les auteurs s'efforcent d'assurer la portabilité et la fiabilité, mais il est néanmoins là. Certains systèmes d'exploitation amateurs, tels que MenuetOS et KolibriOS, sont entièrement écrits en langage d'assemblage. En même temps, MenuetOS et KolibriOS sont placés sur une disquette et contiennent une interface graphique multi-fenêtres.
  • Programmation de microcontrôleurs (MC) et autres processeurs embarqués. Selon le professeur Tanenbaum, le développement de MC répète le développement historique des ordinateurs modernes. Or (2013), le langage assembleur est très souvent utilisé pour programmer MK (bien que des langages comme le C soient également largement utilisés dans ce domaine). Dans MK, vous devez déplacer des octets et des bits individuels entre différentes cellules de mémoire. La programmation MK est très importante, car, selon Tanenbaum, dans une voiture et un appartement d'une personne civilisée moderne, il y a en moyenne 50 microcontrôleurs.
  • Création de pilotes. Les pilotes (ou certains de leurs modules logiciels) se programment en langage assembleur. Bien qu'à l'heure actuelle, les pilotes aient également tendance à écrire dans des langages de haut niveau (il est beaucoup plus facile d'écrire un pilote fiable dans un langage de haut niveau) en raison des exigences accrues de fiabilité et de performances suffisantes des processeurs modernes (la vitesse assure la synchronisation des processus dans l'appareil et le processeur) et perfection suffisante des compilateurs avec des langages de haut niveau (absence de transferts de données inutiles dans le code généré), la grande majorité des pilotes modernes sont écrits en langage d'assemblage. La fiabilité des pilotes joue un rôle particulier, car sous Windows NT et UNIX (y compris Linux), les pilotes s'exécutent en mode noyau du système. Une erreur subtile dans un pilote peut planter tout le système.
  • Création d'antivirus et autres programmes de protection.
  • Écriture de code pour les bibliothèques de bas niveau des traducteurs de langages de programmation.

Liaison de programmes dans différentes langues

Comme pendant longtemps seuls des fragments de programmes ont souvent été codés en langage assembleur, ils doivent être reliés au reste du système logiciel écrit dans d'autres langages de programmation. Ceci est réalisé de deux manières principales :

  • Au stade de la compilation - insertion de fragments d'assembleur (eng. assembleur en ligne) dans le code source d'un programme dans un langage de haut niveau à l'aide de directives de langage spéciales. La méthode est pratique pour les transformations de données simples, mais il est impossible de créer un code assembleur à part entière avec des données et des sous-programmes, y compris des sous-programmes avec de nombreuses entrées et sorties qui ne sont pas prises en charge par un langage de haut niveau.
  • Au stade du lien lors de la compilation séparément . Pour que les modules composables interagissent, il suffit que les fonctions importées (définies dans certains modules et utilisées dans d'autres) prennent en charge certaines conventions d'appel. Des modules séparés peuvent être écrits dans n'importe quel langage, y compris le langage d'assemblage.

Syntaxe

La syntaxe du langage d'assemblage est déterminée par le jeu d'instructions d'un processeur particulier.

Jeu de commandes

Les commandes typiques du langage d'assemblage sont (la plupart des exemples sont donnés pour la syntaxe Intel de l'architecture x86):

  • Commandes de transfert de données (mov, etc.)
  • Commandes arithmétiques (add , sub , imul etc.)
  • Opérations logiques et au niveau du bit (ou , et , xor , shr , etc.)
  • Commandes de contrôle de flux de programme (jmp , loop , ret , etc.)
  • Instructions d'interruption d'appel (parfois appelées instructions de contrôle) : int
  • Commandes d'E/S vers les ports (in , out)
  • Les microcontrôleurs et les micro-ordinateurs sont également caractérisés par des commandes qui effectuent des vérifications et des transitions par condition, par exemple :
  • cjne - sauter si non égal
  • djnz - décrémenter, et si le résultat est différent de zéro, sautez
  • cfsneq - comparer, et s'il n'est pas égal, ignorer la commande suivante

Des instructions

Format d'enregistrement de commande typique

[libellé :] [ [préfixe] mnémocode [opérande (, opérande)] ] [ ;commentaire]

mnémocode- directement mnémonique de l'instruction au processeur. Des préfixes peuvent y être ajoutés (répétitions, changements de type d'adressage, etc.).

Les mnémoniques utilisés sont généralement les mêmes pour tous les processeurs de la même architecture ou famille d'architectures (parmi les mnémoniques les plus connus figurent x86, ARM, SPARC, PowerPC, processeur M68k et mnémoniques de contrôleur). Ils sont décrits dans la spécification du processeur. Dérogations possibles :

  • si l'assembleur utilise la syntaxe AT&T multiplateforme (les mnémoniques d'origine sont convertis en syntaxe AT&T) ;
  • s'il existait initialement deux normes d'écriture des mnémoniques (le système d'instructions a été hérité du processeur d'un autre fabricant).

Par exemple, le processeur Zilog Z80 a hérité du jeu d'instructions Intel 8080, l'a étendu et a modifié les mnémoniques (et les désignations de registre) à sa manière. Les processeurs Motorola Fireball ont hérité du jeu d'instructions Z80, le réduisant un peu. Dans le même temps, Motorola est officiellement revenu aux mnémoniques Intel et pour le moment la moitié des assembleurs Fireball travaillent avec des mnémoniques Intel, et l'autre moitié avec des mnémoniques Zilog.

directives

Un programme en langage assembleur peut contenir directives: instructions qui ne se traduisent pas directement en instructions machine, mais contrôlent le fonctionnement du compilateur. Leur ensemble et leur syntaxe varient considérablement et ne dépendent pas de la plate-forme matérielle, mais du traducteur utilisé (donnant naissance à des dialectes de langages au sein d'une même famille d'architectures). En tant que "gentleman's set" de directives, on peut distinguer:

  • définition des données (constantes et variables),
  • gérer l'organisation du programme en mémoire et les paramètres du fichier de sortie,
  • réglage du mode compilateur,
  • toutes sortes d'abstractions (c'est-à-dire des éléments de langages de haut niveau) - de la conception de procédures et de fonctions (pour simplifier la mise en œuvre du paradigme de programmation procédurale) aux structures et boucles conditionnelles (pour le paradigme de programmation structurée),

Exemple de programme

Exemples de programmes Hello, world! pour différentes plates-formes et différents dialectes :

SECTION .data msg: db " Hello , world " , 10 len: equ $-msg SECTION .text global _start _start: mov edx , len mov ecx , msg mov ebx , 1 ; stdout mov eax , 4 ; write(2) int 0x80 mov ebx , 0 mov eax , 1 ; sortie(2) entier 0x80

SECTION .data msg: db " Hello , world " , 10 len: equ $-msg SECTION .text global _start syscall: int 0x80 ret _start: push len push msg push 1 ; stdout mov eax , 4 ; write(2) call syscall add esp , 3 * 4 push 0 mov eax , 1 ; exit(2) appelle l'appel système

386 .model flat , option stdcall casemap : none include \ masm32 \ include \ windows.inc include \ masm32 \ include \ kernel32.inc includelib \ masm32 \ lib \ kernel32.lib .data msg db " Hello , world " , 13 , 10 len equ $-msg .data ? jj écrit ? .code start: push - 11 call GetStdHandle push 0 push OFFSET écrit push len push OFFSET msg push eax call WriteFile push 0 call ExitProcess end start

format PE console entry start include " include \ win32a.inc " section " .data " data readable writeable message db " Hello , world ! " , 0 section " .code " code readable executable start: ; Macro CINVOKE dans FASM. ; Permet d'appeler les fonctions CDECL. cinvoke printf , message cinvoke getch ; INVOKE est une macro similaire pour les fonctions STDCALL. invoquer ExitProcess , 0 section " .idata " importer le noyau de la bibliothèque lisible par les données , " kernel32.dll " , \ msvcrt , " msvcrt.dll " importer le noyau , \ ExitProcess , " ExitProcess " importer msvcrt , \ printf , " printf " , \ getch , "_getch"

;yasm-1.0.0-win32.exe -f win64 HelloWorld_Yasm.asm;setenv /Release /x64 /xp ;lien HelloWorld_Yasm.obj Kernel32.lib User32.lib /entry:main /subsystem:windows /LARGEADDRESSAWARE:NO bits 64 global main extern MessageBoxA extern ExitProcess section .data mytit db " Le monde 64 bits de Windows & assembleur... " , 0 mymsg db " Hello World ! " , 0 section .text main: mov r9d , 0 ; uType = MB_OK mov r8 , mytit ; LPCSTR lpCaption mov rdx , mymsg ; LPCSTR lpText mov rcx , 0 ; hWnd = HWND_DESKTOP call MessageBoxA mov ecx , eax ; uExitCode = MessageBox(...) call ExitProcess ret

Section ".data" hello : .asciz "Hello World !\n" .section ".text" .align 4 .global main main: save %sp , - 96 , %sp ! allouer de la mémoire mov 4 , %g1 ! 4 = WRITE (appel système) mov 1 , %o0 ! 1 = STDOUT set bonjour , %o1 mov 14 , %o2 ! nombre de caractères jusqu'à 8 ! appel système ! sortie du programme mouvement 1 , %g1 ! move 1 (exit () syscall ) dans %g1 mov 0 , %o0 ! déplacer 0 (adresse de retour) dans %o0 vers 8 ! appel système

ORG 7 C00H USE16 Code JMP NOP DB "HELLOWRD" Sectsize DW 00200 H Clustsize DB 001 H Ressecs DW 00001 H Fatcnt DB 002 H Rootsiz DW 000 E0H Totsecs DW 00 B40H Media DB 0 F0H Fatsize DW 00009 H TRKSECS DW 00012 H Headcnt DW h HidnSec dw 00000 h code: cli mov ax , cs mov ds , ax mov ss , ax mov sp , 7 c00h sti mov ax , 0 b800h mov es , ax mov di , 200 mov ah , 2 mov bx , MessStr msg_print: mov al ,[ cs : bx ] mov [ es : di ], ax inc bx add di , 2 cmp bx , MessEnd jnz msg_print loo: jmp loo MessStr equ $ Message db " Hello , World ! " MessEnd equ $

Histoire et terminologie

Ce type de langage tire son nom du nom du traducteur (compilateur) de ces langages - assembleur (assembleur anglais - assembleur). Le nom est dû au fait que le programme était "automatiquement assemblé" et non entré manuellement commande par commande directement dans les codes. Dans le même temps, il existe une confusion des termes: l'assembleur est souvent appelé non seulement un traducteur, mais également le langage de programmation correspondant («programme assembleur»).

Excusez-moi, avez-vous une minute pour parler de notre sauveur, assembleur ? Dans le dernier article, nous avons écrit notre première application hello world en asma, appris à la compiler et à la déboguer, et également appris à faire des appels système sous Linux. Aujourd'hui, nous allons nous familiariser directement avec les instructions assembleur, le concept de registres, la pile, et tout cela. Les assembleurs pour les architectures x86 (alias i386) et x64 (alias amd64) sont très similaires, et il n'est donc pas logique de les considérer dans des articles séparés. De plus, je vais essayer de me concentrer sur x64, en notant les différences avec x86, le cas échéant. Ce qui suit suppose que vous savez déjà, par exemple, comment une pile diffère d'un tas, et il n'est pas nécessaire d'expliquer de telles choses.

Registres à usage général

Un registre est un petit morceau de mémoire (généralement 4 ou 8 octets) dans un processeur avec une grande vitesse accéder. Les registres sont divisés en registres but spécial et registres généraux. Nous nous intéressons maintenant aux registres à usage général. Comme vous pouvez le deviner d'après le nom, le programme peut utiliser ces registres pour ses propres besoins, à sa guise.

Sur x86, huit registres à usage général 32 bits sont disponibles - eax, ebx, ecx, edx, esp, ebp, esi et edi. Les registres n'ont pas de type prédéfini, c'est-à-dire qu'ils peuvent être traités comme des entiers signés ou non signés, des pointeurs, des booléens, des codes de caractères ASCII, etc. Bien qu'en théorie ces registres puissent être utilisés de n'importe quelle manière, en pratique chaque registre est généralement utilisé d'une manière particulière. Ainsi, esp pointe vers le haut de la pile, ecx joue le rôle d'un compteur et eax est le résultat d'une opération ou d'une procédure. Il existe des registres 16 bits ax, bx, cx, dx, sp, bp, si et di, qui sont les 16 bits les moins significatifs des registres 32 bits correspondants. Sont également disponibles les registres 8 bits ah, al, bh, bl, ch, cl, dh et dl, qui représentent respectivement les octets haut et bas de ax, bx, cx et dx.

Prenons un exemple. Disons que les trois instructions suivantes sont exécutées :

(gdb) x/3i $pc
=> 0x8048074 : mov $0xaabbccdd,%eax
0x8048079 : mouvement $0xee,%al
0x804807b : mov $0x1234,%ax

Enregistrez les valeurs après avoir écrit 0 à eax X AABBCCDD :

(gdb) p/x $eax
$1 = 0xaabbccdd
(gdb) p/x $ax
$2 = 0xccdd
(gdb) p/x $ah
3 $ = 0xcc
(gdb) p/x $al
4 $ = 0xjj

Valeurs après écriture 0 pour s'inscrire al X EE :

(gdb) p/x $eax
5 $ = 0xaabbccee
(gdb) p/x $ax
6 $ = 0xccee
(gdb) p/x $ah
7 $ = 0xcc
(gdb) p/x $al
8 $ = 0xee

Enregistrez les valeurs après avoir écrit 0 à ax X 1234:

(gdb) p/x $eax
$9 = 0xaabb1234
(gdb) p/x $ax
10 $ = 0x1234
(gdb) p/x $ah
$11 = 0x12
(gdb) p/x $al
12 $ = 0x34

Comme vous pouvez le voir, rien de compliqué.

Noter: La syntaxe GAS vous permet de spécifier explicitement les tailles des opérandes en utilisant les suffixes b (octet), w (mot, 2 octets), l (mot long, 4 octets), q (quadword, 8 octets) et quelques autres. Par exemple, au lieu de la commande mov $0xEE , % al tu peux écrire mouvement $0xEE , %al , à la place de mov $0x1234 , % hachemouvement $0x1234 , %ax , etc. Dans GAS moderne, ces suffixes sont facultatifs et personnellement, je ne les utilise pas. Mais ne vous inquiétez pas si vous les voyez dans le code de quelqu'un d'autre.

Sur x64, la taille du registre a été augmentée à 64 bits. Les registres correspondants sont nommés rax, rbx, etc. De plus, il y a seize registres à usage général au lieu de huit. Les registres supplémentaires sont nommés r8, r9, ..., r15. Les registres correspondants, qui représentent les 32, 16 et 8 bits inférieurs, sont appelés r8d, r8w, r8b, et par analogie pour les registres r9-r15. De plus, des registres sont apparus, qui sont les 8 bits inférieurs des registres rsi, rdi, rbp et rsp - sil, dil, bpl et spl, respectivement.

À propos de l'adressage

Comme déjà noté, les registres peuvent être traités comme des pointeurs vers des données en mémoire. Pour déréférencer de tels pointeurs, une syntaxe spéciale est utilisée :

mov(%rsp) , %rax

Cette entrée signifie "lire 8 octets à partir de l'adresse dans le registre rsp et les stocker dans le registre rax". Lorsqu'un programme est démarré, rsp pointe vers le haut de la pile, qui stocke le nombre d'arguments passés au programme (argc), des pointeurs vers ces arguments, ainsi que des variables d'environnement et d'autres informations. Ainsi, à la suite de l'exécution de l'instruction ci-dessus (bien sûr, à condition qu'aucune autre instruction n'ait été exécutée avant), le nombre d'arguments avec lesquels le programme a été lancé sera écrit dans rax.

Dans une commande, vous pouvez spécifier l'adresse et le décalage (positif et négatif) par rapport à celle-ci :

mov 8 (% rsp ) , % rax

Cette entrée signifie "prenez rsp, ajoutez-y 8, lisez 8 octets à l'adresse résultante et mettez-les dans rax". Ainsi, rax contiendra l'adresse de la chaîne représentant le premier argument du programme, c'est-à-dire le nom du fichier exécutable.

Lorsque vous travaillez avec des tableaux, il peut être pratique de faire référence à un élément à un index spécifique. Syntaxe pertinente :

# L'instruction xchg permute les valeurs
xchg 16 (% rsp , % rcx , 8 ) , % rax

Il se lit comme suit : "calculez rcx * 8 + rsp + 16 et échangez 8 octets (taille du registre) à l'adresse résultante et à la valeur du registre rax." En d'autres termes, rsp et 16 jouent toujours le rôle d'un décalage, rcx joue le rôle d'un index dans le tableau et 8 est la taille de l'élément du tableau. Lorsque vous utilisez cette syntaxe, les seules tailles d'élément valides sont 1, 2, 4 et 8. Si une autre taille est requise, vous pouvez utiliser la multiplication, le décalage binaire et d'autres instructions, dont nous parlerons ensuite.

Enfin, le code suivant est également valide :

Données
msg :
. ascii "Bonjour, monde !\n"
. texte

Globl_start
_Démarrer:
# réinitialiser rcx
xor %rcx , %rcx
mov msg(,% rcx , 8 ) , % al
mov msg, %ah

Dans le sens où vous ne pouvez pas spécifier un registre avec un décalage ou aucun registre du tout. À la suite de l'exécution de ce code, le code ASCII de la lettre H, ou 0, sera écrit dans les registres al et ah. X 48.

Dans ce contexte, je voudrais mentionner une autre instruction assembleur utile :

# rax:= rcx*8 + rax + 123
lea 123 (% rax , % rcx , 8 ) , % rax

L'instruction lea est très pratique, car elle vous permet d'effectuer des multiplications et plusieurs additions à la fois.

faits amusants! Sur x64, le bytecode d'instruction n'utilise jamais de décalages 64 bits. Contrairement à x86, les instructions fonctionnent souvent non pas avec des adresses absolues, mais avec des adresses relatives à l'adresse de l'instruction elle-même, ce qui vous permet d'accéder aux +/- 2 Go de RAM les plus proches. Syntaxe pertinente :

movb msg(% rip) , % al

Comparons les longueurs des opcodes mov "réguliers" et "relatifs" (objdump -d ):

4000b0 : 8a 0c 25 e8 00 60 00 mov 0x6000e8,%cl
4000b7 : 8a 05 2b 00 20 00 mov 0x20002b(%rip),%al # 0x6000e8

Comme vous pouvez le voir, le mov "relatif" est également plus court d'un octet ! De quel type de registre s'agit-il, nous le découvrirons un peu plus bas.

Pour écrire la valeur 64 bits complète dans le registre, une instruction spéciale est fournie :

movabs $0x1122334455667788 , %rax

En d'autres termes, les processeurs x64 codent les instructions avec autant de parcimonie que les processeurs x86, et de nos jours, il est peu logique d'utiliser des processeurs x86 dans des systèmes avec quelques gigaoctets de RAM ou moins (appareils mobiles, réfrigérateurs, fours à micro-ondes, etc.). Il y a de fortes chances que les processeurs x64 soient encore plus efficaces grâce à davantage de registres disponibles et plus grande taille ces registres.

Opérations arithmétiques

Considérez les opérations arithmétiques de base :

# initialiser les valeurs de registre
mov $123 , %rax
mov $456 , %rcx

# incrément : rax = rax + 1 = 124
inc%rax

# décrémenter : rax = rax - 1 = 123
déc%rax

# addition : rax = rax + rcx = 579
ajouter %rcx , %rax

# soustraction : rax = rax - rcx = 123
sous % rcx , % rax

# changer de signe : rcx = - rcx = -456
%rcx négatif

Ici et ci-dessous, les opérandes peuvent être non seulement des registres, mais aussi des zones mémoire ou des constantes. Mais les deux opérandes ne peuvent pas être des emplacements de mémoire. Cette règle s'applique à toutes les instructions assembleur x86/x64, au moins celles abordées dans cet article.

Exemple de multiplication :

mov $100 , % al
mouvement $3 , %cl
mul %cl

Dans cet exemple, l'instruction mul multiplie al par cl et stocke le résultat de la multiplication dans la paire de registres al et ah. Ainsi, ax prendra la valeur 0 X 12C ou 300 en notation décimale. Dans le pire des cas, cela peut prendre jusqu'à 2*N octets pour stocker le résultat de la multiplication de deux valeurs de N octets. Selon la taille de l'opérande, le résultat est stocké dans al:ah, ax:dx, eax:edx ou rax:rdx. De plus, le premier de ces registres et l'argument passé à l'instruction sont toujours utilisés comme multiplicateurs.

La multiplication signée se fait exactement de la même manière en utilisant l'instruction imul. De plus, il existe des variantes d'imul avec deux et trois arguments :

mov $123 , %rax
mov $456 , %rcx

#rax=rax*rcx=56088
imul % rcx , % rax

#rcx=rax*10=560880
imul $10 , % rax , % rcx

Les instructions div et idiv font le contraire de mul et imul. Par example:

mouvement $0 , %rdx
mov $456 , %rax
mouvement $123 , %rcx

# rax = rdx:rax / rcx = 3
# rdx = rdx:rax % rcx = 87
div %rcx

Comme vous pouvez le voir, le résultat d'une division entière a été obtenu, ainsi que le reste de la division.

Ce ne sont pas toutes des instructions arithmétiques. Par exemple, il y a aussi adc (addition, prise en compte du drapeau de portage), sbb (soustraction, prise en compte du prêt), ainsi que des instructions correspondantes qui positionnent et effacent les drapeaux correspondants (ctc, clc), et beaucoup d'autres. Mais ils sont beaucoup moins courants, et ne sont donc pas considérés dans le cadre de cet article.

Opérations logiques et sur les bits

Comme déjà noté, il n'y a pas de typage spécial dans l'assembleur x86/x64. Par conséquent, ne soyez pas surpris qu'il n'ait pas d'instructions distinctes pour effectuer des opérations booléennes et des instructions distinctes pour effectuer des opérations sur les bits. Au lieu de cela, il existe un ensemble d'instructions qui fonctionnent avec des bits, et la façon d'interpréter le résultat dépend du programme spécifique.

Ainsi, par exemple, le calcul de l'expression logique la plus simple ressemble à :

mov $0 , % rax # a = faux
mov $1 , % rbx # b = vrai
mov $0 , % rcx # c = faux

# rdx:= un || !(avant JC)
mouvement % rcx , % rdx # rdx = c
et % rbx , % rdx # rdx &= b
pas %rdx#rdx=~rdx
ou % rax , % rdx # rdx |= a
et $1 , % rdx # rdx &= 1

Notez qu'ici nous avons utilisé un bit le moins significatif dans chacun des registres 64 bits. Ainsi, des déchets se forment dans les bits de poids fort, que nous remettons à zéro avec la dernière commande.

Une autre instruction utile est xor (ou exclusif). Dans les expressions booléennes, xor est rarement utilisé, mais il réinitialise souvent les registres. Si vous regardez les opcodes d'instructions, vous comprendrez pourquoi :

4000b3 : 48 31 db x ou %rbx,%rbx
4000b6 : 48 ff c3 inc %rbx
4000b9 : 48 c7 c3 01 00 00 00 mov $0x1,%rbx

Comme vous pouvez le voir, les instructions xor et inc sont codées avec seulement trois octets chacune, tandis que l'instruction mov qui fait la même chose occupe jusqu'à sept octets. Chaque cas individuel, bien sûr, est préférable de comparer séparément, mais la règle heuristique générale est la suivante - plus le code est court, plus il s'intègre dans les caches du processeur, plus il fonctionne rapidement.

Dans ce contexte, il convient également de rappeler les consignes de bit shift, bit test (bit test) et bit scan (bit scan) :

# mettre quelque chose dans le registre
movabs $0xc0de1c0ffee2beef , %rax

# décaler vers la gauche de 3 bits
# rax = 0x0de1c0ffee2beef0
shl $4 , % rax

# décalage vers la droite de 7 bits
#rax = 0x001bc381ffdc57dd
shr $7 , % rax

# tourner à droite de 5 bits
#rax=0xe800de1c0ffee2be
ror $5 , % rax

# tourner à gauche de 5 bits
#rax = 0x001bc381ffdc57dd
lancer $5 , % rax

# idem + set bit (bit test et set)

bts 13 $, % rax

# idem + reset bit (bit test et reset)
#rax=0x001bc381ffdc57dd, CF=1
btr $13 , % rax

# idem + invert bit (bit test et complément)
#rax=0x001bc381ffdc77dd, CF=0
btc 13 $ , % rax

# trouve l'octet différent de zéro le moins significatif (balayage des bits vers l'avant)
#rcx=0, ZF=0
bsf %rax , %rcx

# trouve l'octet différent de zéro le plus significatif (bit scan reverse)
#rdx=52, ZF=0
bsr % rax , % rdx

# si tous les bits sont à zéro, ZF = 1, la valeur rdx est indéfinie
xor % rax , % rax
bsf %rax , %rdx

Il existe également des décalages de bits signés (sal, sar), des décalages cycliques avec un indicateur de retenue (rcl, rcr) et des décalages à double précision (shld, shrd). Mais ils ne sont pas utilisés si souvent et vous en aurez assez d'énumérer toutes les instructions en général. Par conséquent, je vous laisse leur étude comme devoir.

Conditionnels et boucles

Certains drapeaux ont été mentionnés ci-dessus à plusieurs reprises, par exemple, le drapeau de transfert. Les drapeaux sont des bits du registre spécial eflags / rflags (le nom sur x86 et x64, respectivement). Ce registre n'est pas accessible directement par les instructions mov, add et similaires, mais il est modifié et utilisé indirectement par diverses instructions. Par exemple, le drapeau de retenue (CF) déjà mentionné est stocké dans le bit zéro de eflags / rflags et est utilisé, par exemple, dans la même instruction bt. D'autres drapeaux fréquemment utilisés incluent le drapeau zéro (ZF, 6e bit), le drapeau de signe (SF, 7e bit), le drapeau de direction (DF, 10e bit) et le drapeau de débordement (OF, 11e bit).

Un autre de ces registres implicites devrait s'appeler eip/rip, qui stocke l'adresse de l'instruction courante. Il n'est pas non plus accessible directement, mais est visible dans GDB avec eflags / rflags si vous dites info registers , et est modifié indirectement tout le monde des instructions. La plupart des instructions augmentent simplement eip / rip de la longueur de cette instruction, mais il existe des exceptions à cette règle. Par exemple, l'instruction jmp saute simplement à l'adresse donnée :

# réinitialiser rax
xor % rax , % rax
suivant
# cette instruction sera ignorée
inc%rax
suivant:
inc%rax

En conséquence, la valeur de rax sera égale à un, puisque la première instruction inc sera sautée. Notez que l'adresse de saut peut également être écrite dans un registre :

xor % rax , % rax
mouvement $suivant, %rcx
jmp*%rcx
inc%rax
suivant:
inc%rax

Cependant, en pratique, il vaut mieux éviter un tel code, car il casse la prédiction de branchement et est donc moins efficace.

Noter: GAS permet de donner aux étiquettes des noms numériques comme 1 : , 2 : , etc., et de sauter à l'étiquette précédente ou suivante la plus proche avec un numéro donné avec des instructions comme jmp1b et jmp 1f. C'est très pratique, car il peut parfois être difficile de trouver des noms significatifs pour les étiquettes. Les détails peuvent être trouvés.

Les sauts conditionnels sont généralement implémentés avec l'instruction cmp, qui compare ses deux opérandes et définit les indicateurs appropriés, suivie d'une instruction des familles je, jg ​​et similaires :

cmp %rax , %rcx

je 1f # sauter si égal (égal)
jl 1f # sauter si signe moins (moins)
jb 1f # sauter si non signé inférieur à (ci-dessous)
jg 1f # sauter si signe supérieur à (supérieur)
ja 1f # sauter si non signé supérieur à (ci-dessus)

Il existe également des instructions jne (saut si non égal), jle (saut si signé inférieur ou égal), jna (saut si non signé non supérieur à), etc. Le principe de leur appellation, je l'espère, est évident. Au lieu de je/jne, on écrit souvent jz/jnz, puisque les instructions je/jne vérifient simplement la valeur de ZF. Il existe également des instructions qui vérifient d'autres indicateurs - js, jo et jp, mais en pratique, ils sont rarement utilisés. Toutes ces instructions réunies sont communément appelées jcc. C'est-à-dire qu'au lieu de conditions spécifiques, deux lettres "c" sont écrites, à partir de "condition". vous pouvez trouver un bon tableau récapitulatif de toutes les instructions jcc et des drapeaux qu'elles vérifient.

En plus de cmp, l'instruction de test est également souvent utilisée :

tester %rax , %rax
jz 1f # sauter si rax == 0
js 2f # sauter si rax< 0
1 :
# du code
2 :
# un autre code

faits amusants! Fait intéressant, cmp et test sont essentiellement les mêmes que sub et et, mais ils ne changent pas leurs opérandes. Cette connaissance peut être utilisée pour exécuter un sous ou et et un branchement conditionnel en même temps, sans cmp ou instructions de test supplémentaires.

Une autre des instructions associées aux sauts conditionnels est la suivante.

jrcxz 1f
# du code
1 :

L'instruction jrcxz saute uniquement si la valeur du registre rcx est zéro.

cmovge %rcx , %rax

Les instructions de la famille cmovcc (déplacement conditionnel) fonctionnent comme mov, mais uniquement lorsque la condition spécifiée est remplie, par analogie avec jcc.

setnz % al

Les instructions setcc définissent un registre à un seul octet ou un octet en mémoire sur 1 si la condition spécifiée est vraie, et sur 0 sinon.

cmpxchg % rcx , (% rdx )

Comparez rax avec le morceau de mémoire donné. Si égal, définissez ZF et stockez la valeur du registre spécifié à l'adresse spécifiée, dans cet exemple rcx. Sinon, effacez ZF et chargez la valeur de la mémoire dans rax. De plus, les deux opérandes peuvent être des registres.

cmpxchg8b(%rsi)
cmpxchg16b(%rsi)

L'instruction cmpxchg8b est principalement nécessaire sur x86. Il fonctionne de la même manière que cmpxchg, sauf qu'il compare et échange 8 octets à la fois. Les registres edx:eax sont utilisés pour la comparaison, et les registres ecx:ebx stockent ce que nous voulons écrire. L'instruction cmpxchg16b, sur le même principe, compare et permute 16 octets à la fois sur x64.

Important! Notez que sans le préfixe de verrouillage, toutes ces instructions de comparaison et d'échange ne sont pas atomiques.

mouvement $10 , %rcx
1 :
# du code
boucle 1b
# loopz 1b
# loopnz 1b

L'instruction de boucle décrémente la valeur du registre rcx de un, et si après cela rcx != 0 , saute à l'étiquette donnée. Les instructions loopz et loopnz fonctionnent de manière similaire, seules les conditions sont plus compliquées - (rcx != 0) && (ZF == 1) et (rcx != 0) && (ZF == 0) respectivement.

Il ne faut pas un cerveau pour comprendre les constructions if-then-else ou les boucles for/while avec ces instructions, alors passons à autre chose.

Opérations "chaînes"

Considérez le morceau de code suivant :

mov $str1, %rsi
mov $str2, % edi
CLD
cmpsb

Les registres rsi et rdi sont remplis avec les adresses de deux chaînes. La commande cld efface le drapeau de direction (DF). L'instruction qui fait le contraire s'appelle std. Ensuite, l'instruction cmpsb entre en jeu. Il compare les octets (%rsi) et (%rdi) et définit des drapeaux en fonction du résultat de la comparaison. Ensuite, si DF = 0, rsi et rdi augmentent de un (le nombre d'octets dans ce que nous avons comparé), sinon ils diminuent. Les instructions similaires cmpsw, cmpsl et cmpsq comparent des mots, longs mots et mots quadruples, respectivement.

Les instructions cmps sont intéressantes car elles peuvent être utilisées avec le préfixe rep, repe (repz) et repne (repnz). Par example:

mov $str1, %rsi
mov $str2, % edi
mov $len, %rcx
CLD
repe cmpsb
jne not_equal

Le préfixe rep répète l'instruction le nombre de fois spécifié dans le registre rcx. Les préfixes repz et repnz font de même, mais seulement après chaque exécution de l'instruction, ZF est en plus vérifié. La boucle est terminée si ZF = 0 dans le cas de c repz et si ZF = 1 dans le cas de repnz. Ainsi, le code ci-dessus vérifie l'égalité entre deux tampons de même taille.

Des instructions similaires movs déplacent les données du tampon dont l'adresse est spécifiée en rsi vers le tampon dont l'adresse est spécifiée en rdi (facile à retenir - rsi signifie source, rdi signifie destination). L'instruction stos remplit le tampon à l'adresse dans rdi avec les octets dans rax (ou eax, ou ax, ou al, selon l'instruction particulière). Les instructions lods font le contraire - copient les octets à l'adresse spécifiée dans rsi vers le registre rax. Enfin, les instructions scas recherchent des octets dans le registre rax (ou les registres plus petits correspondants) dans le tampon dont l'adresse est spécifiée dans rdi. Comme cmps, toutes ces instructions fonctionnent avec les préfixes rep, repz et repnz.

Sur la base de ces instructions, les procédures memcmp, memcpy, strcmp et similaires sont facilement mises en œuvre. Il est intéressant de noter que, par exemple, pour réinitialiser la mémoire, les ingénieurs d'Intel recommandent d'utiliser des processeurs modernes représentant stosb, c'est-à-dire réinitialiser octet par octet, et non, disons, des mots quadruples.

Gestion de la pile et procédures

Avec une pile, tout est très simple. L'instruction push pousse son argument sur la pile et l'instruction pop extrait une valeur de la pile. Par exemple, si vous oubliez temporairement l'instruction xchg, vous pouvez échanger la valeur de deux registres comme ceci :

pousser %rax
mouvement % rcx , % rax
pop %rcx

Il y a des instructions qui poussent et pop le registre rflags / eflags sur la pile :

pousser
# faire quelque chose qui change les drapeaux
popf
# drapeaux restaurés, c'est le moment de faire jcc

Et ainsi, par exemple, vous pouvez obtenir la valeur du drapeau CF :

pousser
pop %rax
et $1 , %rax

Sur x86, il existe également des instructions pusha et popa qui sauvegardent et restaurent les valeurs de tous les registres de la pile. Dans x64, ces instructions ne sont plus disponibles. Apparemment, parce qu'il y a plus de registres et que les registres eux-mêmes sont maintenant plus longs, il est devenu beaucoup plus coûteux de les sauvegarder et de les restaurer tous.

Les procédures sont généralement "créées" à l'aide des instructions call et ret. L'instruction d'appel pousse l'adresse sur la pile instruction suivante et transfère le contrôle à l'adresse spécifiée dans l'argument. L'instruction ret lit l'adresse de retour dans la pile et en transfère le contrôle. Par example:

someproc :
# prologue de procédure typique
# par exemple, allouer 0x10 octets sur la pile pour les variables locales
# rbp - pointeur vers le cadre de la pile
pousser %rbp
mov % rsp , % rbp
sous $0x10 , % rsp

# une sorte de calcul ici...
mov $1 , %rax

# épilogue de procédure typique
ajouter $0x10 , %rsp
pop %rbp

# procédure de sortie
ret

Démarrer:
# comme avec jmp, l'adresse de saut peut être dans un registre
appeler quelqu'un
tester %rax , %rax
erreur jnz

Noter: Un prologue et un épilogue similaires peuvent être écrits en utilisant les instructions entrez $0x10 , $0 et laisser. Mais ces instructions sont rarement utilisées de nos jours, car elles sont plus lentes à exécuter en raison de la prise en charge supplémentaire des procédures imbriquées.

En règle générale, la valeur de retour est passée dans le registre rax ou, si sa taille n'est pas assez grande, elle est écrite dans la structure dont l'adresse est passée en argument. Sur la question du passage des arguments. Il existe de nombreuses conventions d'appel. Dans certains, tous les arguments sont toujours passés à travers la pile (une question distincte est dans quel ordre) et la procédure elle-même est responsable de l'effacement de la pile d'arguments, dans d'autres, certains des arguments sont passés à travers des registres, et d'autres à travers la pile , et l'appelant est responsable de l'effacement de la pile d'arguments, ainsi que de nombreuses options au milieu, avec des règles distinctes pour aligner les arguments sur la pile, en passant ceci s'il s'agit d'un langage POO, et ainsi de suite. Dans le cas général, pour une architecture, un compilateur et un langage de programmation arbitraires, la convention d'appel peut être n'importe quoi.

JE] ;
}
hachage de retour ;
}

Liste du désassembleur (lorsqu'il est compilé avec -O0 , les commentaires sont les miens):

# prologue de procédure typique
# register rsp ne change pas, car la procédure n'appelle aucun
# autres procédures
400950 : 55 push %rbp
400951 : 48 89 e5 mov %rsp,%rbp

# initialisation des variables locales :
# -0x08(%rbp) - caractère const non signé *données (8 octets)
# -0x10(%rbp) - const size_t data_len (8 octets)
# -0x14(%rbp) - hachage int non signé (4 octets)
# -0x18(%rbp) - int i (4 octets)
400954 : 48 89 7d f8 mov %rdi,-0x8(%rbp)
400958 : 48 89 75 f0 mov %rsi,-0x10(%rbp)
40095c : c7 45 ce 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
400963 : c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)

#rax := je. si data_len est atteint, sortir de la boucle
40096a : 48 63 45 e8 movslq -0x18(%rbp),%rax
40096e : 48 3b 45 f0 cmp -0x10(%rbp),%rax
400972 : 0f 83 28 00 00 00 jae 4009a0

# eax:= (hachage<< 5) + hash
400978 : 8b 45ec mov -0x14(%rbp),%eax
40097b : c1 e0 05 shl $0x5,%eax
40097e : 03 45 ec ajouter -0x14(%rbp),%eax

# eax += données[i]
400981 : 48 63 4d e8 movslq -0x18(%rbp),%rcx
400985 : 48 8b 55 f8 mov -0x8(%rbp),%rdx
400989 : 0f b6 34 0a movzbl(%rdx,%rcx,1),%esi
40098d : 01 f0 ajouter %esi,%eax

# hachage := eax
40098f : 89 45 ec mov %eax,-0x14(%rbp)

# i++ et aller au début de la boucle
400992 : 8b 45 e8 mov -0x18(%rbp),%eax
400995 : 83 c0 01 ajouter $0x1,%eax
400998 : 89 45 e8 mov %eax,-0x18(%rbp)
40099b : e9 ca ff ff ff jmpq 40096a

# la valeur de retour (hash) est placée dans le registre eax
4009a0 : 8b 45ec mov -0x14(%rbp),%eax

# épilogue typique
4009a3 : 5d pop %rbp
4009a4 : c3 retq

Ici, nous avons rencontré deux nouvelles instructions - movs et movz. Ils fonctionnent exactement comme mov, sauf qu'ils étendent un opérande à la taille du second, respectivement signé et non signé. Par exemple, l'instruction movzbl (%rdx,%rcx,1),%esi lit un octet (b) à l'adresse (%rdx,%rcx,1) , le développe en un long mot (l) en ajoutant des zéros (z ) et place le résultat dans le registre esi.

Comme vous pouvez le voir, deux arguments ont été passés à la procédure via les registres rdi et rsi. Il semble utiliser une convention appelée System V AMD64 ABI . Ceci est prétendu être la norme de facto pour x64 sur les systèmes * nix. Je ne vois aucune raison de redire la description de cette convention ici, les lecteurs intéressés peuvent lire la description complète sur le lien fourni.

Conclusion

Inutile de préciser que dans le cadre d'un article, il n'est pas possible de décrire l'ensemble de l'assembleur x86/x64 (d'ailleurs, je ne suis pas sûr de bien le connaître moi-même la totalité). Au minimum, des sujets tels que les opérations sur les nombres à virgule flottante, les instructions MMX, SSE et AVX, ainsi que toutes sortes d'instructions exotiques telles que lidt, lgdt, bswap , rdtsc, cpuid, movbe, xlatb ou prefetch ont été laissés de côté. scènes. Je vais essayer de les couvrir dans de futurs articles, mais je ne promets rien. Il convient également de noter que dans la sortie de objdump -d pour la plupart des programmes réels, vous verrez très rarement autre chose que ce qui est décrit ci-dessus.

Un autre sujet intéressant laissé dans les coulisses est celui des opérations atomiques, des barrières de mémoire, des spinlocks, et c'est tout. Par exemple, comparer et échanger est souvent implémenté simplement comme une instruction cmpxchg préfixée par lock . Par analogie, un incrément atomique, un décrément, etc. sont mis en œuvre. Hélas, tout cela tire sur un sujet pour un article séparé.

Comme sources d'informations supplémentaires, nous pouvons recommander le livre Modern X86 Assembly Language Programming et, bien sûr, les manuels d'Intel. Le livre x86 Assembly sur wikibooks.org est également très bon.

À partir des références en ligne pour les instructions d'assemblage, vous devez faire attention aux points suivants :

Connaissez-vous le langage d'assemblage, et si oui, trouvez-vous cette connaissance utile ?

Ces informations ont été publiées à l'origine sur la page Explications du tableau des clés. Mais ensuite, il a été décidé que ces longs arguments généraux devraient être mis sur une page séparée. Cependant, après un tel transfert, ces arguments ont pris un peu plus d'ampleur. Maintenant, peut-être qu'ils ne conviennent que pour la section "Notes diverses" ...

Instructions de montage et instructions de la machine

Tout d'abord, il ne faut pas oublier que les instructions en langage assembleur et les instructions en langage machine sont deux choses différentes. Bien qu'il soit clair que ces deux concepts sont étroitement liés.

Une instruction assembleur est un nom mnémonique. Pour les processeurs de la famille x86, ce nom est écrit en anglais. Par exemple, la commande d'addition porte le nom AJOUTER, et la commande de soustraction a un nom SOUS.

Équipe affiche le nom de la commande en langage assembleur.

La base d'une instruction machine est l'opcode, qui est simplement un nombre. Pour les processeurs x86 (mais aussi pour les autres processeurs), il est d'usage d'utiliser des nombres hexadécimaux. (Au passage, notons que les nombres octaux ont été adoptés pour les ordinateurs soviétiques, il y avait moins de confusion avec eux, car ces nombres ne sont constitués que de chiffres et ne contiennent pas de lettres).

Dans les tableaux de ce manuel dans la colonne Le code affiche l'opcode de l'instruction machine, et dans la colonne Format montre le format de l'instruction machine.

On peut supposer que le nombre d'instructions machine différentes pour un processeur donné est égal au nombre de codes opération possibles. Par le format, vous pouvez savoir de quels composants se compose une instruction machine donnée. Différentes instructions machine peuvent avoir des formats différents. L'opcode d'une instruction machine définit complètement son format.

Souvent, une instruction assembleur a plusieurs variantes différentes d'instructions machine. De plus, les formats de ces commandes machine peuvent être différents pour différentes options.

Par exemple, l'instruction assembleur ADD a dix variantes d'instructions machine avec différents opcodes. Mais il y a moins de formats différents, seulement trois. Et chacun de ces trois formats nécessite différents types d'opérandes lors de l'écriture d'une instruction en langage assembleur.

Il est important de noter ici que toutes ces dix instructions machine effectuent la même opération élémentaire, qui en langage assembleur est appelée ADD.

Et, donc, il s'avère qu'il semble possible de raisonner ainsi : le processeur peut effectuer autant d'opérations élémentaires différentes qu'il y a d'instructions assembleur différentes. Cependant, ce principe simple nécessite encore des réserves et des notes. Étant donné que certaines des commandes de l'assembleur ont également des synonymes.

Une liste générale de toutes les instructions du processeur peut être construite de différentes manières, en choisissant un ordre d'instructions différent. Les deux principaux moyens sont.

Méthode (1). Prenez les commandes du langage d'assemblage comme base et organisez les commandes par ordre alphabétique. Ensuite, des tableaux comme celui-ci peuvent être obtenus. Toutes les commandes par ordre alphabétique (brièvement)

Méthode (2). Prenez l'opcode de l'instruction machine comme base et organisez les instructions dans l'ordre des opcodes. Dans ce cas, il serait préférable que la liste générale soit divisée en deux parties, de faire des listes séparées pour les commandes avec un opcode à un octet et pour les commandes avec un opcode à deux octets. Premier octet d'opcode Deuxième octet d'opcode

Bien sûr, il existe également une troisième voie, qui est généralement utilisée dans les manuels. Divisez toutes les commandes en groupes selon leur signification et étudiez-les en groupes, en commençant par les plus simples.

Octet d'opcode principal

Dans le système de commande x86, un octet (256 combinaisons différentes) n'était pas suffisant pour encoder toutes les commandes. Par conséquent, l'opcode dans une instruction machine occupe soit un octet, soit deux octets.

Si le premier octet contient le code 0F, alors l'opcode se compose de deux octets.

Si l'opcode dans une instruction machine se compose d'un octet, alors cet octet unique est l'octet principal de l'opcode. Et le contenu de cet octet détermine quelle est l'opération.

Si l'opcode dans une instruction machine se compose de deux octets, alors pas le premier, mais le deuxième octet sera l'octet principal et de définition dans l'opcode.

Dans les tableaux manuels qui montrent le codage des instructions machine, l'octet principal du code d'opération est généralement affiché deux fois, d'abord dans la colonne "Code" sous forme de nombre hexadécimal, puis dans la colonne "Format" sous la forme de huit tirets conditionnels. , sur lesquels des bits spéciaux sont marqués, s'il y en a dans l'octet principal de l'opcode.

Principales pages du manuel

manuel d'instructions du processeur x86 - page principale (voici une carte de toutes les pages de manuel)

L'assembleur est un langage de programmation de bas niveau utilisé pour programmer divers processeurs, microprocesseurs et microcontrôleurs. Ce test considère l'assembleur pour les processeurs x86.

Les programmes en langage assembleur consistent en un ensemble d'instructions spécifiques. Ces commandes sont ensuite, à l'aide d'un traducteur, converties en code machine, qui est ensuite exécuté par le processeur central. À l'aide de commandes, vous pouvez effectuer des calculs arithmétiques, travailler avec de la mémoire et des ports, etc.

En règle générale, l'assembleur est utilisé lorsqu'il est nécessaire d'optimiser des sections critiques de code pour la vitesse, dans les pilotes de périphériques, dans les virus et autres logiciels malveillants, dans les systèmes d'exploitation, les compilateurs, etc.

Public cible du test Assembleur x86

Le test teste la connaissance du langage d'assemblage et de l'architecture x86. Le test est davantage axé sur la connaissance pratique du langage et de l'architecture et sera donc intéressant pour les programmeurs système et les étudiants pour tester leurs connaissances, et également utile à tous les programmeurs pour améliorer leurs connaissances sur l'architecture informatique et la programmation de bas niveau.

Structure de test en assembleur x86

Les sujets suivants peuvent être arbitrairement identifiés :

  • Questions générales
  • Modes de fonctionnement du processeur (réel, protégé)
  • Instructions du processeur

Poursuite du développement du test d'assembleur x86

À l'avenir, nous prévoyons d'ajouter des questions sur des sujets non couverts (FPU, travailler avec des périphériques / ports). De plus, en développement, il y a un test pour le niveau moyen, qui sera bientôt disponible pour passer.