Publi

Cómo empotrar datos dentro de un ejecutable hecho en C

photo-1453179592584-e2587867cfff

En nuestras andanzas como programadores, seguro que nos hemos encontrado alguna vez en esta situación. Tenemos un programa que vamos a distribuir, pero que tiene ciertos archivos asociados (imágenes, texto, scripts, etc) que deben ir junto con el programa.
En un primer momento podemos distribuir los archivos junto con el programa, y es una buena solución hasta que a alguien le da por cambiar esos archivos y consiguen que nuestro programa haga cosas diferentes a aquellas para las que ha sido pensado originalmente.

Esto se ha hecho durante años para almacenar este tipo de recursos, incluso algunos IDEs y compiladores lo hacen sin que nosotros hagamos nada. Pero somos valientes, y lo haremos con GCC.

Un poco más de contexto

Tal vez para imágenes no sea demasiado crítico, pero sí para código. Es decir, muchas veces, aunque hagamos un programa en C, éste a su vez tendrá fragmentos programados en LUA, Python, AngelScript, o incluso SQL. Y claro, si incluimos estos scripts en el código estamos forzando a recompilar el programa cuando hay un cambio en estos scripts, cosa que puede tardar mucho, y no es necesaria (lo mismo pasa con iconos, imágenes, etc).
Con recompilar el programa, me refiero a compilar un archivo .c o lo que es peor, realizar un cambio en un .h que se incluya en varias partes. Si habéis trabajado con algún programa grande, sabréis a lo que me refiero, un pequeño cambio puede suponer tener parado el ordenador varios minutos.

Lo que podemos hacer, es que en modo depuración, los recursos se lean desde los archivos externos, pero cuando el programa sea definitivo se lea de lo que tenemos almacenado en el ejecutable.

Un primer ejemplo

Vamos a empezar empotrando un pequeño texto dentro de un archivo ejecutable. Para ello, nos serviremos tanto de GCC como de objcopy. Es importante saber que este método sólo funcionará con GCC/G++, con otro compilador, debemos hacer las cosas de otra manera.
Lo primero será hacerlo con archivos de texto, para ello, crearemos un archivo llamado (pruebas.txt) y escribiremos dentro un texto.
Luego haremos este programa en C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

extern char _binary_pruebas_txt_start;
extern char _binary_pruebas_txt_end;

int main(int argc, char* argv[])
{
    char* c = &_binary_pruebas_txt_start;

    printf ("------------- Datos empotrados en mi ejecutable ----------\n");
    while (c != &_binary_pruebas_txt_end)
        printf("%c", *c++);
    printf ("--------- Fin de datos empotrados en mi ejecutable --------\n");
   
    return 0;
}

Ahora vamos a compilar el archivo pruebas.txt y el ejecutable:

objcopy --input binary --output elf64-x86-64 --binary-architecture i386 pruebas.txt pruebas.o
gcc -c main.c
gcc -o main main.o pruebas.o

Y listo, cuando lancemos ./main nos escribirá en pantalla el mensaje que tenemos configurado.

Eso sí, tenemos que tener especial cuidado con la arquitectura. Este ejemplo es para x86-64, si quisiéramos una salida para x86 en 32bit pondríamos elf32-i386.

Otro pequeño ejemplo con idea

Contamos con que no manipulen nuestro archivo ejecutable cambiando los datos que contiene, aunque podríamos introducir mecanismos como hacer que los primeros bytes sean un hash y cuando arranque nuestro programa comprobar que todo está bien. Pero este tipo de cosas puede servirnos para rellenar información en un struct, por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

extern char _binary_struct_data_start;
extern char _binary_struct_data_end;

struct TDatos
{
    uint32_t id;
    char url[100];
    char nombre[100];
};

int main(int argc, char* argv[])
{
    struct TDatos datos;
   
    char* c = &_binary_struct_data_start;
    memcpy(&datos, c, sizeof(struct TDatos));

    printf ("ID: %d\n", datos.id);
    printf ("URL: %s\n", datos.url);
    printf ("Nombre: %s\n", datos.nombre);
   
    return 0;
}

Eso sí, ahora tenemos que hacer un archivo binario en el que incluyamos:

  • 4 bytes (ID, un número, que tal vez en texto sea ilegible).
  • 100 bytes de texto (url, hay que tener cuidado y meter un terminador, 0x00 cuando termine el texto).
  • 100 bytes de texto (nombre, incluyendo un terminador como antes).

Podemos generar el fichero de datos de varias maneras: una de ellas sería creando otro programa en C que escriba los datos en un archivo binario, otra forma puede ser utilizar un editor hexadecimal, como el pantallazo que muestro a continuación (Sí, es Emacs como editor hexadecimal, que Emacs vale para todo):
Screenshot 12-09-2016-020913.
Luego llamamos a este archivo struct.data, creamos el objeto con objcopy y compilamos el programa enlazando el objeto. El resultado será algo como:

objcopy --input binary --output elf64-x86-64 --binary-architecture i386 struct.data struct.o
gcc -c main.c
gcc -o main main.o struct.o
./main
ID: 65
URL: https://poesiabinaria.net
Nombre: Poesía Binaria

Alternativa para depuración

Si queremos hacer, como dije al principio que el dato se coja de un archivo sólo si estamos en modo depuración y si no, se coja del ejecutable, debemos hacer algo como esto (es una idea, nada más, no está optimizado ni nada):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#define DEBUG 1
#define FREE_RESOURCE(res) if (DEBUG) free(res)
#define GET_RESOURCE(res, tam, resource_name)                                                       \
    {                                                                                                                                           \
        if ( (DEBUG) && (file_exists(# resource_name ".data")) )                        \
            tam = get_resource(&res, # resource_name ".data");                              \
        else                                                                                                                                \
            {                                                                                                                                   \
                res = & _binary_ ## resource_name ## _data_start;                               \
                tam = (& _binary_ ## resource_name ## _data_end) -                          \
                    (& _binary_ ## resource_name ## _data_start);                                   \
            }                                                                                                                                   \
    }


extern char _binary_text_data_start;
extern char _binary_text_data_end;

/* Existe el archivo ? */
short file_exists(char *filename)
{
    FILE* fd = fopen(filename, "r");
    if (fd)
        {
            fclose(fd);
            return 1;
        }
    else
        return 0;
}

size_t get_resource(char** res, char* filename)
{
    FILE *f = fopen(filename, "rb");
    printf ("%p\n", f);
    fseek(f, 0, SEEK_END);
    size_t fsize = ftell(f);
    fseek(f, 0, SEEK_SET);  //same as rewind(f);

    *res = malloc(fsize + 1);
    fread(*res, fsize, 1, f);
    fclose(f);

    (*res)[fsize] = 0;
    return fsize;
}

int main(int argc, char* argv[])
{
    char* data;
    long tam;
    GET_RESOURCE(data, tam, text);
    printf ("El recurso ocupa: %ld\n", tam);
    printf ("Datos: %s\n", data);
    FREE_RESOURCE(data);                    /* Sólo libera memoria cuando estamos en depuración */
   
    return 0;
}

Y el fichero texto.data podrá contener lo que queráis. En este caso veremos que cuando compilamos y DEBUG es 0, SIEMPRE cogeremos la información empotrada en el ejecutable, pero cuando DEBUG vale 1 y además el fichero texto.data existe, leeremos el fichero y cogeremos de ahí la información. Si cuando dejamos de utilizar el recurso llamamos a FREE_RESOURCE, liberaremos el puntero que se reserva en modo depuración, pero en real no hace nada.

Nota de la foto: ¿Por qué frutas? Considero trozos de datos o información a las frutas, y cada una de ellas debe ser empotrada en un ejecutable sin perder sus propiedades.

Foto: Roman Davayposmotrim

También podría interesarte....

Only 1 comment left Ir a comentario

  1. Pingback: Cómo empotrar datos dentro de un ejecutable hecho en C | PlanetaLibre /

Leave a Reply