Qu’est-ce qu’un langage de programmation ?

Vous entendez souvent parler de langages de programmation : HTML, C, Pascal, assembleur, etc. Mais en quel sens peut-on parler de langage pour un objet dénué de pensée ?

Le fondement de l’informatique

Imaginez que vous voulez construire une maison. Vous ne commencez pas par vous demander de quelle couleur sera le papier peint ou quels jouets on mettra dans la chambre des enfants, car pour avoir du papier peint il faut déjà avoir des murs, et pour avoir des murs il faut avoir des fondations, et pour avoir des fondations il faut déjà avoir un terrain.

Il en va de même en informatique. Un parallèle qu’il ne faut jamais perdre de vue, car il démystifie beaucoup de choses : l’informatique ne rompt pas avec une conception mécanique de la nature, mais se place dans sa continuité. Le monde des ordinateurs, des fichiers et des logiciels n’est pas un nouveau monde, un monde virtuel, mais une partie de l’ancien monde qui a simplement beaucoup enflé.

Mais quel rapport, me direz-vous, entre la grenouille galvanisée et le pingouin de Linux ? La première est matérielle, le second virtuel ; la première appartient au monde physique, le second au monde informatique. Mais j’entends déjà le râle des disciples d’Auguste Comte : « l’inférieur porte le supérieur ». Eh oui ! car la galvanisation de la grenouille est précisément ce qui a rendu possible la création de Tux, le pingouin de Linux. L’informatique repose sur l’électronique, qui elle-même repose sur l’électricité.

Le langage machine

Inintelligibilité pour l’homme

Votre ordinateur n’est en effet pas si malin que ça, puisqu’il ne parle que par impulsions électriques et ne comprend que ça. C’est bien là le problème : nous autres humains ne comprenons pas si couramment les impulsions électriques... La communication avec les ordinateurs nous échapperait ainsi :

Le programme

Un jour des hommes se dirent qu’en écrivant des programmes ils pouvaient gagner du temps. Ainsi, une seule commande permettrait d’en convoquer plusieurs autres, ce qui représente un gain énorme en temps. Qu’est-ce qu’un programme ? C’est une série d’instructions regroupées en une seule, de telle sorte que celle-ci appelle celles-là (comme l’est en théorie un programme politique : « si vous m’élisez, je réaliserai ceci, puis ceci, puis cela, etc. ; si vous voulez toutes ces choses, il y a donc plus simple que de les réaliser vous-mêmes, c’est de m’en déléguer la tâche. »).

Le programme est donc un moment central de la constitution de l’informatique : sans programmes (le pluriel est voulu, car il en faut une quantité énorme) elle ne serait rien. Seulement, avec les programmes constitués de 1 et de 0, si l’on y gagne en temps, on n’y gagne que peu en simplicité. Et c’est pourquoi une grande quantité de langages seront constitués, reposant les uns sur les autres, dans un procès d’approche de l’utilisateur comme terme ultime. Mais procédons par ordre.

L’assembleur comme premier langage de programmation évolué

Nous avons d’abord étudié le langage machine ou code machine : langage natal du processeur, autrement dit du cerveau même de votre ordinateur, langage austère constitué d’une série d’octets. C’est le seul langage compréhensible directement par la machine. Il est composé de concepts élémentaires : lire, écrire. Tous les langages ultérieurs, toutes les applications et les tous les programmes sont fondés dessus, et n’en sont que d’habiles combinaisons ; ce qui n’est déjà pas si mal.

Comment créer un autre langage : tout simplement par détournement d’un autre. Vous n’aimez pas les 1 et les 0 ? Qu’à Dieu ne plaise ! Il vous suffit :

Racine, grand programmeur de vers de la deuxième moitié du XVIIe siècle, exprimait ainsi ce processus :
J’embrasse mon rival, mais c’est pour l’étouffer.
(Racine, Britannicus, IV, 3, v. 1314)

Le premier langage informatique s’appelait Ada et était un véritable être humain, dont la fonction était de traduire les instructions que lui donnait le scientifique Charles Babbage en manipulations de la machine analytique qu’il était en train de construire ; Ada Lovelace, fille du poète Byron, fut ainsi à la fois le premier programmeur et le premier langage de programmation de l’histoire : un langage beaucoup plus récent porte son prénom.

Le paradoxe logique du langage qui s’engendre lui-même

C’est la démarche qui fut adoptée par Dennis Ritchie et Brian Kernighan, concepteurs du langage C : leur objectif était d’avoir un langage de programmation à la fois performant et proche de la machine.

Ne vous laissez pas tromper par l’apparente contradiction logique d’un compilateur écrit dans son propre langage (le paradoxe fut répété plus récemment avec un autre langage : « Java is written in Java »). N’y voyons pas trop vite un analogon de l’exploit du baron de Munchausen, qui se sort lui-même de l’eau avec son cheval en se tirant par les cheveux... La contradiction disparaît en effet dès que l’on prend en considération le fait que le premier compilateur fut écrit dans un autre langage ; ensuite, les produits compilés de ce premier compilateur font ce qu’ils veulent, même compiler à leur tour d’autres compilateurs.

Les niveaux de programmation

Ce nouveau langage, qui assemble des morceaux de langage machine pour faire un (petit) pas vers le langage humain, c’est précisément le langage assembleur. Une fois que vous avez compris ce passage du langage machine au langage assembleur, vous avez compris la dialectique des langages informatiques : chacun tue son père (le langage dont il procède) et rêve de coucher avec sa mère (l’utilisateur), et il en est ainsi à chaque génération. C’est ce processus qu’explicite la notion de niveaux. On parle de langages de bas niveau lorsqu’ils se situent près de l’interface machine, donc de ce qui est quasi inintelligible pour un être humain, et de langages de haut niveau lorsqu’ils tendent de manière infinie vers l’utilisateur. De manière infinie ? Oui, ou à peu de chose près : avec les interfaces graphiques, puis la reconnaissance vocale, etc., la proximité avec l’utilisateur est à la fête !

Niveaux élevés et atavisme

Une fête ? Peut-être pas tant que ça... Imaginons un scénario catastrophe : un bug dans la programmation du langage assembleur. Ceci supposerait que tous les langages fondés dessus, donc tous ceux qui à leur tour sont fondés sur ceux-ci, mais aussi tous les logiciels fondés sur ces derniers, etc. - tous seraient contaminés par ce même bug. C’est le principe d’atavisme en hérédité, et comme en informatique il n’y a pas d’exogamie, n’espérez même pas que le gène soit remplacé par un autre plus sain...

Ce scénario n’est pas une simple hypothèse théorique : dans sa conférence « Reflections on Trusting Trust », Ken Thompson, créateur d’UNIX avec Dennis Ritchie, se sert d’un exemple vécu pour montrer que l’on ne peut, en matière de sécurité informatique, faire confiance qu’à des codes que l’on a soi-même écrits.

Ken Thompson a écrit un compilateur qui, s’il reconnaissait que le code compilé était le programme login (qui sert, sur les machines UNIX, à authentifier l’utilisateur, à lui demander son mot de passe et à en vérifier la conformité ; c’est donc un programme crucial pour la sécurité informatique), y ouvrait une porte dérobée (backdoor) lui permettant de se connecter en usurpant l’identité de n’importe quel utilisateur.

Thompson est même allé plus loin : si le compilateur qu’il a écrit repère que l’utilisateur est en train de compiler un nouveau compilateur (vraisemblablement sans porte dérobée), le bug volontaire se reproduit dans ce nouveau compilateur. Ainsi, si l’on compile le programme login avec un compilateur que l’on a soi-même créé, même si le bug est absent du code source (et pour cause ! l’utilisateur ne l’y a pas mis), il est présent dans le code compilé en langage machine.

Chaque programme d’authentification ou de compilation généré, directement ou indirectement, à partir de ce compilateur truqué par Thompson en partagera donc les failles de sécurité, et ce, de façon totalement invisible pour l’utilisateur (à moins qu’il n’envisage de lire lui-même l’intégralité du code compilé en langage binaire, ce qui relève de l’impossible).

La pyramide des langages

Une même commande peut être créée dans de nombreux programmes : si un langage enfant peut le faire, c’est parce que et seulement parce que le langage parent le pouvait avant lui ; mais ce que le langage enfant gagne sur le parent, c’est en général l’accessibilité.

Une commande shell

L’exemple classique est l’affichage du message "Hello world". Dans un simple shell, on peut écrire par exemple :

echo "Hello world !"

soit tout simplement une commande pour spécifier à quel type de shell la commande s’adresse (la première ligne), et la commande en elle-même, composée du message lui-même (Hello world !) ainsi que de l’instruction permettant de l’afficher (echo).

La commande, telle que nous l’avons rapportée, se présente de cette façon avant son exécution. Mais jetez donc un regard à la complexité des appels systèmes qu’elle génère lors de son exécution !

<strong>meles@philonous ~ $</strong> strace echo
'Hello world !' 1> strace_helloworld 2>&amp;1
execve("/bin/echo", ["echo", "Hello world !"], [/* 66 vars */]) = 0                        
uname({sys="Linux", node="philonous", ...}) = 0                                            
brk(0)                                  = 0x804c000                                        
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40015000  
open("/etc/ld.so.preload", O_RDONLY)    = -1 ENOENT (No such file or directory)            
open("/etc/ld.so.cache", O_RDONLY)      = 3                                                
fstat64(3, {st_mode=S_IFREG|0644, st_size=80662, ...}) = 0                                  
old_mmap(NULL, 80662, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40016000                            
close(3)                                = 0                                                
open("/lib/tls/libc.so.6", O_RDONLY)    = 3                                                
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220O\1"..., 512) = 512            
fstat64(3, {st_mode=S_IFREG|0755, st_size=1165108, ...}) = 0                                
old_mmap(NULL, 1175436, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x4002a000  
old_mmap(0x40143000, 16384, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x118000) = 0x40143000                                                                        
old_mmap(0x40147000, 8076, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40147000                                                                              
close(3)                                = 0                                                
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40149000

... et ainsi de suite sur environ 80 lignes.. Et encore ! Tout ceci est exprimé en langage à peu près intelligible ; en langage machine, ce serait bien plus long et bien plus complexe...

À travers cet exemple, vous pouvez bien voir comme cela se passe : la commande (ou le programme) envoie des instructions au système d’exploitation, qui lui-même les traduit en langage binaire pour le processeur, qui renvoie une information en langage binaire que le système d’exploitation renvoie sous la forme suivante à l’écran :

Hello world !

Un programme en C

Imaginons le même programme, mais dans un autre langage, le langage C. Nous écrirons :

             
#include &lt;stdio.h&gt;<br />
#define EXIT_SUCCESS 0<br />  
<br />                        
int main(void)<br />          
{<br />                      
 printf("Hello world !\n");<br />
 return EXIT_SUCCESS; <br />                                              
}                                                                          

Comme vous pouvez le constater, le fichier a grossi par rapport à la commande simple echo "Hello world !" . Mais n’allez pas croire que le C rend complexe ce qui est si simple et si intuitif en script shell ; c’est bien au contraire le script shell qui simplifie les commandes C. Le shell est en effet écrit, en général, en C ; ainsi, lorsque vous tapez une commande simple dans le shell, sachez qu’il ne s’agit que d’un raccourci pour un ensemble de commandes complexes en C, qui est lui-même un raccourci pour un ensemble de commandes encore plus complexes en langage binaire.

Et ce n’est pas tout ! Car ces lignes complexes ne sont encore que le fichier tel qu’on l’a écrit ; or, en C (mais ce n’est pas propre au C), pour qu’un programme soit exécutable, il faut le compiler. Qu’est-ce qu’une compilation ? C’est une traduction d’un fichier d’un langage en un autre, de plus bas niveau.

Ce que le programmeur a donc écrit depuis un langage relativement commode, l’ordinateur le traduit en un autre langage, interprétable directement par le processeur.

Compilation et interprétation

Cette traduction d’un langage évolué en langage binaire peut avoir lieu de deux façons différentes :

Les langages de programmation se partagent donc en deux grandes catégories selon le moment de leur traduction en langage binaire. Parmi les langages compilés figurent le C, le C++, etc. ; parmi les langages interprétés, Perl, les scripts shell, etc. Le cas de Caml est un peu ambigu, car ce langage peut être aussi bien compilé qu’interprété. L’interprétation est une forme de compilation, mais une compilation à la volée, ce qui suffit à la distinguer de la compilation au sens étroit du terme.

Compilation

Les avantages de la compilation sont :

Ses inconvénients sont :

Interprétation

Les avantages de l’interprétation sont :

Ses inconvénients sont :

L’écart qui sépare la compilation de l’interprétation est exactement celui qui sépare le langage de programmation du langage binaire : si le programme compilé, étant en langage machine, est illisible d’une façon courante pour l’homme, le programme à interpréter est, avant son interprétation, inintelligible pour le processeur ; ou plutôt, il ne lui apparaît que comme un simple fichier texte (à ceci près qu’il possède un attribut d’exécutabilité).

Conclusion

Un langage de programmation est donc l’ensemble des procédures normalisées (pour une définition plus précise de ce qu’est une norme informatique, cf. la page Pourquoi des normes en informatique ? conformément auxquelles des commandes humainement intelligibles sont traduites en langage machine.

C’est donc la notion de langage de programmation qui porte l’essentiel de l’appareil informatique, si l’on définit cet essentiel comme l’accessibilité à l’être humain des ressources que le puissance de calcul de la machine met à sa disposition : les langages de programmation sont la médiation entre l’intellect humain et la puissance de calcul de l’ordinateur, médiation unilatérale car elle se résume au rapport impératif du programme sur l’activité du processeur.

Parler de langages de programmation pour un ordinateur reste éminemment métaphorique : à proprement parler, une machine ne peut posséder de langage, et si cette machine particulière qu’est l’ordinateur en possède un, ce ne pourrait être que le langage machine, ou langage binaire. Mais par l’intermédiaire de la compilation, ou de l’interprétation qui n’en est qu’une espèce, est possible la médiation, le passage continu d’un langage à l’autre.

Le caractère pyramidal des langages de programmation, i.e le fait qu’ils soient fondés les uns sur les autres, est ainsi ce qui permet l’élaboration de tâches de plus en plus complexes : les tâches les plus élémentaires et les plus répétitives sont déléguées au langage lui-même (i.e au compilateur ou à l’interpréteur), ce qui nous rend disponible pour des l’implémentation d’opérations plus abstraites et moins immédiates.

Haut de page