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

в 10:40, , рубрики: ассемблер, библиотеки, метки: ,

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

Ненормальное программирование / Сетевое обращение к библиотекам и рантайм формирование вызовов функций
И вот как-то так, через хитро закрученную ***у, оно и работает ©

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

Несколько лет назад работали над системой, предназначенной для <a rel="nofollow" href="http://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%B1%D0%B8%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0_%D0%92%D0%BE%D0%BE%D1%80%D1%83%D0%B6%D1%91%D0%BD%D0%BD%D1%8B%D1%85_%D0%A1%D0%B8%D0%BB">платформы наших доблестных ВС. Существенная часть работы протекала в портировании (с 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 :) В защиту своей поделки скажу, что в таком виде она безотказно проработала год, и, воспользовавшись этой универсальностью, удалось сократить время на портирование и некоторых других библиотек. А через год, во время планового обновления, уже поспела портированная версия сепулических библиотек, и всё закончилось хорошо.

Автор: augur


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js