Embarquer un exécutable dans une application C

Il existe plusieurs façons d’embarquer des fichiers dans un code source C. La méthode suivante à l’avantage d’être indépendante du compilateur utilisé et donc plus portable.

Codes sources

L’archive complète est disponible ici :
embed_binaries_c.tar.zst

emb.c :

c
#include <stdio.h>

int main(void)
{
    printf("Hello World !\n");
    return 0;
}

embb.h :

c
unsigned char emb[] = {
  0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
  0x50, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00, …
};
unsigned int emb_len = 15952;

main.c :

c
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <sys/mman.h>
#include "embb.h"

int
main(void)
{
    int fd = memfd_create("embb", 0);
    if (fd == -1)
        fprintf(stderr, "fd error %d\n", errno);

    int bw = write(fd, emb, emb_len);
    if (bw != emb_len)
        fprintf(stderr, "bytes write %d\n", bw);

    char *newargv[] = { "emb", NULL };
    char *newenvp[] = { NULL };

    fexecve(fd, newargv, newenvp);
    perror("fexecve");
    fprintf(stderr, "fexecve error %d\n", errno);

    return 0;
}

Compilation

shell
make embb main

On peut alors tester les exécutables produits avec `./emb` et `./main` dans le terminal. Les exécutables donnent alors la même sortie `Hello World !`, cela signifie que tout s’est déroulé comme prévu.

Explications

Du côté du fichier emb.c, il n’y a pas grand-chose à dire, le programme se contente d’afficher `Hello World !`.

Dans le fichier embb.h il y a un tableau de type `unsigned char` remplit de valeurs hexadécimaless et une variable de type `unsigned int`. À bien regarder, la valeur de cette variable correspond à la taille du fichier emb en octets. C’est aussi la taille du tableau. En fait, embb.h est un hex dump formaté dans le format C de l’exécutable emb. Chaque valeur hexadécimale du tableau est un octet du fichier emb. Ainsi le fichier est stocké tel-quel dans un format accessible via du code C. Pour cela, l’outil xxd¹ est utilisé avec la commande `xxd -i emb > embb.h`.

Enfin, le fichier main.c est le plus complexe. Le cœur du programme est constitué de trois parties :

memfd_create

c
#define _GNU_SOURCE
#include <sys/mman.h>
#include <stdio.h>
#include <errno.h>

int fd = memfd_create("embb", 0);
if (fd == -1)
    fprintf(stderr, "fd error %d\n", errno);

`memfd_create`² va créer un fichier anonyme dans la mémoire RAM et renvoyer un file descriptor du fichier qui permet d’y accéder. Attention, un file descriptor est de type `int`, c’est une abstraction des fichiers différente d’un file pointer de type `FILE *`. Les deux dernières lignes s’assurent que l’appel système s’est déroulé sans encombres.

write

c
#include <unistd.h>
#include <stdio.h>
#include "embb.h"

int bw = write(fd, emb, emb_len);
if (bw != emb_len)
    fprintf(stderr, "bytes write %d\n", bw);

`write`³ va écrire le contenu du tableau contenu dans le fichier embb.h dans notre fichier anonyme nouvellement créé et ainsi recréer le fichier emb d’origine dans la mémoire RAM. Là encore, les deux dernières lignes nous signalent les éventuelles erreurs.

fexecve

c
#include <unistd.h>
#include <stdio.h>
#include <errno.h>

char *newargv[] = { "emb", NULL };
char *newenvp[] = { NULL };

fexecve(fd, newargv, newenvp);
perror("fexecve");
fprintf(stderr, "fexecve error %d\n", errno);

`fexecve`⁴ va à enfin exécuter notre binaire, qui se trouve dans la mémoire RAM, en lui donnant des arguments et un environnement spécifiés respectivement par `newargv` et `newenvp`. Les deux tableaux de chaînes de caractère doivent se finir par la valeur `NULL` et en général le premier élément des arguments est le nom de l’exécutable appelé, sinon sa valeur doit être `NULL`. Si l’appel de l’exécutable fonctionne correctement, le programme appelant ne se termine jamais et se transforme pour laisser la place à l’exécutable appelé. Dans le cas d’une erreur lors de l’appel, le programme continue et les deux dernières ligne affichent un message d’erreur correspondant au problème. Pour éviter que le programme appelant ne soit remplacé par le programme appelé, il est possible d’utiliser l’appel système `fork`⁵.

Références

¹ xxd
² memfd_create
³ write
⁴ fexecve
⁵ fork

Retour au gemlog
Retour à l’accueil

Commentaires (0)

Pas encore de commentaires

Écrire un commentaire