La salutation Hello World
(thecoder08.github.io)-
Exploration du monde des abstractions caché derrière le programme Hello World moderne
- Cet article porte sur un programme Hello World écrit en C. Parmi les langages de haut niveau pour lesquels il n'est pas nécessaire de se demander ce que fait réellement le langage avant l'exécution du programme, via un interpréteur, un compilateur ou un JIT, C est celui qui reste le plus proche du bas niveau.
- À l'origine, le texte devait être compréhensible par toute personne ayant un minimum de pratique du code, mais quelques connaissances en C ou en assembleur seront sans doute utiles.
-
Début du programme Hello World
- Tout le monde connaît sans doute le programme Hello World. En Python, le premier programme que vous avez probablement écrit ressemblait à
print('Hello World!'). - Dans cet article, nous allons examiner un Hello World écrit en langage C. En C, on ne peut pas exécuter un programme en invoquant simplement un interpréteur. Il faut d'abord lancer le compilateur pour le transformer en code machine que le processeur peut exécuter directement.
- Tout le monde connaît sans doute le programme Hello World. En Python, le premier programme que vous avez probablement écrit ressemblait à
-
Analyse de notre programme
- En analysant le fichier du programme compilé, on constate qu'il s'agit d'un exécutable ELF destiné à l'architecture de jeu d'instructions x86-64.
- Un exécutable ELF est, sous Linux, l'équivalent d'un fichier .exe sous Windows.
- x86-64 est l'architecture CPU utilisée sur les PC depuis l'introduction de l'IBM PC en 1981.
- Ce fichier contient du code machine, le seul langage que le CPU puisse comprendre.
-
Analyse du code assembleur
- On cherche le point d'entrée, l'adresse de démarrage du programme, puis on analyse le code assembleur.
- Le langage assembleur est une représentation lisible par l'humain du code machine.
- On y voit du code d'initialisation ajouté automatiquement par le compilateur, ou plus précisément par l'éditeur de liens, ainsi qu'un appel à la fonction
__libc_start_main. - Mais ce code n'est pas défini dans notre programme : il se trouve ailleurs.
-
La bibliothèque standard C
- La fonction
__libc_start_mainest définie danslibc.so.6, la bibliothèque standard C de notre système. - La bibliothèque standard C est un ensemble de routines et de fonctions utilisées par presque tous les programmes de nos ordinateurs.
- Elle effectue le travail d'initialisation, puis appelle la fonction
main()que nous avons écrite. Lorsquemain()retourne, elle termine le programme avec le code de sortie fourni.
- La fonction
-
Analyse de la fonction
main()- Dans la fonction
main(), une stack frame est mise en place, l'adresse de la chaîne Hello World est passée en argument à un appel de fonction, puisputs()est appelée. puts()remplace iciprintf()à la suite d'une optimisation du compilateur.printf()est plus complexe, alors queputs()se contente d'afficher une chaîne sans formatage.
- Dans la fonction
-
La chaîne Hello World
- La chaîne est constituée de
"Hello World!", suivie d'un terminateur NULL. - En C, une chaîne ne contient aucune information de longueur : sa fin est signalée par ce terminateur NULL. Sans lui, le programme continuerait à lire de la mémoire non autorisée jusqu'à se terminer par une Segmentation Fault.
- En raison de l'optimisation du compilateur, le saut de ligne (
\n) utilisé avecprintf()a été supprimé, carputs()ajoute déjà un retour à la ligne après l'affichage.
- La chaîne est constituée de
-
La fonction
puts()- La fonction
puts()appelle à son tour du code situé dans la bibliothèque standard. - En regardant le code de glibc, on peut voir une chaîne d'appels
_IO_puts -> _IO_new_file_xsputn, mais le code est trop complexe pour être expliqué facilement. - Dans le cas de musl libc, c'est plus simple :
puts -> fputs -> fwrite -> __fwritex -> __stdio_write -> syscall.
- La fonction
-
Appel système
- Aussi vaste que soit la bibliothèque C, elle ne peut pas communiquer directement avec le matériel. Seul le noyau en est capable.
- Ainsi, l'appel à
puts()se termine inévitablement par une requête à l'OS pour lui demander d'effectuer une action. Ici, il s'agit d'écrire une chaîne dans le flux de sortie. - musl libc utilise l'appel système
writev, qui permet d'écrire plusieurs buffers en une seule fois. - Un appel système consiste à placer les paramètres dans des registres puis à exécuter l'instruction
syscall. Le contrôle passe alors au noyau, qui lit les paramètres et effectue l'appel système demandé.
-
Le noyau
- Le noyau Linux doit exécuter l'action demandée via l'appel système. L'appel
writedemande au noyau d'écrire dans un fichier ouvert ou dans un flux du système de fichiers. writereçoit trois paramètres : le descripteur de fichier où écrire, le buffer à écrire et le nombre d'octets à écrire.- L'endroit réel où l'écriture aboutit dépend du contexte. Dans le cas d'un émulateur de terminal, cela apparaît comme un terminal virtuel (pty) ; pour une connexion distante, c'est transmis à
sshd; pour un terminal physique, cela passe par un adaptateur série-USB. Dans le cas d'une console framebuffer, le noyau rend le texte et l'affiche à l'écran.
- Le noyau Linux doit exécuter l'action demandée via l'appel système. L'appel
-
Conclusion
- Les systèmes logiciels modernes fonctionnent de manière extrêmement complexe et sophistiquée au-dessus du matériel, au point qu'il est illusoire de vouloir comprendre complètement la moindre petite action d'un ordinateur.
- Pour tout expliquer, il a fallu laisser de côté de nombreux éléments.
- Envoyer le message Hello World n'est qu'une opération parmi l'innombrable quantité d'appels système et de programmes en cours d'exécution sur un ordinateur moderne.
L'avis de GN⁺
- Cet article montre bien comment chaque couche d'un système informatique masque la complexité de la couche inférieure grâce à l'abstraction, ce qui permet aux développeurs de créer des applications plus facilement.
- Il rappelle aussi à quel point de nombreuses opérations se produisent en coulisses pour exécuter une seule ligne d'application, et pourquoi le débogage est si difficile.
- À mon sens, tout programmeur devrait bien connaître au moins les couches système situées sous le langage qu'il utilise le plus souvent. Il n'est pas nécessaire de tout maîtriser, mais comprendre comment fonctionnent concrètement les abstractions est important.
- Même si l'on utilise un langage de haut niveau, étudier des notions de programmation système comme l'organisation de la mémoire, la pile et le tas, ou encore les appels système, aide énormément pour le débogage et l'optimisation des performances.
- Un développeur applicatif aura rarement à manipuler directement le compilateur ou la bibliothèque C, mais comprendre comment le programme que l'on écrit utilise finalement le système est, selon moi, essentiel pour devenir un bon programmeur.
Aucun commentaire pour le moment.