C / C ++ dengan GCC: Secara statis menambahkan file resource ke executable / library

94

Apakah ada yang punya ide bagaimana mengkompilasi file sumber daya secara statis ke file executable atau shared library menggunakan GCC?

Misalnya, saya ingin menambahkan file gambar yang tidak pernah berubah (dan jika berubah, saya harus tetap mengganti file tersebut) dan tidak ingin file tersebut berada di sekitar sistem file.

Jika ini memungkinkan (dan saya pikir itu karena Visual C ++ untuk Windows juga dapat melakukan ini), bagaimana cara memuat file yang disimpan dalam biner sendiri? Apakah file yang dapat dieksekusi mengurai sendiri, menemukan file, dan mengekstrak datanya?

Mungkin ada opsi untuk GCC yang belum saya lihat. Menggunakan mesin pencari tidak benar-benar menghasilkan hal yang benar.

Saya akan membutuhkan ini untuk bekerja untuk perpustakaan bersama dan ELF-executable normal.

Bantuan apa pun dihargai

Atmocreations
sumber
3
Kemungkinan duplikat stackoverflow.com/questions/1997172/…
blueberryfields
Tautan obyektif dalam pertanyaan yang ditunjuk blueberryfields adalah solusi umum yang bagus untuk ini juga
Flexo
@blueberryfields: maaf telah menduplikasi. Kamu benar. Biasanya saya akan memilih hampir sebagai duplikat. Tetapi karena mereka semua memposting jawaban yang sangat bagus, saya hanya akan menerimanya.
Atmocreations
Dapatkah saya menambahkan bahwa metode John Ripley mungkin yang terbaik di sini karena satu alasan besar - penyelarasan. Jika Anda melakukan objcopy standar atau "ld -r -b binary -o foo.o foo.txt" dan kemudian melihat objek yang dihasilkan dengan objdump -x, sepertinya perataan blok disetel ke 0. Jika Anda mau penyelarasan menjadi benar untuk data biner selain char, saya tidak bisa membayangkan ini adalah hal yang baik.
mengukir

Jawaban:

49

Dengan imagemagick :

convert file.png data.h

Memberikan sesuatu seperti:

/*
  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, 

....

Untuk kompatibilitas dengan kode lain, Anda kemudian dapat menggunakan baik fmemopenuntuk mendapatkan objek "biasa" FILE *, atau sebagai alternatif std::stringstreamuntuk membuat iostream. std::stringstreamtidak bagus untuk ini meskipun dan Anda tentu saja dapat menggunakan penunjuk di mana pun Anda dapat menggunakan iterator.

Jika Anda menggunakan ini dengan automake, jangan lupa untuk menyetel BUILT_SOURCES dengan tepat.

Hal yang menyenangkan tentang melakukannya dengan cara ini adalah:

  1. Anda mendapatkan teks keluar, sehingga dapat di kontrol versi dan patch dengan bijaksana
  2. Ini portabel dan didefinisikan dengan baik di setiap platform
Flexo
sumber
2
Bleahg! Itu solusi yang saya pikirkan juga. Mengapa ada orang yang ingin melakukan ini di luar kemampuan saya. Menyimpan potongan data dalam namespace yang didefinisikan dengan baik adalah untuk apa sistem file itu.
Omnifarious
35
Terkadang, Anda memiliki file yang dapat dijalankan yang berjalan di mana tidak ada sistem file, atau bahkan tidak ada sistem operasi. Atau algoritme Anda memerlukan beberapa tabel yang telah dihitung sebelumnya untuk pencarian. Dan saya yakin ada lebih banyak kasus ketika menyimpan data dalam program sangat masuk akal.
ndim
15
Penggunaan konversi ini persis sama denganxxd -i infile.bin outfile.h
greyfade
5
Satu kelemahan dari pendekatan ini adalah bahwa beberapa kompiler tidak dapat menangani array statis yang begitu besar, jika gambar Anda sangat besar; cara untuk menyiasatinya adalah, seperti yang disarankan ndim , digunakan objcopyuntuk mengonversi data biner secara langsung ke file objek; namun hal ini jarang menjadi perhatian.
Adam Rosenfield
3
Perlu diingat bahwa mendefinisikannya di header seperti ini berarti bahwa setiap file yang menyertakannya akan mendapatkan salinannya sendiri. Lebih baik mendeklarasikannya di header sebagai extern dan kemudian mendefinisikannya di cpp. Contoh di sini
Nicholas Smith
90

Perbarui Saya telah tumbuh untuk lebih memilih kontrol penawaran solusi berbasis perakitan John Ripley.incbin dan sekarang menggunakan varian untuk itu.

Saya telah menggunakan objcopy (GNU binutils) untuk menautkan data biner dari file foo-data.bin ke bagian data yang dapat dieksekusi:

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

Ini memberi Anda foo-data.ofile objek yang dapat Anda tautkan ke file yang dapat dieksekusi. Antarmuka C terlihat seperti ini

/** 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");

sehingga Anda dapat melakukan hal-hal seperti

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

atau

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);

Jika arsitektur target Anda memiliki batasan khusus tentang tempat penyimpanan data konstan dan variabel, atau Anda ingin menyimpan data tersebut di .textsegmen agar sesuai dengan jenis memori yang sama dengan kode program Anda, Anda dapat bermain-main dengan objcopyparameter lagi.

ndim
sumber
ide bagus! Dalam kasus saya itu tidak terlalu berguna. Tapi ini adalah sesuatu yang benar-benar akan saya masukkan ke dalam koleksi potongan saya. Terima kasih telah membagikan ini!
Atmocations
2
Ini sedikit lebih mudah digunakan ldkarena format output tersirat di sana, lihat stackoverflow.com/a/4158997/201725 .
Jan Hudec
52

Anda dapat menyematkan file biner dalam file yang dapat dieksekusi menggunakan ldlinker. Misalnya, jika Anda memiliki file foo.barmaka Anda dapat menyematkannya di executable menambahkan perintah berikut keld

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

Jika Anda memohon ldmelalui gccmaka Anda perlu menambahkan-Wl

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

Di sini --format=binarymemberi tahu linker bahwa file berikut adalah biner dan --format=defaultberalih kembali ke format input default (ini berguna jika Anda akan menentukan file input lain setelahnya foo.bar).

Kemudian Anda dapat mengakses konten file Anda dari kode:

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

Ada juga simbol bernama "_binary_foo_bar_size". Saya pikir itu adalah tipe uintptr_ttetapi saya tidak memeriksanya.

Simon
sumber
Komentar yang sangat menarik. Terima kasih telah membagikan ini!
Atmocreations
1
Bagus! Hanya satu pertanyaan: mengapa data_endarray, bukan pointer? (Atau apakah ini C idiomatik?)
xtofl
2
@xtofl, jika data_endakan menjadi pointer maka compiler akan berpikir bahwa ada pointer yang disimpan setelah konten file. Sama halnya, jika Anda akan mengubah tipe datamenjadi pointer maka Anda akan mendapatkan pointer yang terdiri dari byte pertama dari sebuah file, bukan pointer ke awalnya. Aku pikir begitu.
Simon
1
+1: Jawaban Anda memungkinkan saya menyematkan pemuat kelas java dan Jar ke dalam exe untuk membuat peluncur java khusus
Aubin
2
@xtofl - Jika Anda ingin menjadikannya pointer, jadikan sebagai const pointer. Kompiler memungkinkan Anda mengubah nilai non-const pointer, tidak memungkinkan Anda mengubah nilai jika itu adalah array. Jadi mungkin kurang mengetik untuk menggunakan sintaks array.
Jesse Chisholm
40

Anda dapat memasukkan semua sumber daya Anda ke dalam file ZIP dan menambahkannya ke akhir file yang dapat dieksekusi :

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

Ini berfungsi, karena a) Sebagian besar format gambar yang dapat dieksekusi tidak peduli jika ada data tambahan di belakang gambar dan b) zip menyimpan tanda tangan file di akhir file zip . Artinya, file yang dapat dieksekusi adalah file zip biasa setelah ini (kecuali untuk file yang dapat dieksekusi dimuka, yang dapat ditangani oleh zip), yang dapat dibuka dan dibaca dengan libzip.

Mainframe Nordik
sumber
7
Jika saya ingin menggabungkan foo0 dan resources.zip ke foo, maka saya perlu> jika saya memberikan kedua input pada baris perintah cat. (karena saya tidak ingin menambahkan apa yang sudah ada di foo)
Nordic Mainframe
1
ah ya, kesalahanku. Saya tidak menemukan angka 0 di nama dengan benar pada pembacaan pertama saya
Flexo
Ini sangat pintar. +1.
Linuxios
1
+1 Luar biasa, terutama jika dipasangkan dengan miniz
mvp
Ini akan menghasilkan biner yang tidak valid (setidaknya di Mac dan Linux), yang tidak dapat diproses oleh alat seperti install_name_tool. Selain itu, biner masih berfungsi sebagai executable.
Andy Li
36

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

Saya baru-baru ini perlu menyematkan file dalam file yang dapat dieksekusi. Karena saya bekerja pada baris perintah dengan gcc, dkk dan bukan dengan alat RAD mewah yang membuat semuanya terjadi secara ajaib, tidak langsung jelas bagi saya bagaimana membuat ini terjadi. Sedikit pencarian di internet menemukan peretasan yang pada dasarnya memasukkannya ke akhir eksekusi dan kemudian menguraikan di mana itu didasarkan pada banyak informasi yang tidak ingin saya ketahui. Sepertinya harus ada cara yang lebih baik ...

Dan ada, itu tujuan penyelamatan. objcopy mengubah file objek atau executable dari satu format ke format lainnya. Salah satu format yang dipahami adalah "biner", yang pada dasarnya adalah file apa pun yang tidak ada dalam salah satu format lain yang dipahami. Jadi, Anda mungkin membayangkan idenya: ubah file yang ingin kita sematkan menjadi file objek, lalu dengan mudah dapat ditautkan dengan kode kita yang lain.

Katakanlah kita memiliki nama file data.txt yang ingin kita sematkan di file yang dapat dieksekusi:

# cat data.txt
Hello world

Untuk mengubahnya menjadi file objek yang dapat kita tautkan dengan program kita, kita hanya menggunakan objcopy untuk menghasilkan file ".o":

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

Ini memberitahu objcopy bahwa file masukan kita dalam format "biner", bahwa file keluaran kita harus dalam format "elf32-i386" (file objek pada x86). Opsi --binary-architecture memberi tahu objcopy bahwa file output dimaksudkan untuk "dijalankan" pada x86. Ini diperlukan agar ld menerima file untuk ditautkan dengan file lain untuk x86. Orang akan berpikir bahwa menentukan format keluaran sebagai "elf32-i386" akan menyiratkan hal ini, tetapi sebenarnya tidak.

Sekarang kita memiliki file objek, kita hanya perlu memasukkannya saat kita menjalankan linker:

# gcc main.c data.o

Ketika kami menjalankan hasilnya, kami mendapatkan output yang didoakan:

# ./a.out
Hello world

Tentu saja, saya belum menceritakan keseluruhan cerita, atau menunjukkan main.c. Ketika objcopy melakukan konversi di atas, ia menambahkan beberapa simbol "linker" ke file objek yang dikonversi:

_binary_data_txt_start
_binary_data_txt_end

Setelah menautkan, simbol-simbol ini menentukan awal dan akhir file yang disematkan. Nama simbol dibentuk dengan memasukkan biner dan menambahkan _start atau _end ke nama file. Jika nama file berisi karakter apa pun yang tidak valid dalam nama simbol, maka akan diubah menjadi garis bawah (misalnya data.txt menjadi data_txt). Jika Anda mendapatkan nama yang belum terselesaikan saat menautkan menggunakan simbol ini, lakukan hexdump -C pada file objek dan lihat di akhir dump untuk nama yang dipilih objcopy.

Kode untuk benar-benar menggunakan file yang disematkan sekarang seharusnya sudah cukup jelas:

#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++);
}

Satu hal penting dan halus untuk diperhatikan adalah bahwa simbol yang ditambahkan ke file objek bukanlah "variabel". Mereka tidak berisi data apa pun, melainkan alamat mereka adalah nilainya. Saya mendeklarasikannya sebagai tipe char karena nyaman untuk contoh ini: data yang disematkan adalah data karakter. Namun, Anda bisa mendeklarasikannya sebagai apa saja, seperti int jika datanya adalah larik bilangan bulat, atau sebagai struct foo_bar_t jika datanya adalah larik dari bilah foo. Jika data yang disematkan tidak seragam, maka char mungkin yang paling nyaman: ambil alamatnya dan berikan penunjuk ke jenis yang tepat saat Anda melintasi data.

Hazok
sumber
36

Jika Anda ingin mengontrol nama simbol yang tepat dan penempatan sumber daya, Anda dapat menggunakan (atau skrip) GNU assembler (bukan bagian dari gcc) untuk mengimpor seluruh file biner. Coba ini:

Perakitan (x86 / lengan):

    .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;
}

Apa pun yang Anda gunakan, mungkin yang terbaik adalah membuat skrip untuk menghasilkan semua sumber daya, dan memiliki nama simbol yang bagus / seragam untuk semuanya.

Bergantung pada data Anda dan spesifikasi sistem, Anda mungkin perlu menggunakan nilai penyelarasan yang berbeda (sebaiknya dengan .balignuntuk portabilitas), atau tipe bilangan bulat dengan ukuran yang berbeda thing_size, atau tipe elemen yang berbeda untuk thing[]larik.

John Ripley
sumber
Terima kasih telah berbagi! jelas terlihat menarik, tapi kali ini bukan yang saya cari =) salam
Atmocreations
1
Persis apa yang saya cari. Mungkin Anda dapat memverifikasi bahwa tidak masalah untuk file dengan ukuran yang tidak dapat diubah oleh 4. Sepertinya thing_size akan menyertakan tambahan padding byte.
Pavel P
Bagaimana jika saya ingin benda menjadi simbol lokal? Saya mungkin dapat menggabungkan keluaran kompiler dengan perakitan saya sendiri tetapi apakah ada cara yang lebih baik?
pengguna877329
Sebagai catatan: Hasil edit saya membahas masalah byte padding ekstra yang dicatat @Pavel.
ndim
4

Membaca semua posting di sini dan di Internet saya telah membuat kesimpulan bahwa tidak ada alat untuk sumber daya, yaitu:

1) Mudah digunakan dalam kode.

2) Otomatis (agar mudah dimasukkan dalam cmake / make).

3) Lintas platform.

Saya telah memutuskan untuk menulis alat itu sendiri. Kode tersedia di sini. https://github.com/orex/cpp_rsc

Untuk menggunakannya dengan cmake sangatlah mudah.

Anda harus menambahkan kode tersebut ke file 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})

Contoh nyata, menggunakan pendekatan tersebut dapat diunduh di sini, https://bitbucket.org/orex/periodic_table

pengguna2794512
sumber
Saya pikir jawaban Anda membutuhkan penjelasan yang lebih baik agar berguna bagi lebih banyak orang.
kyb