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