lalahop

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.

Envoyer des SMS automatiquement à une date définie

Il y a quelques temps, je me demandais, par simple curiosité, comment on pouvait envoyer un SMS automatiquement à une heure définie sur les "vieux" téléphones. Nan parce que sur les smartphones, je suis sûr qu'il doit y avoir 20 applis par store qui font ça mais quid pour un bête Nokia 6300, par exemple ?

À l'époque, j'avais trouvé quelques logiciels comme Automsg mais, n'étant pas signé, il ne pouvait envoyer de SMS sans me demander une confirmation avant ... Ce qui est pas trop l'objectif d'un envoi automatique. Je me rappelle avoir tenté de bidouiller de ce côté là : tenter de signer le logiciel, tenter de forcer l'autorisation (car elle était grisée dans le menu de mon téléphoe). Rien n'avait fonctionné.

Aujourd'hui, je me suis à nouveau intéressé à ce sujet. J'ai décidé d'interfacer mon téléphone avec mon ordinateur avec gammu (logiciel libre de gestion de téléphone portable/modem compatible avec un large panel de téléphones). Je me suis dit que couplé à cron (pour un envoi régulier) ou at (pour un envoi ponctuel), ça devait le faire.

Alors certes, il faut une "proximité" avec un ordinateur mais avec le bluetooth, ça ne devrait pas poser de problèmes. Alors certes, il faut que l'ordinateur associé reste allumé mais avec la "démocratisation" des machines à faible consommation, ça ne devrait pas être un problème et puis, ce n'est que pour tester et pour le fun qu'on fait ça 😛 .

Gammu s'installe sur une Debian de manière classique :

sudo apt-get install gammu

Ensuite, il faut raccorder votre téléphone à votre machine. Dans mon cas, cela se fera en Bluetooth. J'utilise donc gnome-bluetooth (installé par défaut normalement) pour appairer mon téléphone et ma machine. Basiquement "Configurer un nouveau périphérique ..." puis suivre l'assistant. Ne pas cocher les cases concernant l'utilisation du téléphone comme modem/périphérique réseau.

Avant de commencer la configuration de gammu, il vous faut l'adresse matérielle bluetooth de votre téléphone (Bluetooth Device Address). Pour l'obtenir :

hcitool scan

Vous obtiendrez une sortie comme celle-là :

00:1D:98:3E:24:12	Nokia 6300

La configuration de gammu se fait facilement :

gammu-config

Une interface semi-graphique s'affiche. Dans "Port", il faut saisir la Bluetooth Device Address obtenue à l'étape précédente. Dans "Connection", il faut choisir "bluephonet", module dédié aux Nokia over Bluetooth. On ne touchera pas au reste. On choisit "Save" et on valide par "OK".

Je vous laisse découvrir les commandes de gammu avec gammu help ... Il y'en a des inutiles donc indispensables comme "nokiavibratest", des intéressantes comme getlocation, des non fiables comme "battery", ... Bref, à vous de découvrir.

Pour tester l'envoi d'un SMS :

echo "sms de test" | gammu sendsms TEXT +336xxxxxxxx OU gammu sendsms TEXT +336xxxxxxxx -text "sms de test"

Pour demander un accusé de réception, il faut ajouter l'option "-report" Il y a aussi l'option -validity pour définir la période de validité si non-remise 😉

Pour envoyer un SMS à une date donnée, il suffit d'utiliser la commande at. Exemple :

$ at 06:00
warning: commands will be executed using /bin/sh
at> /usr/bin/gammu sendsms TEXT +336xxxxxxxx -text "Bonjour !"    
at> <EOT> (Pressez CTRL+D)
job 8 at Thu Nov 29 06:00:00 2012

Ici nous envoyons un SMS à 6h le jour même.

Pour envoyer un mail régulièrement, il faut utiliser cron. crontab -e puis :

00 06 * * * /usr/bin/gammu sendsms TEXT +336xxxxxxxx -text "Bonjour !"

Ici, nous envoyons le SMS tous les matins à 6h.

Pour l'intérêt d'une telle manip, je ne trouve rien de sérieux donc ... être le premier à souhaiter un anniversaire (envoi d'un SMS à minuit pile), gagner un pari stupide du genre "je pari que je m'endors après toi" (envoi d'un sms toutes les 5 minutes pour dire qu'on ne dort pas encore alors qu'en vrai si) ... Désolé.

En dehors de cron/at, gammu doit pouvoir s'interfacer avec les logiciels de monitoring pour envoyer des alertes SMS si l'on ne souhaite pas utiliser les API des opérateurs ou autres API. Gammu doit aussi pouvoir s'utiliser dans un script de spam par SMS même si il existe des logiciels plus performants pour ce genre de choses.

DDNS indépendant sur OpenWRT

Table des matières

But

D'un côté, j'ai mon WRT54GL sous OpenWRT derrière une ligne ADSL (chez un FAI classique) avec une IP dynamique. Pour maintenir une correspondance entre un nom et l'adresse IP changeante, j'utilise les services de dyn.com.

De l'autre, j'ai un nom de domaine dont je gère moi-même le serveur de nom primaire. J'utilise BIND comme logiciel serveur.

Il serait bien que je prenne moi-même en charge la correspondance entre le nom de mon OpenWRT et son adresse IP. Pour ceux qui se demandent encore l'intérêt de la manip' : indépendance et tout ce que ça implique.

Contraintes

On parle de mon WRT54GL donc :

  • La méthode traditionnelle est de faire du DDNS (Dynamic DNS) dont l'échange est sécurisé grâce à une paire de clés grâce à un client comme nsupdate. Néanmoins, je n'ai pas la place pour installer un client sur mon OpenWRT.
  • Je n'ai pas la place pour installer les libs nécessaires au fonctionnement d'HTTPS avec curl ou wget. Or, si je fais mon propre système, je veux que tout soit sécurisé (de la récupération de l'IP jusqu'à sa mise à jour sur le serveur de nom.
  • Mon modem donne une IP RFC 1918 à mon WRT54GL donc le seul moyen de récupérer mon IP externe est de faire appel à un service externe. Or, je ne veux pas.

Ce que l'on va faire

OpenWRT dispose d'un client SSH ... donc toute la partie sécurité sera gérée par SSH lui-même.

Pour la périodicité de la vérification, OpenWRT dispose, par défaut, du classique cron.

Pour obtenir l'adresse IP de l'OpenWRT, pas besoin d'aller la récupérer sur un site web : on l'a récupérera depuis la variable d'environnement "SSH_CLIENT".

Pour mettre à jour la zone, on utilisera nsupdate qui sera déporté sur le serveur. Pas besoin de sécuriser l'échange avec une paire de clé vu que l'échange se fera sur localhost.

Pour résumer : Sur l'OpenWRT, cron lancera, de manière répétée, le client ssh. Le client SSH présent sur l'OpenWRT ira se connecter au serveur de nom en utilisant sa clé privée. Il lancera un script présent sur le serveur de nom. Ce script prendra la nouvelle IP en paramètre, fera quelques vérifications et utilisera nsupdate pour mettre à jour la zone.

Mise en œuvre

Dans la suite, "serveur" désignera le serveur de nom, "OpenWRT" désignera mon WRT54GL sous OpenWRT et "openwrt.guiguishow.info" sera le nom de domaine attribué à mon OpenWRT.

Script

#!/bin/bash
 
if [ $# -ne 1 ]
then
        echo "Trop ou pas assez d'arguments"
        logger "DDNS openwrt - Trop ou pas assez d'arguments"
        exit 2
fi
 

echo $1 | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}" > /dev/null
if [ $? -ne 0 ]
then
        echo "La paramètre \"IP\" n'est pas au bon format."
        logger "DDNS openwrt - Paramètre IP pas au bon format."
        exit 2
fi
 
# Un petit délire pas utile : vérifier rapidement l'AS à qui a
# été distribuée l'IP passée en paramètre. Si ce n'est pas l'AS
# de mon FAI, alors soit il y a une erreur, soit le script
# n'est pas lancé depuis l'OpenWRT ... de là à parler d'une
# usurpation ... 
as=`whois $1 | grep origin | grep -oE "AS[0-9]{1,5}"`
if [ $? -eq 0 ] && [ $as != "AS5410" ]
then
        logger "DDNS openwrt - Usurpation"
        echo "." | mail -s "DDNS openwrt - USURPATION" guigui chez guiguishow pointinfo
        exit 1
fi
 
# Si l'IP passée en paramètre est la même que celle 
# actuellement dans la zone, on ne fait rien.
ipActuelle=`dig +short @127.0.0.1 openwrt.guiguishow.info`
if [ $ipActuelle !=  $1 ]
then
        cat > ./majDNSopenwrt << EOF
server 127.0.0.1
zone guiguishow.info
update delete openwrt.guiguishow.info. A
update add openwrt.guiguishow.info. 60 A $1
show
send
EOF

        nsupdate -v ./majDNSopenwrt > /dev/null 2>&1
 
        if [ $? -eq 0 ]
        then
                logger "DDNS openwrt - MAJ OK"
                echo "." | mail -s "DDNS openwrt - MAJ OK" guigui chez guiguishow pointinfo
        else
                logger "DDNS openwrt - MAJ NEEDED BUT FAIL"
                echo "." | mail -s "DDNS openwrt - MAJ NEEDED BUT FAIL" guigui chez guiguishow pointinfo
        fi
fi

ÉDIT du 06/08/2014 à 16h20 : b4n propose une version mieux écrite de ce script :

#!/bin/bash
 
EMAIL="guigui chez guiguishow pointinfo"
 
if [ $# -ne 1 ]
then
  echo "Trop ou pas assez d'arguments" >&2
  logger "DDNS openwrt - Trop ou pas assez d'arguments"
  exit 2
fi
 
if ! grep -qxE '([0-9]{1,3}\.){3}[0-9]{1,3}' <<<"$1"
then
  echo "Le paramètre \"IP\" n'est pas au bon format." >&2
  logger "DDNS openwrt - Paramètre IP pas au bon format."
  exit 2
fi
 
# Un petit délire pas utile : vérifier rapidement l'AS à qui a
# été distribuée l'IP passée en paramètre. Si ce n'est pas l'AS
# de mon FAI, alors soit il y a une erreur, soit le script
# n'est pas lancé depuis l'OpenWRT ... de là à parler d'une
# usurpation ...
if [ "$(whois "$1" | grep origin | grep -oE 'AS[0-9]{1,5}')" != "AS5410" ]
then
  logger "DDNS openwrt - Usurpation"
  mail -s "DDNS openwrt - USURPATION" "$EMAIL" <<<"."
  exit 1
fi
 
# Si l'IP passée en paramètre est la même que celle
# actuellement dans la zone, on ne fait rien.
if [ "$(dig +short @127.0.0.1 openwrt.guiguishow.info)" != "$1" ]
then
  if nsupdate -v /dev/stdin >/dev/null 2>&1 <<EOF
server 127.0.0.1
zone guiguishow.info
update delete openwrt.guiguishow.info. A
update add openwrt.guiguishow.info. 60 A $1
show
send
EOF
  then
    logger "DDNS openwrt - MAJ OK"
    mail -s "DDNS openwrt - MAJ OK" "$EMAIL" <<<"."
  else
    logger "DDNS openwrt - MAJ NEEDED BUT FAIL"
    mail -s "DDNS openwrt - MAJ NEEDED BUT FAIL" "$EMAIL" <<<"."
  fi
fi

Fin de l'édit

Chez moi, ce script s'appelle "DDNSOpenWRT.sh".

Installer nsupdate sur le serveur

Sous Debian, nsupdate se trouve dans le package dnsutils :

apt-get install nsupdate

Réglage de BIND

Il faut autoriser le DDNS sur la zone depuis localhost. Cela se passe dans la déclaration de la zone, donc normalement dans le fichier /etc/bind/named.conf.local. Il faut ajouter la directive "allow-update { 127.0.0.1; };". Exemple :

zone "guiguishow.info." IN {
        type master;
        [...]
        allow-update { 127.0.0.1; };
};

Au niveau des droits, BIND doit avoir le droit d'écrire (w) sur le fichier qui défini la zone ainsi que sur le dossier contenant ce fichier car BIND écrit des journaux (fichiers avec une extension .jnl).

Utilisateur dédié et paire de clés

Sur le serveur, on va créer un utilisateur (openwrt) qui sera dédié uniquement à l'activité DDNS.

adduser openwrt

Mettre un mot de passe.

On met le script dans le home directory de cet utilisateur (/home/openwrt).

On génére une paire de clé pour l'utiliser avec SSH :

ssh-keygen -t rsa

La partie publique va sur le serveur, dans /home/openwrt/.ssh/authorized_keys, comme d'habitude.

La clé privée doit aller sur l'OpenWRT. Néanmoins, dropbear accepte les clés uniquement sous un certain format (sinon erreur "ssh: Exited: String too long"). On installe dropbear sur notre machine de travail et on converti la clé openwrt.rsa -> openwrt.dropb) :

sudo apt-get install dropbear
/usr/lib/dropbear/dropbearconvert openssh dropbear openwrt.rsa openwrt.dropb

On transfère la clé sur l'OpenWRT :

scp openwrt.dropb root@openwrt:.

Un peu de sécurité

L'utilisateur openwrt ne doit rien pouvoir faire sur le serveur à part lancer le script. On va donc restreindre les commandes possibles à une seule avec la directive de configuration ForceCommand de SSH. On ajoute donc ceci au fichier /etc/ssh/sshd_config :

Match User openwrt
ForceCommand ~/DDNSOpenWRT.sh `env | grep SSH_CLIENT | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}"`

Créer un cron sur OpenWRT

Par défaut, crond n'est pas lancé s'il n'y a aucun fichier dans /etc/crontabs/ (voir le script dans /etc/init/ pour vous en convaincre).

Il suffit de créer un fichier /etc/crontabs/root avec le contenu suivant :

*/5 * * * * /usr/bin/ssh -i /path/to/cle/privee openwrt@serveur_de_nom '~/DDNSOpenWRT.sh `env | grep SSH_CLIENT | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}"`'

Ainsi, toutes les 5 minutes, une vérification sera faite.

Au prochain reboot, crond démarrera tout seul mais cette fois-ci, c'est à vous de le lancer :

/etc/init.d/cron start

Pour terminer, il ne reste plus qu'à supprimer les ddns-scripts. Pour cela, je commence à killer les processus (ps aux + kill -9). Ensuite je supprime le package (opkg remove --autoremove). Pensez aussi à supprimer luci-app-ddns si jamais vous l'avez. Puis je supprime les paramètres conservés par uci (uci delete ddns.myddns) . Enfin : reboot.

Inspiration

L'idée d'utiliser nsupdate côté serveur, dans un script, me vient de : Mise à jour dynamique d’entrée DNS chez GuiguiABloc.

Impression de 4 pages format A6 sur une feuille format A4

Ce billet parle d'impression de flyers (tracts) qui sont généralement au format A6 sur une feuille A4.

On ne parlera pas de la création de ce flyer (Inkscape, GIMP, ...) : on suppose simplement que l'on a 2 fichiers PDF qui contiennent chacun un côté du flyer sur une page au format A6 (le recto dans un fichier et le verso dans un autre fichier). On supposera aussi que vous êtes sous un GNU/Linux ou un BSD. On supposera enfin que la configuration pour imprimer dans un PDF est OK.

Il peut arriver que l'imprimeur demande 2 PDF (un ayant le recto au format A6 et l'autre ayant le verso au format A6) quand la quantité est faible (pas d'utilisation d'offset) ou pour un bout d'essai. Pour la première fois, un imprimeur m'a demandé 1 PDF un peu spécial qui contient 2 pages : la première contenant 4 recto et la deuxième contenant 4 verso le tout sans aucune marge blanche.

La première difficulté est d'imprimer 4 pages sur une même feuille quand le PDF source n'en a qu'une ... On ouvre le PDF qui contient le recto avec la visionneuse habituelle et on demande l'impression :

On lance l'impression. Et là, c'est quitte ou double :

  • Soit le PDF généré est impeccable, chacun des "flyers" prenant précisément 1/4 de la feuille A4. Dans ce cas, vous imprimez le recto de la même manière et passez à la fin de ce billet pour la fusion de 2 PDF.
  • Soit il reste du blanc entre les flyers et de grosses marges de chaque côtés de la feuille. Nous allons y remédier.

On reouvre le PDF contenant le recto et on l'imprime. Ce coup-là, on va imprimer 4 fois la première page sur 4 feuilles différentes. Les paramètres sont donc ceux par défaut :

  • Général -> Plage - Pages : 1,1,1,1
  • Mise en page -> Pages par côtés : 1
  • Mise en page -> Échelle : 100%
  • Taille du papier : A6

On installe le paquet pdfjam :

apt-get install pdfjam

On exécute la commande :

pdfnup  --nup 2x2 --paper a4paper --noautoscale true --no-landscape recto.pdf

Explications :

  • --nup : 2 lignes et 2 colonnes par page
  • --paper a4paper : vous ne croyez quand même pas que je vais expliquer ça ? 😀
  • --noautoscale true : pareil ...
  • --no-landscape : on veut le mode portrait, pas paysage

On obtient un pdf, recto-nup.pdf, qui contient bien 4 fois le recto de notre flyer sur une seule feuille sans aucun marge. On réalise la même manipulation (impression en "4 pages sur 4 feuilles puis utilisation de pdfnup") avec le PDF qui contient le verso.

On a donc deux fichiers : recto-nup.pdf et verso-nup.pdf. Mais l'imprimeur veut le recto et le verso dans un même PDF. Qu'à cela ne tienne, utilisons gs (source : [CUPS-PDF] : assemblage de document) :

gs -dNOPAUSE -sDEVICE=pdfwrite -sOUTPUTFILE=recto-verso.pdf -dBATCH recto-nup.pdf verso-nup.pdf

On obtient bien un PDF qui contient 2 pages : la première contient 4 recto et la deuxième 4 verso, le tout sans grosses marges abominables.

P.-S. : Pour regrouper plusieurs pages sur une même feuille, il existe aussi mpage ou psnup. Le premier n'a jamais fonctionné chez moi. Le deuxième bloque sur certains PDF, en fonction de leurs contenus, sans doute.

Tout est dans le titre : l'installation de cups-pdf avec apt-get sous Debian Wheezy se bloque/gèle/freeze/hang sur la ligne "Reloading Common Unix Printing System: cupsd.". Le même problème peut survenir lors de la désinstallation de cups-pdf.

Un ps aux | grep dpkg permet de constater qu'il y a un processus "/bin/sh /var/lib/dpkg/info/cups-pdf.postinst" qui tourne. Comme par hasard, quand on kill se processus, l'installation continue. Certes elle échoue mais elle continue.

En décortiquant ce script, on observe l'utilisation de lpstat. OK, on se documente sur le bouzin et on essaye nous même, en ligne de commande, "lpstat -h localhost -r" : aucun retour ...

Hum ... que donne "lpstat -h 127.0.0.1 -r" ? "scheduler is running" ...

Hum ... problème de résolution des noms locaux ... On vérifie le contenu de /etc/hosts : il contient bien une association localhost <=> 127.0.0.1. Il contient évidement le même type d'association pour IPv6 ...

Hum ... donc s'il ne résout pas les noms locaux, un "lpstat -h nom_de_la_machine -r" va échouer ... Même pas ... Donc c'est bien un soucis interne à lpstat.

La solution est évidente (mais pas forcement propre, je n'ai aucun avis sur la question) : on modifie /var/lib/dpkg/info/cups-pdf.postinst pour remplacer toutes les occurrences de localhost (même celles qui ne sont pas utilisées comme paramètre d'un lpstat).

Tant qu'à faire, pour s'éviter des problèmes lors de la déinstallation/suppression de cups-pdf, on modifie les scripts /var/lib/dpkg/info/cups-pdf.postprerm et /var/lib/dpkg/info/cups-pdf.postpostrm de la même manière.

Ensuite si ce n'est pas déjà fait, on kill le processus /bin/sh /var/lib/dpkg/info/cups-pdf.* qui bloque l'installation/désinstallation de cups-pdf.

Enfin, on lance juste dpkg --configure -a et l'installation/déinstallation de cups-pdf se termine sans problèmes.