Выполнение разделяемых библиотек в Linux

Выполнение разделяемых библиотек в Linux

Известно, что при запуске практически любой unix-программы, передав ей в качестве ключа --help, --usage или --version, мы получим некоторую информацию о данной программе: опции командной строки, комментарии, email-адреса разработчиков, версию программы и так далее. Эта информация дает возможность сразу пользоваться программой, не залезая в man'ы и не погружаясь в чтение документации. Даже для того, чтобы понять смысл программы wc, достаточно запустить ее с ключом --help:

chumka bin # wc --help Usage: wc [OPTION]. [FILE]. Print newline, word, and byte counts for each FILE, and a total line if more than one FILE is specified. With no FILE, or when FILE is -, read standard input. -c, --bytes print the byte counts -m, --chars print the character counts -l, --lines print the newline counts -L, --max-line-length print the length of the longest line -w, --words print the word counts --help display this help and exit --version output version information and exit

Report bugs to <bug-coreutils@gnu.org>.

Однако такой возможности практически не существует для разделяемых библиотек. Им невозможно передать аргументы командной строки, более того - их даже нельзя запустить. В ответ на попытку запуска мы получим банальный Segmentation fault. Библиотека-исключение - это стандартная, хорошо известная libc. Параметры командной строки она не принимает, но запускаться может, выдавая тем самым информацию о себе самой.

В данной статье мы поговорим о создании библиотек с возможностью их запуска и передачи им аргументов из командной строки. В ответ библиотека выдаст нам информацию об авторе и документацию своих функций. В качестве примера создадим небольшую библиотеку myexec:

Функции f1() и f2() являются функциями библиотеки, из-за которых она собственно и пишется. Нас они не интересуют, поэтому они будут просто выводить свое имя сразу после вызова. Именно эти функции мы будем документировать. Функция my_main() будет точкой входа в библиотеку. Она показывает все входные аргументы, переданные из командной строки. Вызов exit() в конце обязателен, так как после выполнения функции my_main() управление никуда не возвращается, как в случае обычных выполняемых файлов. Поэтому нам нужно самостоятельно завершить выполнение.

Собираем, запускаем и видим то, что ожидали:

# gcc -fPIC -shared my.c -o libmyexec.so chumka

# ./libmyexec.so Segmentation fault

Почему же ошибка?

При запуске выполняемого файла в Unix, ядро ОС сначала загружает в память специальный загрузчик, расположенный в заголовке ELF-файла, а именно в строке INTERP. Эта строку заголовка формирует линкер при компоновке файла, основываясь на секции .interp объектного файла. Далее ядро передает управление этому загрузчику, который, в свою очередь, загружает образ файла в память и передает ему управление. Просмотрим ELF-заголовок нашей только что собранной библиотеки и ELF-заголовок обычного выполняемого файла, для примера возьмем уже знакомый wc (часть вывода обрезана):

# objdump -p `which wc` /bin/wc: file format elf32-i386

Program Header: PHDR off 0x00000034 vaddr 0x08048034 paddr 0x08048034 align 2**2 filesz 0x00000120 memsz 0x00000120 flags r-x INTERP off 0x00000154 vaddr 0x08048154 paddr 0x08048154 align 2**0 filesz 0x00000013 memsz 0x00000013 flags r-- LOAD off 0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12 filesz 0x00004108 memsz 0x00004108 flags r-x LOAD off 0x00004108 vaddr 0x0804d108 paddr 0x0804d108 align 2**12 filesz 0x000001ac memsz 0x00000344 flags rw- DYNAMIC off 0x0000411c vaddr 0x0804d11c paddr 0x0804d11c align 2**2 filesz 0x000000c8 memsz 0x000000c8 flags rw- NOTE off 0x00000168 vaddr 0x08048168 paddr 0x08048168 align 2**2 filesz 0x00000020 memsz 0x00000020 flags r-- EH_FRAME off 0x00004090 vaddr 0x0804c090 paddr 0x0804c090 align 2**2 filesz 0x0000001c memsz 0x0000001c flags r-- STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags rw- PAX_FLAGS off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags --- 2800 . chumka

# objdump -p ./libmyexec.so /home/lamer/lib/libmyexec.so: file format elf32-i386

Program Header: LOAD off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**12 filesz 0x00000690 memsz 0x00000690 flags r-x LOAD off 0x00000690 vaddr 0x00001690 paddr 0x00001690 align 2**12 filesz 0x00000104 memsz 0x00000108 flags rw- DYNAMIC off 0x000006a4 vaddr 0x000016a4 paddr 0x000016a4 align 2**2 filesz 0x000000c0 memsz 0x000000c0 flags rw- STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags rw- PAX_FLAGS off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags --- 2800 .

Видно, что строка INTERP в заголовке нашей библиотеки отсутствует, тем самым ядро не может найти загрузчик в библиотеке и выдает ошибку. Наша задача - создать эту строку, но мы поручим эту задачу линкеру ld. А сами тем временем просто добавим секцию .interp в объектный файл. Сделать это можно по-разному, например, просто добавить в начало исходного файла имя загрузчика с указанием компилятору поместить эту строку в секцию .interp:

const char interp[] __attribute__((section(".interp"))) = "/lib/ld-linux.so.2";

и собрать заново:

# gcc -fPIC -shared -Wl,-e,my_main my.c -o libexec.so

Файл /lib/ld-linux.so.2 является загрузчиком для ОС Linux.

Другой способ не требует вмешательства в исходный код библиотеки. Требуемая секция добавляется в объектный файл с помощью утилиты objcopy:

# echo -ne "/lib/ld-linux.so.2\0" > ./tmpfile chumka

# gcc -c -fPIC my.c chumka

# objcopy --add-section .interp=./tmpfile --set-section-flags .interp=contents,alloc,load,readonly,data my.o chumka

# gcc -shared -Wl,-e,my_main my.o -o libmyexec.so

Параметр -Wl,-e,my_main заставляет gcc указать линкеру, что точкой входа будет являться функция my_main().

Проверим, что линкер создал строку INTERP и секцию .interp (часть вывода вырезана):

chumka lib # objdump -s -j .interp -p ./libmyexec.so ./libmyexec.so: file format elf32-i386

Program Header: PHDR off 0x00000034 vaddr 0x00000034 paddr 0x00000034 align 2**2 filesz 0x000000e0 memsz 0x000000e0 flags r-x INTERP off 0x000006e4 vaddr 0x000006e4 paddr 0x000006e4 align 2**0 filesz 0x00000013 memsz 0x00000013 flags r-- LOAD off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**12 filesz 0x000006fc memsz 0x000006fc flags r-x LOAD off 0x000006fc vaddr 0x000016fc paddr 0x000016fc align 2**12 filesz 0x00000104 memsz 0x00000108 flags rw- DYNAMIC off 0x00000710 vaddr 0x00001710 paddr 0x00001710 align 2**2 filesz 0x000000c0 memsz 0x000000c0 flags rw- STACK off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags rw- PAX_FLAGS off 0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**2 filesz 0x00000000 memsz 0x00000000 flags --- 2800 . Contents of section .interp: 06e4 2f6c6962 2f6c642d 6c696e75 782e736f /lib/ld-linux.so 06f4 2e3200 .2.

Строка в секции .interp обязательно должна заканчиваться нулевым символом, иначе ядро не сможет правильно определить путь к загрузчику. Теперь проверим запуск библиотеки:

# ./libmyexec.so 4 5 Main started. Argc: -1078608707 Segmentation fault

Функция my_main() запустилась, но параметры передались не в том порядке_ в котором мы ожидали. За передачу параметров от ядра к функции main() отвечает специальный код компилятора, запакованный в объектный файл сrt1.o. При передаче управления от ядра к выполняемому файлу в стеке процесса содержатся:

  1. Число аргументов командной строки
  2. Массив указателей на аргументы командной строки

Однако функция my_main() справедливо полагает, что первое значение в стеке есть адрес возврата вызвавшей ее функции. Ядро, как мы знаем, этот адрес не кладет в стек. Таким образом, мы теряем аргумент argc, остальные аргументы можно достать средствами языка C. Решение, кажется, лежит на поверхности. Достаточно "обернуть" вызов функции my_main() в какую-нибудь другую функцию, например:

Тогда вызывающая функция pre() положит в стек свой адрес возврата, который нам вообще-то не нужен, и my_main() благополучно доберется до нужных значений в стеке. Однако, просмотрев код с помощью objdump -d -j .text ./libmyexec.so, увидим, что вызов pre() засоряет стек трижды, планируя вернуть все на свои места после своего завершения, то есть уже после вызова my_main():

00000660 <pre>: 660: 55 push %ebp <-- 1 661: 89 e5 mov %esp,%ebp 663: 53 push %ebx <-- 2 664: 83 ec 04 sub $0x4,%esp <-- 3 667: e8 00 00 00 00 call 66c <pre+0xc> 66c: 5b pop %ebx 66d: 81 c3 dc 11 00 00 add $0x11dc,%ebx 673: e8 cc fe ff ff call 544 <my_main@plt> 678: 83 c4 04 add $0x4,%esp <-- 4 67b: 5b pop %ebx <-- 5 67c: c9 leave <-- 6 67d: c3 ret

В п. 1,2,3 стек засоряется, а в п. 4,5,6 вычищается. Как видно, п. 4,5,6 происходят после вызова my_main(), поэтому нам придется очистить стек самостоятельно до вызова нашей входной функции. По адресу 667-66с делается операция получения текущего адреса выполнения программы в регистр ebp: инструкция call положит адрес возврата в стек, а инструкция pop тут же его оттуда вытащит, поэтому эта часть на положение стека не влияет. Регистр ebp необходим для доступа к таблице PLT нашей библиотеки.

Также нам нужно создать указатель на массив указателей на аргументы командной строки, так как наша my_main(), как и обычная main(), принимает его в качестве второго аргумента. Итак, нам нужно очистить три двойных слова из стека и создать указатель. Вариантов сделать это предостаточно, вот один из вариантов функции pre():

В п.1 очищаем ebp, далее очищаем стек на четыре двойных слова (вместо трех), обращаемся к памяти за стеком и достаем argc. В п. 4 создаем нужный указатель и в п.5 складываем готовые аргументы в стек для передачи my_main(). И наконец, в п.6 вызываем ее. Вставляем этот код в начало библиотеки, собираем c новой точкой входа pre и запускаем:

# ./libmyexec.so arg1 arg2 arg3 Main started. Argc: 4 ./libmyexec.so arg1 arg2 arg3

Функция вывела все 4 аргумента (первый - это имя самой программы). Ну а дальше все в руках программиста. Используя стандартный getopt(), параметры перебираются и обрабатываются в нужном русле. Не забудем запустить какую-нибудь тестовую программку test для проверки работы самой библиотеки:

📎📎📎📎📎📎📎📎📎📎