Научная статья на тему 'СПОСОБЫ ДОПОЛНЕНИЯ ФУНКЦИОНАЛЬНОСТИ ПРОГРАММЫ БЕЗ ИЗМЕНЕНИЯ ИСХОДНОГО КОДА'

СПОСОБЫ ДОПОЛНЕНИЯ ФУНКЦИОНАЛЬНОСТИ ПРОГРАММЫ БЕЗ ИЗМЕНЕНИЯ ИСХОДНОГО КОДА Текст научной статьи по специальности «Компьютерные и информационные науки»

CC BY-NC
133
24
i Надоели баннеры? Вы всегда можете отключить рекламу.
Ключевые слова
ФУНКЦИОНАЛЬНОСТЬ / ТЕСТИРОВАНИЕ / ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ / ТЕХНОЛОГИИ / ПРОИЗВОДИТЕЛЬНОСТЬ / КОМПИЛЯТОРЫ / ОПЕРАЦИОННЫЕ СИСТЕМЫ

Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Бояринцева Юлия Андреевна, Шерминская Анна Алексеевна, Николаев Андрей Анатольевич

Объектом данной работы является программное обеспечение (ПО) с некоторыми заданными требованиями к функционированию. Для объекта принято допущение о невозможности изменения его исходного кода со стороны специалиста-разработчика. Целью работы является внедрение дополнительных вариантов функционирования в заданное ПО с учетом принятого допущения. Выполнено исследование способов дополнения функциональности для различных операционных систем, компиляторов и языков программирования. Проведено сравнение этих способов по следующим критериям: этап выполнения, поддерживаемые типы сборки, возможность работы в среде разработки, быстродействие и простота внедрения, а также широта внедряемого функционала. В результате выбран наиболее перспективный способ и обозначено направление дальнейших исследований. Рассматриваемые способы дополнения функциональности могут найти применение при решении широкого спектра задач при разработке и тестировании сложного ПО с повышенными требованиями к надежности.

i Надоели баннеры? Вы всегда можете отключить рекламу.

Похожие темы научных работ по компьютерным и информационным наукам , автор научной работы — Бояринцева Юлия Андреевна, Шерминская Анна Алексеевна, Николаев Андрей Анатольевич

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.
i Надоели баннеры? Вы всегда можете отключить рекламу.

INTEGRATION OF ADD-ONS WITHOUT SOURCE CODE MODIFICATION

This paper discusses a notional software with certain pre-defined functionality requirements assuming that developer cannot edit its source code. The purpose of this work was to integrate this software with add-ons taking this restriction into account. The study investigates how add-ons can be applied in various operation systems, compilers and programming languages, comparing these methods as per the following criteria: implementation stage, supported types of assembling, possibility of working in development framework, operation speed and ease of implementation, as well as the spectrum of newly added functions. As a result, the authors managed to choose the most promising method and outline the direction of further research efforts. These methods for implementation of add-ons could be helpful for a great variety of tasks related to development and testing of complex software with stringent reliability requirements.

Текст научной работы на тему «СПОСОБЫ ДОПОЛНЕНИЯ ФУНКЦИОНАЛЬНОСТИ ПРОГРАММЫ БЕЗ ИЗМЕНЕНИЯ ИСХОДНОГО КОДА»

DOI: 10.24937/2542-2324-2021-2-S-I-84-90 УДК 004.4

Ю.А. Бояринцева, А.А. Шерминская, А.А. Николаев

АО «Концерн «Моринформсистема-Агат», Москва, Россия

СПОСОБЫ ДОПОЛНЕНИЯ ФУНКЦИОНАЛЬНОСТИ ПРОГРАММЫ БЕЗ ИЗМЕНЕНИЯ ИСХОДНОГО КОДА

Объектом данной работы является программное обеспечение (ПО) с некоторыми заданными требованиями к функционированию. Для объекта принято допущение о невозможности изменения его исходного кода со стороны специалиста-разработчика. Целью работы является внедрение дополнительных вариантов функционирования в заданное ПО с учетом принятого допущения.

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

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

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

Авторы заявляют об отсутствии возможных конфликтов интересов.

DOI: 10.24937/2542-2324-2021-2-S-I-84-90 UDC 004.4

Yu. Boyarintseva, A. Sherminskaya, A. Nikolaev

JSC Concern Morinformsystem Agat, Moscow, Russia

INTEGRATION OF ADD-ONS WITHOUT SOURCE CODE MODIFICATION

This paper discusses a notional software with certain pre-defined functionality requirements assuming that developer cannot edit its source code. The purpose of this work was to integrate this software with add-ons taking this restriction into account.

The study investigates how add-ons can be applied in various operation systems, compilers and programming languages, comparing these methods as per the following criteria: implementation stage, supported types of assembling, possibility of working in development framework, operation speed and ease of implementation, as well as the spectrum of newly added functions.

As a result, the authors managed to choose the most promising method and outline the direction of further research efforts. These methods for implementation of add-ons could be helpful for a great variety of tasks related to development and testing of complex software with stringent reliability requirements.

Keywords: functionality, testing, software, technologies, performance, compilers, operation systems. Authors declare lack of the possible conflicts of interests.

Для цитирования: Бояринцева Ю.А., Шерминская А.А., Николаев А.А. Способы дополнения функциональности программы без изменения исходного кода. Труды Крыловского государственного научного центра. 2021; Специальный выпуск 2: 84-90.

For citations: Yu. Boyarintseva, A. Sherminskaya, A. Nikolaev. Application of add-ons without source code modification. Transactions of the Krylov State Research Centre. 2021; Special Issue 2: 84-90 (in Russian).

Введение

Introduction

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

В настоящей статье описаны способы увеличения функциональности программ посредством использования функций-оберток или отладчика GDB для языков C и C++ в ОС семейства Windows и Linux. Произведено сравнение рассматриваемых способов.

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

Рассмотрены варианты реализации функций-оберток для следующих компиляторов: MS VC; GCC, язык C; GCC, язык C++. Использование отладчика GDB предусматривает компиляцию программы с помощью GCC. При принятии и обосновании технических решений, а также разработке программ использовались работы [1] и [2].

Обоснование применения

Motivation for application

Развитие технических систем неизбежно приводит к усложнению программного обеспечения: увеличению объема исходного кода, структурной и иерархической сложности, возрастанию необходимости выполнения распределенных вычислений и решения задач масштабирования.

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

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

самым уменьшающих влияние человеческого фактора в процессе тестирования.

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

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

Функции-обертки

Wrap function

Принципы использования

Функции-обертки (Wrap Function) - подходящий инструмент для изменения функционала программы с сохранением исходного кода. Принцип его использования состоит в изменении последовательности запусков функций (рис. 1 и 2): вместо вызова реальной функции происходит вызов функции-обертки, которая, в свою очередь, вызывает реальную функцию.

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

return return

Рис. 2. Порядок вызова программы с использованием функции-обертки

Fig. 2. СэМ^ sequence with wrap function

Real Function

Рис. 1. Порядок вызова исходной программы Fig. 1. Calling sequence for initial software

Wrap Function

Real Function

Программная реализация функций-оберток

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

Функции-обертки для ОС Windows (компилятор MSVC)

В данном случае используется тот факт, что компилятор MSVC строит дисассемблированный код следующим образом: каждая функция имеет свою метку, которая указывает на команду jmp <смещение> (рис. 3).

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

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

1) в зависимости от стандарта вызова осуществляется передача аргументов функции в стек;

2) выполняется команда call по адресу метки функции (например, callReal Function (01663A0h));

00F31708 jmp realFunc (0F37810h)

"^lbDol HoDk(void *toHook, void *ourFunc, int lcn)

VirtualProtect(toHcokj len, PftG E _ EXEC U T E _READWRI7E, «cunnP^otection)j OUOP.D r-wluLivwAilüшьь = ((DWOR[>)uurFuNt - <DWORD) LuhuulO " 5;

//устанавливаем первым бейт в метке исходной функции (OxhU opcode кочанды jip)

*(D'rfDRn")((DWORD)tnlInnlr + 1) - re [vFÄddi-ряч; //возврацаем старое значение защиты памят^

3) выполняется команда jmp по смещению на некоторый адрес, рассчитываемый на этапе компиляции всей программы (подмена смещения уже на этапе выполнения программы дает возможность выполнить переход не на реальную функцию, а на функцию-обертку). На рис. 4 показан пример функции, осуществляющей замену команды jmp в метке функции. Функция Hook в качестве аргументов требует указатели на адрес метки реальной функции, а также функции, которой мы хотим ее подменить. Изначально адрес метки функции имеет статус защищенного. Для его редактирования снимем защиту, воспользуясь функцией библиотеки win-dows.h VirtualProtect.

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

Функции-обертки

для кроссплатформенной реализации (компилятор GCC, язык C)

В компиляторе GCC дисассемблированный код вызова функции имеет отличный от MSVC вид. Тогда как в MSVC команда call сначала вызывает метку реальной функции, а после по ее адресу выполняет jmp на саму реализацию, в GCC call выполняет переход сразу на реализацию функции, минуя метку (рис. 5 и 6). Т.е. подменить смещение для команды jmp по адресу метки функции нельзя.

К тому же по причине того, что Unix-подобные ОС имеют отличный от Windows API, возникает проблема разрешения доступа к памяти по метке. Ввиду трудоемкости решения перечисленных проблем возникла необходимость найти более удобный способ реализации функций-оберток.

Для программ на языке C удобным способом является использование опции линковщика gcc -Wl - wrap = funcname. С ее помощью на этапе лин-ковки происходит замена исходной функции на функцию-обертку. Способ работает как в ОС семейства Linux, так и в ОС семейства Windows. Осо-

Рис. 4. Пример функции Hook Fig. 4. Example of Hook function

Рис. 3. Метка jmp Fig. 3. JMP label

бенности реализации функций-оберток показаны в работе [3].

На рис. 7, 8, 9 приведены примеры реализации способа. Функция с префиксом_real будет осуществлять переход к исходной функции. Т.е. на этапе сборки проекта линковщик определяет func() как_wrap_func(), а_real_func() - как func().

Заметим, что все 3 функции должны иметь схожие прототипы, т.е. одинаковые параметры и возвращаемые значения.

Данный способ работает исключительно для языка С, поскольку таблица импорта программ, написанных на нем, содержит такие же имена функций, как и исходный код. Из-за используемого в языке C++ механизма перегрузки функций имена в таблице импорта программы кодируются с помощью стандарта name-mangling. Это не позволяет линковщику выбрать нужную нам функцию для ее замены функцией-оберткой.

Использование LD_PRELOAD (компилятор GCC, языки C+ + , C)

Для программ, написанных на языке С++ в ОС Linux, вызов функций-оберток может быть произведен с помощью переменной среды LD_PRELOAD [4]. Переменная среды LD_PRELOAD указывает линковщику, какая библиотека должна подгружаться к проекту первой. Таким образом, создав динамическую библиотеку с функциями, прототип которых похож на исходники, можно загружать ее в проект до основных библиотек и использовать ее функции как обертки.

Данный метод работает с языками C и C++ благодаря поддержке стандарта name-mangling C++. Отличие заключается лишь в разных именах функций в таблицах импорта.

Таблица импорта необходима для нахождения имени интересующей функции, для которой требуется написать обертку. В стандарте таблиц импорта для С++ оно будет иметь вид _ZN7Module18module_1Ev, где Modulel - имя класса, в котором соответствующая функция объявлена, а module_1 - имя функции (для данного случая - имя метода класса).

С помощью консольной команды export LD_PRELOAD = <путь> осуществляются принудительная загрузка библиотеки с функцией-оберткой перед загрузкой остальных библиотек и замена реальных функций на их функции-обертки.

На рис. 10 приведен пример реализации функции-обертки с помощью LD_PRELOAD.

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

Метка realFunction

Тело функции

Рис. 5. Схема вызова функций для компилятора MSVC

Fig. 5. Function calling sequence for MSVC compiler call

Тело функции

Рис. 6. Схема вызова функций для компилятора GCC Fig. 6. Function calling sequence for GCC compiler

double _real_func<);

double _wrap_funcO

<

double ret » _real_func();

printf("wrapped func"); return ret;

Рис. 7. Файл с реализацией обертки wrap_func.c

Fig. 7. File with wrapping function wrap func.c

double func()

printf("real_func");

Рис. 8. Файл main.c

Fig. 8. File main.c

Рис. 9. Файл с реализацией реальной функции func.c

Fig. 9. File with real function func.c

typedef module_l_out(*origMlType)(void* instance);

extern "C" module_l_out _ZN"?Modulel8module_lEv (void* instance) I

roodule_l_out ret; origMlType origMl;

origMl = (origMlType)dlsym(RTLD_NEXT, "_ZN7Modulel8module_lEv"); if<origMl=^NUbL)(

iНе можете найти то, что вам нужно? Попробуйте сервис подбора литературы.

printf("dlopen:%s\n", dlerror()); return ret;

}

ret ■» origMl(instance);

//Здесь, например, можно вывести нужные нам значения в структуре ret return ret;

)

Рис. 10. Пример функции-обертки Fig. 10. Example of wrap function

Отладчик GDB

GDB debugger

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

Выполнение необходимого функционала (трассировка, вывод значений параметров и т.д.) осуществляется с помощью точек останова (breakpoints), точек наблюдения (watchpoints) и специальных команд отладчика [5].

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

■ После остановки программы на точке начала функции используем команду info args для печати в файл переданных функции аргументов;

■ Пропишем команду finish для перехода в конец функции и вывода возвращаемых значений;

■ Командой continue продолжаем выполнение программы до следующей точки останова. Порядок вызова функций можно напечатать

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

Таблица

Каждый вызов функции сопровождается созданием кадра (frame), зная номер которого можно получить доступ к адресу функции и последней выполненной в ней строке. Дополнительную информацию о кадре получаем командой info frame.

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

Сравнение различных способов

Comparison of different methods

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

№ 1. Замена метки функции в дисассемблере № 2. Использование опции линковщика wrap № 3. Использование LD_PRELOAD № 4. Использование отладчика GDB

Этап На этапе компиляции На этапе сборки На этапе сборки На этапе выполнения

ОС Windows Linux/Windows Linux Linux

Поддерживаемые языки C/C++ C C/C++ C/C++

Компилятор MSVC GCC GCC GCC

Поддерживаемый тип сборки Debug Debug/Release Debug/Release Debug

Возможность работы в среде разработки + + - -

Быстродействие ~19,7 с Медленная работа обусловлена, в т.ч., использованием ОС Windows ~16,8 c ~17,4 c ~17,6 c Медленный способ, т.к. реализуется при помощи сторонних скриптов и требует записи большого объема данных в файл

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

19,4 19,6 19,8 20,0

Способ № l,fcp= 19,681617

17,2 17,4 17,6 17,8 18,0 18,2 18,4 18,6 Способ № 3, 17,371604

16,5 17,0 17,5 18,0 Способ № 1, fcp= 19,681617

17,6 17,7 17,8 17,9 18,5 Способ № 4, ¿cP = 17,644256

Рис. 11. Графики распределения времени выполнения программ Fig. 11. Time share charts for software execution

без изменения исходного кода на тестовой программе.

Для оценки быстродействия используется программа, которая реализует расчет числа пи с помощью ряда Лейбница (разложение арктангенса в ряд Тейлора в точке х = 1) с остаточным членом о((х - 1)5108.

Для способов № 1, 3, 4 программа написана на языке С++, для способа № 2 - на С. Графики распределения времени выполнения программ (число запусков N = 100) для различных способов дополнения функциональности представлены на рис. 11.

Результат выполнения тестовой программы показал самую низкую производительность способа № 1. Однако при трассировке с помощью отладчика вББ время выполнения программы существенно возрастает с увеличением количества функций. Например, один запуск реального варианта расчета

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

Область применения

Application area

Данные способы находят применение при решении широкого спектра задач при разработке и тестировании сложного ПО:

■ подтверждение идентичности результатов расчета на разных вычислительных средствах (путем печати и автоматического сравнения промежуточных данных);

■ анализ и сравнение трассы следования программы на разных вычислительных средствах или при разных входных данных;

■ определение незадействованных цепочек условий для определения необходимости формирования дополнительных наборов входных данных;

■ подтверждение правильности расчета программных модулей (путем печати и анализа входных и выходных параметров модулей);

■ локализация участков кода, повлекших за собой некорректное функционирование и т.д.

В дальнейшем ряд рассматриваемых способов будет внедрен в ПО, специально разрабатываемое для отладки и тестирования программ.

Заключение

Conclusion

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

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

Следующим этапом разработки способов дополнения функциональности является исследование вариантов сокращения времени выполнения программы при использовании отладчика GDB.

Список использованной литературы

1. Джосаттис Н.М. Стандартная библиотека С++11: справочное руководство. 2-е изд. Москва [и др.]: Ви-льямс, 2014. 1129 с.

2. Васильев А.Н. Объектно-ориентированное программирование на С++. СПб.: Наука и техника, 2016. 544 с.

3. Stroustrup B. Wrapping C++ member function calls // The C++ Report. 2000. Vol. 12, No. 6. 12 p.

4. Практическое применение LD_PRELOAD или замещение функций в Linux [Электронный ресурс] // Хабр: [сайт]. 2013. 26 окт. URL: https://rn.habr.com/ru/ post/199090 (дата обращения: 20.03.2021).

5. GDB: The GNU Project Debugger [Электронный ресурс] // Free the Software : [site]. Boston: Free Software Foundation, 2021. URL: https://www.gnu.org/software/ gdb/documentation (Accessed: 10.06.2021).

References

1. N.M. Josuttis. The C++11 Standard Library: A Tutorial and Reference. 2nd ed., 2014 (in Russian).

2. A. Vasilyev. Object-oriented programming in C++. St. Petersburg: Nauka i Tekhnika, 2016, 544 pp. (in Russian).

3. Stroustrup B. Wrapping C++ member function calls // The C++ Report. 2000. Vol. 12, No. 6. 12 p.

4. Application of LD_PRELOAD or function substitution in Linux [Electronic resource], URL: https://rn.habr.com/ ru/post/199090 accessed on 20.03.2021 (in Russian).

5. GDB: The GNU Project Debugger [Electronic resource] // Free the Software : [site]. Boston: Free Software Foundation, 2021. URL: https://www.gnu.org/software/gdb/ documentation (Accessed: 10.06.2021).

Сведения об авторах

Бояринцева Юлия Андреевна, инженер-программист АО «Концерн «Моринсис-Агат». Адрес: 105275, Москва, шоссе Энтузиастов, д. 29. E-mail: justrage19@yandex.ru. Шерминская Анна Алексеевна, инженер-программист АО «Концерн «Моринсис-Агат». Адрес: 105275, Москва, шоссе Энтузиастов, д. 29. E-mail: SherminskayaAA@ya.ru. Николаев Андрей Анатольевич, к.ф.-м.н., начальник отдела АО «Концерн «Моринсис-Агат». Адрес: 105275, Москва, шоссе Энтузиастов, д. 29. E-mail: a.a.nikolaev@yandex.ru.

About the author

Yulia A. Boyarintseva, Engineer-Programmer, JSC Concern Morinsys Agat. Address: 29, Entuziastov sh., Moscow, Russia, post code 105275. E-mail: justrage19@yandex.ru. Anna A. Sherminskaya, Engineer-Programmer, JSC Concern Morinsys Agat. Address: 29, Entuziastov sh., Moscow, Russia, post code 105275. E-mail: SherminskayaAA@ya.ru. Andrey A. Nikolaev, Cand.Sc., Head of Department, JSC Concern Morinsys Agat. Address: 29, Entuziastov sh., Moscow, Russia, post code 105275. E-mail: a.a.nikolaev@yandex.ru.

Поступила / Received: 15.11.21 Принята в печать / Accepted: 22.11.21 © Коллектив авторов, 2021

i Надоели баннеры? Вы всегда можете отключить рекламу.