Assembleur contre C et C++ Le duel... |
Maj : 26/11/21
Les deux protagonistes
Et bien non cela n'a rien d'un duel, ce n'est un comparatif élémentaire des deux outils fondamentaux, bases de la programmation.
Il en existe bien d'autres, mais la connaissance de ces piliers ouvre l'esprit pour aborder facilement tous les autres langages par la suite. Les grands principes de la programmation sont valables pour tous les langages.
Le compilateur C
Avant l'assembleur, les programmes ne s'écrivaient qu'avec la liste des commandes du processeur sous les yeux, en rentrant directement les codes des opérations et en calculant à la main les sauts et les adresses nécessaires, Cela était très lourd, l'assembleur a été une grande facilité, car il a remplacé les obscurs codes machines par des mots faciles à retenir et il a permis de calculer automatiquement les différentes positions mémoire.
Le C est un langage très bien écrit, d'une puissance considérable. Il est secondé par le C++ (plus complexe) qui ouvre le monde illimité de la programmation objet.
Un programme développé en C ou C++sera (presque) transférable sur n'importe quelle plate-forme matérielle du plus petit microcontrôleur au plus gros ordinateur, en tenant bien évidemment compte des limites et des capacités du hardware cible. La grande richesse des bibliothèques externes permet la programmation rapide d'applications très complexes.
Cette portabilité est un avantage exceptionnel. Les langages qui semblent d'une approche plus facile pour le débutant, comme les vieux basic ou Pascal ne présentent pas cet atout.
Attention toutefois, le C++ et le Java évoluent très vite, ils deviennent de plus en plus complexes et pour qui n’est pas spécialiste, certaines subtilités exotiques deviennent incompréhensibles.
Le programmateur du dimanche ne s’aventurera pas dans de telles arcanes.
Qui de l'œuf ou de la poule...
Bien évidemment les premiers langages C ont été développés en assembleur, et ensuite le serpent se mord la queue, le C a été développé en C...
L'assembleur
C'est le langage roi, au plus près de la machine. Rien ne peut être plus rapide, plus compact et plus pur qu'une routine assembleur bien écrite.
Programmer en assembleur implique d'avoir en permanence sous le coude le gros databook (version papier !) correspondant précisément au processeur choisi. Il faudra s'y référer en permanence pour découvrir tous les détails du composant, ses adresses internes, le rôle de chacun des nombreux flags des registres qui commandent les actions. Ce passionnant ouvrage de référence sera l'outil fondamental, l'environnement assembleur par lui-même ne présentant pas de difficultés. Le debuggeur associé sera un outil précieux qui permettra de tester ses routines. Il sera complété par les nombreuses notes d'exemples du constructeur, pleines de ressources, car très bien écrites.
Pour chaque famille de processeurs ou microcontrôleur, il existe plusieurs assembleurs. Très souvent le constructeur offre gratuitement un outil de base, mais avec la pratique il est souhaitable d'investir dans un outil commercial plus complet qui permet d'investiguer finement dans un code buggé. Il est valable pour la famille complète, le type précis du composant sera configuré dans le menu.
En assembleur, on se préoccupe du nombre de cycles que prend une routine pour optimiser l'écriture. L'oscilloscope et l'analyseur logique resteront branchés sur le prototype en développement pour s'assurer que les timings prévus sont bien respectés. Avec le debugger (et beaucoup d'expérience...) on disposera ainsi d'une grande puissance pour réaliser des codes très performants.
En assembleur, il faut tout écrire !
En C, pour envoyer un message sur un périphérique quelconque dénommé xxx, un :
xxx.print "bonjour" ;
suffit, tout le gros travail est fait par le langage qui comprend d'énormes bibliothèques bien optimisées pour tout faire, et qui seront appelées à la demande pour s'ajouter automatiquement par le linker, à votre code.
En assembleur, il faudra des jours la première fois pour mettre en œuvre une telle procédure car tout doit être défini. Cela peut paraître monstrueux, mais c'est un passionnant jeu de l'esprit.
En assembleur tout est maîtrisé aux bas niveaux, toutes les variables, registres, adresses mémoires sont définies individuellement, il y a un contrôle absolu, du nombre de cycles d'exécutions, du timing...
En C la manière dont l'outil utilisera les ressources systèmes sont totalement inconnues de l'utilisateur. C'est une boîte noire, des "choses" entrent d'un côté, d'autres sortent de l'autre...
Pour des routines très rapides utilisant un minimum de ressources, par exemple un moniteur débuggeur, un loader, un noyau temps réel, l'assembleur est irremplaçable.
Il serait possible de réaliser cela en C, mais en consommant plus de ressources et en allant moins vite car les routines assembleur iront à l'essentiel. Par son côté généraliste, le C ne peut pas atteindre de telles performance, bien qu'extrêmement performant. La programmation en assembleur est une véritable passion, c'est le jeu le plus addictif.
L'assembleur a toutefois deux inconvénients !
Spécificité du matériel
Il est spécifique à un type particulier de contrôleur. Cela n'est pas absolument pas gênant pour qui travaille sur un seul matériel simultanément, mais pose de gros problèmes quand on développe sur plusieurs familles pour des clients ou des projets différents, les instructions se mélangent dans la tête ce qui fait perdre du temps.
Chaque famille de processeurs possède un esprit et un langage différent dont il faut s'imprégner.
Quand on est polyglotte, il est facile de commuter sans erreur d'une langue à l'autre au gré de ses interlocuteurs, mais quand on est multi-assembleur c'est moins facile.
Il faut longtemps pour maîtriser une famille et encore plus beaucoup de différentes.
Un logiciel parfaitement au point sur une cible devra être totalement ré-écrit si l'on change de contrôleur, rien n'est récupérable.
Applications complexes
Le deuxième problème de l'assembleur est plus ennuyeux, il concerne le développement d'applications très complexes.
J'ai rencontré mes premiers problèmes après avoir développé avec passion pendant dix ans en continu, des automates biomédicaux devenus de plus en plus complexes. Ils étaient basés sur un noyau Perl multitâche que j'avais entièrement développé en quelques dizaines de milliers de lignes. Lorsqu'il a fallu se connecter à de grosses bases de données sécurisées par de l'Ethernet puis de l'Internet, la tâche de tout écrire à la main devenait insurmontable. Faire des manipulations de grosses matrices en assembleur n'est pas très raisonnable, sauf si l'on cherche la rapidité maximale. Développer une telle application en C et C++ prendra cent fois moins de temps et sera transposable.
Il fallait abandonner cette voie bouchée pour passer au C afin de bénéficier des puissantes bibliothèques existantes et tout remettre à plat.
Toute l'algorithmique était bien rodée, il fallait "simplement" tout réécrire. J'ai fait plus de travail fini et propre en quelques mois en C qu'en dix ans d'assembleur.
évidemment le nouveau code en C (pour les mêmes caractéristiques) était plus gros et un peu moins rapide qu'en assembleur, mais cela n'a pas d'importance car bien que le contrôleur exécute énormément de tâches, il passe plus de 95% du temps en mode sommeil "low power". La vitesse n'intervient que lorsque l'on fait du temps réel, (exemple : guidage de missiles en poursuite) ou des calculs mathématiques très lourds (exemple : décimales de Pi).
Aujourd'hui, je pourrais rapidement reprendre mon code en C pour ajouter ou updater des modules, ou changer de plateforme matérielle, mais je suis totalement incapable d’en faire le millième sur mes premiers travaux en assembleur, je n’ai plus les outils avec lesquels cela était développé, j’ai oublié les instructions, le noyau matériel (CPU) n’existe plus…
Il est très important de bien hiérarchiser et commenter son code pour pouvoir le comprendre quand il faudra le reprendre plus tard.
Organisation des bibliothèques personnelles
Une fois la routine ou la macro-commande écrite, elle sera testée finement et classée pour des réutilisations ultérieures. Il faut faire des blocs les plus petits possibles pour un réemploi facile.
Essayez de ne pas dépasser une petite page pour une belle fonction bien commentée. L'assemblage judicieux de nombreux petits blocs élémentaires constituera un gros programme.
Il faut hiérarchiser, c'est à dire réaliser des fonctions à différents niveaux. Le programme final sera petit car il ne fera appel qu'à quelques grosses fonctions, c'est une construction arborescente.
Dans une routine, il ne faut pas faire coexister des appels à des niveaux différents, si cela se produit, développer le code de plus bas niveau dans une nouvelle fonction plus petite et l’appeler.
Tous les noms des fonctions doivent être très explicites, utilisez des noms longs. En lisant le nom de la fonction, vous devez savoir ce qu'elle fait !
On oublie vite, il sera difficile de comprendre ce que l'on a écrit quelques années plus tôt, ou par un autre ; s'il faut reprendre un programme qui n'a pas été très bien commenté et structuré, cela sera très lourd.
Pour chaque fonction il faut commenter largement l'en-tête, en indiquant précisement à quoi elle sert, quelles sont les variables d'entrée, de sortie, les bugs éventuels connus à traiter ultérieurement. Soyez généreux dans les commentaires en pensant à la maintenance future.
Au cours des ans j'ai accumulé des masses de routines dans tous les langages. Elles sont classées en diverses catégories :
Routines "blindées"
Il s'agit de routines parfaites, totalement optimisées, la pratique ayant montré qu'elles n'ont pas de bug, très bien commentées. Elles ont subi de sévères batteries de tests. Il est très peu probable de pouvoir faire mieux. Elles peuvent aussi provenir des bibliothèques du constructeur qui sont souvent excellentes. Les routines mathématiques en sont de bons exemples.
Malheureusement, toutes mes routines n'ont pas une telle qualité !
Routines agréées
Fonctionnelles, efficaces, pas de bugs détectés pour le moment, mais tests trop sommaires. Il faudra passer du temps pour améliorer. Les routines suivantes sont repérées par un commentaire initial indiquant leur état précaire. En passant plus de temps, il doit être possible de mieux les écrire.
Routines jetables
Elles sont écrites à la va-vite, pour un besoin précis, mais sont très douteuses, souvent buggées, à ne jamais réutiliser (quick and dirty programming). Les conditions aux limites et pour les cas particuliers ne sont pas testées. Elles permettent de faire une maquette sale rapidement, mais les nombreux problèmes engendrés finissent par coûter très cher et décrédibilisent le programmeur.
"Dummies" routines (bidons)
Il s'agit de calcul seulement simulé provisoirement pour avancer dans un développement. C'est un artifice indispensable pour préparer une maquette dans le but de présenter un projet à un client. La seule chose qui l'intéresse est l'aspect visuel et l'ergonomie. Il n'est pas utile que les calculs internes fonctionnent vraiment pour le moment, une simulation grossière suffit. Si cela convient le gros morceau sera traité séparément ultérieurement après validation du concept. Ces particularités seront clairement explicitées et repérées dans le planning de développement.
Cela consiste à bâtir le gros œuvre, la très longue finition intérieure n’interviendra que plus tard. Pour le moment cela est plutôt un décor de cinéma fait pour créer l’illusion du réel.
Petit exemple des deux approches
Beaucoup de débutants ont appris le C, ils aimeraient bien écrire
en assembleur pour améliorer les performances mais ont peur du monstre
et cherchent à transformer automatiquement et sans effort le code C en
assembleur.
Cela est-il vraiment possible ?
Pour comprendre le problème, il faut étudier comment travaille
le compilateur C. Prenons par exemple une boucle d'attente élémentaire et regardons
les deux approches.
En C, nous allons écrire
For (x=0 ; x<1000 ; x++ )
{ pas de code car nous faisons une boucle vide}
Nous n'avons pas oublié avant de signaler que nous voulions utiliser
une variable x en la déclarant : int x ;
Cette ligne est une boucle d'attente vide, qui va simplement perdre du temps
en décomptant 1000 fois.
Remarque : Attention il n'y aura que 1000 exécutions, pas 1001, car la condition est "strictement
inférieur ".
Il faut toujours être certain que le résultat est bien celui espéré en fonction des conditions écrites, pour vérifier :
For (x=0 ; x<2 ; x++ ) printf ( "\nrésultat
= %d ", x);
Ce code imprime deux lignes car dans la boucle For, la variable est incrémentée avant le test.
Avec une boucle Do...While, il y aura 3 lignes avec x++<2, la variable est incrémentée après le test!
Avec une boucle Do...While, il y aura 2 lignes avec ++x<2, la variable est incrémentée avant le test!
main() { int x; for (x=0 ; x<2 ; x++ ) |
main() do |
main() do |
résultat = 0 |
résultat = 0
|
résultat = 0
|
Revenons à nos moutons, enfin ceux qui ont encore échappé à la fièvre aphteuse.
En assembleur l'écriture est un peu différente.
En C la déclaration des variables est automatique, il suffit de déclarer
<int x ;> et le compilateur se charge d'affecter les cases mémoires
dans l'espace disponible sans que l'utilisateur ne s'en préoccupe.
En assembleur il faudra tout définir et choisir à quelle adresse
sera affectée la valeur de ce compteur. Si c'est une boucle longue, l'assembleur
étant très rapide, il faudra décompter sur 2 octets (espace
de 64 k).
Il faut donc choisir le type de mémoire dans l'espace interne (sans planter
la pile) ou externe, mais les modes d'adressage sont différents, ou utiliser
un registre interne privilégié, mais ce n'est pas une bonne idée
car il faudra le sauver puis le restaurer (c'est lourd) en entrée puis
sortie et s'il y a interruption. Nous allons donc utiliser par exemple un registre "poubelle ", ce qui veut dire qu'il est utilisé par diverses
routines qui l'initialisent chaque fois.
;----> Voici une boucle générale, très utilisée
dans les programmes (exemple en 8051).
r2loop: djnz R2,$ ;Boucle générale d'attente,
décompte de la valeur chargée
ret ;
;----> Délai de base 10 ms utilisant l'accumulateur en le sauvant
puis en le restaurant.
del10mil: push ACC ;
mov A,#0Ah ;Délai réglé à 10 millisecondes
del1m1: lcall del01mil ;
djnz ACC,del1m1 ;
pop ACC ;
ret ;
;----> Délai de base 1 ms usage général ne touche que
R2 (pour un quartz 11.0592 MHz)
del01mil: mov R2,#199 ;922 périodes : 1 millisec
lcall r2loop ;Lcall+ret 4 cycles
ljmp r2loop ;4+4+4+512+398
Cet exemple est extrêmement optimisé pour être au plus près
du processeur, il est évident qu'aucun compilateur C ne pourrait générer
ces deux attentes avec si peu de ressources.
Il devrait charger des bibliothèques générales très
lourdes pour arriver à ce résultat.
Le compilateur générera un code qui fonctionne parfaitement,
mais en analysant la sortie désassemblée, le débutant sera
dérouté par la complexité du résultat. Tout ce long baratin pour expliquer qu'il ne faut pas rêver sur le traducteur
magique de C en assembleur.
Le C est plus accessible pour le débutant car il l'a appris dans les
bases de l'informatique.
Les débutants ont souvent peur d'attaquer l'assembleur. C'est comme
le vélo, il y a un petit apprentissage au départ et ensuite
cela va tout seul. Je vous joins un projet complet d'évaluation d'une
carte avec un Dallas 89c420, pour montrer comment est structuré un
programme assembleur en 51 et le règles de base d'écriture.
Il utilise de nombreuses astuces que vous découvrirez peu à
peu suivant votre niveau. Les routines I2C sont un classique d'origine Philips. |
Projet d'évaluation |
Le 89c420 a été remplacé par le 80c430 qui consomme moins et le 89c440 (plus de mémoire). Le 89c450 est supprimé.
Conclusion
La connaissance de ces deux bases fondamentales ouvre vers tous les autres langages et les infinies possibilités de la programmation.
Faites clignoter votre première led en utilisant le C et l'assembleur, ensuite développez suivant vos envies pour découvrir ces deux mondes passionnants et poursuivre la quête infinie.
Il y a maintenant une multitude de langages qui ont le vent en poupe, outre C++, Java, Python et de multiples autres, il en sort tous les jours.
Il est maintenant désuet et inefficace de programmer en assembleur à l’ancienne en faisant tout à la main. Adopter un langage moderne permet de bénéficier d’une masse de drivers et d’outils bien écrits qui accélèrent considérablement le développement.
Il est très agréable de traverser le désert de Gobi à pied avec une gourde, mais c’est plus confortable avec un 4*4 polluant.
Voir la page principale Arduino