Sam86 | |
Présentation / Téléchargement / Manuel / Tutoriel : "Assembleur et Amorce" |
Programmer en assembleur |
IntroOn vous a toujours dit qu'un ordinateur ça fonctionne avec des "1" et des "0", que l'on compte en binaire, que l'on agence les bits en octet, que ça utilise des portes logiques ET, OU, OU-EXCLUSIF et peut-être même l'astuce du complément à 2... bla bla bla.
En voila des choses bien intéréssantes ! Mais avec tout ça avez-vous vraiment compris comment ça marche :-\ ? Voici le programme :
2. La famille des 80x86 3. Autour du processeur 4. L'Architecture IA-32 5. Le B.A.BA
5.2 INC et DEC 5.3 ADD, ADC, SUB, SBB, AND, OR, XOR et CMP 5.4 JMP 5.5 PUSH, POP, Bref la pile ! 5.6 CALL et RET/RETF - les appels 5.7 INT 7. Votre première amorce ! Ce document est librement distribuable à condition que cela soit dans son intégralité, ce paragraphe en faisant partie. Si vous constatez une faute d'orthographe ou, horreur, une erreur ou si vous avez une suggestion, merci de me contacter pour en faire profiter le plus grand nombre.
1. A l'intérieur du processeur
Un processeur est composé de registres (petites mémoires), d'une Unité Arithmetique et Logique (UAL ou ALU), etc... Sur chacun de ces éléments il y a une ou plusieurs lignes de contrôle. Elle permet par exemple d'indiquer à un registre qu'il doit prendre la valeur d'un bus, ou à l'UAL si elle doit faire une addition, une multiplication, une opération booléene ou encore un décalage, etc... 2. La famille des 80x86
Il faut savoir qu'Intel a toujours pratiqué la compatibilité ascendante*. C'est à dire qu'un programme qui fonctionnait sur un 8086 fonctionne sur un P4 (en un peu plus vite). L'inverse n'est pas forcement vrai car Intel a rajouté des instructions au fil des générations. *sauf l'instruction "mov cs, ax" qui a disparu avec le 286. Une instruction qu'utilisait entre autres le célèbre virus ping-pong. 3. Autour du processeur
Le processeur communique avec l'exterieur via des ports, une ligne d'interruption et aussi par la mémoire à laquelle il accède directement. Par exemple : La mémoire est découpée en segment qui peuvent contenir soit du code, soit des données. L'offset (en français le déplacement) est l'adresse d'une donnée par rapport au début d'un segment. L'ensemble Segment:Offset désigne un emplacement mémoire. 4. L'Architecture IA-324.1 Les registres de segment
4.2 Les registres généraux
Bien sur les significations sont données à titre indicatif et rien ne vous empêche de faire systématiquement des additions entre EBX et ECX. Sachez tout de même que certaines instructions utilisant EAX sont optimisées. Pourquoi toujours utiliser des registres 32 bits ? Parfois 16 suffisent amplement. AX, BX, CX et DX sont des registres 16 bits. En fait, ce sont des sous registres de EAX, EBX, ECX et EDX. Donc attention : si en modifiant AX, vous modifiez aussi les 16 bits de poids faible de EAX. Pourquoi toujours utiliser des registres 16 ou 32 bits ? Parfois 8 suffisent amplement. AL, BL, CL et DL sont des registres 8 bits. En fait ce sont des sous registres de AX, BX, CX et DX (mais vous l'aviez compris). Pareil : si en modifiant AX, vous modifiez aussi les 8 bits de poids faible (Low) de AX. Et avec ça qu'est ce que je vous mets ? ESI, EDI, ESP, EBP sont des registres 32 bits (comme le préfixe E l'indique).
4.3 Le mot d'état machine (MSW)Encore un petit effort, c'est le dernier registre que nous allons voir mais il est hyper important ! Vous voulez tester l'égalité de deux registres. Faites une soustraction, puis faites un saut conditionnel en fonction de l'indicateur Z (zero). (Nous reverrons cela plus loin). Machine Status Word (MSW) 5. le B.A.BA
5.1 - MOVmov ax, 10 ; Ici c'est du commentaire mov bx, 10h ; Après un point virgule le texte mov cx, [123h] ; est ignoré. En assembleur les mov dx, bx ; commentaires sont très vivement recommandés. Que fait ce programme ? Il met 10 dans AX. Il met 10h dans BX (h=héxadécimal). Il met le contenu du mot à l'adresse DS:123h dans CX et le contenu de BX dans DX. A votre avis que fait "mov ax, [es:di+3]" ? Bien sûr vous ne pouvez pas écrire "mov bl, dx" car les opérandes destination et source ne sont pas de même dimension. 5.2 - INC et DECCes instructions servent à incrémenter ou décrémenter une opérande. inc ax ; ax = ax + 1 dec [123h] ; [123h] = [123h] - 1 Certains programmes d'assemblage vous permette d'écrire "inc ax, bx, al" ce qui signifie "inc ax" puis "inc bx" puis "inc al". 5.3 - ADD, ADC, SUB, SBB, AND, OR, XOR et CMP
add ax, 10 ; ax = ax + 10 add ebx, [123h] ; ebx = ebx + [123h] Notez que le résultat va dans la première opérande. Les indicateurs du mot d'état machine sont mis à jour. mov ax, 10 jmp saut add ax, 10 ; Cette ligne n'est jamais éxécutée. saut: add ebx, [123h]Saut est ce qu'on appelle une étiquette. Elle représente une adresse dans le programme. En faisant "JMP Saut", vous allez directement à l'instruction qui suit l'étiquette. 5.4 - Jcc : les sauts conditionnels
Vous pouvez nier les conditions ci-dessus en intercalant un 'N' après le J. (par exemple JNB)
mov cx, 10 ; cx = 10 boucle: jcxz fin ; Si cx = 0 alors fin ... dec cx ; cx = cx - 1 jmp boucle fin: ...Exemple 2: mov ax, 0 ; ax = 0 boucle: cmp ax, 10 ; ax - 10 ? jz fin ; Si = 0 alors fin ... inc ax ; ax = ax + 1 jmp boucle fin: ... Vous remarquerez qu'il est plus simple d'utiliser CX pour les boucles, mais vous pouvez toujours contourner les recommandations. 5.5 - PUSH, POP, Bref la pile !Elle vous créera bien des ennuis mais vous ne pourrez plus vous en passer : la pile (stack). C'est assez délicat à expliquer (et donc je pense aussi à comprendre), mais vous verrez c'est très pratique. La pile est un endroit en mémoire, dans le segment mémoire SS, où l'on stocke temporairement des données, ce qui évite de créer des variables (surtout quand on ne sait pas exactement combien il en faudra). Imaginez que tous vos registres sont occupés (et que vous ne voulez pas créer de variable). Que faire ! Empilez des données, utilisez les registres, puis récupérez ces données plus tard. Exemple :push ax ; ax -> Pile ... pop bx ; bx <- Pile Oho, cela ne parait pas si compliqué que ça ! Mais voyons, pour mieux comprendre, un équivalent : ; push ax sub sp, 2 ; parceque AX prends 2 octets mov [ss:sp], ax ... ; pop bx mov bx, [ss:sp] add sp, 2 Le registre SP pointe sur le dernier élément empilé. Notez bien que SP decroit quand on empile. La pile doit toujours être initialisé avant de l'utiliser. Autrement vous êtes suceptible d'écrire n'importe où en mémoire. Parfois les OS s'en occupe. Parfois : mov ax, cs ; "mov ss, cs" n'existe pas mov ss, es jmp saut1 db 10 dup ("PILE ") saut1: mov sp, offset saut1 ; une petite subtilité ... Notez aussi l'existence de PUSHF/POPF qui permette d'empiler/de dépiler le mot d'état machine, ainsi que PUSHA et POPA qui empile/dépile tous les registres d'un coup. 5.6 - CALL et RET/RETF - les appelsjmp debut routine: inc ax dec di ret debut: xor ax, ax ; ax = 0 call routine Lorsque l'on fait un call, IP (ou CS:IP selon les cas) est empilé puis on fais un saut à l'étiquette indiquée. A l'exécution de RET respectivement RETF, on dépile IP respectivement CS:IP. N'oubliez pas désormais de mettre un RET à la fin de vos programme pour rendre la main à celui qui vous a appellé. 5.7 - INTJe ne détaillerai pas ici les mécanismes précis des appels à interruptions logicielles. Sachez tout de même qu'avant l'avènement des librairies dynamiques de fonctions (.DLL, .OVL, etc...), tous les appels systèmes fonctionnaient ainsi. Dans AH, on mets un numéro de fonction. Dans les autres registres les paramètres, puis on appelle une interruption, par exemple pour les appels au BIOS Graphique, la 10h. Exemple :mov ah, 0eh ; fonction "écrire un caractère" mov al, 'A' ; le dit caractère mov bx, 1Eh ; en Jaune (E) sur du bleu (1) int 10h ; appel au BIOS Graphique ret Vous trouverez facilement sur internet ou dans une bible PC, la liste des fonctions. Il existe encore de nombreuses instructions mais restons en là pour le B.A.BA. 6. Votre premier programmeIl est temps de passer à la pratique. Pour cela munissez vous d'un éditeur de texte (edit, bloc-notes, vi, etc...) et d'un assembleur (sam86, a86, tasm, masm, nasm, fasm). J'ai personnellement une préférence pour Sam86 (que vous comprendrez dans la partie 7). Sam86, a86 génèrent par défaut des programmes ".com" qui tourne sous DOS (org 100h). (Tasm génére des .exe, donc avec une entête)
mov ax, cs mov ds, ax mov si, offset msg ; Avec nasm => "mov si, msg" boucle: mov al, [si] ; [ds:si] => al inc si ; si = si + 1 cmp al, 0 ; Si AL = 0 je fin ; Alors fin mov ah, 0eh ; Sinon ecrire le caractère mov bx, 07h ; en gris sur noir int 10h jmp boucle fin: ret msg db 'Hello World !!!',0Enregistrez le avec une extension .asm Compilez le ("sam mon_prgm.asm", "nasm mon_prgm.asm -o mon_prgm.com") Lancez le ! Oups, c'est vrai nous n'avons pas encore vu les DB, DW et DD, mais vous avez compris qu'il s'agissait d'intégrer des données au programme (avec une étiquette). C'est une variable quoi ! Mettons cela sous forme de procédure. jmp debut ecrit_: mov al, [si] ; [ds:si] => al inc si ; si = si + 1 cmp al, 0 ; Si AL = 0 je fin ; Alors fin mov ah, 0eh ; Sinon ecrire le caractère mov bx, 07h ; en gris sur noir int 10h jmp ecrit_ fin: ret debut: mov ax, cs mov ds, ax mov si, offset msg ; Avec nasm => "mov si, msg" call ecrit_ ret msg db 'Hello World !!!',0 Mettons la "procédure" dans un autre fichier. Le fichier ecrit.inc :ecrit_: mov al, [si] ; [ds:si] => al inc si ; si = si + 1 cmp al, 0 ; Si AL = 0 je fin ; Alors fin mov ah, 0eh ; Sinon ecrire le caractère mov bx, 07h ; en gris sur noir int 10h jmp ecrit_ fin: retVotre programme : jmp debut include "ecrit.inc" debut: mov ax, cs mov ds, ax mov si, offset msg ; Avec nasm => "mov si, msg" call ecrit_ ret msg db 'Hello World !!!',0 Et rajoutons un macro pour la route. (syntaxe pour Sam86 uniquement) fichier 'ecrit.inc' :MACRO ecrit msg mov si, offset {db msg, 10, 13, 0} call ecrit_ ENDM ecrit_: lodsb cmp al, 0 je fin mov ah, 0eh mov bx, 07h int 10h jmp fin_ecrit fin_ecrit: retVotre programme : jmp debut include "ecrit.inc" ; recopie le fichier ecrit.inc ici debut: mov ax, cs mov ds, ax ; DS = CS ecrit "Hello World !!!" ecrit "Coucou" ret Ca ne parait plus très simple, mais en fait ça vous simplifiera la suite des opérations. Regardez votre programme, on dirait presque un langage évolué ! 7. Votre première amorceLe but de ce chapitre est de faire un petit programme que l'on transferera sur l'amorce d'une disquette. Il s'executera lorsque vous tenterez de démarrer dessus. Mais avant de faire cela il va falloir créer un outil qui nous manque. 7.1 Transferer du code vers l'amorce - tea.asmC'est l'occasion pour vous de lire du code (c'est à dire la meilleur manière de progresser). Que va faire ce programme :
; TEA -> Tranfert ficher En Amorce ; Avant d'exécuter ce programme insérez une disquette ne contenant aucune donnée ; importante dans le lecteur A. Celle-ci ne sera plus lisible par un OS standard. ; Vous pourrez la récuperrer en la formatant. ; Attention avant de faire quoi que ce soit, soyez sûr de comprendre ce que vous ; allez faire car vous allez écrire en amorce d'un disque ! Je décline... ; Ne pas modifier des lignes au hasard, sous peine de perdre des données du hdd. mov ax, cs ; DS <- CS mov ds, ax jmp debut ecrit: lodsb cmp al, 0 je @f mov ah, 0eh mov bx, 07h int 10h jmp ecrit @@: ret msg1 db 'TEA -> Tranfert ficher En Amorce', 10, 13, 0 msg2 db 'Impossible d''ouvrir amorce.bin', 10, 13, 0 msg3 db 'Erreur lors de l''ecriture en amorce', 10, 13, 0 msg4 db 'Ok', 10, 13, 0 nom_fichier db 'amorce.bin' fichier dw 0 debut: mov si, offset msg1 call ecrit mov ah, 3dh ; ouverture d'un fichier mov al, 0 ; en mode lecture seule mov dx, offset nom_fichier int 21h jnc @f ; On teste si ça c'est bien passé. mov si, offset msg2 ; Bah non call ecrit ; Donc message d'erreur ret ; et ciao @@: mov [fichier], ax mov ah, 3fh ; Lire le fichier mov bx, [fichier] mov cx, 512 ; 512 octets mov dx, offset tampon int 21h ; Appel MS-DOS mov ax, cs mov ds, ax ; es <- cs mov bx, offset tampon mov ah, 03h ; Ecrire mov al, 1 ; 1 secteur mov ch, 0 ; sur la piste 0 mov cl, 1 ; le numéro 1 mov dh, 0 ; face 0 mov dl, 0 ; sur le lecteur A int 13h ; int. disque jnc @f ; On teste si ça c'est bien passé. mov si, offset msg3 ; Bah non call ecrit ; Donc message d'erreur ret ; et ciao @@: mov si, offset msg4 call ecrit ret tampon db 512 dup (0) 7.2 L'amorce - amorce.asm
Que va faire notre amorce ? org 7C00h ; prgm d'amorce mov ax, cs mov ds, ax mov si, offset msg boucle: lodsb ; AL = [DS:SI], Si = Si + 1 cmp al, 0 ; Si AL = 0 je fin ; Alors fin mov ah, 0eh ; Sinon ecrire le caractère mov bx, 07h ; en gris sur noir int 10h jmp boucle fin: mov ah, 0 ; Lire un caractère int 16h ; Int. clavier int 19h ; On continue le retente de démarrer msg db 'Hello World !!!',10,13,'Appuyez sur une touche !',0 org 7DFEh ; Avec nasm : "times 512-($-$$)-2 db 0" dw 0AA55h ; SignatureNote : Avec Sam86, utilisez l'option -boot et ne copiez pas les lignes vertes. 7.3 Action !Pour l'exécuter :
(avec Sam : sam -boot amorce, avec Nasm : nasm amorce.asm -f bin -o amorce.bin" et n'oubliez pas d'enlever directives "offset".) Compilez le ! Si vous avez utilisé a86, supprimez tous les octets nuls qui sont avant le code (il y en a 7C00h). Mettez une disquette vide dans le lecteur A. Exécutez tea.com Redémarrez sur la disquette Eventuellement il faudra changer des options dans le BIOS pour que le système cherche à démarrer sur la disquette. Quand vous aurez finit de vous amuser, vous pourrez reformater la disquette et consulter d'autres tutoriels pour approfondir vos connaissances. Jean-Michel MORANI
Ce document est librement distribuable à condition que cela soit dans son intégralité, ce paragraphe en faisant partie. Si vous constatez une faute d'orthographe ou, horreur, une erreur ou si vous avez une suggestion, merci de me contacter pour en faire profiter le plus grand nombre. Merci Marco. Première Version : 25/6/03 |