C / C ++ với GCC: Thêm tĩnh các tệp tài nguyên vào thư viện / tệp thực thi


94

Có ai có ý tưởng về cách biên dịch tĩnh bất kỳ tệp tài nguyên nào thành tệp thực thi hoặc tệp thư viện được chia sẻ bằng GCC không?

Ví dụ: tôi muốn thêm các tệp hình ảnh không bao giờ thay đổi (và nếu có, tôi vẫn phải thay thế tệp) và không muốn chúng nằm xung quanh trong hệ thống tệp.

Nếu điều này là có thể (và tôi nghĩ đó là do Visual C ++ dành cho Windows cũng có thể làm điều này), làm cách nào để tải các tệp được lưu trữ trong tệp nhị phân của riêng mình? Tập tin thực thi có tự phân tích cú pháp, tìm tệp và trích xuất dữ liệu ra khỏi nó không?

Có lẽ có một tùy chọn cho GCC mà tôi chưa thấy. Sử dụng công cụ tìm kiếm không thực sự tìm ra những thứ phù hợp.

Tôi sẽ cần điều này để hoạt động cho các thư viện được chia sẻ và các tệp thực thi ELF bình thường.

Mọi sự giúp đỡ đều được đánh giá cao



Liên kết đối tượng trong câu hỏi mà blueberryfields đã chỉ đến cũng là một giải pháp tốt, chung cho vấn đề này
Flexo

@blueberryfields: xin lỗi vì đã sao chép. Bạn đúng. Thông thường, tôi sẽ bỏ phiếu cho gần như là trùng lặp. Nhưng vì tất cả họ đều đăng những câu trả lời rất hay nên tôi chỉ chấp nhận một câu.
Atmocreations

Tôi có thể nói thêm rằng phương pháp của John Ripley có lẽ là phương pháp tốt nhất ở đây vì một lý do rất lớn - sự liên kết. Nếu bạn thực hiện đối tượng tiêu chuẩn hoặc "ld -r -b binary -o foo.o foo.txt" và sau đó nhìn vào đối tượng kết quả với objdump -x, có vẻ như căn chỉnh cho khối được đặt thành 0. Nếu bạn muốn căn chỉnh để được chính xác cho dữ liệu nhị phân khác với char, tôi không thể tưởng tượng đây là một điều tốt.
carveone

1
có thể có bản sao của các tài nguyên Nhúng trong .exe bằng GCC
jww

Câu trả lời:


49

Với imagemagick :

convert file.png data.h

Cung cấp một cái gì đó như:

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

....

Để tương thích với mã khác, bạn có thể sử dụng fmemopenđể lấy FILE *đối tượng "thông thường" hoặc cách khác std::stringstreamđể tạo iostream. std::stringstreamkhông phải là tuyệt vời cho điều này mặc dù và tất nhiên bạn có thể chỉ cần sử dụng một con trỏ ở bất kỳ nơi nào bạn có thể sử dụng trình vòng lặp.

Nếu bạn đang sử dụng tính năng này với automake, đừng quên đặt BUILT_SOURCES thích hợp.

Điều tốt đẹp khi làm theo cách này là:

  1. Bạn nhận được văn bản, vì vậy nó có thể nằm trong kiểm soát phiên bản và các bản vá lỗi một cách hợp lý
  2. Nó có tính di động và được xác định rõ ràng trên mọi nền tảng

2
Chà! Đó là giải pháp mà tôi cũng nghĩ ra. Tại sao mọi người lại muốn làm điều này ngoài tôi. Lưu trữ các phần dữ liệu trong một không gian tên được xác định rõ ràng là mục đích của hệ thống tệp.
Omnifarious

35
Đôi khi, bạn có một tệp thực thi chạy ở nơi không có hệ thống tệp hoặc thậm chí không có hệ điều hành. Hoặc thuật toán của bạn cần một số bảng tính toán trước để tra cứu. Và tôi chắc chắn rằng có rất nhiều trường hợp khi lưu trữ dữ liệu trong chương trình có ý nghĩa rất nhiều .
ndim

15
Việc sử dụng quy đổi này giống hệt nhưxxd -i infile.bin outfile.h
greyfade

5
Một nhược điểm của cách tiếp cận này là một số trình biên dịch không thể xử lý các mảng tĩnh khổng lồ như vậy, nếu hình ảnh của bạn đặc biệt lớn; cách để giải quyết vấn đề đó, như ndim gợi ý, sử dụng objcopyđể chuyển đổi dữ liệu nhị phân trực tiếp sang tệp đối tượng; tuy nhiên điều này hiếm khi là một mối quan tâm.
Adam Rosenfield

3
Hãy nhớ rằng việc xác định nó trong tiêu đề như thế này có nghĩa là mỗi tệp bao gồm nó sẽ nhận được bản sao của chính nó. Tốt hơn là khai báo nó trong tiêu đề là extern và sau đó định nghĩa nó trong một cpp. Ví dụ ở đây
Nicholas Smith

90

Cập nhật Tôi đã phát triển để thích kiểm soát giải pháp dựa trên lắp ráp của John Ripley.incbin cung cấp và bây giờ sử dụng một biến thể trên đó.

Tôi đã sử dụng objcopy (GNU binutils) để liên kết dữ liệu nhị phân từ tệp foo-data.bin vào phần dữ liệu của tệp thực thi:

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

Điều này cung cấp cho bạn một foo-data.otệp đối tượng mà bạn có thể liên kết vào tệp thực thi của mình. Giao diện C trông giống như

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

vì vậy bạn có thể làm những thứ như

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

hoặc là

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

Nếu kiến ​​trúc đích của bạn có các ràng buộc đặc biệt về nơi lưu trữ dữ liệu hằng và biến hoặc bạn muốn lưu trữ dữ liệu đó trong .textphân đoạn để làm cho nó phù hợp với cùng loại bộ nhớ với mã chương trình của mình, bạn có thể thử với các objcopytham số khác.


ý tưởng tốt! Trong trường hợp của tôi, nó không hữu ích lắm. Nhưng đây là thứ mà tôi thực sự sẽ đưa vào bộ sưu tập đoạn mã của mình. Cảm ơn vì đã chia sẻ điều này!
Atmocreations

2
Nó dễ sử dụng hơn một chút ldvì định dạng đầu ra được ngụ ý ở đó, hãy xem stackoverflow.com/a/4158997/201725 .
Jan Hudec

52

Bạn có thể nhúng tệp nhị phân vào tệp thực thi bằng ldtrình liên kết. Ví dụ: nếu bạn có tệp foo.barthì bạn có thể nhúng tệp đó vào tệp thực thi bằng cách thêm các lệnh sau vàold

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

Nếu bạn đang gọi ldthông qua gccthì bạn sẽ cần thêm-Wl

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

Ở đây --format=binarycho trình liên kết biết rằng tệp sau đây là tệp nhị phân và --format=defaultchuyển trở lại định dạng đầu vào mặc định (điều này hữu ích nếu bạn sẽ chỉ định các tệp đầu vào khác sau đó foo.bar).

Sau đó, bạn có thể truy cập nội dung tệp của mình từ mã:

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

Có cả biểu tượng được đặt tên "_binary_foo_bar_size". Tôi nghĩ nó thuộc loại uintptr_tnhưng tôi đã không kiểm tra nó.


Nhận xét rất thú vị. Cảm ơn vì đã chia sẻ điều này!
Atmocreations

1
Đẹp quá! Chỉ một câu hỏi: tại sao lại là data_endmảng, không phải là con trỏ? (Hoặc là C thành ngữ này?)
xtofl

2
@xtofl, nếu data_endsẽ là một con trỏ thì trình biên dịch sẽ nghĩ rằng có một con trỏ được lưu trữ sau nội dung tệp. Tương tự, nếu bạn thay đổi kiểu của datamột con trỏ thì bạn sẽ nhận được con trỏ bao gồm các byte đầu tiên của tệp thay vì con trỏ đến đầu của nó. Tôi nghĩ vậy.
Simon

1
+1: Câu trả lời của bạn cho phép tôi nhúng một trình tải lớp java và một Jar vào một exe để xây dựng một trình khởi chạy java tùy chỉnh
Aubin

2
@xtofl - Nếu bạn định biến nó thành một con trỏ, hãy biến nó thành a const pointer. Trình biên dịch cho phép bạn thay đổi giá trị của các con trỏ không phải const, nó không cho phép bạn thay đổi giá trị nếu nó là một mảng. Vì vậy, có lẽ ít phải gõ hơn để sử dụng cú pháp mảng.
Jesse Chisholm

40

Bạn có thể đặt tất cả tài nguyên của mình vào tệp ZIP và nối tài nguyên đó vào cuối tệp thực thi :

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

Điều này hoạt động, bởi vì a) Hầu hết các định dạng hình ảnh thực thi không quan tâm nếu có thêm dữ liệu đằng sau hình ảnh và b) zip lưu trữ chữ ký tệp ở cuối tệp zip . Điều này có nghĩa là tệp thực thi của bạn là tệp zip thông thường sau tệp này (ngoại trừ tệp thực thi trả trước của bạn, tệp zip có thể xử lý), tệp này có thể được mở và đọc bằng libzip.


7
Nếu tôi muốn nối foo0 và resources.zip vào foo, thì tôi cần> nếu tôi cung cấp cả hai đầu vào trên dòng lệnh cat. (bởi vì tôi không muốn thêm vào những gì đã có trong foo)
Nordic Mainframe

1
à vâng, sai lầm của tôi. Tôi đã không phát hiện đúng số 0 ở đó trong tên trong lần đọc đầu tiên của tôi
Flexo

Điều này rất thông minh. +1.
Linuxios

1
1 Wonderful, đặc biệt là khi kết hợp với miniz
MVP

Điều này sẽ tạo ra một tệp nhị phân không hợp lệ (ít nhất là trên Mac và Linux), không thể được xử lý bằng các công cụ như install_name_tool. Bên cạnh đó, tệp nhị phân vẫn hoạt động dưới dạng tệp thực thi.
Andy Li

36

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

Gần đây tôi có nhu cầu nhúng tệp vào tệp thực thi. Vì tôi đang làm việc với dòng lệnh với gcc, et al chứ không phải với một công cụ RAD ưa thích khiến tất cả diễn ra một cách kỳ diệu nên tôi không rõ ràng là làm thế nào để biến điều này thành hiện thực. Một chút tìm kiếm trên mạng đã phát hiện ra một vụ hack về cơ bản đưa nó vào cuối tệp thực thi và sau đó giải mã vị trí của nó dựa trên một loạt thông tin mà tôi không muốn biết. Có vẻ như phải có một cách tốt hơn ...

Và đây, đó là phản đối của cuộc giải cứu. objcopy chuyển đổi các tệp đối tượng hoặc tệp thực thi từ định dạng này sang định dạng khác. Một trong những định dạng mà nó hiểu là "nhị phân", về cơ bản là bất kỳ tệp nào không thuộc một trong các định dạng khác mà nó hiểu. Vì vậy, bạn có thể đã hình dung ra ý tưởng: chuyển đổi tệp mà chúng tôi muốn nhúng thành tệp đối tượng, sau đó nó có thể được liên kết đơn giản với phần còn lại của mã của chúng tôi.

Giả sử chúng ta có tên tệp data.txt mà chúng ta muốn nhúng vào tệp thực thi của mình:

# cat data.txt
Hello world

Để chuyển đổi tệp này thành tệp đối tượng mà chúng tôi có thể liên kết với chương trình của mình, chúng tôi chỉ cần sử dụng objcopy để tạo tệp ".o":

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

Điều này cho đối tượng biết rằng tệp đầu vào của chúng tôi ở định dạng "nhị phân", tệp đầu ra của chúng tôi phải ở định dạng "elf32-i386" (tệp đối tượng trên x86). Tùy chọn --binary-architecture cho đối tượng biết rằng tệp đầu ra có nghĩa là "chạy" trên x86. Điều này là cần thiết để ld sẽ chấp nhận tệp để liên kết với các tệp khác cho x86. Người ta sẽ nghĩ rằng việc chỉ định định dạng đầu ra là "elf32-i386" sẽ ngụ ý điều này, nhưng không phải vậy.

Bây giờ chúng ta có một tệp đối tượng, chúng ta chỉ cần đưa nó vào khi chạy trình liên kết:

# gcc main.c data.o

Khi chúng tôi chạy kết quả, chúng tôi nhận được đầu ra được cầu nguyện:

# ./a.out
Hello world

Tất nhiên, tôi chưa kể toàn bộ câu chuyện, cũng như không cho bạn xem main.c. Khi objcopy thực hiện chuyển đổi ở trên, nó sẽ thêm một số ký hiệu "trình liên kết" vào tệp đối tượng được chuyển đổi:

_binary_data_txt_start
_binary_data_txt_end

Sau khi liên kết, các ký hiệu này chỉ định điểm bắt đầu và kết thúc của tệp nhúng. Các tên ký hiệu được hình thành bằng cách viết trước mã nhị phân và thêm _start hoặc _end vào tên tệp. Nếu tên tệp chứa bất kỳ ký tự nào không hợp lệ trong tên ký hiệu, chúng sẽ được chuyển đổi thành dấu gạch dưới (ví dụ: data.txt trở thành data_txt). Nếu bạn nhận được các tên chưa được giải quyết khi liên kết bằng cách sử dụng các ký hiệu này, hãy thực hiện hexdump -C trên tệp đối tượng và xem các tên mà objcopy đã chọn ở cuối kết xuất.

Mã để thực sự sử dụng tệp nhúng bây giờ phải rõ ràng một cách hợp lý:

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

Một điều quan trọng và tinh tế cần lưu ý là các ký hiệu được thêm vào tệp đối tượng không phải là "biến". Chúng không chứa bất kỳ dữ liệu nào, đúng hơn, địa chỉ của chúng là giá trị của chúng. Tôi khai báo chúng là kiểu char vì nó thuận tiện cho ví dụ này: dữ liệu nhúng là dữ liệu ký tự. Tuy nhiên, bạn có thể khai báo chúng dưới dạng bất kỳ thứ gì, chẳng hạn như int nếu dữ liệu là một mảng số nguyên, hoặc như struct foo_bar_t nếu dữ liệu là bất kỳ mảng thanh foo nào. Nếu dữ liệu nhúng không đồng nhất, thì char có lẽ là thuận tiện nhất: lấy địa chỉ của nó và truyền con trỏ đến kiểu thích hợp khi bạn duyệt dữ liệu.


36

Nếu bạn muốn kiểm soát tên ký hiệu chính xác và vị trí của tài nguyên, bạn có thể sử dụng (hoặc tập lệnh) trình hợp dịch GNU (không thực sự là một phần của gcc) để nhập toàn bộ tệp nhị phân. Thử cái này:

Assembly (x86 / arm):

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

Dù bạn sử dụng gì đi nữa, có lẽ tốt nhất là bạn nên tạo một tập lệnh để tạo ra tất cả các tài nguyên và có các tên ký hiệu đẹp / thống nhất cho mọi thứ.

Tùy thuộc vào dữ liệu của bạn và thông tin cụ thể của hệ thống, bạn có thể cần sử dụng các giá trị căn chỉnh khác nhau (tốt nhất là .baligncho tính di động) hoặc các loại số nguyên có kích thước khác cho thing_sizehoặc một loại phần tử khác cho thing[]mảng.


cám ơn vì đã chia sẻ! chắc chắn trông thú vị, nhưng lần này không phải là thứ tôi đang tìm kiếm =) liên quan đến
Atmocreations

1
Chính xác những gì tôi đang tìm kiếm. Có lẽ bạn có thể xác minh rằng nó cũng ổn đối với các tệp có kích thước không được ẩn bằng 4. Có vẻ như thing_size sẽ bao gồm thêm byte đệm.
Pavel P

Điều gì xảy ra nếu tôi muốn một thứ trở thành biểu tượng địa phương? Tôi có thể có mèo đầu ra trình biên dịch cùng với lắp ráp của riêng tôi nhưng có cách nào tốt hơn không?
user877329

Đối với bản ghi: Bản chỉnh sửa của tôi khắc phục sự cố của byte đệm thêm mà @Pavel đã lưu ý.
ndim

4

Đọc tất cả các bài đăng ở đây và trên Internet, tôi đã kết luận rằng không có công cụ nào dành cho tài nguyên, đó là:

1) Dễ sử dụng trong mã.

2) Tự động (dễ dàng đưa vào cmake / make).

3) Đa nền tảng.

Tôi đã quyết định viết công cụ cho chính mình. Mã có sẵn ở đây. https://github.com/orex/cpp_rsc

Để sử dụng nó với cmake rất dễ dàng.

Bạn nên thêm vào tệp CMakeLists.txt của mình mã như vậy.

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

Bạn có thể tải xuống ví dụ thực tế bằng cách sử dụng phương pháp này tại đây, https://bitbucket.org/orex/periodic_table


Tôi nghĩ câu trả lời của bạn cần được giải thích tốt hơn để trở nên hữu ích cho nhiều người hơn.
kyb
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.