samedi 8 mars 2014

Tribulations d'un processus chez les Jivaros (construire un tout petit programme Linux)

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.

Digression

D'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 ShuarAchuar,  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 $?
0
fa $

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_nomain
fa $ 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.

Tsantzas

Pourquoi, j'ai appelé mon programme tsantzas?

1 commentaire:

  1. 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