Programmation Assembleur/x86/Copie entre registres et mémoire

Un livre de Wikilivres.
Programmation Assembleur/x86
Modifier ce modèle


Syntaxes des assembleurs[modifier | modifier le wikicode]

Il existe deux variantes principales et opposées de syntaxe pour les instructions en assembleur :

  • la syntaxe Intel où l'opérande destination est placé avant l'opérande source.
Exemple : mov ax, bx ; AX <- BX transfert des 16 bits de BX vers AX.
  • la syntaxe AT&T où l'opérande source est placé avant l'opérande destination.
Exemple : mov %bx, %ax ; BX -> AX transfert des 16 bits de BX vers AX.

L'interprétation peut donc être ambiguë si on ne sait pas quelle syntaxe est employée. Les assembleurs sous DOS et Windows utilisent la syntaxe Intel, qui semble plus naturelle pour les opérations arithmétiques et conserve l'ordre des expressions arithmétiques :

add eax, 7  ; EAX <- EAX+7

Les assembleurs sous Linux utilisent la syntaxe AT&T, qui conserve l'ordre des mots pour expliquer les expressions arithmétiques :

add $7, %eax  ; Ajouter 7 à EAX

La distinction entre les deux syntaxes est donc importante à faire :

  • Dans la syntaxe Intel où l'opérande destination est placé avant l'opérande source, le nom des registres et les valeurs littérales n'ont pas de préfixe.
  • Dans la syntaxe AT&T où l'opérande source est placé avant l'opérande destination, le nom des registres est préfixé par le caractère pourcent et les valeurs littérales sont préfixé par le caractère dollar.
syntaxe Intel (DOS, Windows, ...) syntaxe AT&T (Linux, ...)
add eax, 7
mov dword ptr [rbp-0x4],edi
imul ecx, [rdi + rax*4 + 20], 12345
add $7, %eax
mov %edi,-0x4(%rbp)
imul $12345, 20(%rdi, %rax, 4), %ecx

Transfert unitaire[modifier | modifier le wikicode]

L'instruction la plus courante est MOV (abréviation de l'anglais move : déplacer) qui permet de transférer le contenu d'un emplacement mémoire vers un registre et inversement, ou entre deux registres de même taille.

Le nombre de bits transféré est défini par la taille du registre source ou cible du transfert. Exemple (syntaxe destination, source) :

mov ax, bx  ; transfert des 16 bits de BX vers AX.

Les assembleurs peuvent employer des suffixes pour le nom de l'instruction indiquant ce nombre d'octets :

  • movb (Move Byte) : 8 bits,
  • movw (Move Word) : 16 bits,
  • movl (Move Long) : 32 bits.

Exemple (syntaxe source, destination) :

.data

valeur:
        .long   2

.text
        .global _start

_start:
        movl    $6, %eax
        # %eax = 6

        movw    %ax, valeur
        # valeur = 6

        movl    $0, %ebx
        # %ebx = 0

        movb    %al, %bl
        # %ebx = 6

        movl    valeur, %ebx
        # %ebx = 6

        movl    $valeur, %esi
        # %esi = l’address de valeur

        xorl    %ebx, %ebx
        # %ebx = 0

        movw    valeur(, %ebx, 1), %bx
        # %ebx = 6

# Linux sys_exit
        mov     $1, %eax
        xorl    %ebx, %ebx
        int     $0x80

Transfert de séquence[modifier | modifier le wikicode]

Une famille d'instructions gère les séquences d'octets en mémoire :

  • les instructions MOVS transfèrent les octets d'une zone mémoire à une autre (Move Sequence),
  • les instructions CMPS comparent le contenu de deux zones mémoire (Compare Sequence),
  • les instructions LODS transfère les octets d'une zone mémoire vers le registre EAX (Load Sequence),
  • les instructions STOS transfère le contenu du registre EAX vers une zone mémoire (Store Sequence),
  • les instructions SCAS comparent le contenu du registre EAX à celui d'une zone mémoire (Scan Sequence).

Quantité traitée[modifier | modifier le wikicode]

Le nom de ces opérations se termine par un préfixe indiquant la quantité traitée à chaque appel :

  • B (Byte) pour un octet de 8 bits : MOVSB, CMPSB, LODSB, STOSB, SCASB ;
  • W (Word) pour un mot de 16 bits : MOVSW, CMPSW, LODSW, STOSW, SCASW ;
  • D (Double word) pour un double mot (32 bits) : MOVSD, CMPSD, LODSD, STOSD, SCASD.

Registres utilisés et opérations[modifier | modifier le wikicode]

Ces opérations utilisent les registres dédiés pour adresser la mémoire :

  • DS:SI / DS:ESI pointant la zone mémoire source (instructions MOVS*, CMPS*, LODS*),
  • ES:DI / ES:EDI pointant la zone mémoire destination (instructions MOVS*, CMPS*, STOS*, SCAS*).

Ces registres sont incrémentés ou décrémentés après chaque traitement par ces opérations, selon la valeur de l'indicateur de direction (DF) :

  • s'il est positionné à 0, les registres (E)SI et (E)DI sont incrémentés de 1, 2 ou 4 selon la quantité traitée à chaque appel,
  • S'il est positionné à 1, les registres (E)SI et (E)DI sont décrémentés de 1, 2 ou 4 selon la quantité traitée à chaque appel.

Mnémotechnique : DF = Decrement Flag

Cet indicateur est généralement positionné juste avant les instructions de traitement en utilisant l'instruction CLD (CLear Direction) ou STD (SeT Direction).

Le tableau ci-dessous résume les opérations effectuées par les instructions et les registres utilisés :

Instructions Source Destination Opération
MOVS* [DS:(E)SI] [ES:(E)DI] Transfert de source vers destination (Move).
LODS* [DS:(E)SI] (E)AX Transfert de source vers le registre EAX (Load).
STOS* (E)AX [ES:(E)DI] Transfert du registre EAX vers destination (Store).
CMPS* [DS:(E)SI] [ES:(E)DI] Met à jour les indicateurs après comparaison entre source et destination.
SCAS* (E)AX [ES:(E)DI] Met à jour les indicateurs après comparaison entre le registre EAX et destination.

Répétition et boucle[modifier | modifier le wikicode]

Ces instructions effectuent le traitement d'une quantité unitaire de données, mais deviennent efficaces et puissantes en combinaison d'instructions répétant l'opération en boucle. Pour répéter ces opérations, les instructions de boucle et répétition utilisent le registre CX (Counter) en le décrémentant et répétant l'opération tant que le compteur n'atteint pas zéro. Le registre CX est donc initialisé avant la boucle ou répétition avec le nombre d'octets, de mots ou de double-mots à traiter.

; Code non montré :
; ... initialiser les registres DS, SI pour la source
; ... initialiser les registres ES, DI pour la destination,
mov cx, 0x1FF   ; transférer 511 mots de 16 bits
cld             ; incrémenter les pointeurs après chaque itération
rep             ; répéter CX fois...
movsw           ; ...le transfert d'un mot de 16 bits
  • L'instruction REPZ répète l'instruction de séquence qui suit tant que le registre CX n'est pas à zéro et que le flag Zero (ZF) vaut 0 (c'est à dire jusqu'à ZF=1 : REPeat until Zero).
  • L'instruction REPNZ répète l'instruction de séquence qui suit tant que le registre CX n'est pas à zéro et que le flag Zero (ZF) vaut 1 (c'est à dire jusqu'à ZF=0 : REPeat until Not Zero).

Le flag Zero (ZF) est pris en compte seulement si l'opération qui suit est CMPS* ou SCAS*.

Il est ignoré pour les opérations de transfert MOVS*, LODS* ou STOS*. Cela signifie que pour ces 3 opérations REPZ et REPNZ sont identiques, et les assembleurs ajoutent donc un alias REP qui en général est compilé en une instruction REPZ.

Les préfixes REPZ et REPNZ ne répète que l'instruction de séquence qui suit. Pour répéter une série d'instructions, notamment pour LODS* où l'assignation répétée dans le registre AX n'a pas d'intérêt réel, il faut utiliser les instructions LOOP*.

Les instructions LOOP* ont 3 variantes dont le comportement ne varient que par rapport au flag zéro (ZF) :

  • L'instruction LOOPZ décrémente le registre (E)CX, puis s'il n'est pas à zéro et que le flag zéro (ZF) vaut 1 (LOOP while CX > 0 and Zero) saute à l'adresse indiquée.
  • L'instruction LOOPNZ décrémente le registre (E)CX, puis s'il n'est pas à zéro et que le flag zéro (ZF) vaut 0 (LOOP while CX > 0 and Not Zero) saute à l'adresse indiquée.
  • L'instruction LOOP décrémente le registre (E)CX, puis s'il n'est pas à zéro (LOOP while CX > 0) saute à l'adresse indiquée.
; Code non montré :
; ... initialiser les registres DS, SI pour la source
  mov cx, 200     ; lire 200 octets
  cld             ; incrémenter les pointeurs après chaque itération
:iteration
  lodsb            ; lecture d'un octet de DS:(E)SI dans AL
  call printchar   ; appel à une sous-routine pour afficher le caractère de code AL
  cmp al, 0        ; compare AL à 00 qui marque la fin de chaîne de caractère
  loopnz iteration ; décrémente CX, et boucle si ZF=0 (AL différent de 00)

Enfin, il existe une instruction JCXZ qui ne modifie pas le registre (E)CX mais saute à l'adresse indiquée si le registre (E)CX vaut 0. Dans le cas d'un compteur d'itération, elle permet donc de sortir d'une boucle infinie.

Préfixes de changement de segment[modifier | modifier le wikicode]

Les préfixes de changement de segment n'influence que le registre de segment DS utilisé pour la source. Cela signifie qu'ils n'influencent que les instructions MOVS*, CMPS*, LODS*.

; Code non montré :
; ... initialiser les registres ES, SI pour la source (même registre ES que la destination)
; ... initialiser les registres ES, DI pour la destination,
mov cx, 0x1FF   ; transférer 511 mots de 16 bits
cld             ; incrémenter les pointeurs après chaque itération
es:             ; utiliser le segment ES au lieu de DS
rep             ; répéter CX fois...
movsw           ; ...le transfert d'un mot de 16 bits

Autres transferts[modifier | modifier le wikicode]

Le transfert entre certains registres pour lesquels il n'existe pas d'instruction peut s'effectuer en utilisant la pile, comme par exemple pour le transfert entre le registre AX et celui des indicateurs (FLAGS) :

; Transfert de FLAGS vers AX
pushf      ; met la valeur du registre FLAGS dans le pile,
pop ax     ; puis la retire de la pile pour la mettre dans le registre AX.

L'instruction LAHF (Load AH from Flags) permet de transférer la valeur des 8 bits de poids faible du registre FLAGS vers le registre AH (bits 15 à 8 du registre AX / EAX). L'instruction SAHF (Save AH to Flags) effectue le transfert inverse.

L'instruction XLAT transfère l'octet pointé par DS:(BX+AL) dans le registre AL.