понедельник, 2 декабря 2013 г.

Linux: Разбирался с GCC и динамическими библиотеками

В общем, дело обстояло так.

Есть некий проект, а точнее, CGI-приложение, написанное на C. Такие штуки, по-моему, должны обладать немаловажным свойством - они должны быть не сильно большими. Ведь при каждом обращении клиента создаётся копия всего кода в памяти. Соответственно, поднимать не требующиеся в данный момент громоздкие функции - это моветон.

Поэтому с самого начала было ясно, что часть исполняемого кода рано или поздно придется выносить в отдельные библиотеки. И вот, наконец, этот момент настал. По этому случаю пришлось немного погуглить, и, в силу своего слабого разумения, выстроить следующую картину, как это делается.

1. Пусть, например, у нас есть проект, который состоит из трех файлов:

Файл FirstModule.c (и соответствующий ему заголовочный файл FirstModule.h):
char *FirstModule_Function1 (char *param) {...}
char *FirstModule_Function2 (char *param) {...}

Файл SecondModule.c (и заголовочный файл SecondModule.h), который активно пользуется функиями из первого файла:
#include "FirstModule.h"
char *SecondModule_Function1 (char *param) {...}
char *SecondModule_Function2 (char *param) {...}

И, наконец, файл Main.c, который, конечно, использует функции из обоих вышеуказанных файлов:
#include "FirstModule.h"
#include "SecondModule.h"
int main()
{
    ...
    char *val = SecondModule_Function1 ("некий параметр");
    ...
}

Как всё это богатство компилируется в один исполняемый файл Main? Примерно так:
gcc -c FirstModule.c -o FirstModule.o
gcc -c SecondModule.c -o SecondModule.o
gcc -c Main.c -o Main.o
gcc FirstModule.o SecondModule.o Main.o -o Main
rm -f *.o

2. Теперь предположим, что SecondModule.c неприлично растолстел. Превращаем его в Shared library (т.е. делаем динамическую библиотеку).

Во-первых, переписываем Main.c:
#include "FirstModule.h"
#include <dlfcn.h>
int main()
{
    void *lib_handle;
    char *lib_error;
    char *(*fn)(char *param);

    // пытаемся загрузить библиотеку из той же директории, что и исполняемый файл
    lib_handle = dlopen("./SecondModule.so", RTLD_LAZY);
    if (!lib_handle) 
    {
        fprintf(stderr, "1. %s\n", dlerror());
        exit(1);
     }

    // получаем ссылку на нужную нам функцию
    fn = dlsym(lib_handle, "SecondModule_Function1");
    if ((lib_error = dlerror()) != NULL)
    {
        fprintf(stderr, "2. %s\n", lib_error);
        exit(1);
    }

    // выполняем функцию
    char *val = (*fn)("некий параметр");

    // используем результат выполнения
    printf("%s\n", val);

    // выгружаем библиотеку
    dlclose(lib_handle);
    ...
}
Немножко длинно, но довольно понятно.

Во-вторых компилируем всё это так:
gcc -c FirstModule.c -o FirstModule.o
gcc -c -fPIC SecondModule.c -o SecondModule.o
gcc -shared SecondModule.o -o SecondModule.so
gcc -c Main.c -o Main.o
gcc -ldl FirstModule.o Main.o -o Main
rm -f *.o
В результате получаются исполняемый файл Main и использующаяся им по мере надобности библиотека SecondModule.so.

3. Теперь интересный момент. Выяснилось, что SecondModule.so по-прежнему активно использует функции, описанные в FirstModule.c и вкомпилированные в основной исполняемый файл. Соответственно, использовать этот SecondModule.so где-нибудь в левом проекте, который не знает про FirstModule, не получится.

Это обстоятельство можно обойти так - собрать SecondModule.so, включив в неё функции из FirstModule.c. То есть, проект в этом случае будет собираться так:
gcc -c -fPIC FirstModule.c -o FirstModule.o
gcc -c -fPIC SecondModule.c -o SecondModule.o
gcc -shared FirstModule.o SecondModule.o -o SecondModule.so
gcc -c Main.c -o Main.o
gcc -ldl FirstModule.o Main.o -o Main
rm -f *.o
Здесь, конечно, и в Main, и в SecondModule.so код из FirstModule будет дублироваться, но зато SecondModule.so станет автономной.

4. Однако, если теперь посмотреть командой nm -D SecondModule.so, то можно увидеть поразительный факт - в этой библиотеке одинаково хорошо видны как FirstModule_Function1, FirstModule_Function2, так и SecondModule_Function1, SecondModule_Function2 ! Если мы не хотим, чтобы были видны ВСЕ функции, придется добавить к проекту ещё один файл, SecondModule.lds, в котором указать, какие именно символы экспортировать:
{
    global:
        SecondModule_Function1;
        SecondModule_Function2;
    local: *;
}

И тогда уже можно проект собрать в окончательно замороченном виде:
gcc -c -fPIC FirstModule.c -o FirstModule.o
gcc -c -fPIC SecondModule.c -o SecondModule.o
gcc -shared -Wl,--version-script=SecondModule.lds FirstModule.o SecondModule.o -o SecondModule.so
gcc -c Main.c -o Main.o
gcc -ldl FirstModule.o Main.o -o Main
rm -f *.o
(Здесь -Wl,что-то-там - это указание передать линковщику параметр "что-то-там")

В результате получаются:
Программа Main, которая использует в нужный момент SecondModule.so.
Библиотека SecondModule.so, из которой видны две функции, SecondModule_Function1, SecondModule_Function2.

Вроде, неплохо. Если нигде не напутал.

А тут - источники знаний:
http://gcc.gnu.org/onlinedocs/gcc/Link-Options.html
http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html
http://www.cprogramming.com/tutorial/shared-libraries-linux-gcc.html
http://stackoverflow.com/questions/8866790/do-shared-libraries-use-the-same-heap-as-the-application
ftp://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/html_node/ld_25.html