Créer un fichier ELF sans même GNU LD

Créer des fichiers ELF sous Linux aussi facilement que des fichiers COM sous MS-DOS

Présentation…

Linux consomme beaucoup de ressources, et il en consomme tellement qu’il ne laisse paradoxalement même pas une chance de créer des applications alternatives dans un environnement restreint. Un environnement traditionnel, même sans passer par GCC, nécessite LD. Mais LD nécessite GLibC… qui est une librairie lourde. Cette page vous présente un moyen de créer des fichiers ELF sans même employer un lieur comme LD. Ce sera aussi je l’espère une occasion de vous donner l’envie de découvrir le format de fichier ELF. Accessoirement, la solution présentée vous permettra même de créer des binaires Linux depuis Windows sans environnement de compilation croisée ( « cross-compilation » ).

Qu’est-ce que le format ELF ?

ELF est un sigle signifiant Executable and Linkable Format. Sans artifices et 100% utile, ce format est employé par plusieurs systèmes, notamment BSD, UNIX SystemV et Linux. Ce format a remplacé maintenant depuis quelque temps l’ancien format a.out, qui posait trop de problèmes techniques ( la liaison dynamique avec le format a.out nécessitait littéralement des hacks, et cela n’était plus tolérable pour une architecture système ). Le format ELF version « 1.2 » existe depuis 1997, et c’est la version la plus récente à cette année 2007. ( la version ELF « 1.1 » date de 1995 ). Vous pourrez consulter cette référence dans ce fichier PDF : TIS ELF « 1.2 » « specifications » ( datée de 1997 ) . La référence est maintenue par l’organisation TIS Tool Interface Standard ).

Windows ne connais pas le format de fichier ELF, et même les Windows à noyau « NT » utilisent encore le format COFF pourtant obsolète. Les autres version de Windows ( c’est-à-dire non-« NT » ) utilisent le format PE et ME. Cette page ne concerne donc que la création de binaires ELF pour BSD, UNIX et Linux.

ELF et la GLibC

Le format ELF a rendu les systèmes plus propres et plus fiables en permettant de se passer de certains hackings ( littéralement « Bricolage » ) officiels. Malheureusement, sous Linux, l’intégration du format ELF a été accompagnée d’une mise à jour, non seulement du noyau Linux, mais également de la GLibC. La GLibC 2, encore appelé LibC 6 est connue pour être celle qui a introduit le support ELF ; vous pourrez vois à ce sujet, la page Liaison dynamique . Pour être exact, la LibC 5 était le première à supporter ELF, mais GLibC 1 qui était alors un fork ( littéralement « Une branche divergente » ), n’a rejoins la LibC qu’avec LibC 6 GLibC 1 passait alors GLibC 2 ). L’ennui est que la LibC 5 était déjà trop volumineuse pour que Linux puisse honnêtement se targuer d’être un système utilisable par les configurations légères… et avec la LibC 6 alias GLibC 2, les chose se sont encore nettement aggravé : on a jamais vu une LibC être aussi lourde.

En conséquence, les systèmes Linux supportant ELF sont des systèmes lourds. Cependant, il n’est pas obligatoire d’avoir une LibC installée pour exécuter des applications au format ELF. En effet, c’est le noyau qui a en charge le chargement de l’application, et celui ci ne fait qu’invoquer le lieur dynamique ld.so.2 pour LibC 6 alias GLibC 2 ) habituellement associé au format ELF. Pour aller plus loin encore, notez que rien n’impose d’employer la LibC 6 ( ou GLibC 2 ) pour exécuter des applications au format ELF. En effet, le format prévoit un champs spécifiant un chemin vers un interpréteur, qui n’est en fait sous Linux, rien d’autre que le chemin vers le lieur dynamique. Ainsi, il est en théorie tout à fait possible de créer des applications au format ELF, qui serait dynamiquement liées avec ld.so.1 et LibC 5 ( ou même encore LibC 4 ou même encore antérieure ).

Mais en pratique, là ou il y a ELF, il y a GLibC 2 alias LibC 6.

Le problème

Le problème que cela pose est assez ennuyeux. Vous avez un Linux trop lourd pour être utilisable sur les anciens PC ( contrairement à ce qu’il prétend ), et cela vous empêche même de créer vos applications légère dans un environnement restreint. Car la création de fichier ELF passe habituellement par GNU LD, et GNU LD requière GLibC 2, et la taille de cette librairie interdit de l’installer sur un système modeste de base. Mais remarquons au passage que la librairie n’est pas la seule en cause, et que le lieur est lui même un peu trop volumineux.

Une solution

Ceux et celles d’entre vous qui ont connu l’âge d’or du MS-DOS, une époque bénis ou on programmais joyeusement avec un système accessible ( entendez par là, compréhensible dans ses détails ) à tous et toutes, et bien documenté, se souviennent des joies qu’ils et elles ont eu en découvrant à quelle point il était facile de créer un fichier « *.COM » . Il n’y avait d’ailleurs même pas besoin d’environnement de développement, puisque le programme DEBUG ( très utile et très léger, il a fait des heureux-ses ), fourni en standard avec MS-DOS, permettait d’enregistrer des fichiers binaires.

La solution qui vous est proposée ressemble tout à fait dans le principe à celle employée sous MS-DOS à l’époque. Et elle est même encore bien plus confortable et efficace.

Nous allons créer des fichiers assembleur ( fichiers sources persistant, donc plus pratique qu’avec DEBUG ), qui seront compilés avec NASM ( qui reconnais un jeux d’instruction bien plus vaste que DEBUG ), et au début desquels nous placerons une structure de donnée caractéristique des fichiers ELF. Cette structure comprendra l’entête du fichier, ainsi que deux entrées de descripteur de segment : une pour le binaire exécutable, et une pour les données en accès lecture et écriture ( plus évolué que les fichiers COM du DOS ).

Avantages de cette solution

Les versions statiques de NASM sont légères ( entre 150 KB et 200 KB environ ). Il existe un éditeur en console que je vous invite à découvrir, qui se nomme E3. Il est beaucoup plus commode à utiliser que VI ou VIM, et même s’il n’a pas encore à la hauteur de l’ergonomie du programme EDIT de MS-DOS, il est encore plus léger : 15 KB. Quelques documentations au format texte pèseront au plus 20 KB. Pour un total de de 185 KB à 235 KB environ, soit même pas le quart d’une disquette 1.44 M, vous avez un environnement de développement parfaitement fonctionnel. Ceci vous permettra de créer des applications légères en environnement restreint, vous autorisant à court-circuiter la lourdeur des installations Linux habituelles. Je vous recommande, de vous documentez sur les Syscalls, qui sont un moyen d’accéder aux fonctions systèmes, sans même employer la moindre librairie ( ne pensez même pas aux LibC… les dernières ne tiendraient même pas sur un petit disque dur ).

Le bonus compilation-croisée ( « cross-compilation » )

Comme il existe une version Windows de NASM, cette méthode vous permettra même de faire de la cross compilation, et de compiler sous Windows, des binaires ELF qui pourront fonctionner sous Linux. Ce bonus sera très appréciable si vous travailler à la création d’une installation légère de Linux : vous pourrez concevoir et compiler vos programme sous Windows, sans même avoir besoin que votre installation Linux soit achevée pour se faire.

À-propos du code

Concrètement, vous emploierez un pattern, que vous éditerez pour simplement y ajouter votre code et vos données. Vous avez le choix entre deux patterns : un pour les programmes ayant un segment de code et un segment de données en lecture et écriture, et un autre pour les programmes n’ayant besoin de rien de plus qu’un segment de code exécutable. Notez que pour les programme ne comprenant que des données en lecture seule, il est plus astucieux d’utiliser la version avec un segment de code seul ( on peut placer des données en lecture seule dans un segment de code, sans que cela ne pose la moindre soucis ).

Une petite remarque sur le code des structures ELF : j’ai utilisé un alignement sur 4 octets pour les segments. Bien que la taille des pages mémoire sous Linux soit de 4096 octets, il ne semble pas obligatoire que les segment soit alignés sur les pages, et une page peut donc apparemment chevaucher deux segments ( la gestion des pages et la gestion des segments sont transparentes l’une à l’autre, aussi bien pour le noyau Linux que pour les processeurs i80x86 ). L’alignement sur 4 octets est nécessaire, car si mes souvenirs sont bons, les processeurs Intel exigent que les adresses de base des segments soient alignées sur 4 octets ( à vérifier, mais trop de prudence ici ne nuira pas ). Il existe un drapeau flag ) dans les descripteurs de page, sur les processeurs Intel, qui commande le déclenchement d’une exception en cas d’accès à des données non alignées sur 4 octets, mais ceci est indépendant de la question qui nous intéresse, car c’est de l’alignement des adresses de base des segments qu’il s’agit ici.

La version avec segments de code et de données

Cette version sera à utiliser pour créer un fichier ELF avec un segment de code et un segment de données en lecture et écriture. Pour l’utiliser, éditez simplement les sections de code et de donnée entre les marqueurs BEGIN et END, en commentaires dans chacun des deux segments. Vous ne devez pas modifier le reste du code, sauf si vous savez vraiment avec quoi vous jouez ;-). Finalement vous compilerez le code avec la commande « nasm -f bin [-O2] votrecode.asm ». L’option « -f bin » crée un format binaire plat, c’est à dire sans format particulier ( le format du fichier est alors simplement celui produit par le contenu du fichier assembleur ). L’option « -O2 » est optionnelle, mais elle est souvent nécessaire pour les codes contenant des sauts conditionnels par exemple ; référez vous à la documentation de NASM pour plus d’informations si nécessaire.

Pensez à consulter la deuxième version simplifiée également ( plus loin ), qui sera a préféré pour les codes ne contenant pas de données en écriture.

Vous pourrez également utiliser ce code comme une introduction au format ELF, si vous désirez l’étudier un jours ; vous en tirerez sûrement du bonheur, et pour étudier ce format en profondeur, vous pourrez consulter la référence citée en début de ce document.

Vous pouvez consulter ce code ici en ligne, ou le télécharger : elf-with-data.asm  ( texte au format MS-DOS « CR-LF » ).

    ; (hint:tabulation size is 3)
    ; (date-dd-mm-yyyy:03/01/2007)

    ; Creating a fully functional ELF file without even GNU/ld
    ; ---------------------------------------------------------
    ;
    ; This a pattern file which allow you to create ELF executable without
    ; even a linker in hands. This is useful for custom installation with
    ; low resource, such as old PC. Indeed, the usual way is to use GNU/ld,
    ; but unfortunatly, ld rely on glibc, which is very-very hugh en
    ; outrageously resource consuming.
    ;
    ; You can use it with nasm (the syntax used here is the one of nasm).
    ; A static version of nasm if very light (between 150 and 200KB). So
    ; with a static version of e3 (which is no more than 13KB), and
    ; a few text files documentations about syscalls and some other good
    ; stuff, you can have a real developpement environement under less
    ; than the quarter of an 1.44M floppy disk.
    ;
    ; Please visit http://www.les-ziboux.rasama.org/elf-without-ld.html
    ; for updates. In the futur, I will mirror a very small static version
    ; of nasm (102KB), a static version of e3, and some useful texte
    ; documentation.
    ;
    ; You can use this pattern in any project… it is not mandatory, but this
    ; would be very nice of you to put this url on top of your product sources :
    ; http://www.les-ziboux.rasama.org/elf-without-ld.html
    ; Thank you :)
    ;
    ; To use this pattern, just edit the place between the BEGIN and END mark,
    ; both in code and data segment.
    ;
    ; If you code only adress read-only data, then you can use the other
    ; "elf-simple.asm", beceause is there are only read-only datas, it is
    ; best to put them in code segment.
    ;
    ; Finaly you have to compile your code with "nasm -f bin [-O2] yourcode.asm"
    ; (the -O parameter is optional, alghtough often requited because of
    ; short-jumps and some stuff like that).

    ; This file is full of comments… you may remove them after reading ;)
    ;
    ; Comments are welcome : les-ziboux@rasama.org (fr/en)
    ; You wanna learn Arabic ? -> http://www.les-ziboux.rasama.org (fr)

    cpu 386
    bits 32

    org 0x08048000 ; Linux applications are loaded at this virtual address.

    ; === Header ================================================================

    elf_header:
    .start
       db 0x7F, "ELF"          ; ELF signature                 -- constant
       db 1                    ; Architecture(1)               -- 1 = 32 bits
       db 1                    ; Data enconding                -- 1 = LSB-First
       db 1                    ; File version                  -- 1 = v1
       db 0,0,0,0,0,0,0,0,0    ; 9 bytes padding               -- should be zero
       dw 2                    ; Type                          -- 2 = executable
       dw 3                    ; Architecture(2)               -- 3 = i386
       dd 1                    ; ELF Version                   -- 1 = ELF-v1
       dd _start               ; Entry point adress in memory  -- virtual adress
       dd segments_table - $$  ; Segments table offset in file
       dd 0                    ; Sections table offset in file -- 0 = none)
       dd 0                    ; File's flags
       dw elf_header.size      ; ELF Header's size
       dw 32                   ; Segments table entries's size
       dw 2                    ; Number of segment descriptors
       dw 0                    ; Sections table entries's size -- 0 = none
       dw 0                    ; Number of sections descriptor -- 0 = none
       dw 0                    ; String table index            -- 0 = none
    .size equ $ - .start

    segments_table:

    code_segment_descriptor:
    .start:
       dd 1                    ; Type            -- 1 = loadable into memory
       dd 0                    ; Offset in file  -- include ELF header and table
       dd $$                   ; Virtual address in memory
       dd 0                    ; Physical adress -- 0 = no physical address
       dd code_size            ; Size in file
       dd code_size            ; Size in memory
       dd 5                    ; Permission flags -- 0x4 + 0x1 = read and execute
       dd 0x4                  ; Alignment in memory (and in file)
    .size equ $ - .start

    data_segment_descriptor:
    .start:
       dd 1                    ; Type -- 1 = loadable into memory
       dd _data - $$           ; Offset in file
       dd _data                ; Virtual adress in memory
       dd 0                    ; Physical adress -- 0 = no physical address
       dd data_size            ; Size in file
       dd data_size            ; Size in memory
       dd 6                    ; Permission flags -- 0x4 + 0x2 = read and write
       dd 0x4                  ; Alignment in memory (and in file)
    .size equ $ - .start

    ; === Code ==================================================================

    _code :

    _start: ; You can move the start label where-ever you want in code segment.

       ; ----- BEGIN of your code -----

       ; Here is a sample code which simply say good bye and terminate.
       ; It give an example of usage of both code segment and data
       ; segment. Although this example does write to data segment,
       ; the data segment is still fully writable. Note that read-only
       ; datas can also reside in code segment (you should use
       ; the pattern "elf-simple.asm" for this purpose).
       ; You can put your own code between the BEGIN and END mark.
       ; Do not modify something else, unless you're sure about what
       ; you are playing with.

       mov  eax, 4                ; function id for sys_write
       mov  ebx, 1                ; descriptor of standard output
       mov  ecx, message          ; offset of bytes to write
       mov  edx, message.length   ; number of bytes to write
       int  0x80                  ; syscall

       mov  eax, 1                ; function id for sys_exit
       mov  ebx, 0                ; return code from the program (0 mean Ok)
       int  0x80                  ; syscall

       ; ----- END of your code -----

    align 4
    code_size equ $ - $$

    ; === Data ==================================================================

    align 4
    _data:

       ; ----- BEGIN of your datas -----

       ; This is a simple data example. This data segment is writable.
       ; Note that you can put read-only data in the code segment. If you
       ; only have read-only data, and no writable data in you application,
       ; then putting them in code segmet, avoid the creation of a data
       ; segment, reducing a bit the size of the executable. For this
       ; purpose, use the other patten name "elf-simple.asm".
       ; You can put your own datas between the BEGIN and END mark.
       ; Do not modify something else, unless you're sure about what
       ; you are playing with.

    message:
       db "Bye, Au-revoir, Ma'a as-salama, Bisbald… :)", 10
    .length equ $ - message

       ; ----- END of your datas -----

    align 4
    data_size equ $ - _data




        

La version avec segment de code seul

Cette version du code est a utiliser pour les applications n’ayant qu’un segment de code, et pas de donnée en écriture ; les données constantes, en lecture seule, peuvent être placées dans le segment de code.

Vous pouvez consulter ce code ici en ligne, ou le télécharger : elf-simple.asm  ( texte au format MS-DOS « CR-LF » ).

    ; (hint:tabulation size is 3)
    ; (date-dd-mm-yyyy:03/01/2007)

    ; Creating a fully functional ELF file without even GNU/ld
    ; ---------------------------------------------------------
    ;
    ; There are no explanations here : read "elf-with-data.asm" for
    ; informations on usage and copyright.
    ;
    ; Compile: nasm -f bin [-O2] yourcode.asm
    ; Updates : http://www.les-ziboux.rasama.org/elf-without-ld.html
    ; Comments are welcome : les-ziboux@rasama.org (fr/en)
    ; You wanna learn Arabic ? -> http://www.les-ziboux.rasama.org (fr)

    cpu 386
    bits 32

    org 0x08048000 ; Linux applications are loaded at this virtual address.

    ; === Header ================================================================

    elf_header:
    .start
       db 0x7F, "ELF"          ; ELF signature                 -- constant
       db 1                    ; Architecture(1)               -- 1 = 32 bits
       db 1                    ; Data enconding                -- 1 = LSB-First
       db 1                    ; File version                  -- 1 = v1
       db 0,0,0,0,0,0,0,0,0    ; 9 bytes padding               -- should be zero
       dw 2                    ; Type                          -- 2 = executable
       dw 3                    ; Architecture(2)               -- 3 = i386
       dd 1                    ; ELF Version                   -- 1 = ELF-v1
       dd _start               ; Entry point adress in memory  -- virtual adress
       dd segments_table - $$  ; Segments table offset in file
       dd 0                    ; Sections table offset in file -- 0 = none)
       dd 0                    ; File's flags
       dw elf_header.size      ; ELF Header's size
       dw 32                   ; Segments table entries's size
       dw 1                    ; Number of segment descriptors -- just one (code)
       dw 0                    ; Sections table entries's size -- 0 = none
       dw 0                    ; Number of sections descriptor -- 0 = none
       dw 0                    ; String table index            -- 0 = none
    .size equ $ - .start

    segments_table:

    code_segment_descriptor:
    .start:
       dd 1                    ; Type            -- 1 = loadable into memory
       dd 0                    ; Offset in file  -- include ELF header and table
       dd $$                   ; Virtual address in memory
       dd 0                    ; Physical adress -- 0 = no physical address
       dd code_size            ; Size in file
       dd code_size            ; Size in memory
       dd 5                    ; Permission flags -- 0x4 + 0x1 = read and execute
       dd 0x4                  ; Alignment in memory (and in file)
    .size equ $ - .start

    ; === Code ==================================================================

    _code :

    _start: ; You can move the start label where-ever you want in code segment.

       ; ----- BEGIN of your code -----

       ; Here is a sample code which simply cleanly terminate :P

       mov  eax, 1                ; function id for sys_exit
       mov  ebx, 0                ; return code from the program (0 mean Ok)
       int  0x80                  ; syscall

       ; ----- END of your code -----

       ; You may have read-only data here as well ;)

    align 4
    code_size equ $ - $$


        

Veuillez agréer Tous mes Vœux d’Inspiration…