Assembleur Intel avec NASM sur machine virtuelle sans système d'exploitation
Date de publication : 20 mars 2012.
III. Des fonctions pour KarlOS
III.1. Pour rigoler
III.2. Table
III.3. CPU
III.4. Date
III.5. Du code
III. Des fonctions pour KarlOS
On a fait une ligne de commande dans laquelle on a mis deux fonctions. On va voir maintenant ce qu'elles font, voire, on va en rajouter.
III.1. Pour rigoler
Mais avant cela : rigolage, comme promis. Alors, je me suis dit, comme ça, dans le train : "Mais z'au fait, mon garçon, c'est quoi, la taille d'une chaîne de caractère ?" Réponse : c'est la position de la première occurence du caractère 0. Comme, ma foi, on fait un SCASB, appeler tailleChaineB, c'est appeler indexChaineB avec 0 dans AL. Et ça, personne n'en parle. Et en quoi c'est rigolo ? C'est rigolo parce que (faut toujours tout expliquer) avec un seul code, on a deux fonctions ! Oui, monsieur, deux fonctions pour le prix d'une ! Même le regretté Ritchie ne nous ferait pas un coup pareil. Regardez plutôt :
tailleChaineB :
xor al , al
indexChaineB :
push edi
or ecx , 0xFFFFFFFF
repne scasb
neg ecx
dec ecx
pop edi
ret
|
Et, ça, aucun compilo au monde ne l'aurait fait... Pas que ce soit forcément bien, notez.
III.2. Table
La toute première fonction codée pour la ligne de commande de KarlOS. Cela ne fait qu'afficher notre table de caractères.
tabCaracteres :
push ecx
push eax
mov ecx , 122
xor eax , eax
.afficheAlphabet :
call afficheCaractere
inc al
loop .afficheAlphabet
call retourChariot
pop eax
pop ecx
ret
|
III.3. CPU
La toute deuxième (quoi, y'a bien la toute première) fonction codée pour la ligne de commande de KarlOS. Cela ne fait qu'afficher une malheureuse information sur le microprocesseur. Bon, on utilise l'instruction CPUID, qui est une instruction versatile, voire lunatique. Utilisez-la dans un langage évolué, celle-là, qu'on rigole un peu. Bon, je vous la livre en l'état.
Il faut d'abord tester si cette fonction est supportée par le processeur. Ce n'est pas évident. Ca se passe dans le registre EFLAGS. Le bit n°21 de ce registre (notez qu'on est nécessairement au moins en 32 bits) peut être modifié ssi CPUID existe. C'est la base du test. Mais, car il faut bien un "mais" sinon on s'ennuie, mais donc, aucune instruction ne permet de modifier ce registre. On aurait voulu nous mettre des bâtons dans les roues qu'on ne s'y serait pas pris autrement. La ruse (car tous les programmeurs de système sont des fourbes félons fielleux et fallots fomentant ferme de fabuleux mais faux complots, bienvenue chez nous, nouvel initié), la ruse consiste à sauver ce registre sur la pile. PUSHFD est notre premier homme. Comme c'est sur la pile, hé, vieux, 'suffit de le récupérer dans un vrai registre ! POP EAX, et nous avons EFLAGS dans un registre d'honnête homme. Il suffirait que POPFD
ait le bon goût d'exister, et nous serions les plus heureux développeurs modifieurs d'EFLAG. Savourons notre bonheur, elle existe. Donc, on récupère dans un registre "normal" EFLAGS, on fait un XOR sur le bit 21, on le met dans le registre maudit, on le relit juste derrière, si la valeur est égale à celle avant le XOR, CPUID n'est pas implanté dans le processeur, merci d'avoir joué, repassez nous voir à la prochaine release.
pushfd
pop eax
mov ebx , eax
xor eax , 00200000h
push eax
popfd
pushfd
pop eax
cmp eax , ebx
jz .NO_CPUID
|
Admettons que l'instruction existe. Que fait-elle ? Et bien, très chers frères, il faut savoir que cette instruction ô combien utlie à l'homme de bien, fut conçue tout près de nos verts bocages, au milieu des vergers de pommes et de poires, sous les douces effluves de Camembert, dans notre Normandie. Conséquemment, ce qu'elle fait, ça dépend. En gros, il en existe autant de versions qu'il y a de fondeurs de processeurs, de familles de processeurs et de modèles de processeurs. Néanmoins, elle fonctionne un peu comme une interruption. Suivant ce qu'on a dans le registre EAX, on obtient différents résultats. Avec 0 (alias "la bulle"), on obtient, partout, toujours et en tout lieu et nonobstant les conditions sus-citées, le nombre de fonctions dont dispose CPUID, mais aussi une chaîne de caractères identifiant le fondeur. On n'arrête pas le progrès des sciences forensiques. Vous noterez, au passage, le tropisme de la valeur zéro.
xor eax , eax
cpuid
mov [nbFonctions], eax
mov edi , chaine
mov eax , ebx
stosd
mov eax , edx
stosd
mov eax , ecx
stosd
xor al , al
stosb
mov esi , chaine
call conversionASCII
call afficheChaine
call retourChariot
|
Dans EAX, on récupère le nombre de fonctions disponibles dans CPUID. Donc, premier coup, on a la marque et jusqu'où on peut aller (cf juste au-dessus) :
.marqueAMD :
inc al
.marqueIntel :
inc al
jmp .marqueInconnue
xor eax , eax
cmp [Intel.ebx], ebx
je .marqueIntel
cmp [AMD.ebx], ebx
je .marqueAMD
.marqueInconnue :
mov byte [Marque], al
call chargeCPUIDFamille
|
La fonction chargeCPUIDFamille prépare un tableau dans lequel on trouvera les noms des familles de processeur du constructeur considéré.
chargeCPUIDFamille :
push edi
push ecx
mov edi , listeFamilles
mov ecx , 16
cmp al , 1
je .setEAXIntel2
cmp al , 2
je .setEAXAMD2
.suite :
call chargeListeSansTest
pop ecx
pop edi
ret
.setEAXIntel2 :
mov eax , Intel.Familles
jmp .suite
.setEAXAMD2 :
mov eax , AMD.Familles
jmp .suite
|
En gros, on se contente d'initialiser les paramètres de ChargeListeSansTest, que je vous présente.
chargeListeSansTest :
push eax
push ebx
mov ebx , eax
.chaineSuivante :
stosd
push ecx
xchg edi , ebx
xor al , al
or ecx , 0xFFFFFFFF
repne scasb
mov eax , edi
xchg edi , ebx
pop ecx
loop .chaineSuivante
pop ebx
pop eax
ret
|
Cette fonction indexe les adresses de ECX chaînes de caractères consécutives en mémoire dans un tableau. Ca n'utilise que des registres, et encore, pas tous, et je n'ai pas plus élégant. L'idée est de stocker l'adresse du caractère suivant chaque caractère nul, plus le premier. Pour ce faire, on va échanger le contenu de EDI avec EBX souvent. EBX parce qu'il m'en fallait un, pas envie de le mettre sur la pile. Tout d'abord, on met dans EBX l'adresse des chaînes à indexer. On met dans EDI EAX, qui contient la même chose que EBX. On échange EDI et EBX, EDI contient maintenant les chaînes à indexer. SCASB va comparer l'octet à l'adresse EDI à la valeur dans AL (donc mettre le drapeau d'égalité au besoin), puis incrémenter EDI. En mettant AL à zéro et en répétant SCASB tant que le drapeau égalité n'est pas mis, EDI sera placé sur le premier caractère de la chaîne suivante, soit pile celui qu'on veut. Non, vraiment, des fois on dirait que les instructions ont été faites pour la programmation. On échange EBX et EDI, on n'oublie pas de mettre EAX à jour, et on est repartis !
Ceci étant fait, allons, courage camarade, essayons la fonction n°1. Chez Intel et AMD, elle renvoie des champs cabalistiques donnant l'identité du processeur dans EAX. Ces
gens bien documentés nous donnent la structure de ce que nous cherchons. Occupons-nous d'abord de la famille. Car la familia, c'est important, disait mon parrain.
Pour l'obtenir, il faut appeler la fonction n°1 de CPUID.
mov eax , 1
cmp eax , [nbFonctions]
jae .fin
cpuid
|
On fera ensuite des déplacements de bits couplés à des ET logiques afin d'isoler les nombres qui nous intéressent. Dans le code de démonstration, vous noterez que je n'ai pas tout fait, loin s'en faut. CPUID a suivi l'histoire des compatibles PC depuis leurs 32 bits et en est la digne héritière : c'est fouillis au possible, et c'est la rencontre entre la technique et la réclame (le marketing pour les franglais). On notera qu'AMD, plus jeune qu'Intel en tant que concepteur de micro-processeurs compatibles PC, est sensiblement plus propre dans ses dénominations.
III.4. Date
Je vous propose maintenant de voir un peu ce que les stockages de masse ont à nous dire. Ce qui n'a rien à voir avec le titre du chapitre. Et en fait, si. Camarade, accroche-toi au pinceau, j'ôte l'échelle. Nous allons tenter de communiquer avec le lecteur de disquettes. Exercice de longue haleine pour nous, mais commençons petitement. Petitement signifie : jouons un peu avec quelque chose de facile à voir qui dispose d'un point commun avec le sujet qui nous intéresse. Le point commun en question, c'est les ports d'E/S. On communique logiquement avec nos camarades disquettes par les ports d'entrée-sortie. Quelque chose dans le même genre est plus facile à appréhender du premier coup, il s'agit du CMOS. CMOS comme Complementary Metal Oxide Semiconductor, soit en français : transistors complémentaires à effet de champ à grille isolée. Ce qui est incompréhensible. On l'appelle comme ça pour des raisons qui m'échappent mais qui sont d'une pertinence confondante, pour peu qu'on soit anglo-saxon et timbré.
Je pourrai évoquer la physique des semi-conducteurs et des batteries, les casse-têtes d'ingénieurs pour fabriquer des mémoires rémanentes, l'histoire moderne des prospecteurs miniers à la recherche des filons purs, nouveaux chercheurs d'or du Klondike (oui, j'aime beaucoup Picsou) pour vous expliquer l'origine de ce terme et ce qu'il désigne. Mais je m'abstiendrai
Parce qu'entre nous, et n'en dites rien, profitez d'être dans la confidence des dieux, il s'agit d'une montre.
Oui, il y a trois bricoles en plus, mais franchement.
Entre nous.
C'est une montre.
Et on lit cette montre par les ports d'E/S.
Cette montre écoute le port 70h et parle sur le 71h. En gros, on demande une case mémoire sur le 70h et on récupère sa valeur sur le 71h.
Alors, ces cases mémoires, de quel choix mirifique disposons-nous ? J'ai trouvé
ceci, qui a le bon goût de correspondre assez bien à ma réalité. Si, vous et moi, ne vivons pas dans le même multivers et que votre réalité ne correspond pas à la mienne, je peux faire un effort et faire le correctif.
Numéro |
Fonction |
0 |
RTC seconds. Contient les secondes de l'heure courante. |
2 |
RTC minutes. Contient les minutes de l'heure courante. |
4 |
RTC hours. Contient les heures de l'heure courante. |
6 |
RTC day of week. Contient le jour de la semaine courant |
7 |
RTC date day. Contient le numéro du jour courant. |
8 |
RTC date month. Contient le mois courant. |
9 |
RTC date year. Contient l'année courante (siècles exclus). |
50 |
RTC date century. Contient le siècle courant. |
Je n'ai écrit ici que les valeurs qui nous préoccupent au premier chef. Je puis donc affirmer qu'au moins une machine sur cette terre utilise ces valeurs. Elles sont codées en "BCD" (Binaire Codé Décimal) ou "DBC" (Décimal Codé Binaire) ou qu'importe le sigle pourvu qu'on ait la valeur. Le BCD est l'art de perdre de la place en mémoire :
Wikipédia, qui est notre ami, explique assez bien de quoi il retourne. Nous, tout ce qu'on a besoin de savoir, c'est que les octets du tableau ci-dessus doivent être passés à cette fonction de conversion :
DCBVersBinaire :
push bx
push ax
and al , 0xF0
push ax
shr al , 1
pop bx
shr bl , 3
add al , bl
pop bx
and bl , 0xF
add al , bl
pop bx
ret
|
Allez, c'est parti, on tranforme notre bête de calcul, notre 64 bits Quad Core Tri Channels Moto Krote en montre à quartz. On commence par charger les chaînes de caractères intéressantes, comme, au pif, les noms des jours et des mois.
mov eax , chMois
mov edi , indexChMois
mov ecx , 12
call chargeListe
mov eax , chJours
mov edi , indexChJours
mov ecx , 7
call chargeListe
|
Et ensuite, on demande gentiment au CMOS, qui s'appelle aussi RTC (Real Time Clock, ou Regarde Ton Cadran - horloge temps réel), la date. On le fait dans une boucle parce que je n'aime pas le code dupliqué :
mov edi , Annee
xor ah , ah
mov ecx , 4
.boucleDate :
mov al , cl
add al , 5
out 0x70, al
in al , 0x71
call DCBVersBinaire
stosb
loop .boucleDate
|
Soyez chics, n'oubliez pas de prévoir de la place dans Annee, comme ceci :
Annee : db 0
Mois : db 0
JourDuMois : db 0
JourDeLaSemaine : db 0
Siecle : db 0
Heure : db 0
Minute : db 0
Seconde : db 0
|
Ca fait 8 octets. Croyez-moi, vous voulez nommer chaque octet. Bon, on récupère le siècle :
mov al , 50
out 0x70, al
in al , 0x71
call DCBVersBinaire
stosb
|
Et enfin l'heure par une boucle de 2 en 2 :
mov ecx , 3
.boucleHeure :
mov al , cl
dec al
shl al , 1
out 0x70, al
in al , 0x71
call DCBVersBinaire
stosb
loop .boucleHeure
|
Reste à faire l'affichage. Et passer un coup de balai, c'est à dire transformer en fonction le code redondant.
III.5. Du code
L'ensemble du code de ce chapitre se trouve ici :
Tuto15