Transparent variadic или безысходность стека

«Уже пол-шестого утра… Ты знаешь, где сейчас твой указатель стека?»
Аноним.

Акт I. Сцена I. Интродукция

Вечер. На сервере лениво поскрипывали регистры, то заполняясь данными, то отдавая их обратно. Указатель на стек замедлялся, перемещаясь все медленнее и медленнее, пока не замер совсем. Блин жесткого диска совершил свой последний на сегодня оборот и замер. Сборка проекта завершилась. Долгие 2.5 секунды компиляции завершились и новая версия увидела мир в первый раз.

Однако страдания сервера на этом не закончились. Завершив первую сборку сервер запустил вторую, за ней третью, затем четвертую и пятую. Казалось, что разным версиям не будет конца. Но что поделать, если целевые машины имеют разный набор библиотек; кому-то нужен openssl версией не выше 0.9.8, кому-то непременно нужна MySQL вместо MariaDB. Мир разнообразен и не все готовы менять то, что уже устоялось и работает долгое время.

Но можно ли снять столь тяжкое бремя с плеч сервера? Можно ли облегчить его боль? Убрать часть агентов, отпустить их на волю? Но ведь тогда статическая линковка с конкретными версиями библиотек не даст возможность запустить их на другом окружении. Что ж, на это существует динамическая линковка. Она сложнее, ведь нужно очень многое сделать, чтобы использовать столь непривычный некоторым механизм.

Займет ли автоматизация процесса неделю? Месяц? Год? Нужно ли расширить счетчик потраченных часов в Jira до 64 бит? Кто знает.

Акт I. Сцена II. Основы линковки

Динамическая линковка — есть добрая противоположность линковки статической. Она не создает привязки к версии, она позволяет «на лету» сменить версию библиотеки или поменять подлежащий драйвер работы с базой. Однако это достигается сложностью подключения. Рассмотрим же механизм сей, дабы не говорить больше о нем.

Создадим отдельную библиотеку, к которой будем линковаться статически, но которая будет динамически искать подходящую нам библиотеку.

Для начала создадим ряд сокращений, чтобы отделить зерна от плевел и агнцев от козлищ:

/* \brief Определим, что мы находимся под Linux */ #if defined(__gnu_linux__) #   define OS_GLX #endif /* \brief Определим, что мы находимся под Windows */ #if defined(_WIN32) || defined(_WIN64) || \     defined(__WIN32__) || defined(__TOS_WIN__) || defined(__WINDOWS__) #   define OS_WIN #endif

Затем создадим абстракции типов, чтобы впасть в благостное неведение:

#if defined(OS_GLX) /*! \brief Тип для динамической библиотеки */ typedef void * library_t; #elif defined(OS_WIN) /*! \brief Тип для динамической библиотеки */ typedef HMODULE library_t; #endif

Объявим помощников наших, делающих работу за нас:

 /*! \brief Загружает динамическую библиотеку  * \param[in] name Имя файла  * \return Хэндлер библиотеки */ library_t library_load(const char * name) { #if defined(OS_GLX)     return dlopen(name, RTLD_LAZY);  #elif defined(OS_WIN)     LPWSTR wname = _stringa2w(name);     library_t lib = LoadLibrary(wname);     free(wname);     return lib; #endif }  /*! \brief Получает указатель на функцию из динамической библиотеки  * \param[in] lib Библиотека  * \param[in] name Имя функции  * \return Указатель на функцию */ void * library_func(library_t lib, const char * name) { #if defined(OS_GLX)     return dlsym(lib, name);  #elif defined(OS_WIN)     return GetProcAddress(lib, name); #endif }  /*! \brief Освобождает ресурсы динамической библиотеки  * \param[in] lib Библиотека */ void library_free(library_t lib) { #if defined(OS_GLX)     dlclose(lib);  #elif defined(OS_WIN)     FreeLibrary(lib); #endif }

Теперь же, забыв, где находимся мы можем приступить к загрузке библиотек, дабы одарили они нас функциями их. Для сего необходимо выполнить три действия:

  1. Открыть библиотеку при помощи library_load

  2. Найти нужные функции при помощи library_func

  3. Закрыть библиотеку при помощи library_free

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

Акт I. Сцена III. Избрание

Для того, чтобы описать загрузку функций необходимо иметь информацию о том, как те функции зовутся и что они могут брать, а что отдавать. Возьмем в руки одну из библиотек, чтобы найти имена несчастных и внести их в список на избавление от страданий:

$ nm -D /usr/lib/x86_64-linux-gnu/libmysqlclient.so | grep ' T ' 0000000000033920 T get_tty_password 000000000006fa00 T handle_options 0000000000061860 T my_init 000000000006d830 T my_load_defaults 00000000000351e0 T my_make_scrambled_password 00000000000220d0 T mysql_affected_rows 0000000000025cd0 T mysql_autocommit 0000000000020cf0 T mysql_change_user 0000000000022160 T mysql_character_set_name 0000000000032e10 T mysql_client_find_plugin 0000000000032590 T mysql_client_register_plugin 000000000002b580 T mysql_close 0000000000025c90 T mysql_commit 00000000000215c0 T mysql_data_seek 0000000000020bb0 T mysql_debug ...

Описание их быта и достатка мы получим, преобразовав манускрипт с объявлениями. Коснемся его компиляторной десницей, дабы избавиться от недомолвок препроцессорных:

gcc -E mysql/mysql.h > mysql_generate.h
[...] const char * mysql_stat(MYSQL *mysql); const char * mysql_get_server_info(MYSQL *mysql); const char * mysql_get_client_info(void); unsigned long mysql_get_client_version(void); const char * mysql_get_host_info(MYSQL *mysql); unsigned long mysql_get_server_version(MYSQL *mysql); [...]

Акт I. Сцена IV. Новое обиталище

Подготовим фундамент для нового обиталища заблудших душ:

/* \brief Экземпляр библиотеки */ static library_t library = NULL;  /* \brief Инициализатор модуля */ void _init(void) __attribute__((constructor)); void _init(void) {      library = library_load(MYSQL_FILENAME_SO);      if (!library)          // Грусть и уныние          exit(EXIT_FAILURE); }  /*! \brief Деинициализатор модуля */ void _free(void) __attribute__((destructor)); void _free(void) {     if (library)         library_free(library); }

Теперь же, руками бездушного автомата пройдемся по списку заблудших душ, да создадим для каждой новый дом:

const char * mysql_stat(MYSQL * mysql) {     return (const char (*)(MYSQL * mysql))library_func(library, "mysql_stat")(mysql);  }  const char * mysql_get_server_info(MYSQL * mysql) {          return (const char (*)(MYSQL * mysql))library_func(library, "mysql_get_server_info")(mysql); }

Жизнь была размеренной и спокойной, пока мы не встретили ужас глубин.

Акт II. Сцена I. Вариативная

int mysql_optionsv(MYSQL * mysql,                    enum mysql_option,                    const void * arg,                    ...);

Сей монстр назывался вариативной функций и не знала история более мерзкого создания, ибо никто не мог сказать сколько голов у данного зверя. Кому-то он показывал одну, кому-то две, а кто-то лицезрел зверя по всём его мерзостном обличие.

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

Даже SWIG, рыцарь из легенд, не cмог побороть монстра, а лишь усыпил его, дав время тому залечить раны да набраться сил.

Уж если сам SWIG не смог, то что делать нам, простым крестьянам, в первый раз взявших в руки вилы? Обратимся к мудрости древних, дабы увидеть, как сей монстр пожирает души врагов своих:

int foo(int argc, ...) {     return argc;  }
gcc -S file.c
_Z3fooiz:         pushq   %rbp         movq    %rsp, %rbp         subq    $72, %rsp         movl    %edi, -180(%rbp)         movq    %rsi, -168(%rbp)         movq    %rdx, -160(%rbp)         movq    %rcx, -152(%rbp)         movq    %r8, -144(%rbp)         movq    %r9, -136(%rbp)         testb   %al, %al         je      .L4         movaps  %xmm0, -128(%rbp)         movaps  %xmm1, -112(%rbp)         movaps  %xmm2, -96(%rbp)         movaps  %xmm3, -80(%rbp)         movaps  %xmm4, -64(%rbp)         movaps  %xmm5, -48(%rbp)         movaps  %xmm6, -32(%rbp)         movaps  %xmm7, -16(%rbp) .L4:         movl    -180(%rbp), %eax         leave         ret

Ужасен монстр сей. Ведь каждый раз он кладет из регистров данные в стек, чтобы нечестивые дети его — va_start, va_arg, да va_end могли лишь перебирая костяшки стека получать оттуда то, что им не принадлежало.

Акт II. Сцена II. Стековая

Рассмотрим, что же являет собой стек. Стек есть суть массив, в котором данные лежат подряд, однако работа с ними ведется по принципу LIFO. Стек может принять много данных, в отличие от регистров, что быстры подобно стрелам, но малочисленны.

Аргументы функций согласно соглашению о вызове передаются через регистры, а затем через стек (на x86_64), если размера регистров не хватает, чтобы удержать все данные при передаче. Вариативная же функция перекладывает всё содержимое регистров в стек, чтобы все данные, отправленные в функцию кроме регистров находились также и на стеке, давая возможность реализовать va_arg(list, type) как

mov rax, [list.rsp] add list.rsp, sizeof(type)

Нужно понимать, что стек никак не выделяет границы переменных, что не позволяет нам знать, сколько переменных в стеке, какого они размера и типа. После перекладывания регистров в стек уже невозможно сказать, какие данные были переданы и были ли они вообще, поскольку если регистр задействован при передаче данных не был, то дух его нечестивый тоже будет положен в стек, засоряя его своим старым значением.

Так как же нам заставить машину передать эти данные в следующую функцию? Ведь мы не можем написать вот так:

int foo(int argc, ...) {     return bar(argc, ...);  }

В некоторых случаях мы могли бы ориентироваться на аргументы, передаваемые с функцией — argc может означать количество аргументов, но не говорит об их типе или размере. NULL-терминированный список аргументов можно перепутать с NULL-параметром в списке аргументов, строковые форматтеры сложны в реализации и имеют много подводных камней.

Так можно ли отправить все аргументы дальше, причем сделать это руками машины, доверив ей написание священных манускриптов самостоятельно?

Акт II. Сцена III. Бездна-void

Мы видели, что происходит внутри такой функции. Но что происходит снаружи?

int foo(int argc, ...) {     return argc; }  void bar(void) {     foo(1, 2, 3, "string"); }
gcc -S file.c
.LC0:         .string "string" _Z3barv:         pushq   %rbp         movq    %rsp, %rbp         movl    $.LC0, %ecx         movl    $3, %edx         movl    $2, %esi         movl    $1, %edi         movl    $0, %eax         call    _Z3fooiz

Как видим, вызов такой функции ничем не отличается от обычной и не передает никакой информации об аргументах. Но раз такой информации нет, значит и функция, читающая эти значения из стека при помощи va_arg тоже неспособна их отличить. А раз после вызова функции регистры и стек сливаются в единое целое, то почему бы нам этим не воспользоваться?

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

int foo(int argc, ...) {     return realfoo(argc, <байты из стека>);  }

Какие же инструменты мы можем применить, чтобы это сделать? В первую очередь мы могли бы работать со стеком напрямую, например через получения указателя на argc и последовательным разыменованием его после прибавления к нему размера машинного слова.

int foo(int argc, ...) {     long long int * rsp = &argc;     return realfoo(argc, *(rsp + 1), *(rsp + 2), <...>); }

Однако такой способ неудобен и неочевиден и нас проклянут все последующие поколения, кто будет читать сей опус. Поэтому мы будем использовать стандартизированные инструменты доступа к стеку — те самые функции va:

#include <stdio.h> #include <stdarg.h>  void params(const char * fmt, ...) {     va_list list;     va_start(list, fmt);     vprintf(fmt, list);     va_end(list); }  void wrapper(const char * fmt, ...) {     va_list list;     va_start(list, fmt);          void * v1 = va_arg(list, void *);     void * v2 = va_arg(list, void *);     void * v3 = va_arg(list, void *);     void * v4 = va_arg(list, void *);     void * v5 = va_arg(list, void *);     void * v6 = va_arg(list, void *);     void * v7 = va_arg(list, void *);     void * v8 = va_arg(list, void *);     void * v9 = va_arg(list, void *);          params(fmt, v1, v2, v3, v4, v5, v6, v7, v8, v9);          va_end(list);  }  int main(void) {     params("%d %s %lld\n", 5, "sss", (long long int) 32);     wrapper("%d %s %lld\n", 5, "sss", (long long int) 32);     return 0; }

Но следует обратить внимание, что вставив va_arg сразу в params мы отдадим порядок их вызова на откуп хитрому компилятору, что сломает порядок чтения.

Этот способ работает как на x86_64, так и на x86 архитектурах. Если по каким-то причинам нам не будет хватать 9 параметров мы всегда сможем поместить v-переменные в массив нужного нам размера и воспользоваться циклом. Чтение из стека «лишних» переменных ни на что не влияет, поскольку в этой же функции стек был уже заполнен «мусорными» регистрами при входе в эту функцию.

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

После вызова функции wrapper переменные забираются из стека, и снова кладутся в регистры, после чего вызывается функция params, которая не замечая, что переменные были преобразованы в void * выведет их так же, как и в первый раз.

Удобнее всего то, что такой код можно сгенерировать автоматически для вариативных функций с любыми аргументами, а вызовы исходных функций в коде не потребуется менять.

Акт II. Сцена IV. Эпилог

Блин диска повернулся еще раз и остановился. Собрав проект один раз, сервер остановился, тяжело вздохнул всеми кулерами, и задумался о том, понадобится ли он еще когда-нибудь или это была последняя сборка в его жизни. Нужен ли он еще или в коде достигнут идеал? Можно ли вообще достигнуть идеала?

Диод на его панели погас и сборочный сервер уснул, так и не придя к однозначному ответу.

ссылка на оригинал статьи https://habr.com/ru/post/524860/

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

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