Проблемы:
Команда чтения из памяти по определенному адресу
mov rax, [0x12345678]
подразумевает знание этого адреса на этапе компиляции и это не подходит для динамических библиотек
Способы решить:
- Специальный регистр, чтобы знать где мы находимся (не очень применяется)
- Переправить все адреса, если она загружена в другом месте (Винда)
- Сгенерировать такой код, который куда бы не загрузился, работал бы (линукс)
Новая проблема: как у одной DLL вызвать другую DLL?
Как создается DLL?
Простая схема (винда)
Берем набор объектников, они линкуются в .dll
, одновременно с этим создается .lib
(import library). Когда хотим в своей программе использовать DLL, то передаем ей объектники и .lib
.
Помимо этого надо передать линковщику DLL’ки .def
, в котором написаны названия функций, который мы хотим экспортировать из библиотеки. Проблема — как передавать перегруженные функции? Писать их полное ассемблерное имя?? Микромягкие попытались это смягчить и есть модификатор
__declspec(dllexport) void f()
Теперь как ссылаются DLL’ки друг на друга. Создается таблица Import Address Table (GOT в линукс)
Теперь пусть в DLL объявлена функция void foo()
, а в другой программе мы ее вызываем, компилятор понятия не имеет что это за функция, из DLL она, не из DLL, так как же компилятор ее вызывает? Какой тип call
использует?
call foo E8xxxxxxxx
Можно сказать что происходит
push &next_instr
rip = rip + xxxxxxxx
То есть для DLL не подходит, так как DLL для каждой программы имеет свой адрес в виртуальной памяти (хотя чаще всего имеет один в физической)
Есть другая форма
call [rip + xxxxxxxx]
Что равносильно
push &next_instr rip = [rip + xxxxxxxx]
Но компилятор генерирует не это, это подменяет линковщик следующим образом:
foo:
jmp [__jmp__foo]
Но получается многовато вызовов, поэтому микромягкие опять решили сделать оптимизацию — функцию можно пометить, что она из DLL’ки
__declspec(dllimport) void foo();
В каком случае DLL копируется? Если мы хотим изменять память, то чтобы это не влияло на другие программы, происходит copy on write
Сложная схема (линукс)
1.cpp #include <iostream> void foo { std::cout << "Hello, world!" }
2.cpp void foo();
void main() { foo(); }
Компиляция библиотеки
g++ -fpic -shared 1.cpp -o 1.so
-fpic
— разрешить шарить (position independent code) (команда компилятору)
-shared
— выдать .so
(команда линковщику)
Компиляция программы
g++ 2.cpp 1.so -o 2
Запуск
LD_LIBRARY_PATH=. ./2
LD_LIBRARY_PATH
— по умолчанию .so
не ищется в текущем каталоге. Поэтому мы и говорим: “Бери .so
и из этого каталоге тоже”
Заметим
Тут мы не писали, какие функции экспортировать. Это имеет негативный эффект: таблицы становятся просто огромными, хоть и удобно не писать всякие __declspec(...)
Также мы можем перекрывать функции из DLL, при этом это будет не ошибка, а просто перекрытие
Если хотим чтобы функция не торчала наружу dll’ки надо указать у функции __attribute__((visibility("hidden")))
Ленивая линковка (PLT (Procedure Linkage Table))
foo:
jmp [__imp__foo]
push yyyyyyyy
jmp [..] inter position
При первом запуске функции в [__imp__foo] лежит адрес push, а в 2-3 строке записывается настоящая позиция функции
При следующем запуске на первом jmp уже сразу будет переходить в правильную функцию
Ускоряет время запуска программы
Best practice
Если заюзать статическую либу в нескольких динамических — будет много копий статической. И между собой динамические не могут нормально пользоваться функциями статической.
Можно либо запрещать взаимодействие вроде “выделить память в одной dll’ке, а освободить в другой”, либо просто сделать статическую динамической