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

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

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

    Комментарии 11

      +1
      Тема не раскрыта. Зачем это всё, когда есть dynamic linker, который это делает автоматически?

      Но даже если и надо зачем-то, то новый дом здорового человека создаётся не вызовом «library_func» каждый раз, а
      typedef const char *(*mysql_stat_t)(MYSQL * mysql);
      mysql_stat_t mysql_stat;
      ...
      void init_new_homes()
      {
        mysql_stat= (mysql_stat_t)library_func(library, "mysql_stat");
      }
      

      и теперь mysql_stat() можно использовать как обычно. Это прекрасно работает и с variadic, тут никакой разницы.

      Если же обязательно надо вызвать variadic функцию из variadic враппера — например, в gcc для этого есть __builtin_va_arg*, может и у VS что-то найдется. Используется так:
                extern int myprintf (FILE *f, const char *format, ...);
                extern inline __attribute__ ((__gnu_inline__)) int
                myprintf (FILE *f, const char *format, ...)
                {
                  int r = fprintf (f, "myprintf: ");
                  if (r < 0)
                    return r;
                  int s = fprintf (f, format, __builtin_va_arg_pack ());
                  if (s < 0)
                    return s;
                  return r + s;
                }
      
        –1
        Если же обязательно надо вызвать variadic функцию из variadic враппера — например, в gcc для этого есть __builtin_va_arg*, может и у VS что-то найдется.

        __builtin_va_arg не являются частью стандартной библиотеке. Решение в статье основано на стандартных функциях.
        то новый дом здорового человека создаётся не вызовом «library_func» каждый раз, а

        Это требует хранения всех этих переменных, создания их экземпляров. При автоматической генерации файла гораздо проще при проходе генератора в цикле сгенерировать файл за один проход.
        Тем более, что при этом способе, если подгружается статическая библиотека-загрузчик, то вам всё равно нужно прокидывать наружу библиотеки функции, которые вызывают «оригинальные»:
        int func(int a) {
            direct_func(a);
        }
        
        0
        конечно __builtin_va_arg не является. но если (я не проверял) у VS есть что-то похожее, то оно все ifdef-ится. Так же как и LoadLibrary с HMODULE.

        А переменные — ну тут или переменные или функции, что-то создавать надо. Только каждый раз дёргать dlsym и искать функцию по имени при каждом вызове — это жуткий перебор.

        А что значит «прокидывать наружу» и зачем это делать?
          0
          А что значит «прокидывать наружу» и зачем это делать?

          Основная идея в том, чтобы не менять исходный код всех проектов, но при этом обеспечить не статическую, а динамическую линковку с целевой библиотекой. Для этого:
          Создается отдельная библиотека в дереве проектов, с которой происходит статическая линковка. Которая динамически загружает нужную нам и производит экспорт символов. Таким образом не меняя код по всем проектам можно отвязать их от линковки установленными в системе библиотеками при компиляции.
            0
            ну да, это смысл любого dlopen/dlsym. А функции-врапперы зачем? Искать функцию по имени при каждом вызове — это дикий overhead, такое надо делать только если есть уважительная причина не кешировать.

            С указателями на функции, как я выше показал, точно так же «не меняя код по всем проектам можно отвязать их от линковки». Это быстрее и никаких проблем с variadic.
              –1
              С указателями на функции, как я выше показал, точно так же «не меняя код по всем проектам можно отвязать их от линковки». Это быстрее и никаких проблем с variadic.

              Как вы собрались не меняя код экспортировать ваши переменные за пределы библиотеки? Это как минимум требует декларации extern в вызывающем коде, иначе переменные не видны.
                0
                ну да. static_library.h:
                typedef const char *(*mysql_stat_t)(MYSQL * mysql);
                extern mysql_stat_t mysql_stat;
                
                typedef int (*mysql_optionsv_t)(MYSQL * mysql,
                                   enum mysql_option,
                                   const void * arg,
                                   ...);
                extern mysql_optionsv_t mysql_optionsv;
                

                static_library.c
                mysql_stat_t mysql_stat;
                mysql_optionsv_t mysql_optionsv;
                
                void init_static_library()
                {
                  mysql_stat= (mysql_stat_t)library_func(library, "mysql_stat");
                  mysql_optionsv= (mysql_optionsv_t)library_func(library, "mysql_options");
                }

                вызывающий код должен сделать #include <static_library.h>, это по-любому надо, хоть с функциями, хоть с указателями
                  –1
                  вызывающий код должен сделать #include <static_library.h>, это по-любому надо, хоть с функциями, хоть с указателями

                  Ну то есть вызывающий код надо модифицировать.
                  А в случае с библиотекой-враппером этого делать не нужно, потому что исходный условный #include <mysql/mysql.h> уже включен и содержит декларации экспортированных из враппера функций.
                  Всё что нужно поменять для такой замены — поменять в сборщике линковку с оригинальной mysql на библиотеку-враппер.

                  А проблема с «оверхед» решается проще через экземпляр статик-переменной с типом функции внутри функции обертки, которая инициализируется при первом вызове.
                    0
                    А-а, совсем не модифицировать. Тогда да. Хотя я бы всё равно подсунул бы свой mysql.h впереди в пути include-ов, а в нём бы include-ил системный mysql.h

                    потому что врапперами проблемы с variadic не решить. да и зачем вызывать сложно, когда можно вызывать просто.

                    Решение в статье работает только для variadic функций, которые берут не более чем 8 void* параметров. А например mysql_optionsv может брать size_t. Хотя практически это может и работать, если всегда sizeof(size_t) == sizeof(void *).

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

                      Решена выше с припиской:
                      Если по каким-то причинам нам не будет хватать 9 параметров мы всегда сможем поместить v-переменные в массив нужного нам размера и воспользоваться циклом.

                      void * на x86 и x86_64 будет размером с машинное слово, следовательно по размеру регистра. Что бы не принимала variadic — этой ширины хватит для регистровой передачи, а что не влезает будет как обычно передано на стеке.
                      Хотя я бы всё равно подсунул бы свой mysql.h впереди в пути include-ов, а в нём бы include-ил системный mysql.h

                      Автоматикой проходиться по нескольким сотням тысяч строк опасно — это раз. Подключаться может не той строкой что вы ожидаете — два (например libxml по инструкции подключается макросом).
                      Если можно грузить разные реализации библиотеки, где функции берут слегка отличающиеся параметры.

                      И это тоже. После автогенерации кода для всех функций это можно поправить внутри враппера руками.
            0
            И да, в VS нет подобного механизма: coderoad.ru/13972329/%D0%95%D1%81%D1%82%D1%8C-%D0%BB%D0%B8-%D1%83-MSVC-%D1%87%D1%82%D0%BE-%D1%82%D0%BE-%D0%B2%D1%80%D0%BE%D0%B4%D0%B5-__builtin_va_arg_pack
            И мало того, этот механизм можно использовать только на встроенных функциях там, где компилятор заранее видит количество аргументов. На экспортируемые функции это не работает.

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

          Самое читаемое