Inclure vos données dans un seul fichier

Principe
  • Lorsque votre programme contient des données séparées, l'amour de l'esthétisme et un légitime souci d'exclusivité peuvent conduire à distribuer un minimum de fichiers.


Fichier ressources
  • Une solution courante est de distribuer un fichier exécutable, et un fichier ressources qui contiendra l'ensemble des données.
  • Le fichier ressources peut être une simple concaténation des données, une archive ou une base de données.
    • La concaténation de données nécessite de maintenir un index, et souvent de recréer l'ensemble du fichier à la moindre modification. C'est par contre la plus rapide à l'exécution.
    • L' utilisation d'un fichier ressources est également rapide (moins les temps de décompression éventuelle), mais aura au moins une dépendance (si vous projetez d'écrire un nouveau packer/depacker... c'est une mauvaise idée). Par contre, vous aurez des possibilités proches d'un système de fichier pour la gestion de votre fichier ressources.
    • L' utilisation d'une base de données est à réserver aux gros projets et sort largement du cadre de ce billet.


Les codeurs n'ont pas froid aux oreilles
  • On peut faire encore plus fort, et tout coller dans le même fichier.
  • Dans ce cas, on n'a plus trop le choix, et il s'agit bien sûr de l'exécutable.


Choix retenu
  • Ici nous allons utiliser le fichier exe pour stocker nos données.
    • avantages : un seul fichier executable à distribuer. On ne risque pas de se tromper quand on distribue les mises à jour.
    • inconvénients : il faut tout redistribuer à chaque modification de l'exécutable. Les temps de linkage sont allongés.



Options techniques

xxd : solution la plus portable
  • xxd -i data.dat : génère un dump de data.dat au format C.
  • Nous n'étudierons pas cette possibilité.
  • En deux mots, elle consiste à définir les datas dans le code source, et à compiler celui-ci.
  • Son inconvénient principal est la lourdeur des codes sources générés.


Injection des données dans l'exécutable
  • Utilisation de objdump et objcopy (ou ld en remplacement de objcopy).
  • Nous allons utiliser les fichiers suivants:
    • data.txt : données à insérer dans l'exécutable (ici, ça sera du texte).
    • main.c : le source C de l'exécutable.



objcopy, objdump

objcopy
  • Il s'agit tout d'abord de transformer le fichier data.txt en un fichier objet.
  • Formats d'entrée et de sortie de objcopy
    • objcopy --info : informations sur les formats d'entrée/sortie pour objcopy.
  • Linux et Windows:
    • objcopy -I binary -O elf32-i386 --binary-architecture i386 data.txt data.o
  • Explication des flags
    • -I : (attention il s'agit d'un i majuscule) input target
    • -O : output target
    • rappel : objcopy --info donne une liste des input/output potentiels.


Parenthèse sur ld
  • Une manipulation équivalente à la commande objcopy ci-dessus:
    • ld -r -b binary data.txt -o data.o data.txt
  • Explication des flags
    • -r : link incrémental. Le fichier généré en sortie par ld pourra reservir comme entrée à ld.
    • -b : input format (et non pas binary, je sais, le raccourci est tentant).


objdump
  • Permet d'obtenir toutes sortes d'informations sur vos objets.
  • Nous l'utilisons pour avoir le nom des symboles du fichier objet.
  • objdump -s data.o
    • donne le format du fichier , et le contenu des sections
  • objdump -x data.o
    • montre tous les headers.
  • Pour avoir uniquement la table des symboles, utiliser le flag -t

$ objdump.exe -t data.o

data.o:     file format pe-i386

SYMBOL TABLE:
[  0](sec  1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x00000000 _binary_data_txt_start
[  1](sec -1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x0000001c _binary_data_txt_size
[  2](sec  1)(fl 0x00)(ty   0)(scl   2) (nx 0) 0x0000001c _binary_data_txt_end


  • plus court, même chose:

$ nm data.o
0000001c D _binary_data_txt_end
0000001c A _binary_data_txt_size
00000000 D _binary_data_txt_start



Utilisation des symboles dans le source

  • Maintenant que nous avons la liste des symboles, nous pouvons y faire référence dans le source en C.
  • Les symboles seront traités comme de vulgaires buffers.
  • Ils sont faciles à utiliser, voici un source d'exemple:
/* main.c */

#include <stdio.h>

extern int binary_data_txt_end;
extern int binary_data_txt_size;
extern int binary_data_txt_start;

int main(void)
{
  int size = (int)&binary_data_txt_size;
  char *data = (char *)&binary_data_txt_start;
  char *end = (char *)&binary_data_txt_end;
    
  printf("taille: %d
", size);
  printf("buffer: %s", data);
  printf("start: 0x%p ; end: 0x%p
",data,end);

  return 0;
}


  • Juste une petite remarque pour Windows.
  • * Sous Windows, la décoration des noms n'est pas la même que sous Unix.
    • Par exemple: _extern int binary_stdout_txt_start deviendra extern int binary_stdout_txt_start
    • Il faut enlever le premier underscore pour chaque symbole.



Et qu'est-ce qu'on fait maintenant ?

  • Il faut simplement compiler le source, et ajouter au linkage notre fichier précédemment pré-linké.
  • Cela va plus vite à écrire qu'à expliquer:
    • gcc -W -Wall main.c data.o
  • Je vous laisse la joie d'exécuter tout ça, et vous épargne le screenshot.



Docteur Hacker et Monsieur Lamer

  • Bien entendu, la contre-mesure est assez évidente pour qui a compris tout ce qui précède.
  • Je ne doute pas que Docteur Hacker, qui me lit (et je j'en suis fort honoré) a déja deviné.
  • Dans le cas où Monsieur Lamer me lirait également, je n'en dirai pas plus.