26 октября

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

Системное программированиеОтладкаC

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

Акт 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. Эпилог

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

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

Теги:Сстекпамять
Хабы: Системное программирование Отладка C
+4
1,1k 18
Комментарии 11
Похожие публикации
Верификатор (UVM)
от 200 000 ₽KraftwayМоскваМожно удаленно
Reverse Engineer
от 3 000 до 4 000 $Hand2NoteМожно удаленно
Программист BIOS
от 160 000 ₽AquariusМосква
Senior system developer/ С++
до 170 000 ₽GETMOBITМосква
Разработчик С/C++
от 80 000 ₽EltexНовосибирск
Лучшие публикации за сутки