Извлекать из памяти?
Я ищу способ загрузить сгенерированный объектный код прямо из памяти.
Я понимаю, что если я запишу его в файл, я могу вызвать dlopen, чтобы динамически загрузить его символы и связать их. Тем не менее, это выглядит немного окольным путем, учитывая, что он начинается в памяти, записывается на диск, а затем перезагружается в память с помощью dlopen. Мне интересно, есть ли какой-нибудь способ динамически связать объектный код, который существует в памяти. Из того, что я могу сказать, может быть несколько разных способов сделать это:
Уловка заставляет думать, что ваша память - это файл, хотя он никогда не покидает память.
Найдите какой-нибудь другой системный вызов, который делает то, что я ищу (я не думаю, что это существует).
Найдите динамическую библиотеку ссылок, которая может связывать код непосредственно в памяти. Очевидно, что это немного сложно для Google, поскольку "библиотека динамического связывания" предоставляет информацию о том, как динамически связывать библиотеки, а не о библиотеках, которые выполняют задачу динамического связывания.
Абстрагируйте некоторый API от компоновщика и создайте новую библиотеку из ее кодовой базы. (очевидно, это наименее желательный вариант для меня).
Итак, какие из них возможны? возможно? Не могли бы вы указать мне на то, что я предположил, существовал? Есть ли другой способ, о котором я даже не думал?
9 ответов
Не существует стандартного способа сделать это, кроме как выписать файл и снова загрузить его с dlopen()
,
Вы можете найти какой-то альтернативный метод на вашей текущей конкретной платформе. Вам решать, будет ли это лучше, чем использование "стандартного и (относительно) портативного" подхода.
Поскольку генерация объектного кода в первую очередь зависит от платформы, дополнительные методы для конкретной платформы могут не иметь значения для вас. Но это призыв к суждению - и в любом случае это зависит от того, существует ли нестандартная техника, что относительно невероятно.
Мне нужно было решить эту проблему, потому что у меня есть система с поддержкой сценариев, которая не имеет файловой системы (с использованием больших двоичных объектов из базы данных) и нуждается в загрузке двоичных плагинов для поддержки некоторых сценариев. Это решение, которое я придумал, работает на FreeBSD, но может и не быть переносимым.
void *dlblob(const void *blob, size_t len) {
/* Create shared-memory file descriptor */
int fd = shm_open(SHM_ANON, O_RDWR, 0);
ftruncate(fd, len);
/* MemMap file descriptor, and load data */
void *mem = mmap(NULL, len, PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(mem, blob, len);
munmap(mem, len);
/* Open Dynamic Library from SHM file descriptor */
void *so = fdlopen(fd,RTLD_LAZY);
close(fd);
return so;
}
Очевидно, что в коде отсутствует какая-либо проверка ошибок и т. Д., Но это основная функциональность.
ETA: мое первоначальное предположение, что fdlopen
POSIX был не прав, это похоже на FreeBSD-изм.
Я не понимаю, почему вы рассматриваете dlopen
, так как для этого потребуется намного больше непереносимого кода для генерации правильного формата объекта на диске (например, ELF) для загрузки. Если вы уже знаете, как генерировать машинный код для вашей архитектуры, просто mmap
память с PROT_READ|PROT_WRITE|PROT_EXEC
и поместите туда свой код, затем назначьте адрес указателю функции и вызовите его. Очень просто.
Мы реализовали способ сделать это в Google. К сожалению, вышестоящая версия glibc не смогла осознать необходимость, поэтому она так и не была принята. Запрос функции с заплатками застопорились. Это известно как
dlopen_from_offset
.
Код dlopen_with_offset glibc доступен в ветках glibc google / grte *. Но никому не должно нравиться изменять собственный glibc.
Вот как вы можете сделать это полностью в памяти в Linux (без записи в/tmp/xxx
) с использованием диска памяти сmemfd_create
:
user@system $ ./main < example-library.so
add(1, 2) = 3
// example-library.c
int add(int a, int b) { return a + b; }
#include <cstdio>
#include <dlfcn.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <vector>
// Compile and then invoke as:
// $ ./main < my-shared-lib.so
int main() {
// Read the shared library contents from stdin
std::vector<char> library_contents;
char buffer[1024];
ssize_t bytes_read;
while ((bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0) {
library_contents.insert(library_contents.end(), buffer,
buffer + bytes_read);
}
// Create a memory file descriptor using memfd_create
int fd = memfd_create("shared_library", 0);
if (fd == -1) {
perror("memfd_create failed");
return 1;
}
// Write the shared library contents to the file descriptor
if (write(fd, library_contents.data(), library_contents.size()) !=
static_cast<ssize_t>(library_contents.size())) {
perror("write failed");
return 1;
}
// Create a path to the file descriptor using /proc/self/fd
// https://sourceware.org/bugzilla/show_bug.cgi?id=30100#c33
char path[100]; // > 35 == strlen("/proc/self/fd/") + log10(pow(2, 64)) + 1
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
// Use dlopen to dynamically load the shared library
void *handle = dlopen(path, RTLD_NOW);
if (handle == NULL) {
fprintf(stderr, "dlopen failed: %s\n", dlerror());
return 1;
}
// Use the shared library...
// Get a pointer to the function "int add(int, int)"
int (*add)(int, int) =
reinterpret_cast<int (*)(int, int)>(dlsym(handle, "add"));
if (add == NULL) {
fprintf(stderr, "dlsym failed: %s\n", dlerror());
return 1;
}
// Call the function "int add(int, int)"
printf("add(1, 2) = %d\n", add(1, 2));
// Cleanup
dlclose(handle);
close(fd);
return 0;
}
Вам не нужно загружать код, сгенерированный в памяти, так как он уже находится в памяти!
Однако вы можете - непереносимым способом - генерировать машинный код в памяти (при условии, что он находится в сегменте памяти mmap) PROT_EXEC
флаг).
(в этом случае не требуется "связывание" или шаг перемещения, поскольку вы генерируете машинный код с определенными абсолютными или относительными адресами, в частности, для вызова внешних функций)
Существуют некоторые библиотеки, которые делают это: в GNU/Linux под x86 или x86-64 мне известны GNU Lightning (который быстро генерирует машинный код, который работает медленно), DotGNU LibJIT (который генерирует код среднего качества) и LLVM & GCCJIT (который способен генерировать довольно оптимизированный код в памяти, но требует времени для его генерации). И у LuaJit есть нечто подобное. С 2015 года GCC 5 имеет библиотеку gccjit.
И, конечно, вы все равно можете сгенерировать код C в файле, разветвить компилятор, чтобы скомпилировать его в общий объект, и добавить этот общий объектный файл. Я делаю это в GCC MELT, доменном языке для расширения GCC. На практике это работает довольно хорошо.
добавлений
Если производительность записи сгенерированного C-файла вызывает беспокойство (так не должно быть, поскольку компиляция C-файла намного медленнее, чем его написание), рассмотрите возможность использования для этого некоторой файловой системы tmpfs (возможно, в /tmp/
которая часто является файловой системой tmpfs в Linux)
Загрузка солиба из памяти имеет неотъемлемое ограничение. А именно, deps DT_NEEDED солиба не могут ссылаться на буфер памяти. Это означает, среди прочего, что вы не можете легко загрузить солиб с помощью deps из буфера памяти. Боюсь, что если спецификация ELF не будет расширена и не позволит DT_NEEDED ссылаться на другие объекты, кроме имен файлов, не будет стандартного API для загрузки солиба из буфера памяти.
Я думаю, вам нужно использовать shm_open() posix, затем отобразить разделяемую память, сгенерировать там свой солиб, а затем использовать обычную dlopen() через точку монтирования /dev/shm. Таким же образом можно обрабатывать deps: они могут ссылаться на обычные файлы или на объекты /dev/shm, в которых есть сгенерированные вами солибы.
Следует отметить, что использование shm_open+dlopen загружает динамическую библиотеку из общей памяти. Если /dev/shm имеет разрешение noexec, динамическая библиотека не сможет загрузиться.
Я нашел решение этой проблемы, создав файл памяти, используяmemfd_create
а затем открытие изdlopen
.