On ne rétro-conçoit bien qu’en sachant à quoi ressemble le langage que la machine parle réellement. L’assembleur est ce plus bas niveau encore lisible : à l’interface entre les langages de haut niveau — C, Java, Python — et le langage machine, le binaire. L’apprendre, ce n’est pas viser à en écrire, mais à comprendre ce que le compilateur a produit.
Pourquoi descendre à l’assembleur
Quatre raisons le justifient, dont une seule nous concerne vraiment ici :
- l’optimisation fine de code critique — jeux, systèmes d’exploitation, cryptographie ;
- la compréhension du bas niveau — voir comment le haut niveau se traduit en instructions ;
- le débogage et la sécurité — désassembler avec
objdump, rétro-concevoir, analyser un malware ; - le contrôle total de la machine, au prix d’une portabilité nulle : un code assembleur est lié à une architecture donnée.
C’est la troisième qui motive notre présence : lire l’assembleur, c’est lire ce que le programme fait, non ce qu’il prétend faire.
32 bits, 64 bits
La différence signifiante entre ces architectures tient à la taille des registres. En 32 bits, l’adressage plafonne à Go ; en 64 bits, il atteint en théorie 16 exaoctets. Changent avec elle la taille des pointeurs, la capacité mémoire adressable, et — ce qui n’est pas anodin en pratique — la compatibilité binaire, qui n’est pas assurée de l’un vers l’autre.
Il vaut la peine de rappeler pourquoi l’accès mémoire coûte si cher : à un milliard d’instructions par seconde, la lumière ne parcourt que trente centimètres entre deux instructions. La latence n’est pas une négligence d’ingénieur, c’est une contrainte physique.
RISC contre CISC
Deux philosophies de jeu d’instructions s’opposent, au moins sur le papier.
Le RISC (Reduced Instruction Set Computer) — ARM, MIPS, RISC-V — vise un jeu réduit : efficacité énergétique, décodage aisé, rapidité. On le trouve dans les smartphones et l’embarqué.
Le CISC (Complex Instruction Set Computer) — x86, AMD64, IBM 360 — offre un jeu riche et complexe, qui raccourcit les programmes au prix de leur coût et de leur simplicité. C’est le monde des PC et des serveurs.
Le clivage s’est toutefois estompé : les processeurs CISC modernes simulent en interne une architecture RISC, décomposant leurs instructions complexes en micro-opérations, si bien que l’opposition tient aujourd’hui davantage de l’histoire que de la réalité micro-architecturale.
AMD64
AMD64 — aussi nommée x86-64 ou x64 — succède à x86 32 bits en doublant la taille des registres. Introduite par AMD en 2000, adoptée par Intel en 2004, elle étend le jeu d’instructions de son prédécesseur pour faciliter la transition des 4 aux 8 octets tout en préservant la rétro-compatibilité des applications existantes.
On y dispose de 64 bits pour les opérations arithmétiques, logiques et de transfert, répartis sur 17 registres de 8 octets. L’adressage mémoire s’y fait sur 48 bits au moins (soit To), au travers de nombreux modes d’adressage.
Le cycle d’exécution
Schématiquement, un programme s’exécute selon une boucle immuable :
- le registre
%ripcontient l’adresse de l’instruction à exécuter ; - on lit un ou plusieurs octets à cette adresse (fetch) ;
- on interprète ces bits comme une instruction (decode) ;
- on exécute l’instruction (execute) ;
- on met à jour
%ripvers l’instruction suivante — généralement celle qui suit immédiatement, sauf saut.
La réalité, bien sûr, est plus retorse. Le processeur traite plusieurs instructions en parallèle (pipeline) et tente d’anticiper l’issue des sauts conditionnels (prédiction de branchement) pour ne pas laisser le pipeline se vider — deux mécanismes dont l’exploitation sécuritaire (Spectre, Meltdown) a montré qu’ils n’étaient pas de pures optimisations innocentes.
NASM, un assembleur pour écrire à la main
Le Netwide Assembler est né d’un constat, formulé sur les forums comp.lang.asm.x86 : il n’existait pas de bon assembleur x86 libre et commode à l’écriture manuelle. Le paysage de l’époque en dit long sur ce manque — a86 était payant et cantonné à DOS ; gas, gratuit et portable, mais conçu comme backend de gcc, offrait peu de vérifications et une syntaxe rebutante à la main ; as86 était propre à Minix/Linux et sous-documenté ; MASM était cher et médiocre ; TASM, meilleur, restait payant, alourdi par sa compatibilité MASM et confiné à DOS. NASM fut conçu pour combler ce vide : libre, portable, et pensé pour l’écriture réelle de code x861.
Footnotes
-
Documentation NASM, Netwide Assembler. ↩