Технологично подменяем библиотеку

в 11:52, , рубрики: linux, Блог компании Нордавинд, отладка, системное программирование, метки: ,

Здравствуйте, уважаемые читатели!

В данном коротком посте хотелось бы поделиться опытом о том, как мы решали задачу подмены библиотеки libpthread. Такая потребность возникла у нас в ходе создания инструмента формирования модели переходов многопоточной программы, о которой мы уже рассказывали в одном из предыдущих постов — habrahabr.ru/company/nordavind/blog/176541/. Технология получилась достаточно универсальной, поэтому спешим ей поделиться с читателим. Надеемся, кому-нибудь пригодится для решения собственных задач.

Итак, начнем с описания задачи, которую решали мы. Для исследования программы на предмет наличия в ней потенциальных ситуаций взаимных блокировок нам необходимо было построить модель программы с точки зрения последовательности обращений к различным средствам синхронизации из различных потоков. Логичным и очевидным решением является «перехват» обращений к средствам синхронизации, которые в нашей системе происходили исключительно через стандартную библиотеку libpthread. Т.е. фактически, решение состоит в подмене библиотеки libpthread на некую нашу библиотеку libpthreadWrapper, которая реализует все функции, которые реализованы в оригинальной библиотеке. При этом в интересующих нас функциях (pthread_mutex_lock, pthread_mutex_unlock, pthread_cond_signal, и др.) добавляется некоторый код, который уведомляет какого-то внешнего listener-а о вызове и параметрах вызова. Реализация всех неинтересующих нас функций оригинальной библиотеки в libpthreadWrapper представляет собой просто вызов соответствующей функции из libpthread.

Все было бы прекрасно и просто, если бы в заголовочном файле pthread.h мы не обнаружили более сотни неинтересующих нас функций, который ну очень не хотелось оборачивать, используя технику копипаста. На помощь нам пришел bash и немного дизассемблера. Решение задачи свелось к автоматической генерации (скрипт мы, понятно дело, назвали generate) исходного кода на Си для установленной в системе библиотеки libpthread и дополнительной информации о том, какие вызовы нас интересуют с точки зрения построения требуемой модели.

Вот так мы получили полный список всех функций, которые реализованы в нашем libpthread:

#! /bin/bash
fnnames=(`sed -rne 's/^extern .* (pthread_[a-zA-Z_0-9]+) (.*/1/p' /usr/include/pthread.h | sort -u`)

Потом мы начали генерировать файл с исходниками. Все начинается с требуемых нам заголовочных файлов и глобальных переменных, которые мы будем использовать для взаимодействия с оригинальной библиотекой libpthread (это я про handle) и с нашим внешним listener-ом (это я про serv и sock):

(
cat <<EOF
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdarg.h>
#include <errno.h>

#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 11111
static void *handle;
static struct sockaddr_in serv;
static int sock=-1;

EOF

Далее, генерируем объявления для указателей на оригинальные функции библиотеки libpthread:

for fn in ${fnnames[*]}
do
    echo "static int (*${fn}_orig_ptr)();"
done

cat <<EOF

Затем формируем требуемые нам служебные функции. Для отправки уведомления о вызове интересующих нас функций libpthread определим __libpthread_send_notification, которая, как видно из приведенного ниже листинга отправляет форматированную (пока еще непонятно кем и как) udp дейтаграмму в инициализированный (пока еще непонятно кем и как) сокет:

__attribute__((visibility("hidden"))) void __libpthread_send_notification(
    const char   *name,
    const char   *fmt,
    ...)
{
    if(sock>=0)
    {
        char buffer[1000];
        unsigned int l;
        int rc;

        pthread_t self=pthread_self_orig_ptr();

        l=snprintf(buffer, sizeof(buffer)-1, "%s@%X:", name, (unsigned int)self);

        va_list ap;
        va_start(ap, fmt);
        l+=vsnprintf(buffer+l, sizeof(buffer)-l-1, fmt, ap);
        buffer[l]=0;
        va_end(ap);

        rc=send(sock, buffer, l+1, MSG_NOSIGNAL);
        if(rc<0)
            fprintf(stderr, "Failed to send %sn", strerror(errno));
        else
            fprintf(stderr, "rc=%dn", rc);
    }
}

Далее отвечаем на обозначенный чуть ранее вопрос «кем и как инициализированным?». Обратите внимание на атрибут constructor для функции __libpthread_safety_init, который приведет к автоматическому выполнению функции в момент загрузки нашей библиотеки libpthreadWrapper:

__attribute__((constructor)) void __libpthread_safety_init(void)
{

Как видим из реализации __libpthread_safety_init, сетевой адрес нашего listener-а мы определяем через значение переменной окружения PTHREAD_ACTIONS_RECEIVER:

    char *ipaddr=getenv("PTHREAD_ACTIONS_RECEIVER");

    memset(&serv, 0, sizeof(serv));
    serv.sin_port=htons(PORT);

    if(ipaddr!=NULL)
    {
        if(!inet_aton(ipaddr, &serv.sin_addr))
        {
            struct hostent *hp=gethostbyaddr(ipaddr, strlen(ipaddr), AF_INET);
            if(hp==(struct hostent *)0)
            {
                serv.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
            }
            else
            {
                serv.sin_addr=*(struct in_addr *)hp->h_addr;
            }
        }
    }
    else
    {
        serv.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
    }

    if((sock=socket(AF_INET, SOCK_STREAM, 0))<0)
    {
        fprintf(stderr, "Failed to create socketn");
    }
    else if(connect(sock, (struct sockaddr *)&serv, sizeof(serv))<0)
    {
        fprintf(stderr, "Failed to connectn");
        close(sock);
        sock=-1;
    }

После инициализации всего сетевого хозяйства, необходимого для отправки уведомлений внешнему listener-у, занимаемся непосредственно оригинальной библиотекой libpthread, которую открываем dlopen-ом, а затем получаем указатели на все оригинальные функции:

    handle=dlopen("/lib/libpthread.so.0", RTLD_NOW);

    fprintf(stderr, "dlopened original libpthread, handle=%pn", handle);
EOF

for fn in ${fnnames[*]}
do
    echo "    ${fn}_orig_ptr=dlsym(handle, "${fn}");"
#    echo "    printf("%s=%pn", "${fn}_orig_ptr", ${fn}_orig_ptr);"
done

cat <<EOF
}

Затем, не забываем объявить функцию, которая освободит сокет при выключении. Опять же обращаем внимание на атрибут destructor:

__attribute__((destructor)) void __libpthread_safety_done(void)
{
    close(sock);
}
EOF

Ну и собственно самое интересное! Генерируем реализацию наших новых функций, которые будут вызываться из нашей программы вместо оригинальных функций libpthread! Пробегаемся в цикле по уже полученному ранее списку функций библиотеки:

for fn in ${fnnames[*]}
do

Проверяем наличие специального файлика для каждой функции (об этом чуть позже). Если файлик есть, то генерируем обертку, которая сначала вызывает __libpthread_send_notification, а затем передает управление оригинальной функции из libpthread:

    if [[ -f "$0.$fn" ]]
    then
        source "$0.$fn"

        cat <<EOF
extern int ${fn}($WRAPPER_ARGS)
{
    __libpthread_send_notification("${fn}", $FORMAT_STRING);
    return ${fn}_orig_ptr($DELEGATED_ARGS);
}
EOF

Если специального файлика нет, то эта функция нас не интересует с точки зрения построения модели, поэтому просто генерируем заглушку, которая пробросит вызов в оригинальную библиотеку libpthread:

    else
        cat <<EOF
extern int ${fn}()
{
#ifdef __x86_64__
    asm(
        "popq %%rbpnt"
        "movq %0,%%raxnt"
        "jmp *%%raxnt"
        : /**/
        : "ri"(${fn}_orig_ptr));
#else
    asm(
        "popl %%ebpnt"
        "movl %0,%%eaxnt"
        "jmp *%%eaxnt"
        : /**/
        : "ri"(${fn}_orig_ptr)
        : "eax");
#endif
    return 666;
}
EOF
    fi
done

Конечно, приведенный код на ассемблере не является portable, но он без проблем может быть определен для используемой вами платформы с помощью дизасемблера.

Ну и, собственно, компилируем сгенерированный нами исходник:

) | tee .autogenerated | gcc -x c -fPIC -shared -Wl,-soname -Wl,libpthread.so.0 -Wall -ldl -o libpthread.so.0 -

Если собрать все фрагменты, приведенные выше в том же самом порядке, то получится работоспособный скрипт. Перед его запуском осталось лишь сформировать файлы с описанием интересующих нас вызовов libpthread. Формат этих файлов, надеюсь, уже понятен из текста скрипта, приведенного выше:

generate.pthread_mutex_lock:

WRAPPER_ARGS="void *ptr"
DELEGATED_ARGS="ptr"
FORMAT_STRING=""%p", ptr"

generate.pthread_mutex_unlock:

WRAPPER_ARGS="void *ptr"
DELEGATED_ARGS="ptr"
FORMAT_STRING=""%p", ptr"

generate.pthread_cond_wait:

WRAPPER_ARGS="void *cond, void *mutex"
DELEGATED_ARGS="cond, mutex"
FORMAT_STRING=""%p,%p", cond, mutex"

И так далее.

Надеюсь, данная техника будет вам полезна в решении ваших задач!

Автор: isvirin

Источник

Поделиться

* - обязательные к заполнению поля