Ou comment faire une intro 4kb Linux avec la Xlib
Je tiens d'abord à préciser une chose : ce texte ne s'adresse pas à ceux qui veulent apprendre l'assembleur. Il est plutôt destiné à ceux qui le connaissent déjà et qui désirent découvrir comment l'utiliser sous linux. Néanmoins, comme on n'apprend jamais mieux qu'en lisant des sources (en tout cas, en ce qui concerne ce "langage" qu'est l'assembleur), vous êtes tous, du débutant au confirmé, invité à lire cette doc et à me faire part de vos suggestions/remerciements/insultes/autres soit en laissant un commentaire sur alrj.org, soit par mail à l'adresse allergy@alrj.org.
Les outils nécessaires seront, outre votre éditeur de texte favori, l'assembleur Nasm
ainsi que ld (ou gcc, qui l'inclut). Comme vous pouvez le constater, point de syntaxe
AT&T ici, juste du "presque Intel".
Bien sûr, vous pouvez aussi vous munir d'un debugger et d'un compilateur C (pour voir le code qu'il
pond).
Si vous êtes prêt, commençons.
Je vais commencer par donner l'exemple, que je commenterai ensuite. Lorsqu'il s'agit d'un programme aussi petit, ca permet d'avoir directement une vue d'ensemble.
Voici donc le code de notre Hello World :
1 BITS 32 2 3 EXTERN puts 4 5 SECTION .data 6 chaine db "Hello world !", 0 7 8 SECTION .text 9 GLOBAL main 10 11 main: 12 push dword chaine 13 call puts 14 add esp, 4 15 ret |
Ce simple exemple nous donne déjà une belle quantité d'informations.
On y découvre, en lignes 5 et 8, des SECTIONS
. Ce sont les segments de notre futur
exécutable. Nous utilisons ici les sections .data
et .text
.
.data
est utilisée pour les données initialisées. C'est à dire, les données
qui doivent avoir une valeur précise au lancement du programme. Dans cet exemple, c'est la chaine
de caractère que nous stockons..text
contient le code de notre exécutable. Il est à noter qu'il existe bien d'autres types de segments (sections), comme .bss
qui contient les données non-initialisées. Le système leur réservera de la place en mémoire
lors de l'exécution du programme, mais elles ne prennent pas le moindre octet dans le binaire lui-même.
Aux lignes 3 et 9, nous pouvons remarquer les mots EXTERN
et GLOBAL
.
Ils sont complémentaires. Le premier permet de spécifier une fonction qui se trouve en fait à l'extérieur
de notre source. Le second spécifie quels sont les symboles qui seront utilisables depuis l'extérieur.
Nous devons ici déclarer puts
en EXTERN
car il sera utilisé par notre programme.
main
est déclaré en GLOBAL
car il sera appelé par le système : c'est le point
d'entrée du programme (comme en C).
Enfin, les lignes 12 à 14 de cet exemple montrent comment sont effectués les appels aux fonctions.
Les arguments sont push
és sur la pile (ligne 12) avant l'appel (ligne 13), et c'est
l'appelant qui les dépile (ligne 14).
L'exemple ici ne le montre pas, mais les arguments sont mis sur la pile en commençant par le
dernier.
C'est à dire que si nous avons une fonction qui prend deux arguments :
int ma_fonction(int arg1, int arg2) |
il faudra d'abord empiler arg2
puis arg1
. Le code de retour est
habituellement donné dans EAX
.
Pour compiler ce programme :
$ nasm -f elf hello.asm $ gcc hello.o -o hello $ ./hello Hello world ! $ |
Vous l'aurez deviné, le $
représente le prompt.
Si vous faites de l'assembleur avec pour objectif la réalisation d'une intro en 4kb, vous aurez constaté un petit problème : ce simple "hello world" est gros. Très gros, même.
$ ls -l hello -rwxr-xr-x 1 allergy allergy 4758 avr 3 22:11 hello |
4758 bytes, c'est trop pour une 4kb, pour laquelle la limite est de 4096 bytes...
Mais tout n'est pas perdu. Certains vous diront qu'il faut commencer à se prendre
la tête avec les en-têtes ELF, utiliser l'option -f bin
de nasm pour générer directement
tout l'exécutable, etc. Rassurez-vous, ces mesures ne sont à prendre qu'en dernier recours.
Tentons de déterminer ce qui prend de la place dans ce programme :
Réduire la taille du code est bien évidemment votre travail. C'est à vous de trouver les optimisations nécessaires. De même, évitez autant que possible l'utilisation des fonctions externes. Il reste bien sûr des cas où elles sont inévitables, mais chaque fois que vous pouvez le faire, tentez de les éliminer.
Voyons maintenant quelques méthodes qui nous permettront de réduire la taille de notre binaire. Je ne parlerai ici que des méthodes applicable de manière générale, pas des optimisations propres à ce programme-ci.
La première chose à faire est de striper l'exécutable (man strip
vous
en dira plus long sur cette commande, si vous ne la connaissez pas encore).
$ strip -s hello $ ls -l hello -rwxr-xr-x 1 allergy allergy 2956 avr 5 06:24 hello |
C'est bien, mais ce n'est pas encore tout à fait ça.
Cela nous laisse un peu plus d'un kilo-octet pour le code et les données. Comptez l'initialisation
graphique, peut-être également une gestion de la carte son, il ne vous reste vraiment plus grand chose.
Il doit donc forcément être possible de faire mieux.
Effectivement.
Je sais, le titre a l'air un peu compliqué, mais ne vous inquiétez pas, c'est presque aussi simple.
Vous savez peut-être que main
n'est le point d'entrée du programme que parce que c'est
le comportement par défaut de ld. Heureusement, on peut lui demander de fonctionner différemment.
Avec le fonctionnement par défaut, main
n'est qu'une fonction comme les autres. Le point
d'entrée réel de l'exécutable se trouve ailleurs et s'appelle _start
.
La sortie de notre programme, qui était un simple ret
ne fait que quitter la fonction main.
Cela signifie qu'en réalité, il y a une partie du code, dans cet exécutable, qui n'est pas de nous
et qui se charge d'appeler notre main
! Il est bien évident que nous devons nous en
débarrasser.
Opérons quelques modifications à notre code.
1 BITS 32 2 3 EXTERN puts 4 5 SECTION .data 6 chaine db "Hello world !", 0 7 8 SECTION .text 9 GLOBAL _start 10 11 _start: 12 push dword chaine 13 call puts 14 add esp, 4 15 mov eax, 1 16 int 0x80 |
Comme vous le constatez, pas grande différence. Aux lignes 9 et 11, nous avons remplacé le
main
par _start
et le ret
de la ligne 15 est devenu un
étrange mov eax, 1 | int 0x80
. Et voilà, vous venez de faire un appel système.
Sous Linux, ces appels se font par l'intermédiaire de l'interruption 0x80. L'appel numéro 1 est celui qui demande au système la fermeture du programme. Rien de bien compliqué jusqu'à présent.
Cependant, si vous tentez de compiler ceci de la même manière que pour l'exemple du chapitre 2, vous risquez d'avoir une mauvaise surprise au moment de l'édition des liens. Voici la marche à suivre :
$ nasm -f elf hello.asm $ gcc -nostdlib -lc hello.o -o hello $ ./hello Hello world ! $ ls -l hello -rwxr-xr-x 1 allergy allergy 2028 avr 5 07:01 hello |
L'argument -nostdlib
permet de s'affranchir du code ajouté lors de l'édition des liens.
Plus de main
, place à _start
.
Le -lc
est l'habituelle demande de lien avec une bibliothèque externe, nécessaire ici
car c'est elle qui contient puts
.
N'oublions pas ce que nous avons vu précédemment, et stripons le binaire obtenu :
$ strip -s hello $ ls -l hello -rwxr-xr-x 1 allergy allergy 1404 avr 5 07:15 hello |
Avouez que c'est déjà mieux ! Il reste maintenant un peu plus de 2.5kb de libre pour votre code.
Et pourtant, il est tellement simple de faire mieux...
Au long de mes pérégrinations sur le net, je suis tombé sur la page des ELFkickers (voir la section
des liens). C'est une compilation d'outils très pratiques, qui tournent tous autour
du format ELF.
L'un d'entre eux, en particulier, a retenu mon attention : sstrip. Dans la même veine que
strip, il permet un travail plus en profondeur. Il retire non seulement les symboles inutiles,
mais simplifie également les en-têtes ELF et retire les sections inutiles.
Voyons ce que ça donne :
$ sstrip hello $ ls -l hello -rwxr-xr-x 1 allergy allergy 612 avr 5 07:35 hello $ ./hello Hello world ! |
612 octets, et il fonctionne toujours ! Et pourtant, ce n'est même pas fini.
Vous vous en doutez sans doute, un bon moyen pour mettre plus de données dans un fichier de petite taille passe par la compression. Le problème, c'est qu'un décompresseur, ça prend de la place. Sauf bien sûr s'il n'est pas réellement inclus dans l'exécutable. Et il existe un compresseur disponible sur presque toute machine Linux : gzip. Alors pourquoi ne pas lui demander de faire le travail pour nous ?
Mais il est toujours plus propre d'avoir un seul fichier exécutable, plutôt qu'un exécutable et un fichier compressé. La solution (volée à titou^shagreen) est de regrouper les deux : un seul fichier qui consiste en deux partie : un script de décompression et d'exécution ainsi que le binaire compressé.
Je vous donne cette astuce telle quelle, sans entrer dans les détails. Si vous avez du mal à comprendre son fonctionnement, n'hésitez pas à me contacter.
$ cat compress.sh #!/bin/sh dd if=$0 bs=1 skip=68|gzip -cd>A chmod +x A;./A rm A exit $ gzip -9 hello $ cat compress.sh hello.gz > hello $ chmod a+x hello $ ./hello 349+0 enregistrements lus. 349+0 enregistrements écrits. Hello world ! $ ls -l hello -rwxr-xr-x 1 allergy allergy 417 avr 5 19:32 hello |
Quelques petits commentaires, cependant.
Certains esprits grincheux contesteront et diront que l'utilisateur n'a pas forcément un accès en
écriture dans le répertoire (et ils auront raison). Or, c'est indispensable pour écrire le fichier
décompressé. La solution est de remplacer le fichier A
par /tmp/A
. Ca augmente
légèrement la taille du script, mais dans une bien faible mesure. N'oubliez cependant pas de modifier
également la valeur de skip
lors du dd.
L'autre désagrément est celui impliqué par la commande dd : les
349+0 enregistrements...
. Soit on s'arrange avec une redirection, soit on se dit que ça n'est
pas dérangeant.
Nous en arrivons donc à un exécutable de 417 octets. Il est évidemment possible de faire
beaucoup mieux. Par exemple, remplacer le puts
par un appel système, comme proposé au
chapitre 3.2. On évite alors le lien avec la libc et le EXTERN
. Comme je suis gentil
et serviable, j'ai mis en annexe, pour ceux qui seraient intéressés, un exemple de
Hello World qui utilise les appels systèmes.
Il est aussi possible de jouer avec les en-têtes, et arriver au final à un exécutable de moins de 100
bytes.
Mais j'estime que cette dernière solution n'a pas sa place dans ce tutoriel, qui se veut plus généraliste.
Peut-être une prochaine fois ?
Ce point-ci offre moins d'intérêt au point de vue assembleur pur. Certes, il est indispensable à la
réalisation d'une animation graphique, mais il consiste surtout à appeler quelques fonctions externes. Il
faudra cependant faire attention : lorsque cette partie est réalisée en C, on ne se rend pas toujours
compte que certains appels sont en réalité des macros et non des fonctions. Il est parfois intéressant
d'écrire une première fois l'initialisation en C et de demander une sortie assembleur à gcc (grâce
à l'option -S
) pour voir de quelle manière il travaille.
Avant tout, nous devons déclarer quelques fonctions externes.
EXTERN XOpenDisplay EXTERN XCreateSimpleWindow EXTERN XMapRaised EXTERN XCreateImage EXTERN XPutImage EXTERN malloc |
Nous aurons également besoin de quelques variables. Elles n'auront pas à avoir de valeur précise lors
du lancement du programme, leur place est donc tout indiquée : le segment .bss
, comme
expliqué au point 2.
SECTION .bss ; pour la Xlib : display: resd 1 sreen: resd 1 win: resd 1 root: resd 1 gc: resd 1 ximage: resd 1 ; Deux buffer : buffer16: resd 1 buffer32: resd 1 |
Nous avons ici deux variables différentes, pour les buffers. Il est en effet plus facile de travailler en 32 bits en interne et de faire une conversion après, si nécessaire
Il n'y aura que très peu de commentaires, dans cette section. À la place, je donnerai chaque fois le code C correspondant.
La première chose à faire est d'obtenir le display
:
display = XOpenDisplay (NULL); |
xor eax,eax push eax call XOpenDisplay add esp, 4 mov [display], eax |
Il faut maintenant obtenir le screen
, la fenêtre root
et le
contexte graphique (gc
). En C, trois macros nous permettent de les avoir facilement.
Ces valeurs sont en fait des membres de structures. Si vous êtes motivés, allez voir dans les headers
de la xlib. Sinon, faites un copier/coller de ce que je vous donne ici :
screen = DefaultScreen (display); root = DefaultRootWindow (display); gc = DefaultGC (display, screen); |
mov edx, [eax+132] ; eax vaut toujours [display] mov [screen], edx imul edx, 80 mov ebx, [eax+140] mov ecx, [ebx+edx+8] mov [root], ecx mov edx, [ebx+edx+44] mov [gc], edx |
Une fois qu'on a tout ceci, on peut enfin faire quelque chose de visible ! Je parle de l'ouverture d'une fenêtre, bien sûr. Une fois de plus, référez-vous à la mage de manuel pour plus de détails sur les paramètres de la fonction.
win = XCreateSimpleWindow (display, root, 10, 10, width, height, 0, 0, 0); |
xor ebx, ebx push ebx ; background : 0 push ebx ; border : 0 push ebx ; border_width : 0 push d 480 ; hauteur push d 640 ; width mov bl, 10 push ebx ; y : 10 push ebx ; x : 10 push ecx ; fenêtre parent : root push eax ; le display : eax call XCreateSimpleWindow add esp, (9*4) mov [win],eax |
Bien. Une fois que nous avons une fenêtre, il faut encore la "mapper" au display
. Cette
fonction permet également de faire passer notre fenêtre au premier plan.
XMapRaised (display, window); |
push eax ; eax vaut [win] push dword [display] call XMapRaised add esp, 8 |
Une fois ceci fait, il ne reste plus grand chose. Nous devons créer un buffer un peu spécial qui
sera lu par le serveur X pour remplir notre fenêtre. C'est ce qu'on appelle une ximage
.
Ici, il faudra faire un peu attention. Cette fonction demande une valeur pour les bits par pixels. Comme
je sais que mon écran est en 16bpp, j'ai mis cette valeur en dur. En faisant un peu de recherches, je
ne doute pas que vous trouverez comment obtenir la profondeur de couleur de l'écran au run-time :)
CopyFromParent
est en fait un #define qui vaut 0.
ximage = XCreateImage (display, CopyFromParent, depth, ZPixmap, 0, NULL, width, height, 32, 0); |
xor eax, eax push eax ; padding : 0 push dword 32 ; alignement push dword 480 ; hauteur push dword 640 ; largeur push eax ; 0 push eax ; 0 push dword 2 ; ZPixmap push dword 16 ; ou 32 : bits par pixel push eax ; CopyFromParent : 0 push dword [display] ; display call XCreateImage add esp, (4*10) mov [ximage], eax |
Enfin ça y est, notre fenêtre est prête ! Il ne nous reste dès lors qu'à allouer un buffer dans
lequel nous dessinerons, avant de l'envoyer à notre ximage
.
Nous allons en fait allouer deux buffers : l'un en 32 bits, dans lequel nous dessinerons, l'autre
en 16bpp, que nous utiliserons au cas où nous devrions faire une conversion.
buffer16 = malloc(largeur * hauteur * 2); buffer32 = malloc(largeur * hauteur * 4); |
Notez que je regroupe les deux appels à malloc
en un seul :
push dword (640 * 480 * 6) call malloc mov [buffer16], eax add eax, (640 * 480 * 2) mov [buffer32], eax add esp, 4 |
Tout ça, c'est bien joli, mais tant que nous n'avons pas de fonction pour envoyer notre buffer à
l'écran, nous n'irons pas loin. Heureusement (vous vous en doutiez), il existe une fonction
pour ça. Avant de l'appeler, il faudra juste prendre soin de spécifier quel buffer sera utilisé par
l'ximage
.
ximage->data = buffer; XPutImage(display, window, gc, ximage, 0, 0, 0, 0, largeur, hauteur); |
mov ebx, [buffer16] ; ou [buffer32] si vous êtes en 32bpp mov eax, [ximage] mov [eax+16], ebx xor eax, eax push dword 480 ; hauteur push dword 640 ; largeur push eax ; dest_y : 0 push eax ; dest_x : 0 push eax ; src_y : 0 push eax ; src_x : 0 push dword [ximage] ; ximage push dword [gc] ; gc push dword [win] ; window push dword [display] ; display call XPutImage add esp, (10*4) |
Si, comme moi, votre affichage est en 16 bits et que vous utilisez le buffer en 32 bits, vous aurez
besoin d'une fonction qui se charge de convertir tout ça.
En voici une, probablement très peu optimisée (sauf peut-être en taille, et encore) mais qui a le
mérite d'exister.
Non, n'insistez pas, je ne ferai pas de commentaires.
Convert_32_to_16: mov esi, [buffer32] mov edi, [buffer16] mov ecx, (640 * 480) .Lconv: xor ebx,ebx lodsd shr al, 3 mov bl, al shr ax, 5 and ax, 11111100000b or bx, ax shr eax, 16 and al, 11111000b or bh, al mov [edi], bx add edi, 2 dec ecx jnz .Lconv ret |
Je vous laisse lâchement vous débrouiller pour les autres conversions, si vous estimez en avoir besoin.
Avoir un moyen pour connaître le temps écoulé depuis le début du programme est très utile. Cela permet d'avoir la même vitesse d'exécution quelle que soit la machine.
La première chose à faire est de demander l'heure au système, par l'intermédiaire de
gettimeofday
. Cette fonction nous donnera un nombre de secondes et un nombre de
micro-secondes, que nous aurons soin de sauvegarder. Cela correspondra pour nous au moment du
lancement de notre programme.
Nous aurons besoin d'un EXTERN
et de quelques variables. Notez que leur ordre est
important : la fonction gettimeofday
demande un pointeur sur une structure comportant deux
entiers.
EXTERN gettimeofday SECTION .bss start_time_sec: resd 1 ; secondes au lancement start_time_usec: resd 1 ; micro-secondes now_time_sec: resd 1 ; secondes actuellement now_time_usec: resd 1 Ticks: resd 1 ; nombre de 'ticks' écoulés |
Et le code :
push dword 0 push dword start_time_sec call gettimeofday add esp, (2*4) |
Pour connaître le temps écoulé, il suffira dès lors de faire un nouvel appel à
gettimeofday
et de soustraire la valeur de départ à celle nouvellement obtenue.
Mais comme les valeurs que nous avons sont scindées en secondes et micro-secondes, quelques calculs
seront nécessaires. Pour obtenir une valeur en millisecondes (que j'appelle ticks), le calcul sera
le suivant :
Ticks = (now_time_sec - start_time_sec) * 1000 + (now_time_usec - start_time_usec) / 1000
Voici le code correspondant :
; appel à gettimeofday push dword 0 push dword now_time_sec call gettimeofday add esp, (2*4) ; Maintenant, faire le calcul ; secondes * 1000 : mov eax, [now_time_sec] mov ebx, 1000 sub eax, [start_time_sec] mul ebx ; eax = (now.sec - start.sec) * 1000 mov ecx, eax ; micro-secondes / 1000 : mov eax, [now_time_usec] sub eax, [start_time_usec] cdq idiv ebx ; eax = (now.usec - start.usec) / 1000 ; additionner les deux valeurs add eax, ecx ; et enregistrer le résultat mov [Ticks], eax |
Pour ma part, j'ai simplement mis le code d'initialisation avec celui de la Xlib, et le calcul
du temps écoulé se fait lors du blit.
Notez qu'avec cette méthode, la granularité du timer est de 10ms. C'est la raison pour laquelle on ne
calcule pas le temps écoulé depuis le dernier blit, mais bien depuis le début du programme, pour éviter
d'avoir de trop grosses erreurs.
Comme pour l'utilisation de la Xlib, il n'y a pas de réel problème en ce qui concerne la gestion du son. De plus, l'avantage ici est que tout est faisable par l'intermédiaire d'appels systèmes. Pas besoin de lier notre programme à une bibliothèque, nous évitons donc les pertes de place.
De manière générale, lorsqu'on doit gérer le son via OSS, on opère de la manière suivante :
Voyons comment ça se passe dans la pratique.
Premièrement, nous aurons besoin de quelques variables.
SECTION .bss file_desc resd 1 ; le descripteur de fichier pour /dev/dsp data resd 22050 ; une seconde de son %assign AFMT_U8 8 SECTION .data devdsp dd "/dev/dsp", 0 ; le nom du périphérique channels dd 0 ; mono (1 pour stéréo) format dd AFMT_U8 ; 8 bits non signé rate dd 22050 ; 22 kHz |
Vous vous demandez sans doute d'où je tire la valeur atribuée à AFMT_U8
.
Je ne l'ai bien sûr pas inventée. Elle est définie dans un des fichiers d'en-tête de OSS. J'ai donc
simplement écrit un petit programme en C, chargé d'afficher cette valeur. Vous trouverez plus de détails
sur OSS en lisant l'Open Sound System Programmer's Guide dont la référence est disponible
dans la section Liens.
Une fois tout ceci déclaré, il ne nous reste plus qu'à coder.
L'ouverture de /dev/dsp se fait par l'intermédiaire de l'appel système 5.
mov eax, 5 ; open mov ebx, devdsp ; l'adresse du nom du périphérique mov ecx, 1 ; pour ouvrir en write-only int 0x80 test eax,eax js error ; à vous de travailler... mov [file_desc], eax |
Une fois ceci fait, nous devons régler le périphérique avec les IOCTLs. Vous trouverez une liste des valeurs des IOCTLs dans votre fichier /usr/src/linux/asm/ioctl.h.
; On passe en mono... mov eax, 54 ; ioctl mov ebx, [fd] mov ecx, SNDCTL_DSP_STEREO ; l'ioctl voulu mov edx, channels ; pointeur sur les données int 0x80 test eax,eax js error ; ... 8 bits ... mov eax, 54 ; ioctl mov ebx, [fd] mov ecx, SNDCTL_DSP_SETFMT mov edx, format int 0x80 test eax,eax js error ; ... 22 kHz mov eax, 54 ; ioctl mov ebx, [fd] mov ecx, SNDCTL_DSP_SPEED mov edx, rate int 0x80 test eax,eax js error |
Comme vous l'aurez peut-être constaté, les valeurs données pour les paramètres sont passés par référence. C'est parce que l'appel à ioctl doit pouvoir modifier leur valeur. Il ne se gênera d'ailleurs pas pour le faire. Si la valeur demandée est impossible à obtenir, il en mettra une autre, qu'il sauvera dans votre variable. C'est donc à vous de vous assurer des paramètres réellement utilisés, chose que je ne fais pas ici.
Nous allons maintenant nous créer un petit sample -- très basique, je vous rassure -- dans
data
. C'est lui que nous enverrons à la carte son.
mov edi, data mov ecx, 22050 xor al,al .create_data: stosb inc al dec ecx jnz .create_data |
Nous sommes presque au bout de nos peines, il ne reste plus qu'à envoyer ce sample à la carte son.
mov eax, 4 ; write mov ebx, [fd] mov ecx, data ; pointeur sur les données mov edx, 22050 ; nombre de bytes à écrire int 0x80 test eax,eax js error |
Et voilà !
Si vous avez compris tout ce que j'ai raconté, il ne devrait pas vous être difficile de continuer
à découvrir par vous-mêmes.
Lors de la réalisation d'une intro, il sera peut-être avantageux d'utiliser (si les règles le
permettent) une bibliothèque comme SDL, qui offre l'avantage de minimiser le nombre d'appels externes
pour l'initialisation. Vous pouvez aussi utiliser OpenGL, ou toute autre bibliothèque qui vous
semblera correspondre à vos besoins.
Gardez cependant à l'esprit que chaque fonction externe utilisée prendra de la place dans votre
exécutable final
Si votre but était surtout d'apprendre comment interfacer l'assembleur avec les bibliothèques existantes, j'espère que cette petite introduction vous aura servi. Vous l'aurez remarqué, c'est assez simple.
Et n'oubliez jamais : c'est toujours intéressant de voir comment gcc transforme votre code C en assembleur.
Bon amusement !
Le code de hello_syscall.asm :
1 BITS 32 2 3 SECTION .data 4 chaine db "Hello world !",10 5 6 SECTION .text 7 GLOBAL _start 8 9 _start: 10 mov eax, 4 11 mov ebx, 1 12 mov ecx, dword chaine 13 mov edx, 14 14 int 0x80 15 mov eax, 1 16 int 0x80 |
Et ce que ca donne :
$ nasm -f elf hello_syscall.asm $ gcc -nostdlib hello_syscall.o -o hello_syscall $ ls -l hello_syscall -rwxr-xr-x 1 allergy allergy 861 avr 9 00:00 hello_syscall $ strip -s hello_syscall $ ls -l hello_syscall -rwxr-xr-x 1 allergy allergy 484 avr 9 00:01 hello_syscall $ sstrip hello_syscall $ ls -l hello_syscall -rwxr-xr-x 1 allergy allergy 174 avr 9 00:01 hello_syscall $ ./hello_syscall Hello world ! |
Comme vous le voyez, une simple dépendance externe peut prendre beaucoup de la place. Notez qu'avec un exécutable de 174 octets, la compression fait passer la taille à 197 octets.