Код программ в системах с Фон-Неймановской архитектурой размещается в памяти точно так же, как и обычные данные.
Таким образом, он может быть загружен или сгенерирован во время работы программы. Некоторые процессоры позволяют контролировать, какие участки памяти могут быть выполняемые, а какие - нет, и кроме того, это контролируется ядром. Таким образом, выполнить код можно только при условии, что он находится в страницах памяти, помеченных как выполняемые.
Объявление вида
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ПУТЬ
- указывается имя каталога для поиска используемых библиотек.
При загрузке 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