Embarquer un exécutable dans une application C

Écrit par Programind, le 8 Mars 2024

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

Codes sources

L'archive complète est disponible à l'adresse embed_binaries_c.tar.zst.

//emb.c
#include <stdio.h>

int main(void)
{
    printf("Hello World !\n");
    return 0;
}
//embb.h
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
#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

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 rempli de valeur hexadécimales 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écimal 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

#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

#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

#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 lignes 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

¹. https://linux.die.net/man/1/xxd

². https://www.man7.org/linux/man-pages/man2/memfd_create.2.html

³. https://www.man7.org/linux/man-pages/man2/write.2.html

⁴. https://www.man7.org/linux/man-pages/man3/fexecve.3.html

⁵. https://www.man7.org/linux/man-pages/man2/fork.2.html