Известно, что при запуске практически любой 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:

#include <stdio.h>

int my_main(int argc,char *argv[]) {
printf("Main started. Argc: %d\n",argc);
for (;*argv;*argv++) printf(" %s\n",*argv);
exit(0);
}
int f1(void) { printf("function %s\n",__FUNCTION__); }
int f2(void) { printf("function %s\n",__FUNCTION__); }

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

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

chumka ~ # gcc -fPIC -shared my.c -o libmyexec.so
chumka ~ # ./libmyexec.so
Segmentation fault

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

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

chumka ~ # 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";

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

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

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

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

chumka ~ # 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 обязательно должна заканчиваться нулевым символом, иначе ядро не сможет правильно определить путь к загрузчику. Теперь проверим запуск библиотеки:

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

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

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

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

void pre(void) {
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():

void pre(void) {
asm volatile (
"xorl %ebp,%ebp\n\t" // <-- 1
"addl $16,%esp\n\t" // <-- 2
"movl -4(%esp),%eax\n\t" // <-- 3
"movl %esp,%ecx\n\t" // <-- 4
"pushl %ecx\n\t" // <-- 5
"pushl %eax\n\t"
);
my_main(); // <-- 6
}

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

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

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

int main(void) {
f1();
f2();
}

что даст нам ожидаемый вывод:

chumka ~ # ./test
function f1
function f2/

Оставить мнение