Assembleur 8086+ : exercice 5

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".

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.

Aucun commentaire.

Ajoutez votre commentaire