Skip to content

Latest commit

 

History

History
 
 

function-pointers

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

Библиотеки функций и их загрузка

Функции и указатели на них

Код программ в системах с Фон-Неймановской архитектурой размещается в памяти точно так же, как и обычные данные.

Таким образом, он может быть загружен или сгенерирован во время работы программы. Некоторые процессоры позволяют контролировать, какие участки памяти могут быть выполняемые, а какие - нет, и кроме того, это контролируется ядром. Таким образом, выполнить код можно только при условии, что он находится в страницах памяти, помеченных как выполняемые.

Типизация указателей на функции

Объявление вида

int (*p_function)(int a, int b);

интерптетируется следующим образом: p_function - это указатель на функцию, которая принимает два целочисленных аргумента, и возвращает целое знаковое число.

Более общий вид указателя на функцию:

typedef ResType (*TypeName)(FuncParameters...);

Здесь ResType - возвращаемый тип целевой функции, TypeName - имя типа-указателя, FuncParameters... - параметры функции.

Использование ключевого слова typedef является необходимым для языка Си, чтобы каждый раз не писать полностью тип (по аналогии со struct).

Объявление указателей на функции необходимо для того, чтобы компилятор знал, как именно использовать адрес какой-то функции, и мог подготовить аргументы, и разбраться с тем, откуда брать возвращаемый результат функции.

Библиотеки

ELF-файл может быть не только исполняемым, но и библиотекой, содержащей функции. Библиотека отличается от исполняемого файла тем, что:

  • содержит таблицу доступных символов - функций и глобальных переменных (можно явно указать её создание опцией -E);
  • может быть размещена произвольным образом, поэтому программа обязана быть скомпилирована в позиционно-независимый код с опцией -fPIC или -fPIE;
  • не обязана иметь точку входа в программу - функции _start и main.

Компиляция библиотеки производится с помощью опции -shared:

 > gcc -fPIC -shared -o libmy_great_library.so lib.c

В Linux и xBSD для именования библиотек используется соглашение libИМЯ.so, для Mac - libИМЯ.dynlib, для Windows - ИМЯ.dll.

Связывание программы с библиотекой подразумевает опции:

  • -lИМЯ - указыватся имя библиотеки без префикса lib и суффикса .so;
  • -LПУТЬ - указывается имя каталога для поиска используемых библиотек.

Runtime Search Path

При загрузке ELF-файла загружаются все необходимые библиотеки, от которых он явно зависит. Посмотреть список зависимостей можно с помощью команды ldd.

Библиотеки располагаются в одном из стандартных каталогов: /lib[64], /usr/lib[64] или /usr/local/lib[64]. Дополнительные каталоги для поиска библиотек определяются в переменной окружения LD_LIBRARY_PATH.

Существует возможность явно определить в ELF-файле, где искать необходимые библиотеки. Для этого используется опция линковщика ld -rpath ПУТЬ.

Для передачи опций ld, который вызывается из gcc, используется опция -Wl,ОПЦИЯ.

В rpath можно указывать как абсолютные пути, так и переменную $ORIGIN, которая при загрузке программы раскрывается в каталог, содержащий саму программу. Это позволяет создавать поставку из программы и библиотек, которые не раскиданы по всей файловой системе:

 > gcc -o program -L. -lmygreat_library program.c \
       -Wl,-rpath -Wl,'$ORIGIN/'.       

Это создаст выполняемый файл program, который использует библиотеку libmy_great_library.so, подразумевая, что файл с библиотекой находится в том же каталоге, что и сама программа.

Загрузка библиотек во время выполнения

Библиотеки можно не привязывать намертво к программе, а загружать по мере необходимости. Для этого используется набор функций dl, которые вошли в стандарт POSIX 2001 года.

  • void *dlopen(const char *filename, int flags) - загружает файл с библиотекой;
  • void *dlsym(void *handle, const char *symbol) - ищет в библиотеке необходимый символ, и возвращает его адрес;
  • int dlclose(void *handle) - закрывает библиотеку, и выгружает её из памяти, если она больше в программе не используется;
  • char *dlerror() - возвращает текст ошибки, связянной с dl.

Если dlopen или dlsym не могут открыть файл или найти символ, то возвращается нулевой указатель.

Пример использования - в файлах lib.c и dynload.c.

Позиционно-независимый исполняемый файл

Опция -fPIE компилятора указывает на то, что нужно сгенерировать позиционно-независимый код для main и _start, а опция -pie - о том, что нужно при линковке указать в ELF-файле, что он позиционно-независимый.

Позиционно-независимый выполняемый файл в современных системах размещается по случайному адресу.

Если позиционно-независимый исполняемый файл ещё и содержит таблицу экспортируемых символов, то он одновременно является и библиотекой. Если отсутствует опция -shared, то компилятор собирает программу, удаляя из неё таблицу символов. Явным образом сохранение таблицы символов задается опцией -Wl,-E.

Пример:

  # файл abc.c содержит int main() { puts("abc"); }
  > gcc -o program -fPIE -pie -Wl,-E abc.c

  # программа может выполняться как обычная программа
  > ./program
  abc

  # и может быть использована как библиотека
  > python3
  >>> from ctypes import cdll, c_int
  >>> lib = cdll.LoadLibrary("./program")
  >>> main = lib["main"]
  >>> main.restype = c_int
  >>> ret = main()
  abc