Pull to refresh

Сетевое обращение к библиотекам и рантайм-формирование вызовов функций

Reading time5 min
Views1.4K
Хочу поделиться историей из профессиональной деятельности, которую можно заслуженно поместить в блог с именем crazydev :) Это рассказ о необычных решениях (тех, что я попытался описать в двух словах в заголовке), к которым меня вынудили прийти еще более необычные ограничения и требования.


И вот как-то так, через хитро закрученную ***у, оно и работает ©

Постановка проблемы


Несколько лет назад работали над системой, предназначенной для платформы наших доблестных ВС. Существенная часть работы протекала в портировании (с windows) уже имеющегося софта. И тут оказывается: важный компонент системы (назовем его библиотекой сепуления), предоставленный партнером, попросту не имеет версии под linux, и его разработчик в общем-то не спешит с портом, т.к. нет таких договоренностей. Ругаться со своим начальством, допустившим этот промах при планированний работ бессмысленно — если в итоге сепулькарии будут стоять, то это незакрытый ТЗ, и виноваты в первую очередь будем мы, как исполнители.

Поиск решений


Вначале здесь была стена текста о перебираемых вариантах решений, которые включали в себя и портирование исходников своими силами, и использований виртуальной машины с Реактивной Осью (дабы не задействовать проприетарный софт вероятного противника), и включение в локальную сеть системника в форм-факторе Mini-ITX с той же ReactOS на борту. И напряженный поиск более-менее стабильной версии wine (системные библиотеки довольно старые, а обновлять нельзя — грозит потерей сертификации ОС).
На варианте эмуляции с помощью Wine (который, как вы знаете, вовсе не эмулятор) мы и остановились. Оставалось продумать, как сепулькарии запускаемые внутри процесса серверного софта будут получать доступ к алгоритмам сепуления, вынуждено находящимся под юрисдикцией wine-процесса. Тут мне приходит в голову идея — организовать сетевой транслятор обращения к библиотекам.

Транслятор


В общем виде это выглядит так:

(кстати, похоже, получается одна из вариаций модели «Программа как услуга» (Soft as a service), но это уже тема другого рассказа)
Что мне кажется интересным в такой схеме, так это то, что клиенты могут работать как в разных инстанциях выбранной библиотеки, так и в одной и той же.
Для реализации есть два пути:
1) Обучить транслятор работе с набором необходимых сейчас библиотек и и потом в случае пополнения этого набора каждый раз дописывать его код (или подключать через адаптеры, не суть важно), обеспечивая сопряжение с каждой новой библиотекой.
2) Обеспечить универсальность транслятора, сделав его по-настоящему просто транслятором запросов, перекладывая функцию формирования нужных запросов к новым библиотекам на клиентскую часть.
Очевидно, если бы я пошел по первому пути, писать в crazydev мне было бы нечего :)

Универсальность повсюду



Простой пример подключения библиотеки и вызова из неё функции.

typedef double (*myfunc_type)(long, long);
...
 void *mylib;
 myfunc_type myfunc;
 double res;
...
 mylib = dlopen("mylib.dll", RTLD_LAZY);
 myfunc = dlsym(mylib, "my_func_name");
 res = (*myfunc)(2, 4);


Как мы видим, для того чтобы обратиться к какой либо функции, мы должны предварительно описать её тип:
typedef double (*myfunc_type)(long, long);

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

Всё правильно, нам приходит на помощь старый добрый ассемблер. Что, по сути, вызов функции? Помещение аргументов в стек, передача управления адресу, дальше получение результата, и, в зависимости от соглашения вызова, очистка стека.
Вот кусок кода (на Delphi), как раз выполнявший в моем трансляторе подобные операции (Внес дополнительные комментарии чтобы исключить непонятные моменты):

//Метод осуществляет непосредственный вызов функции,
//которую обслуживает объект класса TDLL_Function
//Возвращает байтовый массив, содержащий результат функции (если он предусмотрен)
function TDLL_Function.Execute(): TByteAr;
var
    i: Integer; //Итератор для цикла
    len : Integer; //Длина байтового массива со значениями параметров
    B1 : Byte; //Буфер для однобайтового параметра
    B2 : Word; //Буфер для двубайтового параметра
    B4 : Cardinal; //Буфер для 4-байтового параметра
    B8 : Double; // Буфер для 8-байтового параметра
    StackPos : Integer; //Переменная для хранения позиции стека
begin
    //ParamBytes это поле класса, байтовый массив содержащий все значения параметров,
    //которые должны быть переданы в функцию
    len:=Length(ParamBytes);

    asm //Запоминаем начальное значение стека (из регистра esp)
        mov StackPos, esp
    end;

    //ParamType это поле класса, массив значений следующего перечисляемого типа:
    //TParamType = (ptOne=1, ptTwo=2, ptFour=4, ptEight=8, ptVoid=0, ptPointer=-1);
    //Содержит типы параметров, которые должны быть переданы в функцию
    //Перебираем массив с конца, т.к. помещать параметры в стек нужно в обратном порядке
    for i := Length(ParamType)-1 downto 0 do
    begin
        case ParamType[i] of //В зависимости от типа текущего элемента
            ptOne:begin
                dec(len,1);
                //Извлекаем из ParamBytes нужное число байт (с конца), кладем в буфер
                Move(ParamBytes[len],B1,1);
                asm //Буфер в регистр, а оттуда в стек
                    MOVSX EAX,B1
                    PUSH EAX
                end;
             end;
            ptTwo:begin //Тоже самое для двух байт
                dec(len,2);
                Move(ParamBytes[len],B2,2);
                asm
                    MOVSX EAX,B2
                    PUSH EAX
                end;
            end;
            ptFour:begin //Тоже самое для четырех байт
                dec(len,4);
                Move(ParamBytes[len],B4,4);
                asm
                    MOV EAX,B4
                    PUSH EAX
                end;
            end;
            //Для указателей пока тоже самое что для четырех байт, но ведь поддержка
            //64-битных систем (8 байт на указатель) есть в перспективе
            ptPointer:begin
                dec(len,4);
                Move(ParamBytes[len],B4,4);
                asm
                    MOV EAX,B4
                    PUSH EAX
                end;
            end;
            ptEight:begin
                dec(len,8);
                Move(ParamBytes[len],B8,8);
                asm //Немного другой вид инструкций для помещения восьми байт
                    PUSH DWORD PTR [B8]+$04
                    PUSH DWORD PTR B8
                end;
            end;
            ptVoid: begin
            end;
        end;
    end;

    //Теперь B4 используется как буфер для чтения результата
case fCallingConv of //Дальше в зависимости от типа соглашения вызова
  ccStdcall:
          begin
            TStdCall(Proc)(); //Proc - указатель на функцию библиотеки
            asm //Считали результат из регистра
              MOV B4,EAX
            end;
          end;
  ccCdecl:begin
            TCdeclCall(Proc)();
            //В случае с Cdecl вызовом мы еще и возвращаем стек в начальное положение
            asm
              MOV B4,EAX
              mov esp, StackPos;
            end;
          end;
end;
  //Если результат не предусмотрен, то мы считали мусор


  case ResultType of //В зависимости от типа результата
  ptOne:begin //Помещаем в массив Result нужное нам число байт из B4
          SetLength(Result,1);
          Move(Byte(B4),Result[0],1);
        end;
  ptTwo:begin
          SetLength(Result,2);
          Move(Word(B4),Result[0],2);
        end;
  ptFour:begin
          SetLength(Result,4);
          Move(B4,Result[0],4);
        end;
  ptPointer:begin
          SetLength(Result,4);
          Move(B4,Result[0],4);
        end;
  ptEight:begin //Отдельный случай для восьми байт, используем B8 в роли буфера
        asm
          FSTP B8
        end;
          SetLength(Result,8);
          Move(B8,Result[0],8);
        end;
  ptVoid:begin
         SetLength(Result,0);
          end;
  end;

end;


Прошу не бить ногами за код, он определенно требует оптимизации.
Про особенности реализации:
1) Доступными сделал только stdcall и cdecl соглашения
2) Нет никаких гарантий в том, что ассемблерные вставки будут так же работать на архитектурах, отличных от тех, под которые это делалось.
1) Нет поддержки 64-битного кода, хотя в целом, кое-какие пути для обеспечения я закладывал
2) Если в функцию передается указатель, например, на массив, то клиент должен был переправить массив целиком, чтобы транслятор развернул его у себя и отправил в функцию указатель на него. Если этот массив нужно было вернуть обратно, то подобным образом организовывалось и его возвращение.

Вообще протокол для общения клиента и транслятора получился достаточно сложным и запутанным, и не знаю, имеет ли смысл его описывать. Скажу только, что он был бинарный :)

Общая последовательность действий для вызова выглядела так:
1) Клиент подключается к транслятору
2) Клиент отправляет имя библиотеки, к которой хочет подключиться
3) Клиент отправляет описание функции библиотеки, которую хочет вызвать
4) Клиент пакует и отправляет параметры для вызова функции библиотеки
5) Транслятор разворачивает параметры в соответствии с полученным описанием функции
6) Транслятор вызывает вышеупомянутый Execute()
7) Транслятор пакует нужные итоги работы (согласно описанию функцию) и отправляет их клиенту.

Вот такой crazydev :) В защиту своей поделки скажу, что в таком виде она безотказно проработала год, и, воспользовавшись этой универсальностью, удалось сократить время на портирование и некоторых других библиотек. А через год, во время планового обновления, уже поспела портированная версия сепулических библиотек, и всё закончилось хорошо.

UPD. Добавил тег «костыли», чтобы не было непоняток с самоидентификацией описанных решений.
Tags:
Hubs:
Total votes 40: ↑35 and ↓5+30
Comments12

Articles