ЧАСТЬ ПЕРВАЯ. ПОСТАНОВКА ЗАДАЧИ
Лептоны (от греч. leptos, что значит "легкий". Ср."лепта" - мелкая разменная монета. Напр. "внести свою лепту" - сделать дела на копейку, а потом орать, что потратился на рубль) - это такие ма-а-асенькие элементарные частицы.
Существуют две теории лептонов: правильная и неправильная.
НЕПРАВИЛЬНАЯ ТЕОРИЯ ЛЕПТОНОВ
По признаку участия или неучастия в сильном взаимодействии все ма-а-асенькие элементарные частицы делятся на два вида: адроны (участвующие) и лептоны (не участвующие). Об адронах мы будем говорить, если вдруг придумаем какой-нибудь адронный стиль программирования.
Пока же не придумали, то вспомним только одно: в старинных русских дворянских родах существовала традиция называть мальчиков именем Адрон, которое должно было дать будущему воину особую жизненную силу. Возможно, таким образом еще в доядерную эпоху проявлялось интуитивное понимание истинного мироздания, суть коего - иррациональное слияние метафизических и материалистических концепций, свойственное исключительно русскому человеку.
Это богоданное свойство хорошо было наблюдать летом 2000 года в очереди к мощам Св.Пантелеймона в Храме Христа Спасителя в Москве. Бесконечной вереницей стояли русские люди по 12-15 часов в надежде приобщиться святости великомученика и целителя, и такие же русские люди бесконечно обходили стоящих, собирая деньги якобы на лекарства, надгробия и восстановление храмов, и такие же русские люди с дракой и бранью прорываются в храм без очереди.
Одним из самых известных Адронов наших дней является знаменитый кинорежиссер Андрон Михалков-Кончаловский, отпрыск древнего дворянского рода Михалковых, известных своей неуемной творческой предприимчивостью, близостью к царям и неиссякаемой мужской силой. Самый популярный фильм Михалкова-Кончаловского - "Танго и Кэш", шедший в прокате города Красноголозадовска (бывш. Бухаринград) под названием "Кто девушку платит, тот ее и танцует".
Лептонов известно науке всего двенадцать штук. Точнее шесть, а остальные шесть - это их античастицы: электрон, мюон, тау-лептон, и три нейтрино: электронное, мюонное и, как вы уже можете догадаться, тау.
Нейтрино - вообще очень интересная штучка. Есть подозрения, что на самом деле Вселенная состоит в основном-то как раз из нейтрино. На миллиард нейтрино приходится одна какая-нибудь другая ма-а-асенькая элементарная частица. И основной вклад в мировую гравитацию вносят именно они - нейтрино. И если вдруг однажды мы доживем до Большого Чпока (процесса, обратного Большому Взрыву), то именно они будут в нем виноваты, свернув своей массой Вселенную обратно в сингулярность, из которой она когда-то родилась. И тогда у Кого-то на экран выскочит синяя маска смерти с предложением нажать Ctrl+Alt+Del. Но это будет уже совсем другая история.
Данная теория лептонов названа неправильной, потому что с точки зрения программирования она совершенно бесплодна.
ПРАВИЛЬНАЯ ТЕОРИЯ ЛЕПТОНОВ
Лептоны - это элементарные частицы мировой информации. Они принципиально неуловимы никакими физическими приборами. Скорость света - это недостижимая для них минимальная скорость, а обычно они движутся гораздо быстрее. Лептонное поле хранит информацию о всех прошедших, настоящих и будущих событиях в любой точке пространства-времени, включительно от чиха простудившегося этим летом микроба до мановения пальца Любимого Руководителя Товарища Ким Чен Ира, указывающего Владимиру Владимировичу направление движения России в сторону окончательной победы идей Чучхе.
В указанном промежутке лежат также разработка ОС Windows, грядущее столкновение нашей Галактики с галактикой Андромеды, передача НТВ Газпрому за долги, хрущовские испытания 50-мегатонной бомбы на Новой Земле, день рождения в прошлую субботу, раскаяние в прошлое воскресенье и многие другие события, которые здесь следовало бы перечислить, но неохота.
Единственный прибор, способный взаимодействовать с лептонами - это человеческая душа, поскольку сама она суть лептонный сгусток. Все, что для нас проявляется как интуиция, озарения, идеи - это тривиальные результаты взаимодействия души с лептонным полем (см., например, песню группы "Любэ" Улочки Московские, где есть такие пророческие слова: "Эх, сегодня я, кажись, надерусь"). Это, так сказать, непроизвольные формы взаимодействия. Существуют также и произвольные формы, то есть взаимодействия, совершаемые человеком сознательно: молитва, медитация, камлание, гадание, транс, сеанс Кашпировского, выборы президента и пр. Как правило, целью произвольных форм взаимодействия является получение из лептонного поля интересующей информации или попытка изменить его конфигурацию желательным для себя образом.
Здесь мы прекратим рассмотрение теорий лептонов, потому что все, что нужно, уже выяснили, и перейдем к практике программирования.
Среди традиционных проблем ассемблерного программиста связывание занимает далеко не последнее место.
Поясняю. Всякий сколько-нибудь значительный проект обычно содержит более одного модуля, то бишь файла с исходным текстом, по той простой причине, что будучи запихнутым в один файл, проект становится неуправляемо громоздким. Модуль еще иногда называют единицей трансляции, имея в виду то обстоятельство, что обрабатывая его и превращая в объектный файл, компилятор ни сном, ни духом не ведает о существовании других модулей. Своевременное и логически обоснованное разбиение проекта на модули столь важно, что некоторые авторы даже склонны рассматривать модульность как отдельную парадигму программирования, наряду с хаотической, процедурной, структурной и объектно-ориентированной (Собоцинский В.В. Практический курс Турбо С++. М, "Свет",1993).
Вообще-то понятие "модуль" достаточно широкое и многозначное. Но в этой статье мы будем иметь ввиду именно модуль как единицу трансляции, то есть самостоятельный asm-файл, содержащий исходный текст, который может быть откомпилирован без ошибок.
Модули должны взаимодействовать друг с другом, иначе программа не окажется единым целым и не будет работать. Взаимодействовать они могут одним из двух способов:
Возможен еще такой способ взаимодействия, как передача управления из одного модуля в другой командой jmp или ее условными формами (je, jb, js и пр.). Но, если только программист не пытается реализовать какую-нибудь специальную идею, применение этого способа следует считать проявлением запредельного случая неправильного стиля программирования.
В Windows существует еще один способ взаимодействия модулей: когда из одних модулей посылаются сообщения окнам, созданным в других модулях. Но об этом ниже.
Стоит проблема: как позволить командам из одного модуля обращаться к данным, описанным в других модулях. Или как из одного модуля вызывать процедуры, тела которых содержатся в других модулях. Решение этой проблемы как раз и называется связыванием.
Родной для ассемблерных программ способ связывания обеспечивается применением сладкой парочки директив PUBLIC и EXTRN (она же EXTERN). Директива PUBLIC объявляет имя переменной или процедуры доступным для обращения из других модулей. Директива EXTERN сообщает компилятору, что команды данного модуля могут обращаться к имени переменной или процедуры, описанной в другом модуле.
Например:
в модуле masha.asm содержится такой фрагмент кода:
PUBLIC mashin_vozrast .data mashin_vozrast dd 17
а в модуле vasya.asm содержится такой фрагмент кода:
EXTRN mashin_vozrast:dword .code ;... cmp mashin_vozrast,18 ;...
Здесь дан наиболее часто применяемый вариант директив PUBLIC и EXTRN. На самом деле их возможности несколько шире, но они не являются предметом этой статьи. Читайте документацию на соответствующий вашим предпочтениям ассемблер.
Обратите внимание, что переменная mashin_vozrast объявлена как двойное слово, что вполне естественно для программирования под win32, даже при том условии, что Маша вряд ли достигнет возраста 4294967296 лет, сколько бы здоровья мы ей ни желали. Настоятельно рекомендуется в подобных случаях (когда числовая переменная объявлена как одиночная, а не как массив) всегда использовать переменные размером в двойное слово, потому что обращение к таким переменным из 32-разрядного кода существенно более компактно, чем к переменным размером в слово или байт.
Как видим, Вася пытается оценить, насколько близкими могут быть его отношения с Машей, чтобы не загреметь по очень неприятной статье. Его, вероятно, ждет разочарование, но только не на этапе компиляции, потому что компилируется он программой-компилятором (например, ml.exe) в одиночку, без участия предмета своих вожделений.
Связыванием же занимается компоновщик, он же линкер, он же link.exe. Это его основная задача, откуда и произошло его название. Схватив полученные в результате двух актов компиляции объектные файлы masha.obj и vasya.obj, он обнаруживает, что Маша публично заявляет о своем возрасте (впрочем, 17-ти лет стесняться не приходится), а Вася внимательно прислушивается. Компоновщик удовлетворяет обоих, выделяя в исполняемом модуле sex.exe место под переменную размером в двойное слово, инициализирует ее числом 17, а в команду cmp, с помощью которой Вася интересуется Машиным возрастом, подставляет адрес этой переменной.
Связывание вызовов процедур выглядит ненамного сложнее:
модуль masha.asm:
PUBLIC get_masha .code get_masha PROC ;... ret get_masha ENDP
модуль vasya.asm:
EXTRN get_masha:near .code ;... call get_masha ;...
Обратите внимание, что процедура get_masha объявлена как ближняя. В win32, поскольку дело происходит в плоской (flat) модели памяти, все процедуры ближние.
Как видим, Вася добрался-таки до Маши. Вероятно, в какой-то момент переменная mashin_vozrast инкрементировалась, или любовь оказалась сильнее условностей. Но так или иначе связывание состоялось.
Сказанное выше описывает стандартный для ассемблера механизм связывания на этапе компоновки приложения. По сути своей это так называемое раннее связывание, то есть связывание, выполняемое во время разработки программы. В отличие от позднего, используемого, например, в C++ для виртуальных функций, что является основой для реализации полиморфизма - одного из столпов объектно-ориентированного программирования.
Есть ли у этого метода недостатки? Да до... в общем, вам по пояс будет.
Представьте себе, что вы коренной русак без малейшей примеси немецкой крови. Тогда, видимо, вы вряд ли являетесь поклонником такого понятия, как инкапсуляция, и глобальных переменных в ваших программах - как блох на мамонте. А каждая глобальная переменная - это одна директива PUBLIC и энное количество директив EXTRN, по одной на каждый модуль, из которого на нее ссылаются. Поменяли имя переменной - будьте любезны править все модули. А если у вас, допустим, сто глобальных переменных? Не слабо?
А даже если вы и наоборот, запредельный педант, и пипифакс рвете исключительно по перфорации, и объектная ориентация для вас важнее, чем сексуальная? Тогда, вероятно, глобальных переменных в ваших программах нет совсем, а межмодульный обмен данными вы обеспечиваете с помощью специальных интерфейсных процедур, на манер публичных функций-членов классов в C++. Ну и что, намного легче вам иметь длиннющие списки директив PUBLIC и EXTRN для процедур, чем для переменных?
Возрадуйтесь! Ваши мучения закончились. С сегодняшнего дня вы можете забыть не только синтаксис директив PUBLIC и EXTRN, но и их названия, потому что они вам больше не понадобятся. На сцену выступает лептонный стиль программирования.
Отвечая на первый попавшийся FAQ, объясним сразу, при чем же здесь все-таки лептоны и почему стиль назван так претенциозно, если не сказать помпезно.
Все началось несколько лет назад, когда мы только-только начинали программировать под Windows. Почти сразу выяснив, что одно из главных достоинств ассемблерных программ - максимальное быстродействие - уже больше не является безусловным благодаря многозадачной архитектуре операционной системы, мы сначала опечалились, а потом, подумав, решили, что это даже хорошо. Можно расслабиться, перестать высчитывать загрузку конвейера процессора и позволить себе некоторые излишества: размочить позавчерашний сухарь водичкой, постирать носовой платок, не буферизировать файлы размером меньше кластера, заглянуть на Интердаму, наконец.
Особенно большое впечатление произвел механизм сообщений Windows. Допустим, нажимаете вы, не подумав, мышкой на кнопку в диалоговом окне. Внешне все происходит просто: нажимается кнопка "Нет" в диалоге "Хотите ли вы сохранить документ, над которым вы работали последние четыре часа"; вы страшно богохульничаете, плюете в монитор и с горя отправляетесь в магазин. Вот и все, и большую часть из этой процедуры вы выполняете вообще без помощи компьютера.
А на самом деле? На самом деле все ужасно сложно и долго. Драйвер мыши обнаруживает судорожное движение вашего пальца. Потом он интересуется местоположением курсора на экране. Полученные координаты он отправляет операционной системе на предмет определения окна, над которым в данный момент находится курсор. А окон открыта целая туча: всякие там кнопочки-шмопочки, документики-шмокументики, списочки-шмисочки. И все перекрываются, и каждое норовит чего-нибудь обработать. И вот операционная система перебирает их все и находит то, которое в точке курсора лежит поверх всех остальных. И отправляет ему сообщение. Методом Pоst между прочим, то есть ставя его в очередь, а не вызывая непосредственно оконную процедуру. Сообщение отправляется родительскому окну и торчит там в очереди, пока не придет пора его обработать. Кроме того, поскольку дело происходит в диалоговом окне, кнопочка еще и перерисовывается, отображая нажатое состояние. А потом еще удаляется с экрана диалоговое окно, и перерисовывается то окно, в котором находился ваш документ, а потом удаляется и оно, и перерисовываются все лежавшие под ним другие окна, при этом тоже не особенно экономя процессорное время и память. И все это сопровождается передачей такого количества сообщений, что Смольный в ночь с 24 на 25 октября (с.с.) 1917 года по сравнению с этим гвалтом покажется вам санаторием ЦК КПСС во время послеобеденного тихого часа, когда пациенты переваривают телятину по-кабардински под белым соусом в сопровождении нежных трапецеидальных импульсов стимулирующих перистальтику кремлевских таблеток. Короче, рассмотрев эту предельно упрощенную схему действий, мы понимаем, что вместо того, чтобы просто уничтожить все ваши труды, ОС сама долго и плодотворно трудится.
И что же, среди этого разврата мы будем экономить, отказывая себе во всем, даже в полкило красной икры на завтрак?
Да никогда!
Мы построим свою программу из модулей. Каждому модулю мы поручим выполнение какой-нибудь конкретной задачи в рамках общего дела. Один пусть занимается работой с настройками: содержит информацию об активных панелях управления, составе кнопок на них и пр., и предоставляет эту информацию другим модулям по их запросам. Другой модуль пусть обеспечивает сохранение состояния приложения в период между его запусками: запоминает размер и положение окон, имена редактировавшихся документов, списки изменяемых данных и еще что-нибудь. И тоже предоставляет эту информацию другим модулям по запросам. Третий модуль пусть обслуживает строку статуса: инициализирует ее, разбивает на поля, показывает в них то или иное содержимое по запросам других модулей. Четвертый модуль пусть предоставляет текстовые строки на выбранном пользователем языке. Пятый модуль...
Впрочем, достаточно. И так уже ясно, что главным элементом архитектуры нашего приложения является обмен запросами и ответами модулей между собой. По сути, модули выступают друг для друга в качестве так любимых всеми современными разработчиками клиентов и серверов. Клиенты хотят иметь информацию или требуют выполнения каких-то действий, и поэтому запрашивают соответствующий сервис у серверов. Сервера выполняют запросы клиентов, не допуская, между прочим, обслуживания некорректных запросов. (Например, некий модуль отвечает за хранение глобальных констант. Следовательно, он должен выдавать значения констант другим модулям, но не должен предоставлять им никаких средств для изменения этих значений.)
Таким образом, общая идея довольно проста и даже тривиальна. И уж конечно, в той или иной степени она реализуется практически в каждой более-менее серьезной программе.
Отличие же лептонного стиля состоит в том, что запрашивающий модуль-клиент не обязан знать, и не может знать, и вовсе даже не интересуется, кто из существующих модулей-серверов обслуживает его запрос, и вообще, существует ли такой сервер.
В этом - квинтэссенция лептонного стиля. Клиент отправляет свой запрос в пространство, не адресуясь ни к кому конкретному, наподобие молитвы всем богам. "Эй, кто-нибудь, кто может, ответьте, пожалуйста, какова сейчас ширина главного окна приложения?" - как бы кричит в эфир модуль, создающий, например, подчиненное окно, занимающее одну восьмую часть главного окна по горизонтали. И получает в ответ неизвестно от кого: "720 пикселов, дружище! Пользуйся на здоровье!".
Теперь ответим на второй первый попавшийся FAQ. Тянет ли эта идея на понятие "стиль программирования"? По-нашему, тянет. Ведь что есть стиль программирования? Это более-менее стабильный набор приемов организации программы и оформления исходного текста, используемый программистом с целью облегчения ее разработки и понимания. С точки зрения этого определения то, о чем мы сейчас говорим - именно и есть стиль. Стабилен ли он? Да вы только попробуйте его хотя бы разок, и обратно уже не запроситесь. Идет ли в данном случае речь об организации программы? Безусловно: и с формальной точки зрения (отсутствие директив PUBLIC/EXTRN), и с фактической (организация специальной системы обмена сообщениями внутри программы) - это инструмент организации программы. Идет ли в данном случае речь об оформлении исходного текста? Конечно, да. Далее мы покажем, как с помощью макросов удобно и наглядно записываются запросы клиентов.
Первая реализация поддержки лептонного стиля программирования была прямо выполнена на штатных средствах обмена сообщениями Windows. До сих пор еще некоторые наши программы используют ее. Общая идея там была такова: при запуске приложения создавалось невидимое окно, называемое нами супервизором, дескриптор которого объявлялся глобальным для всего проекта. Оконная процедура супервизора, таким образом, становилась доступной из любой точки программы для передачи (SendMessage) или посылки (PostMessage) пользовательских сообщений, то есть относящихся к диапазону WM_USER (00400h...07fffh) и/или диапазону WM_APP (08fffh...0bfffh):
invoke SendMessageA,hWnd,Msg,wParam,lParam
В каждом модуле объявлялась как PUBLIC одна-единственная процедура, названная нами диспетчером. Упрощенно система работала так:
Как параметры (wParam и lParam), так и ответ (eax) могли представлять собой непосредственные данные, если они помещались в размер двойного слова, либо являться адресами (указателями) блоков памяти (структур, строк), в которые модуль-клиент и/или модуль-сервер помещали соответствующие данные. В каждом конкретном случае это определялось смыслом запроса, то есть значением сообщения (Msg).
У этой схемы, при ее общей очевидной работоспособности, имелись несколько неприятных недостатков (мы настаиваем на этом определении, ибо всякий поживший на свете человек знает, что некоторые недостатки иногда бывают очень даже приятны):
Поэтому мы не станем рассматривать здесь вариант реализации лептонного стиля на базе системы сообщений Windows, а сразу перейдем к более прогрессивному - автономному варианту. Его отличительной особенностью является применимость не только под Windows, а в любой ассемблерной программе, в том числе под DOS, Linux и пр. Если он вам понравится, вы можете использовать приведенный здесь код один к одному. Он вполне работоспособен и достаточно функционален. Но еще правильнее воспринимать его как набор идей, которые вы свободно можете развить и дополнить так, как посчитаете нужным. Или, например, перевести на другой язык программирования.
Реализация и заключительные замечания - во второй части статьи.
ЧАСТЬ ВТОРАЯ. РЕАЛИЗАЦИЯ
В реализации задействованы файлы:
Ниже приведены фрагменты кода, включаемые в перечисленные файлы и обеспечивающие поддержку лептонного стиля программирования, и комментарии к ним.
Файл глобальных макросов @stuct.inc:
;------------------------- эквиваленты параметров молитвы @par0 EQU esi @par1 EQU edi @par2 EQU edx @par3 EQU ecx ;------------------------- макрос молитвы @pray MACRO pray,par0,par1,par2,par3 IFNBmov esi,par0 ENDIF IFNB mov edi,par1 ENDIF IFNB mov edx,par2 ENDIF IFNB mov ecx,par3 ENDIF invoke supervisor,pray ENDM ;------------------------- макрос формирования списка диспетчеров @dispatchers MACRO IFDEF MODULES @id_offset=1 @id_size SIZESTR MODULES :next IF @id_offset GT @id_size EXITM ENDIF @module_id SUBSTR MODULES,@id_offset,2 @module_name CATSTR ,@module_id @module_name PROTO :DWORD dd offset(@module_name) @id_offset=@id_offset+3 GOTO next ENDIF ENDM ;------------------------- директивы определения модели .386 .Model flat,stdcall ;------------------------- прототип супервизора supervisor PROTO :DWORD ;------------------------- идентификатор модуля @module SUBSTR @FileName,1,2 ;------------------------- конструкции, зависимые от модуля IFIDN @module,<00> ;если это модуль супервизора .const ;создать список диспетчеров dispatchers dd 0 dup(0) @dispatchers ;вызов макроса формирования списка диспетчеров dd 0 ELSE ;если это обычный модуль @dispatcher CATSTR ,@module ;сконструировать имя диспетчера ENDIF
Пояснения:
Этот текст после компиляции будет преобразован в последовательность команд:@pray P_GET_STRING,offset(string_buffer),buffer_size,string_id
Видно, что в отличие от описанной выше реализации но основе средств обмена сообщениями Windows, этот код оптимален: кодируются только те параметры, которые используются. Другие примеры молитв и бродкастов:mov esi,offset(string_buffer) mov edi,buffer_size mov edx,string_id push P_GET_STRING call supervisor
@pray B_START,hinstance @pray P_SET_MAIN_WINDOW_TITLE,offset(main_window_name) @pray P_STORE_STATUS, STATUS_MAIN_WINDOW_POSITION, offset(main_window_rectangle), show_status @pray B_STOP
Файл глобальных констант globals.inc:
;------------------------- молитвы P_BASE =0 ;базовый номер молитв P_HINSTANCE =P_BASE+0 P_MAIN_WINDOW =P_BASE+1 P_GET_STRING =P_BASE+2 P_GET_MAIN_WINDOW_SIZE =P_BASE+3 P_SET_BACKGROUND_COLOR =P_BASE+4 ;------------------------- бродкасты B_BASE =P_BASE+100 ;базовый номер бродкастов B_START =B_BASE+0 B_STOP =B_BASE+1 B_MAIN_WINDOW_SIZED =B_BASE+2
Пояснения:
Файл главного модуля 00_main.asm:
;------------------------- список идентификаторов модулей MODULES EQU <01,02,03,04> ;------------------------- include-файлы include @struct.inc include windows.inc include globals.inc ;... .code ;... ;------------------------- супервизор supervisor PROC USES ebx ecx edx esi edi pray mov eax,pray ;........................ молитвы главного модуля @if(eax==P_HINSTANCE) invoke GetModuleHandleA,NULL @elseif(eax==P_MAIN_WINDOW) mov eax,main_window @else ;....................... опрос диспетчеров модулей mov ebx,offset(dispatchers) ;ebx - указатель в списке диспетчеров @while(dword ptr[ebx]!=0) push pray call dword ptr[ebx] ;вызов очередного диспетчера @if(pray
Пояснения:
Файл модуля XX_*.asm:
;------------------------- include-файлы include @struct.inc include windows.inc include globals.inc ;... .code ;... ;------------------------- диспетчер @dispatcher PROC USES ebx ecx edx esi edi pray mov eax,pray ;....................... обработка бродкастов @if(eax==B_START) ;... @elseif(eax==B_STOP) ;... ;....................... обработка молитв @elseif(eax==P_GET_STRING) mov buffer_address,@par0 ;пример использования параметров молитвы mov buffer_size,@par1 mov id,@par2 ;... jmp ok ;....................... завершение диспетчера @endif nok: ;не обработано xor eax,eax ok: ;обработано ret @dispatcher ENDP
Пояснения:
В заключение несколько дополнительных замечаний:
[C] Svet(R)off