Pourquoi?Dans la suite du billet précédent sur l'occupation mémoire d'un processus, lançons-nous dans la confection d'un petit programme sur Linux . Faire le plus petit programme possible. Enfin pas exactement, mais un petit programme construit à partir d'un fichier C, qui occupe le moins de pages possible en mémoire lors de son exécution et si possible ait une taille réduite sur disque. Avec un tel programme, plus besoin de tronquer les résultats de cat /proc/[pid]/smaps. Confort et paresse.DigressionD'où l'idée des Jivaros, réducteurs de têtes. Et là surprise! En recherchant le terme Jivaros, on arrive directement sur la page de Wikipedia référençant les Shuars qui sont un des 5 peuples "regroupés" sous le terme Jivaros... Je ne vais pas vous recopier la page Wikipedia, allez-y faire un tour. Ah, encore une chose Jivaros dériverait de l'espagnol Xibaros (barbare, sauvage) utilisé par les envahisseurs espagnols... Mais le dictionnaire là, n'a pas l'air de connaitre le terme... |
Excuses
Toujours est-il que c'est pas très gentil... Je m'en excuse auprès des Shuar, Achuar, Shiwiars, Aguarunas et Huambisas.Références
Évidemment, je ne suis pas le premier à me lancer sur cette piste aussi sotte que grenue. Voici un site en anglais qui a poussé le bouchon bien plus loin que moi. Confort et paresse (bis). Mais là où l'auteur finit totalement en assembleur, nous allons rester à mi-chemin entre C et assembleur. A noter que son programme qui fait exit(42) a une taille finale sur disque de 45 octets!Voici un autre site où on trouve un "hello world" rikiki de 142 octets : Smallest x86 ELF Hello World (That I could achieve) .
But
On ne cherche pas à faire le plus petit programme, juste un programme qui occupe le moins de page possible, disons donc: 1 page de code et 1 page de pile. Si quelqu'un sait faire moins sur un système paginé, je suis tout ouïe.C'est parti!
Voilà donc mon programme:fa $ cat tsantzas.c int main(void) { return 0;} fa $ $ gcc -Wall -o tsantzas tsantzas.c fa $ ./tsantzas fa $ echo $? 0 fa $ |
Ben oui, c'est un programme qui ne fait rien, sauf se terminer avec une valeur de sortie égale à 0. Ne me dites pas que c'est un programme stupide qui ne sert à rien! Je viens de réécrire la commande true (/bin/true) en plus simple : les arguments --help ni --version ne sont pas traités.
Regardons la(les) taille(s):
fa $ ls -l tsantzas -rwxrwxr-x 1 francois francois 7332 Mar 8 10:55 tsantzas fa $ size tsantzas text data bss dec hex filename 1053 276 4 1333 535 tsantzas fa $ getconf PAGESIZE 4096 fa $ |
Le fichier occupe 7332 octets, le code fait 1053 octets, et il y a 280 octets de données globales. On reviendra plus tard sur la différence entre 1053 + 280 = 1333 et 7332, Je pourrais me satisfaire de cette taille: 1053 est inférieur à la taille d'une page sur mon système, mais on a"hérité" de données globales bien qu'aucune ne soit définie dans le programme, et prenons-nous au jeu en essayant de réduire les tailles du fichier et du segment de code.
Par ailleurs si on regarde quel type de fichier a été créé, on trouve:
fa $ file tsantzas tsantzas: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=0x9c0c6a7f31cc229fd51b6139cbc76329a4f054d6, not stripped fa $ |
Première réduction: strip
Faisons simple! Le fichier est dit "non-strippé". Il contient donc une table des symboles, consultable par la commande nm. Si on se débarrasse de cette table, le fichier devrait être logiquement plus petit.fa $ nm tsantzas | head -n 2 0804a01c B __bss_start 0804a01c b completed.6382 fa $ strip tsantzas fa $ nm tsantzas nm: tsantzas: no symbols fa $ $ ls -l tsantzas -rwxrwxr-x 1 francois francois 5600 Mar 8 11:49 tsantzas fa $ size tsantzas text data bss dec hex filename 1053 276 4 1333 535 tsantzas fa $ |
La taille du fichier a diminué de 1732 octets. Les tailles des segments de code et de données sont inchangées.
Edition de liens: statique ou dynamique?
La commande file nous a indiqué que le fichier était un binaire construit par édition de liens dynamique, utilisant des bibliothèques partagées... Donc le fichier "tsantzas" ne contient pas l'intégralité du code et des données qui seront présents lors de l'exécution, ce fichier sera complété par l'adjonction (réalisée, pour faire simple, par le système d'exploitation) de bibliothèques partagées. Lesquelles? La commande ldd va nous l'indiquer.fa $ ldd tsantzas linux-gate.so.1 => (0xb7700000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb753f000) /lib/ld-linux.so.2 (0xb7701000) fa $ |
On ne rentrera pas ici dans l'analyse de ces dépendances et de ces fichiers. Mais il certain que lors de l'exécution notre programme aura une taille (virtuelle, mais aussi probablement une occupation en RAM) qui sera finalement éloignée de notre "cible". Pour vérifier, il faudrait accéder au fichier /proc/[pid]/smaps pendant l'exécution du programme tsantzas.... Vu son activité, sa durée d'exécution ne vas pas nous le permettre, à moins d'insérer avant le return, un appel à pause().
Une édition de liens statique ne devrait importer dans notre programme rikiki, que le strict nécessaire de la bibliothèque C. Comme le programme ne fait aucun appel à la bibliothèque C, on devrait y gagner. Essayons!
Edition de liens statique
fa $ gcc -s -Wall -o tsantzas tsantzas.c -static fa $ file tsantzas tsantzas: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=0x183763c254775f6065616b2a9372255df51b30ea, stripped fa $ ls -l tsantzas -rwxrwxr-x 1 francois francois 684720 Mar 8 12:03 tsantzas fa $ size tsantzas text data bss dec hex filename 675915 3276 5892 685083 a741b tsantzasfa $ |
gcc est invoqué avec l'option -s (stripped) pour éviter d'avoir à exécuter la commande strip en suite, et l'option -static pour se "débarrasser" des bibliothèques dynamiques.
Résultat:le fichier est beaucoup plus gros ainsi que les sections de code et de données! Pourtant le programme ne fait pas (explicitement) appel à la bibliothèque C! En fait, le programme commence son exécution avant main dans une fonction fournie par la bibliothèque C et de nom "_start".
fa $ gcc -Wall -o tsantzas tsantzas.c -static fa $ readelf -h tsantzas | grep -e 'Entry point address:' Entry point address: 0x8048e08 fa $ nm tsantzas | grep -e ' main$' 08048f14 T main fa $ nm tsantzas | grep -e ' _start$' 08048e08 T _start fa $ |
Tout ce qui est fait avant main, relève un peu de la "magie noire" propre à la bibliothèque C (mise en place des "FILE" pour stdin, stdout, stderr; appel éventuel de constructeurs statiques,....), il faut aussi faire en sorte que lorsque le programme quitte la fonction main, on fasse un appel à exit, qui va, entre autre, fermer tous les "FILE" ouverts.
Plus de main!
Comme notre programme n'utilise pas de "FILE" ni d'arguments, essayons de nous débarrasser de ce qui précède le main, et de tout ce qui suit main! On va donc rebaptiser main en _start et faire un appel à "_exit" plutôt que return.fa $ $ cat tsantzas_nomain.c void _start(void) {_exit(0);} fa $ gcc -s -o tsantzas_nomain tsantzas_nomain.c -static tsantzas_nomain.c: In function ‘_start’: tsantzas_nomain.c:1:20: warning: incompatible implicit declaration of built-in function ‘_exit’ [enabled by default] /tmp/cclCyeeH.o: In function `_start': tsantzas_nomain.c:(.text+0x0): multiple definition of `_start' /usr/lib/gcc/i686-linux-gnu/4.7/../../../i386-linux-gnu/crt1.o:(.text+0x0): first defined here /usr/lib/gcc/i686-linux-gnu/4.7/../../../i386-linux-gnu/crt1.o: In function `_start': (.text+0x18): undefined reference to `main' collect2: error: ld returned 1 exit status fa $ |
Oups! La définition de _start de notre programme rentre en conflit avec celle de la bibliothèque. Corrigeons le tir!
fa $ gcc -s -o tsantzas_nomain tsantzas_nomain.c -static -nostartfiles tsantzas_nomain.c: In function ‘_start’: tsantzas_nomain.c:1:20: warning: incompatible implicit declaration of built-in function ‘_exit’ [enabled by default] fa $ |
L'option -nostartfiles indique à l'éditeur de liens de laisser tomber les fichiers avant main. Bon, on a oublié le prototype de _exit. C'est pas très grave, pour notre propos.
fa $ ./tsantzas_nomain fa $ echo $? 0fa $ |
Et les tailles?
fa $ file ./tsantzas_nomain ./tsantzas_nomain: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, BuildID[sha1]=0xf66d036a4e9ada03c3a40dfa1af1148e58c565ad, stripped fa $ ls -l ./tsantzas_nomain -rwxrwxr-x 1 francois francois 680432 Mar 8 14:16 ./tsantzas_nomainfa $ size ./tsantzas_nomain text data bss dec hex filename 672293 3260 5284 680837 a6385 ./tsantzas_nomain fa $ |
Oups. Toujours imposant! La taille du fichier est passée de 684720à 680432! Nous sommes loin du compte! En fait, notre programme fait référence à la fonction _exit. Et qui sait ce que cela entraine en terme de volume de code amené depuis la bibliothèque?
Et si on essayait de fournir notre propre biliothèque _exit()?
Définissons _exit()
fa $ cat tsantzas_exit.c void myexit() { __asm__ ( " movl $1,%eax \n" " movl $42,%ebx \n" " int $0x80 \n" ); } _start() { myexit(); }fa $ |
Ouch! On a défini une fonction, codée en assembleur dans le fichier .c. Cette fonction invoque le noyau Linux (via la dernière instruction assembleur int 0x80) en lui demandant de procéder à l'appel système _exit (pour ça on a mis la valeur 1 dans le registre %eax dans la première ligne d'assembleur). Et on a indiqué que l'on voulait sortir avec la valeur 42 (en mettant 42 dans le registre %ebx sur la 2ème ligne d'assembleur).
Pas de magie, ici. Il s'agit juste de conventions.
Pour savoir comment utiliser de l'assembleur en C, voir Using Inline Assembly in C/C++.
Pour connaitre la correspondance entre appels systèmes et numéro à mettre dans %eax, on peut par exemple visiter: Linux System Call Table.
Essayons avec notre _exit
fa $ gcc -s -o tsantzas_exit tsantzas_exit.c -static -nostartfiles fa $ ./tsantzas_exit ; echo $? 42 fa $ ls -l tsantzas_exit -rwxrwxr-x 1 francois francois 640 Mar 8 15:03 tsantzas_exit fa $ size tsantzas_exit text data bss dec hex filename 151 0 0 151 97 tsantzas_exit fa $ |
Yippie! Notre programme retourne bien la valeur 42! La taille du fichier est passée à 640 octets. La taille du code est de 151 octets, et il n'y a plus de données globales!
Encore plus petit?
Le fichier binaire exécutable ELF, qui contient notamment une entête (readelf -h tsantzas_exit), une table d'entête de programme (readelf -l tsantzas_exit) et une table d'entête de sections (readelf -S tsantzas_exit). Lors de l'exécution, en général seules les entêtes de programme sont utiles, on doit donc pouvoir se débarrasser des (de certaines des) entêtes de sections. Essayons grâce à la commande strip.fa $ strip -R .shstrtab -R .comment -R .eh_frame -R .note.gnu.build-i ./tsantzas_exit fa $ ls -l ./tsantzas_exit -rwxrwxr-x 1 francois francois 408 Mar 8 15:29 ./tsantzas_exit fa $ size ./tsantzas_exit text data bss dec hex filename 63 0 0 63 3f ./tsantzas_exit fa $ ./tsantzas_exit; echo $? 42 fa $ |
L'argument -R "toto" indique à strip d'éliminer les sections correspondantes. Bon, j'ai en fait laissé une section ".text" La taille du fichier a réduit de 640 octets à 408!
Mais plus surprenant, une partie du code a aussi disparu! Le programme marche-t-il encore?
Oui!
Elfkickers!
Le site elfkickers (commun avec le programme qui fait exit(42) en 45 octets) propose une suite d'outils pour réduire les tailles de fichiers ELF. Plutôt que de rechercher toutes les astuces utilisées dans les 2 sites déjà mentionnés, voyaons voir ce que donnent ces outils. Confort et paresse.fa $ ./ELFkickers-3.0a/bin/sstrip -z ./tsantzas_exit fa $ ./tsantzas_exit; echo $? 42 fa $ ls -l ./tsantzas_exit -rwxrwxr-x 1 francois francois 220 Mar 8 15:45 ./tsantzas_exit fa $ |
Yippie bis! 220 octets au lieu de 408! Bon je ne suis pas aux 45 octets du record, mais je pars d'un programme encore partiellement en C!
Mais le plus drôle est à venir! La commande Elfkickers sstrip fait dans la prestidigitation! Du genre accroche-toi aux entêtes, j'enlève le code!
fa $ size ./tsantzas_exit text data bss dec hex filename 0 0 0 0 0 ./tsantzas_exit fa $ |
La commande size (après passage de sstrip) dit qu'il n'y a plus de segment de code! Et pourtant, il tourne! Le programme fonctionne encore! Je n'ai pas encore fouillé en détail. Soit sstrip met à 0 des champs lu par la commande size, soit il essaye de mettre le code dans les parties inutilisées des entêtes ELF... Attention, l'usage non contrôlé de la commande sstrip conduit relativement facilement à des programmes qui ne fonctionnent pas!
On pourrait encore gagner en taille de fichier. Si on analyse avec readelf le fichier retravaillé par sstrip, on voit qu'il y a encore des entêtes probablement peu utiles:
fa $ $ readelf -S ./tsantzas_exit There are no sections in this file.fa $ readelf -l ./tsantzas_exit Elf file type is EXEC (Executable file) Entry point 0x80480c9 There are 3 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x000d3 0x000d3 R E 0x1000 NOTE 0x000094 0x08048094 0x08048094 0x00024 0x00024 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 fa $ |
Il est à peu près certain que l'entête "NOTE" pourrait être supprimée sans dommage.
Conclusion
Étonnant, non?Comme souvent, ne pas croire ce qu'on voit! "size" dit qu'il n'y a pas de code... mais c'est "size" qui semble se fourvoyer, sur ce cas.
On a maintenant de quoi produire des petits programmes, on pourra tenter de s'en servir pour regarder la gestion mémoire Linux en action.
Il faut noter que la taille du plus petit programme calculant quelque chose (c'est mathématiquement correctement défini) s'appelle sa complexité de Kolmogorov (cherchez sur Internet). Par contre, trouver le plus petit programme qui calcule ce qui est demandé est un défi... La version proposée est rigolote.
RépondreSupprimer