Статья посвящена написанию пользовательской библиотеки (DLL) на ассемблере для универсального OPC-сервера Fastwel. Освещаются общие принципы программирования на ассемблере в Windows, даются рекомендации по выбору компилятора и интегрированной среды разработки (IDE), приводится рабочий пример написания тестовой DLL.
У многих системных интеграторов при необходимости реализации системы управления верхнего уровня, где часто используются такие современные программные средства, как SCADA-системы, возникает потребность программного сопряжения старого оборудования (для общности будем говорить о контроллере) с собственно пакетом SCADA. Современные SCADA-системы в качестве программного элемента сопряжения с оборудованием используют OPC-серверы. Чаще всего программный интерфейс старого оборудования реализует «самобытный» протокол связи, на текущий момент позабытый, и в силу этого не существует готового OPC-сервера, позволяющего достаточно просто реализовать вопрос стыковки. При этом есть два пути решения возникшей проблемы. Первый — переписать программу контроллера с целью обеспечения стандартного протокола обмена, и второй — написать собственный OPC-сервер. К сожалению, довольно часто первый путь затруднителен, так как связан с существенными программными и материально-техническими издержками, либо просто невозможен. Остаётся безальтернативный путь — писать собственный OPC-сервер. Это довольно сложная работа, требующая наличия программиста высокой квалификации, причём неизбежны большие временные затраты на разработку и отладку. В этой ситуации фирмой Fastwel предложен оригинальный программный продукт — универсальный OPC-сервер [1], позволяющий существенным образом снизить требования к квалификации программиста и сократить временные затраты, связанные с написанием собственного OPC-сервера, так как часть работы (наиболее «наукоёмкая») уже проделана. После приобретения этого программного продукта разработчику предлагается написать библиотеку DLL (Dynamic Link Library) пользователя, осуществляющую только обмен с устройством, всю остальную работу по обеспечению обмена данными между OPC и SCADA осуществляет OPC-сервер. Поставляется и программная заготовка на языке С++, на основе которой пользователь может создавать эту DLL. В данной статье рассматривается программная заготовка для написания пользовательской DLL на ассемблере. Программирование на ассемблере под Windows не только не сложнее написания программ на ассемблере под DOS, а даже проще! Необходимость навыков работы с этим языком актуальна до сих пор, и популярность его ничуть не уменьшается, а иногда применение его остаётся единственным эффективным средством решения проблемы.
На текущий момент хорошо известны следующие средства разработки приложений на ассемблере под Windows: MASM (Microsoft), TASM (Borland), NASM, FASM. Основными пакетами (в пакет входят, как минимум, транслятор, компоновщик и компилятор ресурсов) являются MASM v.6.1X и TASM v.5.0.
Для решения поставленной задачи выбран пакет MASM32 v.8.2, являющийся достаточным для программирования приложений для ОС Windows и содержащий, кроме транслятора MASM v.6.14, компоновщика и редактора ресурсов, компактный редактор, набор специализированных утилит и большое количество примеров. Пакет распространяется бесплатно (http://www.masm32.cjb.net). Программировать на ассемблере под Windows и пользоваться командной строкой и bat-файлами, на мой взгляд, слишком консервативно. Я опробовал две интегрированные среды разработки (IDE) для ассемблера – WinAsm Studio v.4.0.1.266 (http://www.winasm.net) и RadASM v.2.1.0.6 (http://radasm.visualassembler.com/download/radasm.html). Версии IDE указаны на момент написания статьи. Оба проекта достаточно динамично развиваются и распространяются бесплатно. Я остановился на RadASM. Установка этой IDE и работа с ней достаточно просты, и я не буду останавливаться на её описании. Автор RadASM программирует на ассемблере (сама оболочка также написана на этом языке), и потому всё продуманно, функционально и быстро. Кроме того, на сайте есть законченные примеры проектов, облегчающих первые шаги в увлекательный мир ассемблера для Windows.
Безусловно, программирование под Windows имеет некоторые отличия по отношению к «старому стилю» программирования на ассемблере, и они связаны с изменившимся «лицом» операционной системы (ОС), осуществляющей встроенную поддержку многого из того, что ранее для DOS реализовывалось на уровне приложений и требовало существенных усилий программиста [2]. Так как статья не посвящена глубокому анализу организации Windows, рассмотрим те моменты, которые существенны для решения поставленной задачи.
Первым приятным моментом является то, что операционная система Windows, работая в защищённом режиме, обеспечивает для каждого запущенного на выполнение приложения отдельное виртуальное адресное пространство размером в 4 Гбайт. Теперь, программируя на ассемблере, не приходиться беспокоиться о существовавшей в DOS проблеме ограничения адресного пространства сегментов в 64 кбайт. Таким образом, в 32-разрядный Windows используется только одна модель памяти – «плоская», обозначаемая ассемблерной директивой MODEL FLAT.
Вторым приятным моментом, существенно изменяющим характер программирования в ОС Windows, является то, что при программировании приложения активно используется огромное количество функций интерфейса API (Application Programming Interface), предоставляемых в рамках самой операционной системы Windows и позволяющих значительно минимизировать труд программиста по реализации каких-либо прикладных задач, будь то создание графического изображения окна или работа с файлом, сводя эту работу к простым вызовам в своей программе. Под DOS приходилось применять системные вызовы (системные прерывания INT 21H) или прибегать к прерываниям BIOS (особенно часто это касалось работы по созданию интерфейса пользователя). Большое богатство функций сосредоточено в системных библиотеках динамической компоновки DLL, основными из которых являются User32.dll, Gdi32.dll, Advapi32.dll и Kernel32.dll. Эти библиотеки предоставляют разработчику программ документированный интерфейс между программами и вызываемыми функциями подсистем. User32.dll реализует функции, связанные с поддержкой пользовательского интерфейса, Gdi32.dll обеспечивает графический интерфейс, а Kernel32.dll отвечает за поддержку работы с памятью и взаимодействие с процессами. В отличие от статически компонуемых библиотек (раннее связывание), привычных нам при программировании в DOS, библиотеки динамической компоновки загружаются в память и обращение к ним идёт динамически, по мере необходимости. При этом распознавание необходимости DLL для загружаемой программы происходит либо автоматически при запуске программы, либо через вызов в этой программе соответствующей функции. Второй способ иногда удобней, так как программа может скорректировать своё выполнение по результатам загрузки DLL и продолжить выполнение, скажем, с ограничением своей функциональности, или самостоятельно выгрузить ранее загруженную и ставшую ненужной DLL. В первом случае Windows сообщит об ошибке и выгрузит приложение. Разные способы загрузки вносят принципиальные изменения и в код программы. Для первого случая в коде программы необходимо наличие строки с директивой includelib и название так называемой библиотеки импорта (например User32.lib). Эта библиотека позволяет редактору связей получить информацию о требуемой при динамической компоновке DLL, которая вносится в исполняемый файл (библиотеки импорта по имени совпадают с именем представляемых ими DLL и имеют расширение .lib). Кроме того, в исполняемом файле необходимо вызывать функцию GetProcAddress для каждой используемой функции, что несколько увеличивает код. При самостоятельной загрузке библиотеки импорта в тексте программы не используются, при этом не исключается необходимость чёткого знания количества и формата передаваемых параметров в используемых функциях. Существует два механизма определения функции при её связывании: по номеру и по имени этой функции. Чаще используется второй вариант, имеющий, как минимум, преимущество читабельности исходной программы, так как название отражает выполняемую функцию. Являясь разделяемым ресурсом в ОС Windows, DLL существенным образом экономят системные ресурсы, так как в оперативной памяти находится только одна копия DLL при обращении к ней нескольких программ. Кроме того, DLL существенно уменьшают объём самих создаваемых программ, тем самым экономя системный ресурс в части занимаемого программой места на жёстком диске. DLL, кроме исполняемого кода, могут содержать и другие данные, например, графические изображения, шрифты и.т.д.
Мы подошли к третьему, очень важному положительному моменту, являющемуся одним из ключевых для ОС Windows, – многозадачности. Как мы помним, в DOS, чтобы написать программу, обеспечивающую параллельное (точнее, псевдопараллельное) выполнение программного кода, приходилось очень потрудиться, так как сама DOS была спроектирована как однозадачная ОС. В Windows эти проблемы в основном решаются на системном уровне, хотя определённые тонкие моменты при программировании остаются (вопросы синхронизации при межзадачном обмене и использовании общесистемных ресурсов, например COM-порта). Вопроса синхронизации доступа для разделяемого участка программы при обращении к общим данным мы ещё коснёмся.
При активном использовании вызовов функций необходимо помнить о порядке передачи параметров через стек (и не только через стек!) в вызываемые процедуры и функции. С этими моментами программисты должны были сталкиваться и раньше, при написании процедур и функций на ассемблере в программах, написанных на языках высокого уровня. Кроме того, при разных подходах является актуальным вопрос о том кто «чистит» стек: вызывающая программа или вызываемая подпрограмма. Для распространенных языков программирования C и Pascal существуют следующие соглашения по этим двум вопросам. По соглашению для С параметры в вызываемую функцию передаются справа налево и вызывающая программа должна очистить стек. Например вызов функции Test
Test (param_1, param_2)
в ассемблерном виде выглядит как
push [param_2]
push [param_1]
call Test
add sp,8
По соглашению для Pascal всё с точностью до наоборот, то есть передача параметров в функцию осуществляется слева направо и вызываемая функция ответственна за очистку стека. Для платформ Win32 используется комбинированный вариант этих соглашений STDCALL. Согласно этому варианту соглашения данные в вызываемую функцию передаются справа налево и вызываемая функция чистит стек. Говоря о вызовах функций, нельзя не коснуться темы существования в MASM32 очень удобной директивы INVOKE, позволяющей минимизировать текстовые затраты программиста на ассемблере при оформлении вызова функции в тексте программы. Как известно, стандартный способ вызова функции на ассемблере — через оператор Call, то есть вызов функции Test2 (param_1,param_2) с двумя параметрами будет выглядеть следующим образом:
Push param_2
Push param_1
Call Test2@N
Здесь N – количество отправляемых в стек байтов.
Использование директивы INVOKE позволяет нам оформить вызов функции Test2 более коротко:
INVOKE Test2 param_1, param_2
При этом передача параметров в стек и очистка стека будут автоматически оформлены компилятором. Параметры, передаваемые в функцию, при этом могут являться адресом (смещением — OFFSET или ADDR), непосредственным значением или значением в регистре. Естественно, для того чтобы транслятор и редактор связей правильно обработали эту директиву (проверили количество и тип параметров вызываемой функции), они должны иметь прототип функции. Прототипы функций имеют вид [имя функции] ключевое слово [PROTO] и далее через двоеточие в соответствии с числом параметров их тип, например DWORD, и хранятся в многочисленных inc-файлах (\masm32\INCLUDE\*.inc). Прототип для Test2 выглядит как Test2 PROTO: DWORD: DWORD.
Одними из фундаментальных понятий в Windows являются окно, сообщение, цикл обработки очереди сообщений. Окно в упрощённом виде можно рассматривать как программный каркас приложения в Windows, в котором определены все свойства этого приложения, включая видимый интерфейс пользователя и внутреннюю логику работы самого приложения. Каждое окно для идентификации операционной системой имеет свой дескриптор окна — четырёхбайтовое число, позволяющее операционной системе понимать, какая из программ её о чём-то просит. Для этого в большинстве вызываемых из приложения функций, связанных с использованием системных ресурсов, в качестве параметра присутствует этот дискриптор. Окно не обязательно должно иметь видимый пользователю интерфейс. Операционная система Windows генерирует и распределяет между запущенными приложениями сообщения о событиях во «внутреннем мире» ОС.
Событиями являются нажатия на клавишу клавиатуры, перемещение мыши, поступление информации в СОМ-порт и т.д. Приложение получает информацию о событиях в виде заполненных структур. Приложение не обязано реагировать на все события и может выбирать, какие из поступивших событий будет обрабатывать. Для каждого окна существует своя очередь предназначенных ему сообщений, обрабатываемая в рамках главной процедуры этого окна. Таким образом, каждое приложение имеет внешний интерфейс пользователя (в случае консольных приложений он отсутствует) с меню и кнопками управления, программу, получающую сообщения об имеющих место событиях, и собственно код главной процедуры окна, обрабатывающий эти события и обеспечивающий функциональность конкретного приложения. Подчеркну ещё раз (это важный момент), что приложению нет необходимости что-либо предпринимать для получения и формирования очереди сообщений — эта функция лежит на самой операционной системе. Программный код главной процедуры приложения только обрабатывает события, и всё. Это, безусловно, упрощает программирование в ОС Windows, так как в случае приложения под DOS оно должно само следить за происходящим в ОС, не полагаясь на её «помощь». DLL не является полнофункциональным приложением в привычном понимании, но тем не менее у неё есть так называемая процедура входа (на неё указывает метка за директивой END), которая получает и обрабатывает события, передаваемые ей операционной системой. Подчинённый характер DLL заключается в ограниченном числе получаемых сообщений и выгрузке из памяти при закрытии основного приложения, которое «вызвало к жизни» DLL, хотя само приложение при этом может продолжать свою работу. При вызове процедуры входа DLL в стеке для неё передаются три параметра, один из которых поясняет, по какому поводу её «побеспокоили». DLL передаются следующие параметры: первый — идентификатор DLL-библиотеки, второй – сообщение о причине вызова, третий — зарезервирован для дальнейших расширений. Причины вызова могут быть следующие:
DLL_PROCESS_ATTACH – сообщение о том, что библиотека загружена в адресное пространство вызвавшего её процесса;
DLL_THREAD_ATTACH – сообщение о создании вызвавшим библиотеку процессом нового потока;
DLL_PROCESS_DETACH — сообщение о том, что библиотека выгружается из адресного пространства вызвавшего ее процесса;
DLL_THREAD_DETACH – сообщение об уничтожении процессом ранее созданного им потока.
;----------------------------------------------------
; SimpleDLL.asm
;----------------------------------------------------
.386
.model flat, stdcall
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
.data
.code
DllEntry proc hInstSimpleDLL: HISTANCE, reason:DWORD, future: DWORD
mov eax, TRUE
ret
DllEntry endp
FirstFunction proc param1:DWORD, param2:DWORD
Mov ebx,param1
Mov eax,param2
Add eax,ebx
ret
FirstFunction endp
End DllEntry
;-----------------------------------------------------
; SimpleDLL.inc
;-----------------------------------------------------
FirstFunction proc param1:DWORD, param2:DWORD
;-----------------------------------------------------
; SimpleDLL.def
;-----------------------------------------------------
LIBRARY SimpleDLL
EXPORTS FirstFunction
Удивительно, но перед нами текст совершенно работоспособной DLL (точнее, три текстовых файла .asm, .inc и .def). Согласитесь, что всё достаточно прозрачно и лаконично. Процедура DllEntry вызывается операционной системой в случаях, определённых перечнем приведенных ранее сообщений. При успешном завершении действий, связанных с конкретным сообщением (реально его можно и не отрабатывать, как в приведенном «каркасе»), процедура возвращает TRUE.
Если для нормальной работы DLL при её загрузке в память необходима инициализация внутренних переменных или просто выделение блока памяти, а это не может быть выполнено, то, вернув FALSE, Вы тем самым сообщаете операционной системе о «неудаче» и DLL будет выгружена. Наша DLL содержит всего лишь одну функцию FirstFunction, которая, получив два параметра, складывает их и возвращает результат сложения (надо сказать, что возвращаются значения в регистре eax). В файле определений (.def) после ключевого слова LIBRARY указывается имя библиотеки. Все функции DLL, которые мы хотели бы сделать доступными для внешних программ, перечисляются с ключевым словом EXPORTS. После компиляции проекта мы получаем два файла, саму библиотеку SimpleDLL.DLL и файл SimpleDLL.lib, последний является библиотекой импорта, о которой мы уже говорили. Напомню, что она необходима при создании программы пользователя, использующей созданную Вами библиотеку в режиме явного связывания. Собственно сам «каркас» приведённой DLL уже является подобным примером, использующим системные библиотеки.
Для начала будем рассматривать простейший вариант, когда в адресном пространстве сервера будет видимым только одно устройство. Как Вы, наверно, помните, все теги, видимые клиентом (публикуемые OPC-сервером), делятся на три типа: аналоговые, битовые и целые. Теги представляют собой структуру данных. Структура – это конструкция в ассемблере (и не только в нём), позволяющая объединять данные различных типов. Таким образом, структура определяет составной тип данных, в котором содержится один или более элементов данных, называемых членами (полями) структуры. Синтаксис определения структуры в ассемблере довольно прост:
Имя STRUC
ENDS
Параметр «Имя» – это присваиваемое структуре имя. Все данные, находящиеся между ключевыми словами STRUC и ENDS, являются членами структуры.
Обращение к членам структуры выглядит следующим образом: «Имя.ИмяПоляСтруктуры», то есть первым идёт имя структуры и далее через разделитель «.» — имя поля структуры. Как уже говорилось, важно, что каждый член структуры (или поле структуры) может иметь разный тип. Учитывая это, при помощи определения различных структур можно создавать описания объектов с набором признаков, присущих этим объектам. В нашем случае каждый тег имеет как минимум два поля: имя тега и само значение. Поле имени тега содержит указатель на строковую переменную, которая содержит имя тега. В поле тега «значение» в зависимости от того, какой тип тега определяется структурой, будет переменная, содержащая в явном виде значение переменной соответствующего типа для ассемблера. Посмотрим, как всё это выглядит на ассемблере.
Структура, описывающая тег, содержащий аналоговую переменную:
anTag struc
ptrname dd NULL
value dd 0
anTag ends
Структура, описывающая тег, содержащий битовую переменную:
bitTag struc
ptrname dd NULL
value db 0
bitTag ends
Структура, описывающая тег, содержащий переменную целого типа:
intTag struc
ptrname dd NULL
value dd 0
intTag ends
При определении структуры переменные инициализировать не обязательно. Если Вы всё же указываете какие-либо значения, они принимаются значениями по умолчанию при объявлении в тексте программы переменной с типом этой структуры.
Следующая структура TDataForOPCServer является основной, так как содержит поля, полностью описывающие конкретное устройство, с точки зрения наличия (или отсутствия) у устройства определённых типов данных (по сути, реальных входов-выходов устройства) и количества логических входов. Структура выглядит следующим образом:
TDataForOPCServer struc
dataIsValid db 1 ; признак качества данных
anTagsQnty dw 1 ; количество аналоговых тегов
TAnTag dd 0 ; указатель на массив аналоговых тегов
bitTagsQnty dw 1 ; количество битовых тегов
TBitTag dd 0 ; указатель на массив битовых тегов
intTagsQnty dw 1 ; количество тегов с значением целого типа
TIntTag dd 0 ; указатель на массив тегов целого типа
structVersionNumber dw 0 ; поддержка версии 0 OPC-сервера Fastwel
DeviceName db «Device1» ; имя устройства
DeviceNameL db 57 dup (« «) ;
Reserved db 128 dup(0) ; зарезервировано
TDataForOPCServer ends
Смысл полей достаточно ясен из текста комментария. Необходимо сделать акцент на том, что поле версии OPC-сервера Fastwel — это не просто порядковый номер текущей версии универсального сервера, а данные, влияющие на характер и количество полей самой структуры TDataForOPCServer, интерпретируемые сервером, то есть определяющие функциональные возможности универсального OPC-сервера в целом.
Так, если в поле structVersionNumber стоит версия 1, то структура является описанием некоторого массива структур, что позволяет «видеть» несколько устройств.
Функции, вызываемые непосредственно OPC-сервером, которые необходимо реализовать в пользовательской DLL (и экспортировать для возможности их использования), следующие:
1. const TDataForOPCServer * PASCAL
GetDataForOPCServer( void );
2. unsigned char PASCAL SetAnTagValue(
unsigned number, // устройства (HIWORD) и
// номер тега в массиве (LOWORD)
float value // новое значение параметра
);
3. unsigned char PASCAL SetBitTagValue(
unsigned number, // устройства (HIWORD) и
// номер тега в массиве (LOWORD)
unsigned char value // новое значение параметра
);
4. unsigned char PASCAL SetIntTagValue(
unsigned number, // устройства (HIWORD) и
// номер тега в массиве (LOWORD)
int value // новое значение параметра
);
Описание функций взято из заголовочного файла dataserv.h, входящего в поставку универсального OPC-сервера фирмы Fastwel.
Как можно видеть, первая функция возвращает OPC-серверу указатель на структуру TDataForOPCServer, о которой мы говорили ранее, тем самым давая информацию о наличии и количестве соответствующих тегов, остальные функции позволяют OPC-серверу изменять значения соответствующих тегов. Посмотрим, как выглядит простейшая реализация этих функций на ассемблере:
1. GetDataForOPCServer proc
mov eax, offset TestTDataForOPCServer
ret
GetDataForOPCServer endp
2. SetAnTagValue proc number:DWORD, value:DWORD
mov eax,value
mov TestAnTag.value,eax
mov eax,1
ret
SetAnTagValue endp
3. SetBitTagValue proc number:DWORD, value:BYTE
mov al,value
mov TestbitTag.value,al
mov eax,1
ret
SetBitTagValue endp
4. SetIntTagValue proc number:DWORD, value:DWORD
mov eax,value
mov TestintTag.value,eax
mov eax,1
ret
SetIntTagValue endp
Как видим, всё достаточно просто. Приведённый текст не является объявлением функций, как это видим в прототипе на языке C++ 5. Я ввёл некоторую конкретику для сокращения дальнейшего обращения к тексту на ассемблере. Реально объявление для функции установки аналогового тега выглядит так:
SetAnTagValue proc number:DWORD, value:DWORD
ret
SetAnTagValue endp
Результат работы первой функции — это указатель, возвращаемый в регистре eax. Если в eax возвращается NULL, это ошибка. Остальные функции получают в качестве параметров номер устройства (старшее слово), в нашем примере оно одно, так как версия 0, номер соответствующего тега в массиве (младшее слово) параметра number и его новое значение — параметр value. При этом приведённые в качестве примера функции не изменяют выходные значения в реальном устройстве, а просто изменяют значение в массиве и сообщают OPC-серверу, что всё в порядке (значение TRUE или 1 в регистре eax).
В действительности в функциях, изменяющих значения тегов, Вам необходимо реализовать не формальное изменение значение тега в массиве данных, а реальное изменение значения в устройстве (это может быть значение в регистре платы, установленной в слот компьютера, или значение на выходе контроллера, обмен с которым осуществляется по коммуникационному порту).
Наверняка у читателя возникнет вопрос, каким образом осуществляется обновление данных в структуре TDataForOPCServer. Законный вопрос. Необходимо иметь некую подпрограмму, которая периодически считывала бы значения из нашего устройства и заносила эти значения в поле value соответствующего тега.
Роль подобных подпрограмм, выполняемых в режиме псевдопараллельности, играют потоки, создаваемые основным приложением. Каждое приложение имеет хотя бы один первичный поток (можно считать, что это основная программа), который может создавать вторичные потоки, которые, в свою очередь, могут создавать свои вторичные потоки, и т.д. Между создающим и создаваемым потоком существуют «родительские» взаимоотношения, выражающиеся в том, что поток, создающий вторичный поток, может его и завершить, обратное неверно. Выделением кванта времени для исполнения какого-либо потока (и собственно его выполнением) занимается сама операционная система.
DLL ничем не хуже других приложений Windows, и поэтому в рамках пользовательской DLL нам необходимо создать поток (подпрограмму), который и будет заниматься вопросом обновления данных. Рассмотренный вопрос о потоках является системным, относящимся к организации ОС Windows, и, конечно же, его необходимо учитывать при программировании приложений не только на ассемблере.
В рамках API есть функции, при помощи которых и осуществляется процесс создания и управления потоками. Допустим, есть процедура, которая через определённый интервал времени, определяемый программной задержкой, считывает текущее значение переменной целого типа из структуры TDataForOPCServer, увеличивает его на единицу и записывает получившееся значение обратно. Надо отметить, что код создаваемых потоков имеет доступ ко всем переменным приложения.
DllThread proc
DllThreadLoop:
pushad
mov cx,100
Thread_delay:
loop Thread_delay
mov eax, TestintTag.value
inc eax
mov TestintTag.value, eax
popad
jmp DllThreadLoop
ret
DllThread endp
Теперь в рамках программы, которая будет выполняться при загрузке DLL в память, необходимо эту процедуру запустить:
invoke CreateThread,NULL,0,offset DllThread, [Dll_Handle],0,offset HTHR
Как видим, это осуществляется вызовом соответствующей функции API (CreateThread) с передачей ей необходимых параметров:
указатель на структуру атрибутов безопасности доступа (актуально для NT). Обычно не используется — NULL;
размер стека потока. Если значение параметра равно 0, принимается размер стека родительского потока;
адрес исполняемой процедуры потока. В нашем случае это DllThread, которая выполняет порученную ей работу по обновлению данных;
параметр для функции потока;
флаг состояния потока. Если параметр нулевой, выполнение потока начинается немедленно. Если созданный поток ожидает запуска (функцией ResumeThread), используется флаг CREATE_SUSPEND;
адрес переменной, в которую записывается дескриптор созданного потока.
Как мы говорили, родительский поток, кроме того что может породить вторичный поток, может его и «убить», вызовом соответствующей функции. Это осуществляется в рамках окончания работы приложения OPC-сервера, которое сообщает DLL о том, что работа приложения завершается и ему необходимо освободить все ресурсы и завершить все созданные потоки.
invoke TerminateThread,[HTHR],0
После рассмотрения всех этих вопросов можно взглянуть на то, что у нас получилось:
; Project DLL for v.0 Universal OPC Server Fastwel
.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\kernel32.inc
includelib \masm32\lib\user32.lib
include DATASERV.Inc
.data
Dll_Handle dd ?
HTHR dd ?
; переменные устройства 0
TestAnTagName db "TestAnTag",0
TestbitTagName db "TestbitTag",0
TestintTagName db "TestintTag",0
TestTDataForOPCServer TDataForOPCServer <>
TestAnTag anTag <TestAnTagName,2.3>
TestbitTag bitTag <?>
TestintTag intTag <?>
.code
DllThread proc
DllThreadLoop:
pushad
mov cx,100
Thread_delay:
loop Thread_delay
mov eax, TestintTag.value
inc eax
mov TestintTag.value, eax
popad
jmp DllThreadLoop
ret
DllThread endp
DllEntry proc hInstDLL:HINSTANCE, reason:DWORD, reserved1:DWORD
mov eax,reason
cmp eax,DLL_PROCESS_DETACH
je @dll1 ; закрытие библиотеки
cmp eax,DLL_PROCESS_ATTACH
je @dll2 ; открытие библиотеки
cmp eax,DLL_THREAD_DETACH
je @dll3
cmp eax,DLL_THREAD_ATTACH
je @dll4
; неизвестная команда !
mov eax,TRUE
jmp dll_exit
;-----------------------------------------
; закрытие библиотеки процессом
@dll1:
invoke TerminateThread,[HTHR],0
mov eax,TRUE
jmp dll_exit
;---------------------------------------------------
; открытие библиотеки процессом
@dll2:
mov eax,dword ptr [ebp+8] ; Взяли дескриптор
mov Dll_Handle,eax
; инициализируем структуру битовой переменной
; устройства 0
mov TestbitTag.ptrname, offset TestbitTagName
mov TestbitTag.value,1
; инициализируем структуру переменной целого типа устройства 0
mov TestintTag.ptrname,offset TestintTagName
mov TestintTag.value,650
; инициализируем структуру TDataForOPCServer
mov TestTDataForOPCServer.dataIsValid,1
mov TestTDataForOPCServer.anTagsQnty,1
mov TestTDataForOPCServer.TAnTag,offset TestAnTag
mov TestTDataForOPCServer.bitTagsQnty,1
mov TestTDataForOPCServer.TBitTag,offset TestbitTag
mov TestTDataForOPCServer.intTagsQnty,1
mov TestTDataForOPCServer.TIntTag,offset TestintTag
; создаём поток опроса значений
invoke CreateThread,NULL,0,offset DllThread, [Dll_Handle], 0,offset HTHR
;--------------------------------------
mov eax,TRUE
jmp dll_exit
;-----------------------------------------------------
; закрылся один из потоков
@dll3:
mov eax,TRUE
jmp dll_exit
;-----------------------------------------------------
; открылся один из потоков
@dll4:
mov eax,TRUE
dll_exit:
ret
DllEntry endp
GetDataForOPCServer proc
mov eax,offset TestTDataForOPCServer
ret
GetDataForOPCServer endp
SetAnTagValue proc number:DWORD, value:DWORD
mov eax,value
mov TestAnTag.value,eax
mov eax,1
ret
SetAnTagValue endp
SetBitTagValue proc number:DWORD, value:BYTE
mov al,value
mov TestbitTag.value,al
mov eax,1
ret
SetBitTagValue endp
SetIntTagValue proc number:DWORD, value:DWORD
mov eax,value
mov TestintTag.value,eax
mov eax,1
ret
SetIntTagValue endp
End DllEntry
В рамках идеологии объектного программирования доступ к переменным (чтение или запись) некоторого объекта должен осуществляться единообразным способом, или методом. То есть если у нас определена подпрограмма SetIntTagValue изменения значения переменной в массиве TDataForOPCServer, то и в программе, отвечающей за считывание и обновление значений переменных, необходимо использовать ту же самую процедуру (метод), а не писать значение напрямую, как это осуществляется у нас в примере кода потока. Выглядеть это должно так:
DllThread proc
DllThreadLoop:
pushad
mov cx,100
Thread_delay:
loop Thread_delay
mov eax, TestintTag.value
inc eax
mov ebx,0
invoke SetIntTagValue, ebx, eax
popad
jmp DllThreadLoop
ret
DllThread endp
Использование одной и той же подпрограммы для изменения состояния одной и той же переменной (её атрибутов), при том что доступ к ней имеют программы DLL и сервера (асинхронный доступ), может приводить к неверным конечным результатам. Чтобы избежать подобной ситуации, существует программный инструмент синхронизации доступа – критическая секция. Для использования этого инструмента необходимо создать структуру DataservCriticalSection RTL_CRITICAL_SECTION <?> и инициализировать её соответствующей функцией
invoke InitializeCriticalSection,addr DataservCriticalSection
Теперь при реализации функции изменения значения переменной при входе и выходе вызываются соответствующие функции, функция invoke EnterCriticalSection – войти в критическую секцию и invoke LeaveCriticalSection — покинуть критическую секцию. То есть в общем виде это выглядит так:
SetIntTagValue proc number:DWORD, value:DWORD
invoke EnterCriticalSection,addr
DataservCriticalSection
; здесь код процедуры изменения значения переменной
invoke LeaveCriticalSection,addr
DataservCriticalSection
mov eax,1
ret
SetIntTagValue endp
Подобная реализация гарантирует, что программа, ограниченная функцией входа и выхода в критическую секцию, будет выполняться только одним потоком приложения.
При завершении работы программы DLL, когда все ресурсы необходимо освободить, вызывается функция для удаления ранее созданного объекта — критическая секция invoke DeleteCriticalSection, addr DataservCriticalSection. В приведённом примере DLL пользователя не используется критическая секция, но сказать о ней необходимо, так как это один из стандартных способов решения вопроса синхронизации работы потоков при программировании (наряду с семафорами, событийной синхронизацией и мьютексами).
В статье рассматривался вариант реализации самой простой версии интерфейса (версия 0) пользовательской DLL доступа к данным. Разработчики фирмы Fastwel постоянно совершенствуют свой программный продукт, и в зависимости от версии Universal OPC-сервер появлялись новые версии интерфейса DLL, который может выбрать для реализации разработчик DLL. Новые версии интерфейса предлагают тот или иной дополнительный сервис при написании кода DLL и публикации дополнительных параметров тегов. В отличие от версии 0, в версии 1 появляется возможность задания нескольких устройств в адресном пространстве OPC-сервера. Версия 2 отличается от версии 1 признаком качества данных в каждом теге. Версия 3 отличается от версии 2 дополнительным полем времени в каждом теге, а также наличием функций синхронизации при обновлении атрибутов тега со стороны DLL.
Интересным моментом при использовании этих функций является то, что параметр tstamp, имеющий тип FILETIME, передаётся в функцию не через стек, а через регистр сопроцессора. В целом реализация пользовательской DLL на ассемблере с учётом возможностей последующих версий интерфейса (1, 2, и 3) не несёт в себе каких-либо принципиальных моментов по отношению к рассмотренным нами вопросам. Хочется надеяться, что базовые моменты описаны достаточно полно и послужат отправной точкой для реализации продвинутых вариантов пользовательской DLL. Главное, чтобы программист понял, что использование ассемблера при реализации программ для Windows ничуть не сложнее того, к чему он привык в DOS. Серьёзные изменения в программировании под Windows в основном связаны с появлением большого количества структур системных объектов, API и с необходимостью учёта многозадачности Windows (вопрос написания драйверов в этой ОС — это отдельная тема). Но тут, как говорится, учи матчасть, без её знания и раньше жилось непросто. ●
Fastwel Universal OPC-сервер версия 1.0. Руководство пользователя. — М.: Fastwel, 1999.
Пирогов В.Ю. Ассемблер для Windows. — М.: ИД Молгачева С.В., 2002.
Автор — сотрудник фирмы ПРОСОФТ
Телефон: (812) 325-3790
Факс: (812) 325-3791
E-mail: info@spb.prosoft.ru
Однофазные источники бесперебойного питания Systeme Electric
Почти все современные сферы промышленности, IT-инфраструктура, а также любые ответственные задачи и проекты предъявляют повышенные требования к питающей сети – электропитание должно быть надёжным, стабилизированным и обеспечивать бесперебойную работу. В данной статье мы рассмотрим решения по однофазному бесперебойному питанию от российской компании Systeme Electric. 28.12.2023 СТА №1/2024 1067 0 0Однопроводный канал телеметрии по PLC
В статье рассматриваются методы реализации однопроводных каналов передачи данных по силовым электросетям в жилых зданиях, загородных и промышленных помещениях. В качестве информационного провода предлагается использовать проводник «нейтраль» электропроводки. Приводятся анализ возможных конфигураций каналов передачи данных этого типа и результаты экспериментальных проверок. Рассматриваются преимущества новых методов по сравнению с традиционными PLC и области возможного применения данной технологии. 28.12.2023 СТА №1/2024 1176 0 0BioSmart Quasar 7 — мал да удал
Компания BIOSMART в пандемийном 2020 году весьма своевременно представила свой первый лицевой терминал Quasar (рис. 1) с диагональю экрана 10 дюймов. Уже в следующем, 2021 году был представлен бесконтактный сканер рисунка вен ладони PALMJET (рис. 2). Ну а в текущем 2023 году компания представила новую уменьшенную модель лицевого терминала Quasar 7 (рис. 3), который смог в компактном корпусе объединить обе передовые технологии бесконтактной биометрической идентификации. 28.12.2023 СТА №1/2024 1094 0 0Открытые сетевые платформы — когда сети и вычисления в одном устройстве
Открытая сетевая платформа (ONP) – это мощное средство для реализации как простых, так и масштабных сетей, а также инструмент, который позволяет в одном высокопроизводительном устройстве реализовать целый вычислительный комплекс, объединяющий внутри себя коммутаторы, маршрутизаторы, межсетевые экраны, а также сам сервер обработки данных. Используя все преимущества данной архитектуры, компания AAEON разработала своё решение, сетевую платформу FWS-8600, на базе высокопроизводительных процессоров Intel Xeon Scalable 2-го поколения. В статье раскрыты детали и особенности ONP, характеристики FWS-8600, а также почему использование процессоров Intel Xeon Scalable 2-го поколения значительно увеличивает потенциал платформы. 28.12.2023 СТА №1/2024 1367 0 0