В данном коротком посте хотелось бы поделиться опытом о том, как мы решали задачу подмены библиотеки 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/
Добавить комментарий