Table des matières
Cela fait longtemps que j'avais promis la suite des billets relatifs à l'assembleur 8086+ 😀 .
Comme d'habitude, je ne détaillerai rien concernant la compilation/l'édition des liens encore cette fois-ci. Reportez-vous au premier billet pour plus d'informations.
Exercice 5 : énoncé
- Écrire deux procédures/fonctions. L'une permettant d'afficher un entier (word ou dword soit 16 ou 32 bits) en hexadécimal. L'autre permettant d'afficher un entier (word ou dword soit 16 ou 32 bits) en binaire. Tester ces deux procédures sur un exemple.
- Un micro-ordinateur reçoit une suite de 10 mots de 16 bits représentants les résultats de 10 mesures successives de 2 grandeurs physiques G1 et G2. Dans chaque mot, G1 est codé sur les 5 octets de poids fort et G2 est codé sur les 11 bits de poids faible restants. Exemple : 1011100110011110. G1 = 10111 et G2 = 00110011110.
- Dans un premier temps, notre ordinateur ne fait que collecter les mots reçus en les stockant en pile. Écrire le traitement qui réalise cette opération en supposant que les mots sont définis par la déclaration suivante (section data) : Mesures DW 0E765h, 6AB3h, 8FA1h, 905Ch, 0A9FDh, 5DA9h, 4807h, 8FA2h, 0A10Bh, 0B95h
- Dans un deuxième temps, les mots en pile sont traités afin d'extraire de chacun la valeur de G1 et celle de G2 et de les ranger respectivement dans des tableaux Tab_G1 et Tab_G2 préalablement déclarés. Écrire le traitement qui réalise cette opération.
- Écrire le traitement qui permet de calculer la moyenne entière des 10 mesures G1. Même chose pour G2.
- Faire afficher la partie entière de la moyenne des G1 en hexadécimal et la partie entière de la moyenne des G2 en binaire.
- Avec les mesures données, la moyenne n'est pas entière. Calculer la moyenne en virgule flottante et l'afficher.
Note : Comme d'habitude, cet énoncé n’est pas de moi : je l’ai repris à T.M. / E.R. et je l’ai modifié.
Exercice 5 : aides
- On a déjà vu comment afficher un nombre en binaire dans l'exercice 3. On a aussi déjà vu comment afficher un nombre en hexadécimal dans l'exercice 4. Donc les algorithmes sont prêts, il n'y a plus qu'à faire les procédures.
- La syntaxe pour créer une fonction/procédure est assez simple : un label d'un côté, l'instruction "ret" de l'autre. L'important est de comprendre les notions attenantes : passage de paramètres, prologue/épilogue (responsabilité appelant/appelé), ... Je vous ai sélectionné quelques ressources : Les procédures en assembleur, 3.2. Les fonctions en assembleur et Function prologue.
- Vous l'aurez compris/remarqué, il est impossible de parler de fonction sans parler de pile. Là encore, la base est simple mais les implications sont importantes. Quelques notions à comprendre : usages (sauvegarder des valeurs, sauvegarder des registres pour protéger contre effacement, appel et retour de fonctions) ; la pile croît depuis les adresses hautes de la mémoire vers les adresses basses (en terme s'adresse, le sommet est plus bas que le paramètre précédent) ; on ajoute/supprime des données par bloc de 2/4 octets (selon architecture, 16 ou 32 bits). Je vous ai sélectionné quelques ressources : Pile en assembleur, Understanding the Stack et Stack.
- Pour faire des calculs en virgule flottante, il faut utiliser la FPU. Dans un temps reculé, il s'agissait d'une unité à part, sur un circuit différent. Depuis le 80486 (au minimum), c'est intégré dans le CPU lui-même. C'est d'ailleurs pour cela que l'on parle aussi de co-processeur arithmétique. Il y a des instructions dédiées pour exécuter des calculs en virgule flottante, une pile dédiée et des registres dédiés. Pour une introduction rapide : ASM Community • The Assembly Language Resource - The Floating Point Unit (FPU). Pour une ressource plus complète : SIMPLY FPU . Je vais vous donner quelques indications supplémentaires : une fois que vous aurez calculé la somme de G1 et la somme de G2, vous les placerez, chacune, dans une variable mémoire de type (sous nasm) dq/dt/do (le choix influe sur la précision mais dans notre cas, même un dq conviendra). Ensuite, vous chargerez la valeur de cette variable au sommet de la pile du coprocesseur grâce à l'instruction dédiée. Puis vous réaliserez la division avec la bonne instruction.
- Le résultat de la division en virgule flottante sera au format IEE754. Plutôt que de parser nous-même cette valeur pour obtenir un affichage cohérent, ce qui est techniquement compliqué, nous allons utiliser la fonction printf du C. Elle sait afficher un nombre réel. En plus, cela nous donnera l'occasion de "linker du code C et de l'assembleur" et de passer des paramètres à une fonction externe via la pile. Pour Windows, je ne sais pas comment il faut faire mais sous Debian, il faut le package libc6-dev (libc6-dev-i386 pour les amd64). En début de programme, il faut rajouter la directive "extern printf". Lors du linkage avec ld, il faudra rajouter les paramètres "-I/lib/ld-linux.so.2 -lc".
Exercice 5 : sous Windows, avec TASM et TLINK
Cette version Windows est un peu vieille, je ne l'ai pas retouché depuis car il n'est pas dans mes préoccupations actuelles d'aller faire un voyage avec Windows et TASM. Donc cette version, contrairement à la version GNU/Linux ne compte ni les calculs en virgule flottante, ni la gestion du fait que la somme des G1, peut dépasser 2^8 selon les valeurs choisies dans Mesures et donc ne pas loger dans un octet/registre 8 bits.
dosseg .model small .stack 100h ; 256 octets suffisent plus qu'amplement pour une pile qui ; va stocker 10 mots mémoire au maximum de son utilisation .data ; exercice 1 ;données nombre dw 12666 tab_hexa db "0123456789abcdef" ; messages écran exercice1 db "Exercice 1 :",13,10,"$" message_binaire db "Affichage du nombre declare dans le segment de donnees (ici : 12666) en binaire : ","$" message_hexa db 13,10,"Affichage du nombre declare dans le segment de donnees (ici : 12666) en hexadecimal : ","$" ; exercice 2 ;données mesures dw 0e765h, 6ab3h, 8fa1h, 905ch, 0a9fdh, 5da9h, 4807h, 8fa2h, 0a10bh, 0b95h tab_g1 db 10 dup(0) tab_g2 dw 10 dup(0) ; messages écran exercice2 db 13,10,10,"Exercice 2 :","$" msg_hexa db 13,10,"Affichage de la moyenne des mesures g1 en hexadecimal : ","$" msg_binaire db 13,10,"Affichage de la moyenne des mesures g2 en binaire : ","$" .code ; initialisation du segment de données mov ax, @data mov ds, ax ; exercice 1 ; affichage du message écran de début de l'exercice 1 mov ah, 09h mov dx, offset exercice1 int 21h ; affichage du message annonçant l'affichage du nombre déclaré dans le segment de données, en binaire mov ah, 09h mov dx, offset message_binaire int 21h mov bx, nombre ; passage d'un paramètre, par un registre, à la procédure call affichage_binaire ; appel de la procédure ; affichage du message annonçant l'affichage du nombre déclaré dans le segment de données en hexadécimal mov ah, 09h mov dx, offset message_hexa int 21h push nombre ; passage d'un paramètre, par la pile, à la procédure call affichage_hexa ; appel de la procédure ; exercice 2 ; affichage du message écran de début de l'exercice 2 mov ah, 09h mov dx, offset exercice2 int 21h ; 1) Empiler les mesures sur la pile ; On fait une boucle pour empiler chacune des mesures. ; Comme la pile du 8086 est de type lifo (= last in, first out), ; nous empilerons la 10eme mesure en premier et la 1er en dernier. ; ainsi, dans le 2), la première mesure dépilée sera bien la première du tableau mesure. ; Attention : "mesures" est un tableau de word (16 bits), pas un tableau de byte (8 bits) ; donc on ne se déplace pas de 1 en 1 entre les cases (entre leurs adresses pour être précis) ; mais de 2 en 2. mov cx, 20 bcl_empile_mesures : cmp cx, 2 jb fin_bcl_empile_mesures mov si, cx ; nous aurions pu ecrire une boucle en utilisant le registre si (ou di ou bx), sub si, 2 ; et en decrementant celui-ci avant le "push". mais, par convention, on ne push [mesures + si] ; touche pas le compteur d'une boucle au milieu du deroulement de celle-ci. d'où 2 registres. sub cx, 2 jmp bcl_empile_mesures fin_bcl_empile_mesures : ; 2) Extraire et classer les mesures g1 et g2 dans deux tableaux independants ; Attention : tab_g1 est un tableau de byte, tab_g2 est un tableau de word ... on ne se déplace pas de la même façon ... ; prévoir 2 "compteurs". mov si, 0 mov cl, 11 mov di, 0 bcl_extraction : cmp si, 10 je fin_bcl_extraction pop ax ; on désempile une mesure mov dx, ax ; on sauvegarde cette valeur afin de pouvoir réaliser l'extraction ; de g1 et de g2 dans la même boucle. ; extraction de la mesure g1 dans le registre al par la méthode du décalage logique à droite ; de 11 rangs binaires puis stockage dans le tableau tab_g1 dédié shr ax, cl mov [tab_g1 + si], al ; extraction de la mesure g2 dans le registre dx par la méthode du masque logique ; puis stockage dans le tableau tab_g2 dédié and dx, 07ffh mov [tab_g2 + di], dx inc si add di, 2 jmp bcl_extraction fin_bcl_extraction : ; 3) calculer la moyenne des mesures g1 et g2 ; attention : tab_g1 est un tableau de byte, tab_g2 est un tableau de word ... on ne se déplace pas de la même façon ... ; prévoir 2 "compteurs". mov si, 0 mov al, 0 mov dx, 0 mov di, 0 ; on fait une boucle qui additionne toutes les valeurs des mesures g1 dans al ; et toutes les valeurs des mesures g2 dans dx bcl_addition : cmp si, 10 je fin_bcl_addition add al, [tab_g1 + si] add dx, [tab_g2 + di] inc si add di, 2 jmp bcl_addition fin_bcl_addition : mov cl, al ; sauvegarde, dans cl, la somme des mesures g1 ; calcul de la moyenne des valeurs des mesures g2 (division) et stockage du résultat sur la pile mov ax, dx mov dx, 0 mov bx, 10 div bx push ax ; calcul de la moyenne des valeurs des mesures g1 (division) et stockage du résultat sur la pile mov al, cl ; restauration de la somme des mesures g1 mov ah, 0 mov bl, 10 div bl mov ah, 0 push ax ; 4) affichage de la moyenne des mesures g1 en hexadécimal et affichage de la moyenne des mesures g2 en binaire ; affichage du message écran annonçant l'affichage de la moyenne des mesures g1 en hexadécimal mov ah, 09h mov dx, offset msg_hexa int 21h call affichage_hexa ; la moyenne des mesures g1 est déjà la dernière donnée empilée. ; la procédure peut donc être appelé. ; affichage du message écran annonçant l'affichage de la moyenne des mesures g2 en binaire mov ah, 09h mov dx, offset msg_binaire int 21h pop bx ; nous mettons la moyennes des mesures g2 dans le registre bx call affichage_binaire ; et nous appelons la procédure. ; retour au dos mov ax, 4c00h int 21h ; affichage_binaire : ; Objectif : Afficher à l'écran, sous forme binaire, un nombre passé en paramètre. ; Passage des paramètre : Par un registre. Le nombre à afficher doit se trouver dans BX. ; Pré-condition : BX doit contenir un nombre. L'interprétation du résultat binaire revient ; à l'utilisateur. Ex : dans le cas d'un nombre réel ... A noter que même ; si BX ne contient pas un nombre, la procédure s'exécutera avec succès ; et affichera le contenu du registre BX mais l'interprétation perdra son sens. affichage_binaire proc near ; signalement (non obligatoire) du début d'une procédure dans le même segment de code que le programme appelant ; ESP désigne le sommet actuel de la pile alors qu'EBP pointe sur le bas "relatif" de la pile. ; Pour l'instant, EBP pointe sur le "bas" de l'espace dédié ("stack-frame") au programma appelant. ; On va donc le faire pointer sur ESP (le sommet) pour dire que l'espace de travail de notre ; programme appelé commence ici. Cela nous fera d'ailleurs, en même temps, une sauvegarde d'ESP. ; Bien sûr, il faut penser à sauvegarder le registre EBP actuel pour pouvoir le faire à nouveau ; pointer sur l'espace dédié au programme appelant à la fin de notre procédure. push ebp ; Sauvegarde du contexte précédent mov ebp, esp ; Sauvegarde du pointeur de pile mov cx, 16 ; on initialise une boucle de 16 itérations nous permettant ainsi de traiter ; des nombres codés sur 16 bits ou moins. bcl0 : debut_si : shl bx, 1 jc aff1 ; on effectue un déplacement logique vers la gauche. le bit sortant active le ; carry flag. il suffit de tester celui-ci pour en déduire l'état du bit sortant mov ah, 02h ; (0 ou 1) et d'afficher en conséquence le bon caractère ('0' ou '1'). mov dl, '0' int 21h jmp fin_si aff1 : mov ah, 02h mov dl, '1' int 21h fin_si : loop bcl0 ; Restauration du contexte : ESP doit pointer sur l'adresse de retour ! mov esp, ebp pop ebp ret ; permet de revenir au programme principal, à l'instruction suivant l'instruction "call" ; qui appellera cette procédure. affichage_binaire endp ; signalement (non obligatoire) de la fin d'une procédure ; affichage_hexa : ; Objectif : Afficher à l'écran, sous forme hexadécimal, un nombre passé en paramètre. ; Passage des paramètre : Par la pile. Le nombre a afficher doit être le dernier élément ; empilé avant l'appel de cette procédure. ; Pré-condition : Le dernier élément empilé doit être un nombre. Même remarques que pour ; affichage_binaire. affichage_hexa proc near ; ESP désigne le sommet actuel de la pile alors qu'EBP pointe sur le bas "relatif" de la pile. ; Pour l'instant, EBP pointe sur le "bas" de l'espace dédié ("stack-frame") au programma appelant. ; On va donc le faire pointer sur ESP (le sommet) pour dire que l'espace de travail de notre ; programme appelé commence ici. Cela nous fera d'ailleurs, en même temps, une sauvegarde d'ESP. ; Bien sûr, il faut penser à sauvegarder le registre EBP actuel pour pouvoir le faire à nouveau ; pointer sur l'espace dédié au programme appelant à la fin de notre procédure. push ebp ; Sauvegarde du contexte précédent mov ebp, esp ; Sauvegarde du pointeur de pile mov ah, 02h bcl1 : cmp cl, 0 ; à chaque itération, on effectue un décalage logique vers la droite de jl fin_bcl1 ; moins en moins important (12, 8, 4, 0) et on applique un masque. cela nous permet ; de garder qu'un seul groupe de 4 rangs binaires à la fois. on affiche ce groupe à l'écran. mov bx, ss:[bp + 4] ; on récupère, dans la pile, le paramètre passé à cette procédure (= le nombre à afficher) shr bx, cl ; ss est facultatif et désigne l'adresse mémoire du segment de pile. and bx, 000fh ; dans ce segment on veut accéder au contenu de l'offset ; bp + 4 (4 car 4 octets = 2 emplacements sur la pile), mov dl, [tab_hexa + bx] ; car c'est dans cet emplacement que se trouve notre paramètre (bp + 2 contient int 21h ; la sauvegarde de bp faite au début de cette procédure. ; il faut remonter (+4) à l'emplacement précèdent car la pile croît vers sub cl, 4 ; les adresses basses de la mémoire donc le sommet se trouve plus bas que le paramètre. jmp bcl1 fin_bcl1 : ; Restauration du contexte : ESP doit pointer sur l'adresse de retour ! mov esp, ebp pop ebp ret 2 ; par défaut, ret récupère l'adresse de retour au programme et la stocke dans ; le registre ip (car prochaine instruction) puis incrémente le registre sp de ; 2 octets afin de "libérer" l'emplacement occupé par cette adresse de retour. affichage_hexa endp ; ici, nous lui demandons de "libérer" 2 octets de plus afin de libérer ; l'emplacement contenant le paramètre passé à cette procédure (= le nombre à afficher). end |
Exercice 5 : sous Windows, avec Nasm et le linker Val
Cette version Windows est un peu vieille, je ne l'ai pas retouché depuis car il n'est pas dans mes préoccupations actuelles d'aller faire un voyage sous Windows. Donc cette version, contrairement à la version GNU/Linux ne compte ni les calculs en virgule flottante, ni la gestion du fait que la somme des G1, peut dépasser 2^8 selon les valeurs choisies dans Mesures et donc ne pas loger dans un octet/registre 8 bits.
segment .data ; exercice 1 ;données nombre dw 12666 tab_hexa db "0123456789abcdef" ; messages écran exercice1 db "Exercice 1 :",13,10,"$" message_binaire db "Affichage du nombre declare dans le segment de donnees (ici : 12666) en binaire : ","$" message_hexa db 13,10,"Affichage du nombre declare dans le segment de donnees (ici : 12666) en hexadecimal : ","$" ; exercice 2 ;données mesures dw 0e765h, 6ab3h, 8fa1h, 905ch, 0a9fdh, 5da9h, 4807h, 8fa2h, 0a10bh, 0b95h tab_g1 times 10 db '0' tab_g2 times 10 dw '0' ; messages écran exercice2 db 13,10,10,"Exercice 2 :","$" msg_hexa db 13,10,"Affichage de la moyenne des mesures g1 en hexadecimal : ","$" msg_binaire db 13,10,"Affichage de la moyenne des mesures g2 en binaire : ","$" segment stack stack resb 64 stackstop: segment .code ..start: ; initialisation du segment de données mov ax, data mov ds, ax ; exercice 1 ; affichage du message écran de début de l'exercice 1 mov ah, 09h mov dx, exercice1 int 21h ; affichage du message annonçant l'affichage du nombre déclaré dans le segment de données, en binaire mov ah, 09h mov dx, message_binaire int 21h mov bx, [nombre] ; passage d'un paramètre, par un registre, à la procédure call affichage_binaire ; appel de la procédure ; affichage du message annonçant l'affichage du nombre déclaré dans le segment de données en hexadécimal mov ah, 09h mov dx, message_hexa int 21h push word [nombre] ; passage d'un paramètre, par la pile, à la procédure call affichage_hexa ; appel de la procédure ; exercice 2 ; affichage du message écran de début de l'exercice 2 mov ah, 09h mov dx, exercice2 int 21h ; 1) Empiler les mesures sur la pile ; On fait une boucle pour empiler chacune des mesures. ; Comme la pile du 8086 est de type lifo (= last in, first out), ; nous empilerons la 10eme mesure en premier et la 1er en dernier. ; ainsi, dans le 2), la première mesure dépilée sera bien la première du tableau mesure. ; Attention : "mesures" est un tableau de word (16 bits), pas un tableau de byte (8 bits) ; donc on ne se déplace pas de 1 en 1 entre les cases (entre leurs adresses pour être précis) ; mais de 2 en 2. mov cx, 20 bcl_empile_mesures : cmp cx, 2 jb fin_bcl_empile_mesures mov si, cx ; nous aurions pu ecrire une boucle en utilisant le registre si (ou di ou bx), sub si, 2 ; et en decrementant celui-ci avant le "push". mais, par convention, on ne push word [mesures + si] ; touche pas le compteur d'une boucle au milieu du deroulement de celle-ci. d'où 2 registres. sub cx, 2 jmp bcl_empile_mesures fin_bcl_empile_mesures : ; 2) Extraire et classer les mesures g1 et g2 dans deux tableaux independants ; Attention : tab_g1 est un tableau de byte, tab_g2 est un tableau de word ... on ne se déplace pas de la même façon ... ; prévoir 2 "compteurs". mov si, 0 mov cl, 11 mov di, 0 bcl_extraction : cmp si, 10 je fin_bcl_extraction pop ax ; on désempile une mesure mov dx, ax ; on sauvegarde cette valeur afin de pouvoir réaliser l'extraction ; de g1 et de g2 dans la même boucle. ; extraction de la mesure g1 dans le registre al par la méthode du décalage logique à droite ; de 11 rangs binaires puis stockage dans le tableau tab_g1 dédié shr ax, cl mov [tab_g1 + si], al ; extraction de la mesure g2 dans le registre dx par la méthode du masque logique ; puis stockage dans le tableau tab_g2 dédié and dx, 07ffh mov [tab_g2 + di], dx inc si add di, 2 jmp bcl_extraction fin_bcl_extraction : ; 3) calculer la moyenne des mesures g1 et g2 ; attention : tab_g1 est un tableau de byte, tab_g2 est un tableau de word ... on ne se déplace pas de la même façon ... ; prévoir 2 "compteurs". mov si, 0 mov al, 0 mov dx, 0 mov di, 0 ; on fait une boucle qui additionne toutes les valeurs des mesures g1 dans al ; et toutes les valeurs des mesures g2 dans dx bcl_addition : cmp si, 10 je fin_bcl_addition add al, [tab_g1 + si] add dx, [tab_g2 + di] inc si add di, 2 jmp bcl_addition fin_bcl_addition : mov cl, al ; sauvegarde, dans cl, la somme des mesures g1 ; calcul de la moyenne des valeurs des mesures g2 (division) et stockage du résultat sur la pile mov ax, dx mov dx, 0 mov bx, 10 div bx push ax ; calcul de la moyenne des valeurs des mesures g1 (division) et stockage du résultat sur la pile mov al, cl ; restauration de la somme des mesures g1 mov ah, 0 mov bl, 10 div bl mov ah, 0 push ax ; 4) affichage de la moyenne des mesures g1 en hexadécimal et affichage de la moyenne des mesures g2 en binaire ; affichage du message écran annonçant l'affichage de la moyenne des mesures g1 en hexadécimal mov ah, 09h mov dx, msg_hexa int 21h call affichage_hexa ; la moyenne des mesures g1 est déjà la dernière donnée empilée. ; la procédure peut donc être appelé. ; affichage du message écran annonçant l'affichage de la moyenne des mesures g2 en binaire mov ah, 09h mov dx, msg_binaire int 21h pop bx ; nous mettons la moyennes des mesures g2 dans le registre bx call affichage_binaire ; et nous appelons la procédure. ; retour au dos mov ax, 4c00h int 21h ; affichage_binaire : ; Objectif : Afficher à l'écran, sous forme binaire, un nombre passé en paramètre. ; Passage des paramètre : Par un registre. Le nombre à afficher doit se trouver dans BX. ; Pré-condition : BX doit contenir un nombre. L'interprétation du résultat binaire revient ; à l'utilisateur. Ex : dans le cas d'un nombre réel ... A noter que même ; si BX ne contient pas un nombre, la procédure s'exécutera avec succès ; et affichera le contenu du registre BX mais l'interprétation perdra son sens. affichage_binaire: ; ESP désigne le sommet actuel de la pile alors qu'EBP pointe sur le bas "relatif" de la pile. ; Pour l'instant, EBP pointe sur le "bas" de l'espace dédié ("stack-frame") au programma appelant. ; On va donc le faire pointer sur ESP (le sommet) pour dire que l'espace de travail de notre ; programme appelé commence ici. Cela nous fera d'ailleurs, en même temps, une sauvegarde d'ESP. ; Bien sûr, il faut penser à sauvegarder le registre EBP actuel pour pouvoir le faire à nouveau ; pointer sur l'espace dédié au programme appelant à la fin de notre procédure. push ebp ; Sauvegarde du contexte précédent mov ebp, esp ; Sauvegarde du pointeur de pile mov cx, 16 ; on initialise une boucle de 16 itérations nous permettant ainsi de traiter ; des nombres codés sur 16 bits ou moins. bcl0 : debut_si : shl bx, 1 jc aff1 ; on effectue un déplacement logique vers la gauche. le bit sortant active le ; carry flag. il suffit de tester celui-ci pour en déduire l'état du bit sortant mov ah, 02h ; (0 ou 1) et d'afficher en conséquence le bon caractère ('0' ou '1'). mov dl, '0' int 21h jmp fin_si aff1 : mov ah, 02h mov dl, '1' int 21h fin_si : loop bcl0 ; Restauration du contexte : ESP doit pointer sur l'adresse de retour ! mov esp, ebp pop ebp ret ; permet de revenir au programme principal, à l'instruction suivant l'instruction "call" ; qui appellera cette procédure. ; affichage_hexa : ; Objectif : Afficher à l'écran, sous forme hexadécimal, un nombre passé en paramètre. ; Passage des paramètre : Par la pile. Le nombre a afficher doit être le dernier élément ; empilé avant l'appel de cette procédure. ; Pré-condition : Le dernier élément empilé doit être un nombre. Même remarques que pour ; affichage_binaire. affichage_hexa: ; ESP désigne le sommet actuel de la pile alors qu'EBP pointe sur le bas "relatif" de la pile. ; Pour l'instant, EBP pointe sur le "bas" de l'espace dédié ("stack-frame") au programma appelant. ; On va donc le faire pointer sur ESP (le sommet) pour dire que l'espace de travail de notre ; programme appelé commence ici. Cela nous fera d'ailleurs, en même temps, une sauvegarde d'ESP. ; Bien sûr, il faut penser à sauvegarder le registre EBP actuel pour pouvoir le faire à nouveau ; pointer sur l'espace dédié au programme appelant à la fin de notre procédure. push ebp ; Sauvegarde du contexte précédent mov ebp, esp ; Sauvegarde du pointeur de pile mov cl, 12 mov ah, 02h bcl1 : cmp cl, 0 ; a chaque itération, on effectue un décalage logique vers la droite de jl fin_bcl1 ; moins en moins important (12, 8, 4, 0) et on applique un masque. cela nous permet ; de garder qu'un seul groupe de 4 rangs binaires à la fois. on affiche ce groupe à l'écran. mov bx, word [bp + 4] ; normalement, on utilise sp pour se déplacer mais tasm est un peu dur de la feuille ... ; on récupère, dans la pile, le paramètre passé à cette procédure (= le nombre à afficher) shr bx, cl ; ss est facultatif et désigne l'adresse mémoire du segment de pile. and bx, 000fh ; dans ce segment on veut accéder au contenu de l'offset ; bp + 4 (4 car 4 octets = 2 emplacements sur la pile), mov dl, [tab_hexa + bx] ; car c'est dans cet emplacement que se trouve notre paramètre (bp + 2 contient int 21h ; la sauvegarde de bp faite au début de cette procédure. ; il faut remonter (+2) à l'emplacement précèdent car la pile croît vers sub cl, 4 ; les adresses basses de la mémoire donc le sommet se trouve plus bas que le paramètre. jmp bcl1 fin_bcl1 : ; Restauration du contexte : ESP doit pointer sur l'adresse de retour ! mov esp, ebp pop ebp ; par défaut, ret récupère l'adresse de retour au programme et la stock dans ret 2 ; le registre ip (car prochaine instruction) puis incrémente le registre sp de ; 2 octets afin de "libérer" l'emplacement occupé par cette adresse de retour. ; ici, nous lui demandons de "libérer" 2 octets de plus afin de libérer ; l'emplacement contenant le paramètre passé à cette procédure (= le nombre à afficher). |
Exercice 5 : sous GNU/Linux, avec Nasm et ld
section .data ; exercice 1 ; données nombre dd 4269666 tab_hexa db "0123456789ABCDEF" ; messages écran exercice1 db "Exercice 1 :",13,10 lenexo1: equ $-exercice1 message_binaire db "Affichage du nombre déclaré dans le segment de données (ici : 4269666) en binaire : " lenmsgbinaire: equ $-message_binaire message_hexa db 13,10,"Affichage du nombre déclaré dans le segment de données (ici : 4269666) en hexadecimal : " lenmsghexa: equ $-message_hexa ; exercice 2 ;données ;mesures dw 0ffffh, 0ffffh, 0ffffh, 0ffffh, 0ffffh, 0ffffh, 0ffffh, 0ffffh, 0ffffh, 0ffffh mesures dw 0e765h,6ab3h, 8fa1h, 905ch, 0a9fdh, 5da9h, 4807h, 8fa2h, 0a10bh, 0b95h ; messages écran exercice2 db 13,10,10,"Exercice 2 :" lenexo2: equ $-exercice2 msg_hexa db 13,10,"Affichage de la moyenne des mesures G1 en hexadecimal : " lenmsg2hexa: equ $-msg_hexa msg_binaire db 13,10,"Affichage de la moyenne des mesures G2 en binaire : " lenmsg2binaire: equ $-msg_binaire ; exercice 3 exercice3 db 13,10,10,"Exercice 3 :" lenexo3: equ $-exercice3 msg_g1_flottant db 10,"Affichage de la moyenne G1 en virgule flottante : ",0 msg_g2_flottant db "Affichage de la moyenne G2 en virgule flottante : ",0 diviseur dq 10 format db "%s%f", 10, 0 ; fin fin: db 10,'fin',10 lenfin: equ $-fin section .bss tab_g1 resw 10 tab_g2 resw 10 dividende_g1: resq 1 dividende_g2: resq 1 compteur: resb 1 section .text global _start extern printf _start: ; EXERCICE 1 ; Affichage du message écran de début de l'exercice 1 mov eax, 4 mov ebx, 1 mov ecx, exercice1 mov edx, lenexo1 int 80h ; Affichage du message annonçant l'affichage du nombre déclaré dans le segment de données, en binaire mov eax, 4 mov ebx, 1 mov ecx, message_binaire mov edx, lenmsgbinaire int 80h mov eax, [nombre] ; Passage d'un paramètre, par un registre, à la procédure call affichage_binaire ; Appel de la procédure ; Affichage du message annonçant l'affichage du nombre déclaré dans le segment de données en hexadécimal mov eax, 4 mov ebx, 1 mov ecx, message_hexa mov edx, lenmsghexa int 80h push dword [nombre] ; Passage d'un paramètre, par la pile, à la procédure call affichage_hexa ; Appel de la procédure ; exercice 2 ; affichage du message écran de début de l'exercice 2 mov eax, 4 mov ebx, 1 mov ecx, exercice2 mov edx, lenexo2 int 80h ; 1) empiler les mesures sur la pile ; on fait une boucle pour empiler chacune des mesures. ; comme la pile du 8086 est de type lifo (= last in, first out), ; nous empilerons la 10eme mesure en premier et la 1er en dernier. ; ainsi, dans le 2), la première mesure désempilée sera bien la première du tableau mesure. ; Attention : "mesures" est un tableau de word (16 bits), pas un tableau de byte (8 bits) ; donc on ne se déplace pas de 1 en 1 entre les cases (entre leurs adresses pour être précis) ; mais de 2 en 2. mov ecx, 20 bcl_empile_mesures : cmp ecx, 2 jb fin_bcl_empile_mesures mov eax, ecx sub eax, 2 push word [mesures+eax] sub ecx, 2 jmp bcl_empile_mesures fin_bcl_empile_mesures : ; 2) extraire et classer les mesures g1 et g2 dans deux tableaux independants mov ecx, 0 bcl_extraction : cmp ecx, 20 je fin_bcl_extraction pop ax ; on désempile une mesure mov dx, ax ; on dupplique cette valeur afin de pouvoir réaliser l'extraction ; de g1 et de g2 dans la même boucle. ; extraction de la mesure g1 dans le registre ax par la méthode du décalage logique à droite ; de 11 rangs binaires puis stockage dans le tableau tab_g1 dédié shr ax, 11 mov [tab_g1+ecx], ax ; extraction de la mesure g2 dans le registre dx par la méthode du masque logique ; puis stockage dans le tableau tab_g2 dédié and dx, 07ffh mov [tab_g2+ecx], dx add ecx, 2 jmp bcl_extraction fin_bcl_extraction : ; 3) calculer la moyenne des mesures g1 et g2 mov eax, 0 mov ecx, 0 mov edx, 0 ; on fait une boucle qui additionne toutes les valeurs des mesures g1 dans ax ; et toutes les valeurs des mesures g2 dans dx bcl_addition : cmp ecx, 20 je fin_bcl_addition add ax, [tab_g1 + ecx] add dx, [tab_g2 + ecx] add ecx, 2 jmp bcl_addition fin_bcl_addition : ; Sauvegarde pour plus tard (calcul flottant entre autres) mov [dividende_g1], ax mov [dividende_g2], dx ; calcul de la moyenne des valeurs des mesures g2 (division) et stockage du résultat sur la pile mov ax, dx mov dx, 0 mov bx, word [diviseur] div bx push eax ; calcul de la moyenne des valeurs des mesures g1 (division) et stockage du résultat sur la pile mov ax, word [dividende_g1] ; restauration de la somme des mesures g1 mov dx, 0 mov bx, word [diviseur] div bx push eax ; 4) affichage de la moyenne des mesures g1 en hexadécimal et affichage de la moyenne des mesures g2 en binaire ; affichage du message écran annonçant l'affichage de la moyenne des mesures g1 en hexadécimal mov eax, 4 mov ebx, 1 mov ecx, msg_hexa mov edx, lenmsg2hexa int 80h call affichage_hexa ; la moyenne des mesures g1 est déjà la dernière donnée empilée. ; la procédure peut donc être appelé. ; affichage du message écran annonçant l'affichage de la moyenne des mesures g2 en binaire mov eax, 4 mov ebx, 1 mov ecx, msg_binaire mov edx, lenmsg2binaire int 80h pop eax ; nous mettons la moyennes des mesures g2 dans le registre eax call affichage_binaire ; et nous appelons la procédure. ; EXERCICE 3 ; Affichage du message écran annonant l'exercice 3 mov eax, 4 mov ebx, 1 mov ecx, exercice3 mov edx, lenexo3 int 80h ; Calcul flottant de la moyenne des G1 fld qword [dividende_g1] ; on positionne le dividende au sommet de la pile du coproc arithmètique fdiv qword [diviseur] ; on effectue la division ; Affichage résultat G1 fstp qword [esp] ; on exporte le résultat de la pile du coproc vers le sommet de la pile "classique" push msg_g1_flottant ; message présentant le résultat push format ; format printf classique : un string puis un flottant call printf ; appel de printf ; Calcul flottant de la moyenne des G2 fld qword [dividende_g2] fdiv qword [diviseur] ; Affichage résultat G2 fstp qword [esp] push msg_g2_flottant push format call printf ; fin mov eax, 4 mov ebx, 1 mov ecx, fin mov edx, lenfin int 80h mov eax, 1 mov ebx, 0 int 80h ; affichage_binaire : ; Objectif : Afficher à l'écran, sous forme binaire, un nombre passé en paramètre. ; Passage des paramètre : Par un registre. Le nombre à afficher doit se trouver dans EAX. ; Pré-condition : BX doit contenir un nombre. L'interprétation du résultat binaire revient ; à l'utilisateur. Ex : dans le cas d'un nombre réel ... A noter que même ; si EAX ne contient pas un nombre, la procédure s'exécutera avec succès ; et affichera le contenu du registre EAX mais l'interprétation perdra son sens. affichage_binaire: push ebp ; Sauvegarde du contexte précédent mov ebp, esp ; Sauvegarde du pointeur de pile push eax ; Pour se simplifier la vie, on stocke le paramètre sur la pile mov byte [compteur], 32 ; on initialise une boucle de 32 itérations nous permettant ainsi de traiter ; des nombres codés sur 16 bits ou moins. bcl0 : cmp byte [compteur], 0 jle fin_bcl10 debut_si : pop eax shl eax, 1 push eax ; On garde la modification, pour ne pas toujours travailler sur le même bit jc aff1 ; on effectue un déplacement logique vers la gauche. le bit sortant active le ; carry flag. il suffit de tester celui-ci pour en déduire l'état du bit sortant ; (0 ou 1) et d'afficher en conséquence le bon caractère ('0' ou '1'). mov eax, 4 mov ebx, 1 mov ecx, tab_hexa mov edx, 1 int 80h jmp fin_si aff1 : mov eax, 4 mov ebx, 1 mov ecx, tab_hexa add ecx, 1 mov edx, 1 int 80h fin_si : dec byte [compteur] jmp bcl0 fin_bcl10 : pop eax ; On supprime le paramètre empilé au début de cette procèdure ; Restauration du contexte : ESP doit pointer sur l'adresse de retour ! mov esp, ebp pop ebp ; On revient au programma appelant ret ; Permet de revenir au programme principal, à l'instruction suivant l'instruction "call" ; qui appellera cette procédure. ; affichage_hexa : ; Objectif : Afficher à l'écran, sous forme hexadécimal, un nombre passé en paramètre. ; Passage des paramètre : Par la pile. Le nombre a afficher doit être le dernier élément ; empilé avant l'appel de cette procédure. ; Pré-condition : Le dernier élément empilé doit être un nombre. Même remarques que pour ; affichage_binaire. affichage_hexa: ; ESP désigne le sommet actuel de la pile alors qu'EBP pointe sur le bas "relatif" de la pile. ; Pour l'instant, EBP pointe sur le "bas" de l'espace dédié ("stack-frame") au programma appelant. ; On va donc le faire pointer sur ESP (le sommet) pour dire que l'espace de travail de notre ; programme appelé commence ici. Cela nous fera d'ailleurs, en même temps, une sauvegarde d'ESP. ; Bien sûr, il faut penser à sauvegarder le registre EBP actuel pour pouvoir le faire à nouveau ; pointer sur l'espace dédié au programme appelant à la fin de notre procédure. push ebp ; Sauvegarde du contexte précédent mov ebp, esp ; Sauvegarde du pointeur de pile ; Déclage de 28 bits au maximum mov byte [compteur], 28 bcl1 : cmp byte [compteur], 0 ; A chaque itération, on effectue un décalage logique vers la droite de jl fin_bcl1 ; moins en moins important (24, 20, 16, 12, 8, 4, 0) et on applique un masque. cela nous permet ; de garder qu'un seul groupe de 4 rangs binaires à la fois. On affiche ce groupe à l'écran. mov ebx, [ebp+8] ; On récupère, dans la pile, le paramètre passé à cette procédure (= le nombre à afficher) mov ecx, [compteur] ; EBP -> Sauvegarde EBP réalisée au début de cette procédure shr ebx, cl ; EBP + 4 -> Adresse de retour au programme appelant and ebx, 000fh ; EBP + 8 -> Notre paramètre ; On incrémente par pas de 4 car on est sur du 32 bits. Chaque "case" de la pile fait donc 32 bits, soit 4 bytes. mov eax, 4 mov ecx, tab_hexa add ecx, ebx mov ebx, 1 mov edx, 1 int 80h sub byte [compteur], 4 jmp bcl1 fin_bcl1 : ; Restauration du contexte : ESP doit pointer sur l'adresse de retour ! mov esp, ebp pop ebp ret 4 ; Par défaut, RET récupère l'adresse de retour au programme et la stocke dans ; le registre IP (car prochaine instruction) puis incrémente le registre ESP de ; 2/4 octets (selon architecture) afin de "libérer" l'emplacement occupé par cette adresse de retour. ; Ici, nous lui demandons de "libérer" 4 octets de plus afin de libérer ; l'emplacement contenant le paramètre passé à cette procédure (= le nombre à afficher). ; Cela permet d'éviter que le paramètre reste sur la pile et fasse confusion à la prochaine execution. |