О технологичной подмене библиотеки

от автора

Здравствуйте, уважаемые Хабраюзеры!

В данном коротком посте хотелось бы поделиться опытом о том, как мы решали задачу подмены библиотеки 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 %s\n", strerror(errno));         else             fprintf(stderr, "rc=%d\n", 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 socket\n");     }     else if(connect(sock, (struct sockaddr *)&serv, sizeof(serv))<0)     {         fprintf(stderr, "Failed to connect\n");         close(sock);         sock=-1;     } 

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

    handle=dlopen("/lib/libpthread.so.0", RTLD_NOW);      fprintf(stderr, "dlopened original libpthread, handle=%p\n", handle); EOF  for fn in ${fnnames[*]} do     echo "    ${fn}_orig_ptr=dlsym(handle, \"${fn}\");" #    echo "    printf(\"%s=%p\n\", \"${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 %%rbp\n\t"         "movq %0,%%rax\n\t"         "jmp *%%rax\n\t"         : /**/         : "ri"(${fn}_orig_ptr)); #else     asm(         "popl %%ebp\n\t"         "movl %0,%%eax\n\t"         "jmp *%%eax\n\t"         : /**/         : "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" 

И так далее.

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

ссылка на оригинал статьи http://habrahabr.ru/company/nordavind/blog/177369/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *