C / C ++ com GCC: adicionar arquivos de recursos estaticamente ao executável / biblioteca

94

Alguém tem uma ideia de como compilar estaticamente qualquer arquivo de recurso no executável ou no arquivo de biblioteca compartilhada usando o GCC?

Por exemplo, gostaria de adicionar arquivos de imagem que nunca mudam (e se mudarem, eu teria que substituir o arquivo de qualquer maneira) e não quero que eles fiquem no sistema de arquivos.

Se isso for possível (e acho que é porque o Visual C ++ para Windows também pode fazer isso), como posso carregar os arquivos que estão armazenados no próprio binário? O executável analisa a si mesmo, encontra o arquivo e extrai os dados dele?

Talvez haja uma opção para o GCC que ainda não vi. O uso de mecanismos de pesquisa realmente não revelou as coisas certas.

Eu precisaria disso para funcionar com bibliotecas compartilhadas e executáveis ​​ELF normais.

Qualquer ajuda é apreciada

Atmocreations
fonte
3
Possível duplicata de stackoverflow.com/questions/1997172/…
blueberryfields
O link objcopy na questão apontada por blueberryfields também é uma boa solução genérica para isso
Flexo
@blueberryfields: desculpe por duplicar. Você está certo. Normalmente eu votaria para fechar como duplicata. Mas como todos eles postaram respostas tão boas, vou aceitar apenas uma.
Atmocreations 01 de
Posso acrescentar que o método de John Ripley é provavelmente o melhor aqui por um grande motivo - alinhamento. Se você fizer um objcopy padrão ou "ld -r -b binary -o foo.o foo.txt" e então olhar para o objeto resultante com objdump -x, parece que o alinhamento do bloco está definido como 0. Se você quiser alinhamento correto para dados binários diferentes de char, eu não posso imaginar que isso seja uma coisa boa.
carveone
1
possível duplicata de recursos
jww

Respostas:

49

Com imagemagick :

convert file.png data.h

Dá algo como:

/*
  data.h (PNM).
*/
static unsigned char
  MagickImage[] =
  {
    0x50, 0x36, 0x0A, 0x23, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x20, 
    0x77, 0x69, 0x74, 0x68, 0x20, 0x47, 0x49, 0x4D, 0x50, 0x0A, 0x32, 0x37, 
    0x37, 0x20, 0x31, 0x36, 0x32, 0x0A, 0x32, 0x35, 0x35, 0x0A, 0xFF, 0xFF, 
    0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 

....

Para compatibilidade com outro código, você pode usar fmemopenpara obter um FILE *objeto "normal" ou, alternativamente, std::stringstreampara fazer um iostream. std::stringstreamnão é ótimo para isso e é claro que você pode usar apenas um ponteiro em qualquer lugar em que possa usar um iterador.

Se você estiver usando com o automake, não se esqueça de definir BUILT_SOURCES apropriadamente.

O bom de fazer dessa maneira é:

  1. Você obtém o texto, para que possa estar no controle de versão e patches de forma sensata
  2. É portátil e bem definido em todas as plataformas
Flexo
fonte
2
Bleahg! Essa é a solução que pensei também. Por que alguém iria querer fazer isso está além de mim. Armazenar pedaços de dados em um namespace bem definido é para que servem os sistemas de arquivos.
Omnifário
35
Ocasionalmente, você tem um executável que é executado onde não há sistema de arquivos ou mesmo sistema operacional. Ou seu algoritmo precisa de alguma tabela pré-calculada para pesquisas. E tenho certeza de que há muito mais casos em que armazenar dados no programa faz muito sentido.
ndim 01 de
15
Este uso de convert é exatamente o mesmo quexxd -i infile.bin outfile.h
greyfade
5
Uma desvantagem dessa abordagem é que alguns compiladores não podem lidar com matrizes estáticas enormes, se suas imagens forem particularmente grandes; a maneira de contornar isso é, como sugere ndim , usarobjcopy para converter os dados binários diretamente em um arquivo de objeto; no entanto, isso raramente é uma preocupação.
Adam Rosenfield
3
Lembre-se de que defini-lo em um cabeçalho como este significa que cada arquivo que o incluir terá sua própria cópia. É melhor declará-lo no cabeçalho como externo e então defini-lo em um cpp. Exemplo aqui
Nicholas Smith
90

Atualização Passei a preferir o controle que a .incbinsolução baseada em montagem de John Ripley oferece e agora uso uma variante disso.

Usei objcopy (GNU binutils) para vincular os dados binários de um arquivo foo-data.bin à seção de dados do executável:

objcopy -B i386 -I binary -O elf32-i386 foo-data.bin foo-data.o

Isso fornece um foo-data.oarquivo de objeto que você pode vincular ao seu executável. A interface C parece algo como

/** created from binary via objcopy */
extern uint8_t foo_data[]      asm("_binary_foo_data_bin_start");
extern uint8_t foo_data_size[] asm("_binary_foo_data_bin_size");
extern uint8_t foo_data_end[]  asm("_binary_foo_data_bin_end");

então você pode fazer coisas como

for (uint8_t *byte=foo_data; byte<foo_data_end; ++byte) {
    transmit_single_byte(*byte);
}

ou

size_t foo_size = (size_t)((void *)foo_data_size);
void  *foo_copy = malloc(foo_size);
assert(foo_copy);
memcpy(foo_copy, foo_data, foo_size);

Se sua arquitetura de destino tem restrições especiais quanto ao local onde os dados constantes e variáveis ​​são armazenados, ou você deseja armazenar esses dados no .textsegmento para fazê-los caber no mesmo tipo de memória que seu código de programa, você pode brincar um objcopypouco mais com os parâmetros.

ndim
fonte
boa ideia! No meu caso não é muito útil. Mas isso é algo que realmente vou colocar na minha coleção de fragmentos. Obrigado por compartilhar isso!
Atmocreations 01 de
2
É um pouco mais fácil de usar, ldpois o formato de saída está implícito aqui, consulte stackoverflow.com/a/4158997/201725 .
Jan Hudec,
52

Você pode incorporar arquivos binários em executáveis ​​usando o ldvinculador. Por exemplo, se você tiver um arquivo foo.bar, poderá incorporá-lo ao executável adicionando os seguintes comandos aold

--format=binary foo.bar --format=default

Se você estiver invocando ldatravés, gccentão você precisará adicionar-Wl

-Wl,--format=binary -Wl,foo.bar -Wl,--format=default

Aqui, --format=binaryinforma ao vinculador que o arquivo a seguir é binário e --format=defaultvolta ao formato de entrada padrão (isso é útil se você especificar outros arquivos de entrada depois foo.bar).

Em seguida, você pode acessar o conteúdo do seu arquivo a partir do código:

extern uint8_t data[]     asm("_binary_foo_bar_start");
extern uint8_t data_end[] asm("_binary_foo_bar_end");

Há também um símbolo denominado "_binary_foo_bar_size". Acho que é do tipo, uintptr_tmas não verifiquei.

Simon
fonte
Comentário muito interessante. Obrigado por compartilhar isso!
Atmocreations
1
Agradável! Apenas uma pergunta: por que é data_endum array, não um ponteiro? (Ou isso é idiomático C?)
xtofl
2
@xtofl, se data_endfor um ponteiro, o compilador pensará que há um ponteiro armazenado após o conteúdo do arquivo. Similarmente, se você mudar o tipo de datapara um ponteiro, então você obterá um ponteiro consistindo dos primeiros bytes de um arquivo em vez de um ponteiro para seu início. Acho que sim.
Simon
1
+1: Sua resposta me permite incorporar um carregador de classes java e um Jar em um exe para construir um lançador Java personalizado
Aubin
2
@xtofl - Se você pretende torná-lo um ponteiro, torne-o um const pointer. O compilador permite que você altere o valor de ponteiros não constantes, mas não permite que você altere o valor se for um array. Portanto, talvez seja menos digitado usar a sintaxe de array.
Jesse Chisholm
40

Você pode colocar todos os seus recursos em um arquivo ZIP e anexá-lo ao final do arquivo executável :

g++ foo.c -o foo0
zip -r resources.zip resources/
cat foo0 resources.zip >foo

Isso funciona porque a) os formatos de imagem mais executáveis ​​não se importam se há dados extras por trás da imagem eb) o zip armazena a assinatura do arquivo no final do arquivo zip . Isso significa que seu executável é um arquivo zip regular depois disso (exceto para o executável inicial, que pode ser gerenciado pelo zip), que pode ser aberto e lido com libzip.

Nordic Mainframe
fonte
7
Se eu quiser juntar foo0 e resources.zip em foo, então preciso> se eu fornecer as duas entradas na linha de comando de cat. (porque não quero acrescentar ao que já está em foo)
Nordic Mainframe
1
ah sim, erro meu. Não localizei o 0 ali no nome corretamente na minha primeira leitura
Flexo
Isso é muito inteligente. +1.
Linuxios
1
+1 Maravilhoso, especialmente quando combinado com miniz
mvp de
Isso produzirá um binário inválido (pelo menos no Mac e Linux), que não pode ser processado por ferramentas como install_name_tool. Além disso, o binário ainda funciona como executável.
Andy Li
36

De http://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967 :

Recentemente, tive a necessidade de inserir um arquivo em um executável. Já que estou trabalhando na linha de comando com gcc, et al e não com uma ferramenta RAD sofisticada que faz tudo acontecer magicamente, não foi imediatamente óbvio para mim como fazer isso acontecer. Um pouco de pesquisa na rede encontrou um hack para basicamente catá-lo no final do executável e, em seguida, decifrar onde ele estava baseado em um monte de informações que eu não queria saber. Parecia que deveria haver uma maneira melhor ...

E há, é objcopy para o resgate. objcopy converte arquivos de objeto ou executáveis ​​de um formato para outro. Um dos formatos que ele entende é "binário", que basicamente é qualquer arquivo que não esteja em um dos outros formatos que ele entende. Então, você provavelmente teve a ideia: converter o arquivo que queremos incorporar em um arquivo de objeto, então ele pode simplesmente ser vinculado ao resto do nosso código.

Digamos que temos um arquivo de nome data.txt que queremos incorporar em nosso executável:

# cat data.txt
Hello world

Para converter isso em um arquivo objeto que podemos vincular ao nosso programa, usamos apenas objcopy para produzir um arquivo ".o":

# objcopy --input binary \
--output elf32-i386 \
--binary-architecture i386 data.txt data.o

Isso diz ao objcopy que nosso arquivo de entrada está no formato "binário", que nosso arquivo de saída deve estar no formato "elf32-i386" (arquivos de objeto no x86). A opção --binary-architecture diz ao objcopy que o arquivo de saída deve "rodar" em um x86. Isso é necessário para que o ld aceite o arquivo para vinculação com outros arquivos do x86. Alguém poderia pensar que especificar o formato de saída como "elf32-i386" implicaria isso, mas não é.

Agora que temos um arquivo-objeto, só precisamos incluí-lo quando executamos o vinculador:

# gcc main.c data.o

Quando executamos o resultado, obtemos o resultado esperado:

# ./a.out
Hello world

Claro, eu não contei a história toda ainda, nem mostrei a você main.c. Quando objcopy faz a conversão acima, ele adiciona alguns símbolos de "vinculador" ao arquivo de objeto convertido:

_binary_data_txt_start
_binary_data_txt_end

Após a vinculação, esses símbolos especificam o início e o fim do arquivo incorporado. Os nomes dos símbolos são formados por binários precedentes e do _start ou _end ao nome do arquivo. Se o nome do arquivo contiver quaisquer caracteres que seriam inválidos em um nome de símbolo, eles serão convertidos em sublinhados (por exemplo, data.txt torna-se data_txt). Se você obtiver nomes não resolvidos ao vincular usando esses símbolos, faça um hexdump -C no arquivo de objeto e procure no final do dump os nomes escolhidos por objcopy.

O código para realmente usar o arquivo incorporado agora deve ser razoavelmente óbvio:

#include <stdio.h>

extern char _binary_data_txt_start;
extern char _binary_data_txt_end;

main()
{
    char*  p = &_binary_data_txt_start;

    while ( p != &_binary_data_txt_end ) putchar(*p++);
}

Uma coisa importante e sutil a notar é que os símbolos adicionados ao arquivo de objeto não são "variáveis". Eles não contêm nenhum dado, em vez disso, seu endereço é seu valor. Eu os declaro como tipo char porque é conveniente para este exemplo: os dados incorporados são dados de caracteres. No entanto, você pode declará-los como qualquer coisa, como int se os dados forem um array de inteiros, ou como struct foo_bar_t se os dados forem qualquer array de foo bars. Se os dados embutidos não forem uniformes, então char é provavelmente o mais conveniente: pegue seu endereço e lance o ponteiro para o tipo apropriado conforme você percorre os dados.

Hazok
fonte
36

Se você deseja controlar o nome exato do símbolo e a localização dos recursos, pode usar (ou fazer um script) o assembler GNU (que não faz parte do gcc) para importar arquivos binários inteiros. Experimente isto:

Montagem (x86 / braço):

    .section .rodata

    .global thing
    .type   thing, @object
    .balign 4
thing:
    .incbin "meh.bin"
thing_end:

    .global thing_size
    .type   thing_size, @object
    .balign 4
thing_size:
    .int    thing_end - thing

C:

#include <stdio.h>

extern const char thing[];
extern const unsigned thing_size;

int main() {
  printf("%p %u\n", thing, thing_size);
  return 0;
}

O que quer que você use, provavelmente é melhor fazer um script para gerar todos os recursos e ter nomes de símbolo bonitos / uniformes para tudo.

Dependendo de seus dados e das especificações do sistema, você pode precisar usar diferentes valores de alinhamento (de preferência com .balignpara portabilidade), ou tipos inteiros de um tamanho diferente para thing_size, ou um tipo de elemento diferente para a thing[]matriz.

John Ripley
fonte
obrigado por compartilhar! definitivamente parece interessante, mas desta vez não é o que estou procurando =) saudações
Atmocreations
1
Exatamente o que eu estava procurando. Talvez você possa verificar se também está ok para arquivos com tamanhos não divisíveis por 4. Parece que thing_size incluirá os bytes de preenchimento extras.
Pavel P
E se eu quiser que algo seja um símbolo local? Provavelmente posso catar a saída do compilador junto com meu próprio assembly, mas existe uma maneira melhor?
user877329
Para registro: Minha edição aborda o problema dos bytes de preenchimento extras que @Pavel observou.
ndim
4

Lendo todos os post aqui e na Internet cheguei à conclusão que não existe uma ferramenta de recursos, que é:

1) Fácil de usar no código.

2) Automatizado (para ser facilmente incluído no cmake / make).

3) Plataforma cruzada.

Decidi escrever a ferramenta sozinho. O código está disponível aqui. https://github.com/orex/cpp_rsc

É muito fácil usá-lo com o cmake.

Você deve adicionar esse código ao seu arquivo CMakeLists.txt.

file(DOWNLOAD https://raw.github.com/orex/cpp_rsc/master/cmake/modules/cpp_resource.cmake ${CMAKE_BINARY_DIR}/cmake/modules/cpp_resource.cmake) 

set(CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR}/cmake/modules)

include(cpp_resource)

find_resource_compiler()
add_resource(pt_rsc) #Add target pt_rsc
link_resource_file(pt_rsc FILE <file_name1> VARIABLE <variable_name1> [TEXT]) #Adds resource files
link_resource_file(pt_rsc FILE <file_name2> VARIABLE <variable_name2> [TEXT])

...

#Get file to link and "resource.h" folder
#Unfortunately it is not possible with CMake add custom target in add_executable files list.
get_property(RSC_CPP_FILE TARGET pt_rsc PROPERTY _AR_SRC_FILE)
get_property(RSC_H_DIR TARGET pt_rsc PROPERTY _AR_H_DIR)

add_executable(<your_executable> <your_source_files> ${RSC_CPP_FILE})

O exemplo real, usando a abordagem, pode ser baixado aqui, https://bitbucket.org/orex/periodic_table

user2794512
fonte
Acho que sua resposta precisa de uma explicação melhor para se tornar útil para mais pessoas.
kyb