Hard du hard • Ça fonctionne comment, un processeur ? (Partie 1) |
————— 12 Mars 2018
Hard du hard • Ça fonctionne comment, un processeur ? (Partie 1) |
————— 12 Mars 2018
Le sigle CPU signifie "Central Processing Unit", ou "Unité de traitement centrale" pour les anglophobes. Il en a existé de nombreux types différents, dont les plus connus du grand public sont aujourd'hui les ARM, utilisés dans l'informatique mobile et les téléphones, et les x86 pour PC de bureau aussi bien que serveurs. Son rôle général est, effectivement, d'exécuter un programme, par exemple le démarrage d'un OS, et cela le plus rapidement possible.
Pour cela, le processeur manipules des données, qu'il charge depuis la RAM (ou initialise directement à une valeur donnée) dans un registre. Un registre est donc un espace mémoire directement accessible par le processeur. Par exemple, les processeurs x86 actuels possèdent 16 registres depuis le passage au 64 bits (bien que les deux événements n'avaient pas de raison d'être directement reliés). Il existe en outre des registres spéciaux, par exemple le PC pour Program Counter ou Compteur Ordinal correspondant à l'adresse de l'instruction prochainement exécutée ; ainsi qu'un registre de condition gardant le résultat d'une opération précédente, utilisé lors de sauts conditionnels.
Le langage - si on peut l'appeler ainsi - utilisé pour transmettre des instructions au processeur est l'assembleur. On classe ainsi les processeurs par leur jeu d'instruction, ou ISA (Instruction Set Architecture). Les jeux d'instructions les plus connus sont aujourd'hui l'AMD64, aussi connu sous le nom de x86_64, issue de l'évolution des vieux Intel 8086 (d'où le nom x86) ; et l'ARM se déclinant sous différentes versions, ce dernier ayant par le passé cassé la rétro-compatibilité (par exemple, un processeur ARMv8-A ne peut pas exécuter un code en ARMv6). Mais des alternatives existent, comme les Intel Itanium (l'implémentation des bleues du 64 bits, cassant la compatibilité x86, qui a échoué en partie car Microsoft décida de ne supporter que l'AMD64 rouges) ou encore le RISC-V, une ISA libre de droit.
Une instruction assembleur est un bloc atomique de programme compris par le CPU. Elle correspond directement à un chiffre binaire indiquant l'état de certains pins du processeur. Bien qu'on l'écrive avec des mnémoniques, un langage plus compréhensible, par exemple "ADD R1, R1, #1" ; il ne s'agit que d'une traduction mot à mot de l'instruction, par exemple 0101000101011111. On la représente d'ailleurs le plus souvent en hexadécimal (4 chiffres binaires = un chiffre-lettre de 0 à F), ici 515F.
Convertion Hexadécimale -> Binaire -> Décimal
Dans une instruction, les premiers bits (leur nombre dépendant de l'ISA) s'appellent opcode, et désignent le type d'opération à effectuer : chargement dans un registre, rangement en mémoire, opération arithmétique, logique, calcul, sauts (aussi appelés branchements). Les autres bits indiquent les opérandes, qui peuvent être soit des registres soit des constantes appelées immédiats (décalage par rapport à une addresse, valeur constante pour un calcul, etc). Un immédiat est dit signé s'il peut être positif ou négatif, sinon il est non signé (unsigned).
Lorsque l'on parle de processeur 64 bits, c'est ici que s'effectuent les changements : les instructions sont capables de gérer des adresses mémoires de 64 bits (wow, surprise !), ainsi que la gestion des calculs arithmétiques et logiques selon des représentations sur 64 bits.
Originellement, on différenciait le RISC (Reduced Instruction Set Computer) du CISC (Complex Instruction Set Computer), le premier prévilégiant un jeu d'instructions à peu d'opérandes, mais plus rapides, et le second à de nombreuses opérandes plus spécialisées. Si l'x86 est de type CISC à la base (bien qu'en interne, le fonctionnement ressemble plus à un RISC émulant du CISC par soucis de rétro-compatibilité), l'ARM est de type RISC, autant dire qu'il n'existe pas de meilleur choix dans l'absolu.
Nom | Opcode | Nom, mais en plus clair | Comment qu'ça s'utilise ? | Et ça fait quoi ? |
---|---|---|---|---|
ADD | 000 | Addition | add rA, rB, rC | rA = rB + rC |
ADDI (bouh) | 001 | Addition d'un immédiat (entier) | add rA, rB, Imm | rA = rB + Imm |
NAND | 010 | Opération NAND : NON-ET | add rA, rB, rC | Le x-ème bit de rA vaut 0 si les x-ème bits de rB et rC valent tout deux 1. Sinon, le x-ème bit de rA vaut 1. |
LUI | 011 | Chargement d'un entier (Load Upper Immediate) | lui rA, Imm | Les 10 bits premièers bits de rA prennent la valeur de Imm |
SW | 100 | Stockage en mémoire (Store Word) | sw rA, rB, Imm | Ecrit la valeur de rA en RAM à l'adresse rB + Imm |
LW | 101 | Charger depuis la mémoire (Load Word) | lw rA, rB, Imm | Ecrit dans rA la valeur en RAM située à l'adresse rB + Imm |
BEQ | 110 | Branchement si égal (Branch if EQual) | beq rA, rB, Imm | Si rA = rB, allors saute Imm instructions. Sinon continue l'exécution. |
JALR | 111 | Saut et chargement de registre (Jump And Load Register) | jalr rA, rB | Saute à l'adresse contenue dans rA et stocke l'adresse actuelle dans rB (utile pour les appels de fonctions). |
Le jeu d'instruction RiSC-16 (machine 16 bits à 8 registres)...
... et le codage binaire correspondant !
Aujourd'hui, rares sont les programmeurs suffisamment aventureux pour se lancer dans la rédaction de bibliothèques en assembleur ; car des langages de programmation bien plus facile à comprendre ont été développés depuis. Cependant, pour un optimisation maximale des programmes, certaines boucles peuvent utiliser des intrinsics, c'est à dire de l'assembleur inséré dans du code. Mais il ne faut pas oublier que - quel que soit le langage utilisé - le processeur ne comprend, in fine, que son assembleur personnel.
On va utiliser l'assembleur RiSC-16 vu précédemment, qui possède 8 registres. Le registre r0 n'est accessible qu'en lecture seule et contient toujours la valeur 0 par convention, les 7 autre registres sont libres. Pour les entier signés, on prend une convention de codage en complément à 2. Le programme suivant "compte" de 0 à 10 via r1 :
addi r2, r0, 10 # Le registre r2 contient 10 addi r1, r0, 0 # Le registre r1 contient 0
loop: beq r1, r2, endloop # Si r1 = r2, aller à endloop:
addi r1, r1, 1 # Incrémenter r1
beq r0, r0, loop # Aller à loop:
endloop: halt # Stop
On notera l'utilisation de labels, par exemple "loop:" afin de simplifier la compréhension des instructions de saut ("beq r0, r0, loop" est plus simple à comprendre que "beq r0, r0, -2" !).
Grâce à la table ci-dessus, on peut transcrire à la main ce programme en binaire :
001 010 000 0001010 001 001 000 0000000 110 001 010 0000010 001 001 001 0000001 110 000 000 1111101
#en codage au complément à 2, -3 s'écrit 1111101 (= 125 en écriture non signée) halt
Et comme le binaire est un peu trop long, on utilise plutôt l'hexadécimal, le programme se résume alors à :
280A 2400 C502 2481 C07D
Maintenant que nous avons vu la base du langage compris par un CPU, passons aux mécanismes internes permettant de l'exécuter, et ce, le plus rapidement possible.
|
Un poil avant ?Adoption de FreeSync : enfin sur XBox One (MAJ) | Un peu plus tard ...Enfin un test pour le Quadstellar |
1 • Préambule |
2 • |
3 • Dans les transistors |
4 • Conclusion (avant la suite) |