Статьи по Assembler

         

Browse info для ассемблера


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

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

А ведь сортировщики куриных яиц еще не сталкиваются с такой проблемой, как глобальность идентификаторов в пределах модуля! Спасибо любимой Microsoft за то, что хоть метки стали локальны в пределах процедуры. Впрочем, специально для садомазохистов разработчики предусмотрели директиву OPTION NOSCOPED - кайфуйте, ребята.

Каждый крутится как может. Шаблонно мыслящие конформисты идут на поводу у Чарльза Симони и украшают свои исходники венгерской тавтологией вроде lpSomeTable. Аристократические отпрыски Йеля и Итона, сложив губки гузкой, выстукивают холенымы ногтями имя функции: PleaseGiveMeAnErrorCode. Раздолбанные хакеры, вытерев рукавом пиво с клавиатуры, без раздумий используют получившуюся комбинацию: p9ijhbgfd, и в течение следующей минуты навешивают на нее как минимум сорок строк кода. Стиль русских программистов старшего поколения, заставших время, когда их называли советскими программистами, особый, и не меняется уже много лет: vodka362, kolbasa220. Изредка попадающиеся в этой экологически нездоровой среде женщины широко используют свои специфические ассоциации: carnation, champagne, alwaysultraplus. Педанты же как всегда педантичны: proc_for_scan_table_of_strings_00000001.


А вот откуда берется файл .bsc - разобраться надо, потому что без понимания этого процесса запустить Browse Info в ассемблерном проекте вряд ли удастся.

Процесс создания базы данных .bsc, как и следует ожидать, происходит на обоих этапах построения проекта - и на этапе компиляции, и на этапе компоновки.

Компилятор (будь то cl.exe для C++ или ml.exe для MASM) создает промежуточный файл базы данных, содержащий, естественно, только информацию об идентификаторах данной единицы компиляции - cpp-файла или asm-файла. Этот промежуточный файл имеет расширение .sbr и имя, по умолчанию совпадающее с именем компилируемого файла. Размещается он там же, где и создаваемый объектный файл - в папке Debug. Для того, чтобы компилятор озаботился созданием sbr-файла, в его командную строку следует включить опцию /FR или /Fr.

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

Опции предоставляют возможность изменить местоположение и имя создаваемого sbr-файла, как, например: /FR"..\Support Files\browseinfo.sbr"

Поскольку компиляция файлов C/C++ поддерживается MS Developer Studio, то для того, чтобы заставить компилятор создавать sbr-файл, следует запустить диалог Project Settings, открыть вкладку C/C++, выбрать в списке Category пункт Listing Files, и поставить галочку в чекбоксе Generate browse info. Если нужно заменить местоположение или имя sbr-файла, следует воспользоваться полем Intermediate browse info file destination. А замена опции /FR на опцию /Fr производится галочкой в чекбоксе Exclude local variables from browse info.

Для ассемблерных файлов опцию в нужной форме следует прямо указать в командной строке на вкладке Custom Build при настройке MS Developer Studio.

На этапе компоновки проекта из промежуточных sbr-файлов, созданных компилятором для каждого из модулей, собирается один на весь проект bsc-файл. Правда, слова "на этапе компоновки" отнюдь не означают, что этим занимается сам компоновщик. Для сборки bsc-файла существует специальная утилита - bscmake.exe, находящаяся в папке исполняемых файлов Vc\bin MS Developer Studio.



Включить вызов утилиты bscmake.exe можно с помощью чекбокса Build browse info file, находящегося на вкладке Browse Info диалога Project Settings. Там же можно при необходимости поменять местоположение и имя bsc-файла, подавить выдачу баннерного текста и задать другие опции командной строки утилиты.

Учитывая обширность заголовочных файлов API, следует ожидать, что удовольствие от применения Browse Info хорошо оплачено ресурсами компьютера. Так оно и есть. Sbr-файл, образованный из чистого windows.h, весит почти восемьсот кило. А образованный из него bsc-файл - в два с лишним раза больше. При включенном Browse Info соответственно увеличивается время компиляции и компоновки проекта. Это обстоятельство могло бы превратить жизнь программиста в сущий ад, если бы не добрые дяди из Microsoft, предусмотревшие инкрементную обработку этих файлов. Суть ее в том, что сборка bsc-файла выполняется каждый раз не заново, а только с учетом реально произошедших изменений в проекте. Определить же, какие именно изменения произошли, утилита bscmake.exe умудряется очень просто. Всякий раз после очередной сборки она усекает до 0 размер участвовавших в сборке sbr-файлов. Таким образов в следующем сеансе сборки примут участие только те sbr-файлы, которые с прошлого раза прошли реальную перекомпиляцию.

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

рабочая среда (workspace) содержит один проект рабочая среда содержит несколько проектов

Вариант с одним проектом:

Создайте и настройте рабочий проект так, как указано в статье "ms devstudio - среда разработки asm"



В диалоге Project Settings на вкладке Custom Build в командную строку компилятора включите опцию:
/FR"Debug\$(InputName).sbr"Это необходимо сделать для всех asm-файлов, для которых вы хотели бы иметь включенным средство Browser Info.

Как видно, sbr-файлы будут помещаться в папку Debug с именем, соответствующим имени asm-файла. Эта папка выбрана нами потому, что по умолчанию в нее же помещаются sbr-файлы модулей, написанных на C/C++. Учтите, что в начале работы с проектом она не существует, поэтому попытка компиляции даст ошибку. Создайте ее вручную.
Если проект смешанный и содержит также файлы С/C++, включите для них опцию Browser Info установкой чекбоксов в диалоге Project Settings на вкладке C/C++ так, описано выше в этой статье. Если проект чисто ассемблерный, то следует обеспечить доступ к заголовочным файлам C/C++ windows.h с тем, чтобы использовать их как справочные при построении собственного файла windows.inc. Для этого:




создайте файл brinfo.cpp и включите его в проект содержимое файла brinfo. cpp должно представлять собой единственную строку:
#include <windows.h&gt

включите для этого файла опцию Browser Info как для обычного C++-файла откомпилируйте его

В диалоге Project Settings на вкладке Browse Info:


установите чекбокс Build browse info file

установите чекбокс Suppress startup banner

дополните содержимое поля Project options следующим текстом:
Debug\*.sbr Таким образом вы дадите утилите bscmake.exe команду включить в сборку все sbr-файлы, содержащиеся в папке Debug

Выполните компиляцию всех модулей проекта, а затем его компоновку. Browser Info готово к работе.

Следует учесть одно неприятное обстоятельство. Рабочая среда определяет необходимость запуска утилиты bscmake.exe по факту выполнения компиляции хотя бы одного файла C/C++. Ассемблерные же файлы, будучи компилируемы посредством Custom Build, к сожалению, такой команды рабочей среде не дают. Поэтому все изменения в составе идентификаторов в ассемблерных файлах остаются втуне до тех пор, пока не будет перекомпилирован хотя бы один cpp-файл проекта и после этого не выполнена его компоновка. В чисто ассемблерных проектах придется вручную вызывать компиляцию файла brinfo.cpp.

В этой беде мог бы помочь вынос вызова утилиты bscmake.exe на этап Post-build step. Однако, этот фокус не проходит: дело в том, что однажды активизировавшись, Browse Info открывает bsc-файл и держит его в дальнейшем в открытом состоянии, запрещая таким образом запись в него всем внешним программам. Этот запрет отменяется только на этапе компоновки проекта, но не на этапе Post-build step.

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

прежде всего следует определиться, имеется ли необходимость иметь единое для всех проектов пространство идентификаторов (не в смысле программирования, а в смысле просмотра их с помощью Browse Info), или у каждого проекта должно быть свое пространство, или проекты можно разбить на группы, с отдельным пространством для каждой группы для каждого пространства следует определить одну папку, в которой должны содержаться все sbr-файлы модулей, относящихся к этому пространству для каждого пространства следует использовать отдельный файл brinfo.spp




Инструкция программиста mycall


Приложение MyCall - это пользовательский интерфейс Remote Access Service для Windows 95/98. Приложение разработано как учебно-экспериментальная задача, целью которой является демонстрация отличий в реализации приложений на ассемблере и на C++. См. статью Зачем он нужен, этот ассемблер?.

Исходный текст реализации на C++ состоит из файлов:

main.cpp - основной файл main.h - файл заголовков mycall.rc - файл описания ресурсов icon1.ico - иконка

Полный комплект файлов, необходимых для компиляции приложения, содержится в zip-файле mycallcb.zip (13192 байта)

Исходный текст реализации на ассемблере состоит из файлов:

main.asm - основной файл main.inc - файл заголовков @struct.inc - файл структурных макросов windows.inc - файл заголовков win32 mycall.rc - файл описания ресурсов icon1.ico - иконка

Полный комплект файлов, необходимых для компиляции приложения, содержится в zip-файле mycallab.zip (15913 байта)

компиляция приложения

Компиляция реализации на C++:

Создайте в MS Developer Studio новый проект приложения win32 ("Win32 Application") с именем mycall Распакуйте файл mycallcb.zip, поместив извлеченные из него файлы в папку проекта Подключите к проекту файлы main.cpp и mycall.rc Установите в качестве активной конфигурации для построения проекта "Win32 Release" Измените установки проекта для сборщика (Settings/Link):

удалите из списка подключаемых библиотек все, кроме kernel32.lib и user32.lib добавьте в список rasapi32.lib отключите библиотеки по умолчанию (флажок "Ignore all default libraries") установите в качестве точки входа функцию WinMain (поле "Entry-point symbol" в категории "Output")

Выполните построение проекта

Компиляция реализации на ассемблере:

Создайте в MS Developer Studio новый проект приложения win32 ("Win32 Application") с именем mycall Распакуйте файл mycallab.zip, поместив извлеченные из него файлы в папку проекта Подключите к проекту файлы main.asm и mycall.rc. (Учтите, что файл main.asm становится виден в окне файлового диалога MS Developer Studio только в режиме "Все файлы") Установите в качестве активной конфигурации для построения проекта "Win32 Release" Измените установки для компиляции файла main.asm (Settings/Custom Build):


Это, в свою очередь, диктует особенности программных решений. Был сознательно выбран хаотический стиль программирования, не использующий практически никаких характерных для C++ объектно-ориентированных средств организации программ. (В результате проект, например, изобилует глобальными переменными, что в общем случае следовало бы считать очень плохим стилем.) Также практически не использовались и специфические приемы ассемблерного программирования.

Но зато, положив рядом два листинга - на C++ и на ассемблере - желающие имеют теперь возможность как угодно глубоко разобраться в отличиях реализаций. Может быть полезно также сравнить дизассемблированный код приложения на C++ с реализацией на ассемблере, что позволяет сделать встроенный отладчик MS Developer Studio.

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

Работа приложения начинается с передачи управления функции WinMain. По своему построению это обычная для win32 функция, выполняющая следующие задачи:

создание главного окна подготовку рабочих данных запуск цикла обработки сообщений завершение работы

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

Кроме того, в функции WinMain выполняется проверка запуска второго экземпляра приложения. Для этого применяется специальный способ, основанный на обмене регистрируемым глобальным сообщением.

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



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

Файл mycall.ini загружается функцией load_ini однократно в начале работы приложения. На основании содержащихся в нем данных устанавливаются значения соответствующих переменных. Если файл mycall.ini не существует, как это бывает, например, при первом запуске приложения, то в переменных остаются значения по умолчанию.

Файл mycall.ini формируется и записывается функцией load_ini непосредственно перед завершением работы приложения. Функция вызывается оконной процедурой superprocedure при обработке ею сообщения WM_DESTROY.

Файл mycall.txt содержит данные, задаваемые пользователем для соединения с провайдером: имена соединений, номера телефонов, логины и пароли.

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

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

Общую диспетчеризацию в MyCall, как того и требует архитектура приложений win32, выполняет оконная процедура superprocedure. Она обрабатывает сообщения:

WM_COMMAND - поступающее при изменении позиции списков WM_USER - используемое при мониторинге состояния соединения для принятия решения о необходимости повтора дозвона WM_MOVE - для запоминания позиции окна при его перемещении WM_DESTROY и WM_CLOSE - для выполнения завершающих операций при окончании работы приложения регистрируемое глобальное сообщение my_message - для обеспечения взаимодействия экземпляров приложения



После выбора соединения, телефона и логина, пользователь, нажав кнопку, переводит приложение в режим дозвона. При этом:

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

Функция ras_dial на основании данных, выбранных пользователем, заполняет структуру RASDIALPARAMS и вызывает функцию API RasDial.

Для мониторинга процесса дозвона используется callback-функция ras_dial_func типа RasDialFunc (см. Platform SDK). Эта функция отображает соответствующие сообщения в строке статуса и, в случае неудачи установления соединения, посылает оконной процедуре сообщение WM_USER с параметром "0" для выполнения повторных попыток дозвона.

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

Если соединение установлено успешно, callback-функция ras_dial_func запускает нить монитора разрыва ras_monitor_func. Такой прием пришлось применить потому, что в сервисе RAS не предусмотрено средств для контроля работающего соединения, и, следовательно, приложение не может никаким иным способом узнать о факте разрыва соединения, например, при потере связи. Нить монитора разрыва каждые 200 мс опрашивает состояние соединения, и при обнаружении его разрыва сообщает об этом основной нити приложения путем посылки сообщения WM_USER с параметром "1", а затем завершается. Получив это сообщение, приложение переходит в режим выбора соединения.

Для прекращения дозвона и принудительного разрыва установленного соединения используется функция ras_hangup. Она вызывает функцию API RasHangUp.

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

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




Компиляция файлов .asm


Этот материал дополняет статью MS Developer Studio - среда разработки для ASM. Здесь приведен формат командной строки компилятора MASM 6.1+, расшифровка ее опций и комментарии по их применению при работе в среде MS Developer Studio.

Командная строка MASM имеет вид:
ML [/options] filename [/options] filename [/link linkoptions]

Здесь:

/options - необязательный перечень опций (см.таблицу ниже) filename - имена файлов исходных текстов, подлежищих компиляции, разделенные пробелами. В среде MS Developer Studio обычно указывается только одно имя в виде шаблона $(InputPath). /link linkoptions - командная строка компоновщика в случае, если используется опция /Bl. В среде MS Developer Studio обычно не применяется.

опция назначение применение для win32
/AT Enable tiny model (.COM file)
Создать файл в формате .com (модель tiny)
Не применяется, так как формат исполняемого файла .com не используется в Win32.
/Bl<linker> Use alternate linker
Использовать альтернативный компоновщик
Обычно не применяется, так как возможностей link.exe вполне достаточно. Используется опция /c
/c Assemble without linking
Только компиляция, без компоновки
Обязательно для применения в среде MS Developer Studio, чтобы выполнять компоновку отдельным этапом.
/Cp Preserve case of user identifiers
Сохранение регистра пользовательских идентификаторов
Применение не обязательно, но возможно для дополнительного контроля синтаксиса. Вызывает ошибку "A2006: undefined symbol" при несовпадении регистра в объявлении идентификатора и обращении к нему. Позволяет избежать ошибок на этапе компоновки в случае, если идентификатор объявлен с неверным регистром.
/Cu Map all identifiers to upper case
Приведение всех пользовательских идентификаторов к верхнему регистру
Не применяется, так как компоновка приложений Win32 чувствительна к регистру.
/Cx Preserve case in publics, externs
Сохранение регистра идентификаторов, объявленных публичными и внешними
В применении нет необходимости. Регистр идентификаторов имеет смысл на этапе компоновки, но не на этапе компиляции.
/coff Generate COFF format object file
Создать файл в формате COFF
Применение обязательно: это стандартный для windows формат объектных и исполняемых файлов.
/D<name>[=text] Define text macro
Описание текстового макроса
Применяется по усмотрению программиста. Аналог директив EQU или =. Если текст содержит пробелы, его следует взять в кавычки. Обычно используется в отладочном версии приложения для объявления имени DEBUG.
/EP Output preprocessed listing to stdout
Вывод листинга препроцессора в stdout
Обычно применять нет необходимости. Листинг препроцессора представляет собой исходный текст вместе с включаемыми файлами.
/F<hex> Set stack size (bytes)
Определить размер стека (байт)
Практически не применяется. То же, что опция /STACK компоновщика link.exe. Обычно используется значение по умолчанию - 1 Мбайт.
/Fe<file> Name executable
Имя исполняемого файла
Не применяется, так как с учетом опции /c компилятор не создает исполняемого файла
/Fl[file] Generate listing
Создание файла листинга
Обычно не применяется, так как средства MS Developer Studio, как правило, достаточны для работы с текстом приложения.
/Fm[file] Generate map
Создание map-файла
Не применяется, так как map-файл создается компоновщиком, а с учетом опции /c компилятор не вызывает компоновщик
/Fo<file> Name object file
Имя объектного файла
Обычно не применяется. Позволяет задать obj-файлу имя, отличное от имени asm-файла.
/FPi Generate 80x87 emulator encoding
Включение кода эмулятора сопроцессора 80x87
Начиная с выхода в свет процессора 486 не применяется, так как с тех пор арифметический сопроцессор является неотъемлемой частью современных процессоров.
/Fr[file] Generate limited browser info
Включить ограниченную информацию броузера
Применение менее предпочтительно, чем /FR, так как в информацию броузера не включа.тся сведения о локальных идентификаторах.
/FR[file] Generate full browser info
Включить полную информацию броузера
Позволяет получать быстрый доступ к любому идентификатору во всем пространстве проекта и заголовочных файлов API win32. См. статью об этом.
/G<c|d|z> Use Pascal, C, or Stdcall calls
Использовать соглашения вызова Pascal, C или Stdcall
В применении нет необходимости. Обычно использование соглашений вызова stdcall регламентируется директивой .model в тексте программы.
/H<number> Set max external name length
Установить максимальную длину внешних имен
Обычно не применяется. Значение по умолчанию - 31, и его достаточно для работы в среде win32.
/I<name> Add include path
Добавить путь для inc-файлов
Не применяется, так как собственных возможностей MS Developer Studio обычно достаточно для определения путей к inc-файлам. Допускается использовать до 10 опций /I.
/link <linker options and libraries>
Опции командной строки компоновщика и подключаемые библиотеки
Не применяется, так как компоновка отключена опцией /c.
/nologo Suppress copyright message
Не показывать баннерный текст компилятора
Как правило, следует применяеть, так как баннерный текст смысловой нагрузки при разработке проекта не несет.
/Sa Maximize source listing
Листинг максимального формата
Применяется редко, так как собственных средств MS Developer Studio обычно достаточно для работы с исходным и компилированным текстом программы, и в выдаче листинга нет необходимости.
/Sc Generate timings in listing
Включить в листинг синхронизацию
То же
/Sf Generate first pass listing
Листинг первого прохода
То же
/Sl<width> Set line width
Длина строки листинга, символов: 60...255 или 0.
То же
/Sn Suppress symbol-table listing
Не включать в листинг таблицу символов
То же
/Sp<length> Set page length
Высота страницы листинга, строк: 10...255 или 0.
То же
/Ss<string> Set subtitle
Текст подзаголовков листинга
То же
/St<string> Set title
Текст заголовка листига
То же
/Sx List false conditionals
Включить в листинг все фрагменты условной компиляции
То же
/Ta<file> Assemble non-.ASM file
Компилировать не-.asm файлы
Обычно не применяется. Служит для компиляции файлов, имя котрых имеет расширение, отличное от .asm.
/w Same as /W0 /WX
То же, что /W0 /WX
См. далее.
/WX Treat warnings as errors
Трактовать предупреждения как ошибки
Обычно в применении нет необходимости. В случае возникновения предупреждений компиляция завершается неуспешно.
/W<number> Set warning level
Установить уровень предупреждеинй
Обычно в применении нет необходимости. Устанавливает перечень событий компиляции, трактуемых как предупреждения.
/X Ignore INCLUDE environment path
Игнорировать путь, установленный переменной окружения INCLUDE
Обычно не применяется, так как при работе в среде MS Developer Studio переменная окружения INCLUDE не используется.
/Zd Add line number debug info
Включить отладочную информацию в виде номеров строк
Обычно не применяется, так как на этапе отладки более целесообразно использовать опцию /Zi.
/Zf Make all symbols public
Объявить все имена публичными
Обычно не применяется.
/Zi Add symbolic debug info
Включить полную отладочную информацию
Обязательно применяется на этапе отладки. Формат отладочной информации MASM полностью совместим с используемым встроенным отладчиком MS Developer Studio.
/Zm Enable MASM 5.10 compatibility
Включить совместимость с MASM 5.10
Обычно не применяется. Отключает полезные для прикладного программирования свойства MASM, введенные в версиях 6.1+.
/Zp[n] Set structure alignment
Установить выравнивание структур
Может быть использован для установки принятого в win32 выравнивания по умолчанию - на 8 байт. Однако обычно в применении нет необходимости, так как в win32 используется два варианта выравнивания структур - на 4 и на 8 байт, и определять их выравнивание целесообразно непосредственно в описании структуры директивой STRUCT. Возможные значения для этой опции - 1, 2, 4 и 8 (последнее - в версиях MASM 6.13 и выше).
/Zs Perform syntax check only
Выполнять только проверку синтаксиса
Обычно не применяется. Подавляет формирование объектного модуля.

Типовая командная строка для этапа отладки (активен проект Win32 Debug) имеет вид:
ml.exe /c /coff /FR"..\browseinfo\$(InputName).sbr" /nologo /Zi /DDEBUG $(InputPath)

Отключена компоновка, формируется объектный модуль в формате COFF, формируется sbr-файл, содержищий информацию для Browse Info, подавлена выдача баннерной информации, в объектный модуль включена полная отладочная информация, определена константа DEBUG.

Типовая командная строка для чистового этапа (активен проект Win32 Release) имеет вид:
ml.exe /c /coff /nologo $(InputPath)

Отключена компоновка, формируется объектный модуль в формате COFF, подавлена выдача баннерной информации.



Main.asm для mycall (ассемблер)


Это основной файл приложения MyCall на ассемблере. Этот файл в текстовом формате вместе со всеми остальными файлами, необходимыми для компиляции приложения MyCall, содержится в zip-файле mycallab.zip (15913 байт). Имеется также Инструкция программиста.

Для получения комментариев щелкaйте по тексту или пользуйтесь групповым управлением:

if(dhtml){document.write("Все комментарии: [+][-]    Открывать: [несколько]");}

;Включаемые файлы:

;@struct.inc - файл структурных макросов

;windows.inc - файл заголовков win32

;main.inc - файл заголовков приложения MyCall

include @struct.inc

include windows.inc

include main.inc

;ГЛАВНАЯ ФУНКЦИЯ ПРИЛОЖЕНИЯ

;///////////////////////////////////////////////////////////// WinMain

.data?

mw_class WNDCLASSEX{}

loop_message MSG{}

win_dim RECT{}

.const

mw_class_name db "MainWindowClass",0

my_message_name db "MyCallMessage",0

.code

WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show

mov dat_buffer,0

mov online,FALSE

;Получение дескриптора экземпляра приложения

;Необходимое действие, так как предполагается компиляция приложения без

;подключения runtime-библиотеки. Подробнее...

invoke GetModuleHandleA,NULL

mov hinst,eax

;Создание главного окна

;Обычное действие, с которого начинаются большинство приложений.

;Единственное отличие в том, что в качестве главного окна в MyCall

;используется окно диалога, описанного в файле ресурсов

;Регистрируется класс главного окна:

mov mw_class.cbSize,sizeof(WNDCLASSEX)

mov mw_class.style,NULL

mov mw_class.lpfnWndProc,offset superprocedure

mov mw_class.cbClsExtra,0

mov mw_class.cbWndExtra,DLGWINDOWEXTRA

mov eax,hinst

mov mw_class.hInstance,eax

invoke LoadIconA,eax,103

mov mw_class.hIcon,eax

mov mw_class.hIconSm,NULL

invoke LoadCursorA,NULL,IDC_ARROW

mov mw_class.hCursor,eax

mov mw_class.hbrBackground,COLOR_WINDOW

mov mw_class.lpszMenuName,NULL

mov mw_class.lpszClassName,offset mw_class_name


@if(ecx==3)

mov ecx,0

@endif

@endif

@if(ecx==0)

@if(ebx==0)

@if(byte ptr[esi]!=0)

@if(byte ptr[esi]!=1)

@push ecx,edx,ebx,esi

invoke SendMessageA,conn_window,CB_ADDSTRING,0,esi

@pop ecx,edx,ebx,esi

inc edx

@if(edx>=MAX_CON)

@break

@endif

@endif

@endif

@endif

@endif

inc ebx

inc esi

@endw

;По данным файла mycall. ini устанавливается прошлая позиция списка conn_window

@if(current_con>=edx)

mov current_con,0

@endif

invoke SendMessageA,conn_window,CB_SETCURSEL,current_con,0

;По состоянию списка conn_window формируются остальные списки

call change_con

;ЦИКЛ ОЖИДАНИЯ СООБЩЕНИЙ И ЗАВЕРШЕНИЕ ПРИЛОЖЕНИЯ

;Неотъемлемый элемент приложений для Windows. В MyCall никаких особенностей не имеет

;Поскольку MyCall собирается без runtime-библиотеки, для завершения работы

;обязательно использовать функцию ExitProcess. Подробнее...

msg_loop:

invoke GetMessageA,offset loop_message,NULL,0,0

@if(!eax)

invoke ExitProcess,loop_message.wParam

@endif

invoke TranslateMessage,offset loop_message

invoke DispatchMessageA,offset loop_message

jmp msg_loop

bad:

invoke fatal,eax

invoke ExitProcess,EXIT_COMMON_ERROR

WinMain ENDP

;ОКОННАЯ ПРОЦЕДУРА

;Стандартный элемент приложений для Windows. В MyCall особенностей не имеет.

;///////////////////////////////////////////////////////////// Оконная процедура

.data?

win_pos RECT{}

.code

superprocedure PROC window_from,message,w_param,l_param

mov eax,message

;Обработка сообщения WM_COMMAND, передаваемого элементами управления диалога

@if(eax==WM_COMMAND)

mov eax,w_param

mov cl,16

shr eax,cl

;Обработка уведомления CBN_SELCHANGE, передаваемого списками при изменении позиции:

;1000: список соединений conn_window. Изменяет содержание phon_window и user_window

;1001: список телефонов phon_window. Устанавливает новый телефон для текущего соединения con_phone[current_con]

;1002: список логинов user_window. Устанавливает новый логин для текущего соединения con_user[current_con]

@if(eax==CBN_SELCHANGE)



mov eax,w_param

and eax,0ffffh

@if(ax==1000)

mov eax,conn_window

and current_con,0ffh

mov esi,offset current_con

@elseif(ax==1001)

mov eax,phon_window

mov esi,offset con_phone

add esi,current_con

@elseif(ax==1002)

mov eax,user_window

mov esi,offset con_user

add esi,current_con

@else

jmp sp_nok

@endif

push esi

invoke SendMessageA,eax,CB_GETCURSEL,0,0

pop esi

@if(eax==CB_ERR)

xor eax,eax

@endif

mov [esi],al

call change_con

jmp sp_ok

@endif

; Обработка уведомления BN_CLICKED, передаваемого кнопкой при клике:

;в состоянии online=TRUE прекращает дозвон (разрывает соединение) и включает списки

;в состоянии online=FALSE отключает списки и начинает дозвон

@if(eax==BN_CLICKED)

mov eax,w_param

and eax,0ffffh

@if(eax==1003)

@if(online)

invoke EnableWindow,butt_window,FALSE

call ras_hangup

invoke EnableWindow,butt_window,TRUE

push FALSE

call disable_controls

@else

push TRUE

call disable_controls

call ras_dial

@endif

jmp sp_ok

@endif

@endif

jmp sp_nok

;Обработка сообщения WM_USER, используемого в MyCall нитью монитора разрыва.

;Обнаружив факт разрыва соединения, нить монитора передает WM_USER. Если был коннект,

;то включаются списки, в противном случает выполняется повторный дозвон

@elseif(eax==WM_USER)

call ras_hangup

@if(l_param)

push FALSE

call disable_controls

@else

call ras_dial

@endif

jmp sp_ok

;Обработка сообщения WM_MOVE: запоминание новой позиции окна

;для последующей записи ее в файл mycall.ini

@elseif(eax==WM_MOVE)

invoke GetWindowRect,main_window,offset win_pos

@if(eax)

push win_pos.left

pop main_win_left

push win_pos.top

pop main_win_top

@else

mov eax,l_param

and eax,0ffffh

mov main_win_left,eax

mov eax,l_param

mov cl,16

rcr eax,cl

and eax,0ffffh

mov main_win_top,eax

@endif

jmp sp_ok

;При завершении работы приложения записать файл mycall.ini,

;и освободить память

@elseif(eax==WM_DESTROY)

call save_ini

@if(dat_buffer)

invoke GlobalFree,dat_buffer

@endif

invoke PostQuitMessage,EXIT_NORMAL



jmp sp_ok

; При закрытии главного окна заблокировать кнопку

;и разорвать соединение

@elseif(eax==WM_CLOSE)

invoke EnableWindow,butt_window,FALSE

@if(online)

call ras_hangup

@endif

jmp sp_nok

;Обработка глобального оконного сообщения, используемого для

;взаимодействия экземпляров приложения. Получив это сообщение,

;экземпляр, запущенный первым, сообщает дескриптор своего главного окна.

;Сообщение, полученное вторым, принимает от первого дескриптор его главного окна,

;выводит его на первый план и завершается. Подробнее...

@if(eax==my_message)

mov ebx,w_param

@if(ebx!=main_window)

@if(!l_param)

invoke SendMessageA,HWND_BROADCAST,eax,main_window,1

@else

invoke SetForegroundWindow,w_param

invoke ExitProcess,EXIT_OVERLOADED

@endif

@endif

jmp sp_ok

@endif

;Возврат необработанных сообщений системе

sp_nok:

invoke DefWindowProcA,window_from,message,w_param,l_param

ret

;Завершение оконной процедуры для обработанных сообщений

sp_ok:

xor eax,eax

ret

superprocedure ENDP

;ЗАГРУЗКА ФАЙЛА ИНИЦИАЛИЗАЦИИ

;Файл mycall.ini хранит состояние списков и положение главного окна на момент

;завершения предыдущего сеанса работы приложения

;///////////////////////////////////////////////////////////// Загрузка файла инициализации

.data?

ini_filedd ?

buffer db INI_FILE_LENGTH dup(?)

bytes_readwrite dd ?

.const

ini_file_name db "mycall.ini",0

.code

load_ini PROC

;Инициализация глобальных переменных

mov main_win_left,MAIN_WIN_DEFAULT_LEFT

mov main_win_top,MAIN_WIN_DEFAULT_TOP

mov current_con,0

mov ebx,MAX_CON

@while(ebx)

dec ebx

mov [con_phone+ebx],0

mov [con_user+ebx],0

@endw

;Попытка загрузки файла mycall.ini

;В случае неудачи используются значения по умолчанию

invoke CreateFileA,offset ini_file_name,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL

@if(eax!=INVALID_HANDLE_VALUE)

;Чтение файла mycall.ini

push eax

invoke ReadFile,eax,offset buffer,INI_FILE_LENGTH,offset bytes_readwrite,NULL

@if(eax)

;Приведение позиции окна к фактическим размерам экрана и запоминание



xor edx,edx

mov dx,[word ptr buffer]

push edx

invoke GetSystemMetrics,SM_CXSCREEN

sub eax,10

pop edx

@if(edx<=eax)

mov main_win_left,edx

@endif

xor edx,edx

mov dx,[word ptr buffer+2]

push edx

invoke GetSystemMetrics,SM_CYSCREEN

sub eax,10

pop edx

@if(edx<=eax)

mov main_win_top,edx

@endif

;Запоминание позиций списков телефонов и логинов

xor edx,edx

mov dl,[byte ptr buffer+4]

mov current_con,edx

xor ecx,ecx

xor ebx,ebx

@while(ecx<MAX_CON)

mov ax,[word ptr buffer+5+ebx]

mov [con_phone+ecx],al

mov [con_user+ecx],ah

add ebx,2

inc ecx

@endw

@endif

pop eax

invoke CloseHandle,eax

@endif

ret

load_ini ENDP

;СОХРАНЕНИЕ ФАЙЛА ИНИЦИАЛИЗАЦИИ mycall.ini

; В файле сохраняются положение главного окна на экране и позиции

;списков на момент завершения работы приложения

;============================================================= Сохранение файла инициализации

save_ini PROC

invoke CreateFileA,offset ini_file_name,GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL

@if(eax!=INVALID_HANDLE_VALUE)

push eax

mov eax,main_win_left

mov [word ptr buffer],ax

mov eax,main_win_top

mov [word ptr buffer+2],ax

mov eax,current_con

mov [buffer+4],al

xor ecx,ecx

xor ebx,ebx

@while(ecx<MAX_CON)

mov al,[con_phone+ecx]

mov ah,[con_user+ecx]

mov [word ptr buffer+5+ebx],ax

add ebx,2

inc ecx

@endw

pop eax

push eax

invoke WriteFile,eax,offset buffer,INI_FILE_LENGTH,offset bytes_readwrite,NULL

pop eax

invoke CloseHandle,eax

@endif

ret

save_ini ENDP

;ЗАГРУЗКА ФАЙЛА ДАННЫХ mycall.dat

;Файл содержит имена соединений, телефоны и логины

;///////////////////////////////////////////////////////////// Загрузка файла данных

.data?

dat_file dd ?

dat_file_size dd ?

.const

dat_file_name db "mycall.txt",0

.code

load_data PROC

;Открытие файла, подготовка буфера для него и считывание файла в буфер

invoke CreateFileA,offset dat_file_name,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL

@if(eax==INVALID_HANDLE_VALUE)



mov eax,FATAL_DAT_FILE_OPEN

jmp ld_out

@endif

mov dat_file,eax

invoke GetFileSize,eax,NULL

@if(eax==0ffffffffh)

mov eax,FATAL_DAT_FILE_SIZE

jmp ld_out

@endif

mov dat_file_size,eax

add eax,3

invoke GlobalAlloc,GMEM_FIXED,eax

@if(!eax)

mov eax,FATAL_DAT_BUF_ALLOC

jmp ld_out

@endif

mov dat_buffer,eax

invoke ReadFile,dat_file,eax,dat_file_size,offset bytes_readwrite,NULL

@if(!eax)

mov eax,FATAL_DAT_FILE_READ

jmp ld_out

@endif

;Замена слэшей, CR, LF и пробелов на 0h, добавление в конец 1h

xor ebx,ebx

mov esi,dat_buffer

@while(ebx<dat_file_size)

mov al,byte ptr [esi][ebx]

xor edx,edx

@if(al==0dh)

inc edx

@elseif(al==0ah)

inc edx

@elseif(al==' ')

inc edx

@elseif(al=='/')

inc edx

@endif

@if(edx)

mov byte ptr [esi][ebx],0

@endif

inc ebx

@endw

mov word ptr [esi][ebx],0

add ebx,2

mov byte ptr [esi][ebx],1

xor eax,eax

ld_out:

push eax

invoke CloseHandle,dat_file

pop eax

ret

load_data ENDP

;ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ

;Используются при разборе содержимого буфера данных:

;skip_nz - пропуск всех символов, пока не встретится 00h

;skip_nz2 - пропуск всех символов, пока не встретится 0000h

;///////////////////////////////////////////////////////////// Пропуски

skip_nz PROC

@while(byte ptr[esi]!=0)

inc esi

@endw

nc esi

ret

skip_nz ENDP

;=============================================================

skip_nz2 PROC

@while(word ptr[esi]!=0)

inc esi

@endw

add esi,2

ret

skip_nz2 ENDP

;ИЗМЕНЕНИЕ СОЕДИНЕНИЯ

;Функция вызывается при выборе в списке соединений нового соединения

;///////////////////////////////////////////////////////////// Изменение соединения

change_con PROC

;Установка указателя на запись текущего соединения

xor edx,edx ;sub_string

xor ecx,ecx ;cur_con

mov esi,dat_buffer

@while(byte ptr[esi]!=1)

@if(ecx==current_con)

@break

@endif

@if(word ptr[esi]==0)

inc edx

@if(edx==3)

inc ecx

xor edx,edx

@endif

inc esi

@endif

inc esi

@endw

call skip_nz2

;Формирование списка телефонов для данного соединения и установка его позиции



push esi

invoke SendMessageA,phon_window,CB_RESETCONTENT,0,0

pop esi

xor edx,edx;number

@while(byte ptr[esi]!=0)

@push esi,edx

invoke SendMessageA,phon_window,CB_ADDSTRING,0,esi

@pop esi,edx

inc edx

call skip_nz

@endw

inc esi

mov ebx,current_con

@if(con_phone[ebx]>=dl)

mov byte ptr con_phone[ebx],0

@endif

xor eax,eax

mov al,con_phone[ebx]

invoke SendMessageA,phon_window,CB_SETCURSEL,eax,esi

; Формирование списка логинов для данного соединения и установка его позиции

invoke SendMessageA,user_window,CB_RESETCONTENT,0,0

xor edx,edx

@while(byte ptr[esi]!=0)

@push esi,edx

invoke SendMessageA,user_window,CB_ADDSTRING,0,esi

@pop esi,edx

inc edx

call skip_nz

call skip_nz

@endw

mov ebx,current_con

@if(con_user[ebx]>=dl)

mov byte ptr con_user[ebx],0

@endif

xor eax,eax

mov al,con_user[ebx]

invoke SendMessageA,user_window,CB_SETCURSEL,eax,esi

ret

change_con ENDP

;АВАРИЙНОЕ ЗАВЕРШЕНИЕ ПРИЛОЖЕНИЯ

;Вызывается в случае, когда продолжение работы приложения невозможно

;///////////////////////////////////////////////////////////// Фатальный аборт

.const

fatal_caption db "MyCall Error",0

fatal_txt db "Can't register main window class",0

db "Can't create main window",0

db "Can't open mycall.txt",0

db "Can't get size of mycall.txt",0

db "Can't allocate memory for mycall.txt",0

db "Can't read mycall.txt",0

db "Remote Access Service fatal error",0

.code

fatal PROC fatal_code

mov esi,offset fatal_txt

@while(fatal_code)

@while(byte ptr [esi])

inc esi

@endw

inc esi

dec fatal_code

@endw

invoke MessageBoxA,NULL,esi,offset fatal_caption,MB_OK OR MB_ICONERROR

ret

fatal ENDP

;В(Ы)КЛЮЧЕНИЕ ОРГАНОВ УПРАВЛЕНИЯ

;Меняет надпись на кнопке и активизирует списки в зависимости от того,

;находится приложение в режиме выбора или в режиме соединения

;///////////////////////////////////////////////////////////// Выключение органов управления

.const

ec_txt0 db "Call",0



ec_txt1 db "HangUp",0

.code

disable_controls PROC disable

mov eax,win_height

@if(disable)

add eax,12

@endif

invoke SetWindowPos,main_window,NULL,NULL,NULL,win_width,eax,SWP_NOMOVE OR SWP_NOZORDER

@if(disable)

mov eax,offset ec_txt1

@else

mov eax,offset ec_txt0

@endif

invoke SendMessageA,butt_window,WM_SETTEXT,0,eax

mov eax,disable

xor eax,1h

push eax

invoke EnableWindow,conn_window,eax

pop eax

push eax

invoke EnableWindow,phon_window,eax

pop eax

invoke EnableWindow,user_window,eax

ret

disable_controls ENDP

;ДОЗВОН

;///////////////////////////////////////////////////////////// Дозвон

.data?

ras_dial_params RASDIALPARAMS{}

.code

ras_dial PROC

;Подготовка структуры RASDIALPARAMS

mov ras_dial_params.dwSize,sizeof(RASDIALPARAMS)

mov ras_dial_params.szCallbackNumber,0

mov ras_dial_params.szDomain,0

mov esi,dat_buffer

xor ebx,ebx

@while(ebx!=current_con)

xor ecx,ecx

@while(ecx<3)

call skip_nz2

inc ecx

@endw

inc ebx

@endw

mov ras_dial_params.szEntryName,0

push esi

invoke lstrcpy,offset ras_dial_params.szEntryName,esi

pop esi

call skip_nz

mov ras_dial_params.szPhoneNumber,0

push esi

invoke lstrcpy,offset ras_dial_params.szPhoneNumber,esi

pop esi

call skip_nz

dec esi

@while(byte ptr[esi]==0)

inc esi

@endw

xor ecx,ecx

mov ebx,current_con

@while(con_phone[ebx]!=cl)

call skip_nz

inc ecx

@endw

push esi

invoke lstrcat,offset ras_dial_params.szPhoneNumber,esi

pop esi

call skip_nz2

xor ecx,ecx

mov ebx,current_con

@while(con_user[ebx]!=cl)

call skip_nz

call skip_nz

inc ecx

@endw

mov ras_dial_params.szUserName,0

push esi

invoke lstrcat,offset ras_dial_params.szUserName,esi

pop esi

call skip_nz

mov ras_dial_params.szPassword,0

push esi

invoke lstrcat,offset ras_dial_params.szPassword,esi

pop esi

;Дозвон

mov ras_conn,0

invoke RasDialA,0,0,offset ras_dial_params,0,ras_dial_func,offset ras_conn

@if(eax)

call ras_hangup

invoke fatal,FATAL_RAS

invoke ExitProcess,EXIT_RAS_ERROR

@else

mov online,TRUE



@endif

ret

ras_dial ENDP

;ФУНКЦИЯ КОНТРОЛЯ СОСТОЯНИЯ СОЕДИНЕНИЯ

;============================================================= RAS Callback function

.const

ras_state_text db "OpenPort",0

db "PortOpened",0

db "ConnectDevice",0

db "DeviceConnected",0

db "AllDevicesConnected",0

db "Authenticate",0

db "AuthNotify",0

db "AuthRetry",0

db "AuthCallback",0

db "AuthChangePassword",0

db "AuthProject",0

db "AuthLinkSpeed",0

db "AuthAck",0

db "ReAuthenticate",0

db "Authenticated",0

db "PrepareForCallback",0

db "WaitForModemReset",0

db "WaitForCallback",0

db "Projected",0

db "StartAuthentication",0

db "CallbackComplete",0

db "LogonNetwork",0

db "SubEntryConnected",0

db "SubEntryDisconnected",0

db "Interactive",0

db "RetryAuthentication",0

db "CallbackSetByCaller",0

db "PasswordExpired",0

db "Connected",0

db "Disconnected",0

db "Unknown state",0

.data?

ras_monitor_id dd ?

.code

ras_dial_func PROC PUBLIC type_of_event,rasconnstate,ras_error

push esi

@if(online)

;Показ состояния соединения в строке статуса

mov edx,rasconnstate

@if(edx>=RASCS_Connected)

sub edx,RASCS_Connected-28

@else

@if(edx>=RASCS_Interactive)

sub edx,RASCS_Interactive-24

@endif

@endif

@if(edx>30)

mov edx,30

@endif

push edx

mov esi,offset ras_state_text

@while(edx)

call skip_nz

dec edx

@endw

invoke SendMessageA,stat_window,WM_SETTEXT,0,esi

pop edx

;Если соединение установлено - запуск нити контроля

@if(rasconnstate==RASCS_Connected)

invoke CreateThread,NULL,0,offset ras_monitor_func,0,0,offset ras_monitor_id

; Если соединение не установлено - передача сообщения WM_USER для

;повтора дозвона

@elseif(rasconnstate==RASCS_Disconnected)



push edx

invoke PostMessageA,main_window,WM_USER,0,0

pop edx

@endif

@endif

pop esi

ret

ras_dial_func ENDP

;НИТЬ МОНИТОРА РАЗРЫВА

; Каждые 200 мс опрашивает состояние установленного соединения.

;При обнаружении факта разъединения посылает сообщение WM_USER

;и завершается

;============================================================= Нить монитора разрыва

.data?

ras_connect_status RASCONNSTATUS{}

.code

ras_monitor_func PROC PUBLIC param

rm_loop:

@if(online)

mov ras_connect_status.dwSize,sizeof(RASCONNSTATUS)

invoke RasGetConnectStatusA,ras_conn,offset ras_connect_status

@if(!eax)

@if(ras_connect_status.rasconnstate!=RASCS_Disconnected)

invoke Sleep,200

jmp rm_loop

@endif

@endif

@endif

invoke PostMessageA,main_window,WM_USER,0,1

ret

ras_monitor_func ENDP

;ПОЛОЖИТЬ ТРУБКУ

;Если происходит дозвон, или соединение установлено,

;дает команду на разрыв и ожидает, когда RAS ее выполнит

;///////////////////////////////////////////////////////////// Положить трубку

ras_hangup PROC

mov online,FALSE

@if(ras_conn)

invoke RasHangUpA,ras_conn

rh_loop:

mov ras_connect_status.dwSize,sizeof(RASCONNSTATUS)

invoke RasGetConnectStatusA,ras_conn,offset ras_connect_status

@if(eax!=ERROR_INVALID_HANDLE)

invoke Sleep,0

jmp rh_loop

@endif

@endif

mov ras_conn,0

invoke Sleep,1000

ret

ras_hangup ENDP

br>

;#############################################################

end


Main.cpp для mycall (c++)


Это основной файл приложения MyCall на C++. Этот файл в текстовом формате вместе со всеми остальными файлами, необходимыми для компиляции приложения MyCall, содержится в zip-файле mycallcb.zip (13192 байта). Имеется также Инструкция программиста.

Для получения комментариев щелкaйте по тексту или пользуйтесь групповым управлением:

if(dhtml){document.write("Все комментарии: [+][-]    Открывать: [несколько]");}

//Включение заголовочных файлов

//Здесь windows.h и ras.h - стандартные из пакета MS Visual C++,

//а main.h - заголовочный файл приложения MyCall

#include "windows.h"

#include "ras.h"

#include "main.h"

//ГЛАВНАЯ ФУНКЦИЯ ПРИЛОЖЕНИЯ

/////////////////////////////////////////////////////////////// WinMain

int WINAPI WinMain(HINSTANCE hinst,HINSTANCE prev_hinst,LPSTR command_line,int cmd_show){

//Получение дескриптора экземпляра приложения

//Необходимое действие, так как предполагается компиляция приложения без

//подключения runtime-библиотеки. Подробнее...

hinst=GetModuleHandle(NULL);

//Создание главного окна

//Обычное действие, с которого начинаются большинство приложений.

//Единственное отличие в том, что в качестве главного окна в MyCall

//используется окно диалога, описанного в файле ресурсов

//Регистрируется класс главного окна:

WNDCLASSEX mw_class;

mw_class.cbSize=sizeof(WNDCLASSEX);

mw_class.style=NULL;

mw_class.lpfnWndProc=superprocedure;

mw_class.cbClsExtra=0;

mw_class.cbWndExtra=DLGWINDOWEXTRA;

mw_class.hInstance=hinst;

mw_class.hIcon=LoadIcon(hinst,MAKEINTRESOURCE(103));

mw_class.hIconSm=NULL;

mw_class.hCursor=LoadCursor(NULL,IDC_ARROW);

mw_class.hbrBackground=(HBRUSH)COLOR_WINDOW;

mw_class.lpszMenuName=NULL;

mw_class.lpszClassName="MainWindowClass";

if(!RegisterClassEx(&mw_class)){fatal(FATAL_MAIN_CLASS_REG);return EXIT_COMMON_ERROR;}

//Главное окно создается:

if(!(main_window=CreateDialog(hinst,MAKEINTRESOURCE(101),NULL,NULL))){fatal(FATAL_MAIN_CLASS_CREATE);return EXIT_COMMON_ERROR;}


// По состоянию списка conn_window формируются остальные списки

change_con();//

//ЦИКЛ ОЖИДАНИЯ СООБЩЕНИЙ

//Неотъемлемый элемент приложений для Windows. В MyCall никаких особенностей не имеет

MSG loop_message;

while (GetMessage(&loop_message,NULL,0,0)){TranslateMessage(&loop_message);DispatchMessage(&loop_message);}

//ЗАВЕРШЕНИЕ РАБОТЫ ПРИЛОЖЕНИЯ

//Поскольку MyCall собирается без runtime-библиотеки, для завершения работы

//обязательно использовать функцию ExitProcess, при этом оператор return оказывается

//недостижимым, но необходим по требованиям синтаксиса. Подробнее...

ExitProcess(loop_message.wParam);

return 0;

}

//ОКОННАЯ ПРОЦЕДУРА

//Стандартный элемент приложений для Windows. В MyCall особенностей не имеет.

/////////////////////////////////////////////////////////////// Оконная процедура

LRESULT CALLBACK superprocedure(HWND window_from,UINT message,WPARAM w_param,LPARAM l_param){

//Разбор и обработка оконных сообщений

switch(message){

//Обработка сообщения WM_COMMAND, передаваемого элементами управления диалога

case WM_COMMAND:

//Обработка уведомления CBN_SELCHANGE, передаваемого списками при изменении позиции:

//1000: список соединений conn_window. Изменяет содержание phon_window и user_window

//1001: список телефонов phon_window. Устанавливает новый телефон для текущего соединения con_phone[current_con]

//1002: список логинов user_window. Устанавливает новый логин для текущего соединения con_user[current_con]

if(HIWORD(w_param)==CBN_SELCHANGE){

switch (LOWORD(w_param)){

case 1000:

current_con=SendMessage(conn_window,CB_GETCURSEL,0,0);

if(current_con==CB_ERR){current_con=0;}

change_con();

return 0;

case 1001:

con_phone[current_con]=SendMessage(phon_window,CB_GETCURSEL,0,0);

if(con_phone[current_con]==CB_ERR){con_phone[current_con]=0;}

return 0;

case 1002:

con_user[current_con]=SendMessage(user_window,CB_GETCURSEL,0,0);

if(con_user[current_con]==CB_ERR){con_user[current_con]=0;}

return 0;

default:

break;



}

return 0;

}

// Обработка уведомления BN_CLICKED, передаваемого кнопкой при клике:

//в состоянии online=TRUE прекращает дозвон (разрывает соединение) и включает списки

//в состоянии online=FALSE отключает списки и начинает дозвон

if((HIWORD(w_param)==BN_CLICKED)&&(LOWORD(w_param))==1003){

if(online){

EnableWindow(butt_window,FALSE);

ras_hangup();

EnableWindow(butt_window,TRUE);

disable_controls(FALSE);

}else{

disable_controls(TRUE);

ras_dial();

}

return 0;

}

break;

//Обработка сообщения WM_USER, используемого в MyCall нитью монитора разрыва.

//Обнаружив факт разрыва соединения, нить монитора передает WM_USER. Если был коннект,

//то включаются списки, в противном случает выполняется повторный дозвон

case WM_USER:

ras_hangup();

l_param?disable_controls(FALSE):ras_dial();

return 0;

//Обработка сообщения WM_MOVE: запоминание новой позиции окна

//для последующей записи ее в файл mycall.ini

case WM_MOVE:

RECT win_pos;

if(GetWindowRect(main_window,&win_pos)){

main_win_left=win_pos.left;

main_win_top=win_pos.top;

/div>

}else{

main_win_left=(int)LOWORD(l_param);

main_win_top=(int)HIWORD(l_param);

}

return 0;

//При завершении работы приложения записать файл mycall.ini,

//и освободить память

case WM_DESTROY:

save_ini();

if(dat_buffer){GlobalFree(dat_buffer);}

PostQuitMessage(EXIT_NORMAL);

return 0;

//При закрытии главного окна заблокировать кнопку

//и разорвать соединение

case WM_CLOSE:

EnableWindow(butt_window,FALSE);

if(online){ras_hangup();}

break;

}

//Обработка глобального оконного сообщения, используемого для

//взаимодействия экземпляров приложения. Получив это сообщение,

//экземпляр, запущенный первым, сообщает дескриптор своего главного окна.

//Сообщение, полученное вторым, принимает от первого дескриптор его главного окна,

//выводит его на первый план и завершается. Подробнее...

if(message==my_message){

if((HWND)w_param!=main_window){

if(l_param==0){

SendMessage(HWND_BROADCAST,my_message,(WPARAM)main_window,1);



}else{

SetForegroundWindow((HWND)w_param);

ExitProcess(EXIT_OVERLOADED);

}

}

return 0;

}

//Возврат необработанных сообщений системе

return DefWindowProc(window_from,message,w_param,l_param);

}

//ЗАГРУЗКА ФАЙЛА ИНИЦИАЛИЗАЦИИ

//Файл mycall.ini хранит состояние списков и положение главного окна на момент

//завершения предыдущего сеанса работы приложения

/////////////////////////////////////////////////////////////// Загрузка файла инициализации

void load_ini(){

//Инициализация глобальных переменных

main_win_left=MAIN_WIN_DEFAULT_LEFT;

main_win_top=MAIN_WIN_DEFAULT_TOP;

current_con=0;

for(int i=0;i<MAX_CON;i++){

con_phone[i]=0;

con_user[i]=0;

}

//Попытка загрузки файла mycall.ini

//В случае неудачи используются значения по умолчанию

HANDLE ini_file=CreateFile(INI_FILE,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

if(ini_file!=INVALID_HANDLE_VALUE){

//Чтение файла mycall.ini

BYTE buffer[INI_FILE_LENGTH];

DWORD bytes_read;

if(ReadFile(ini_file,buffer,INI_FILE_LENGTH,&bytes_read,NULL)){

//Приведение позиции окна к фактическим размерам экрана и запоминание

main_win_left=(((INT)buffer[1])<<8)|((INT)buffer[0]);

if(main_win_left>=GetSystemMetrics(SM_CXSCREEN)-10){main_win_left=MAIN_WIN_DEFAULT_LEFT;}

main_win_top=(((INT)buffer[3])<<8)|((INT)buffer[2]);

if(main_win_top>=GetSystemMetrics(SM_CYSCREEN)-10){main_win_top=MAIN_WIN_DEFAULT_TOP;}

//Запоминание позиций списков телефонов и логинов

current_con=(INT)buffer[4];

for(int i=0;i<MAX_CON;i++){

con_phone[i]=(INT)buffer[(i<<1)+5];

con_user[i]=(INT)buffer[(i<<1)+6];

}

}

CloseHandle(ini_file);

}

}

//СОХРАНЕНИЕ ФАЙЛА ИНИЦИАЛИЗАЦИИ mycall.ini

//В файле сохраняются положение главного окна на экране и позиции

//списков на момент завершения работы приложения

/////////////////////////////////////////////////////////////// Сохранение файла инициализации

void save_ini(){

HANDLE ini_file=CreateFile(INI_FILE,GENERIC_WRITE,0,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);



if(ini_file!=INVALID_HANDLE_VALUE){

BYTE buffer[INI_FILE_LENGTH];

buffer[0]=(BYTE)main_win_left;

buffer[1]=(BYTE)(main_win_left>>8);

buffer[2]=(BYTE)main_win_top;

buffer[3]=(BYTE)(main_win_top>>8);

buffer[4]=(BYTE)current_con;

for(int i=0;i<MAX_CON;i++){

buffer[(i<<1)+5]=(BYTE)con_phone[i];

buffer[(i<<1)+6]=(BYTE)con_user[i];

}

DWORD bytes_written;

WriteFile(ini_file,buffer,INI_FILE_LENGTH,&bytes_written,NULL);

CloseHandle(ini_file);

}

}

//ЗАГРУЗКА ФАЙЛА ДАННЫХ mycall.dat

// Файл содержит имена соединений, телефоны и логины

/////////////////////////////////////////////////////////////// Загрузка файла данных

BOOL load_data(){

//Открытие файла, подготовка буфера для него и считывание файла в буфер

HANDLE dat_file=CreateFile(DAT_FILE,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

if(dat_file==INVALID_HANDLE_VALUE){fatal(FATAL_DAT_FILE_OPEN);return FALSE;}

DWORD dat_file_size=GetFileSize(dat_file,NULL);

if(dat_file_size==0xffffffff){CloseHandle(dat_file);fatal(FATAL_DAT_FILE_SIZE);return FALSE;}

dat_buffer=(LPBYTE)GlobalAlloc(GMEM_FIXED,dat_file_size+3);

if(dat_buffer==NULL){CloseHandle(dat_file);fatal(FATAL_DAT_BUF_ALLOC);return FALSE;}

DWORD bytes_read;

if(!ReadFile(dat_file,dat_buffer,dat_file_size,&bytes_read,NULL)){CloseHandle(dat_file);fatal(FATAL_DAT_FILE_READ);return FALSE;}

CloseHandle(dat_file);

//Замена слэшей, CR, LF и пробелов на 0h, добавление в конец 1h

ptr=dat_buffer;

for(DWORD i=0;i<dat_file_size;i++){

if((*ptr==0xd)||(*ptr==0xa)||(*ptr=='/')||(*ptr==' ')){*ptr=0;}

ptr++;}

dat_buffer[i]=0;dat_buffer[i+1]=0;dat_buffer[i+2]=1;

return TRUE;

}

//ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ

//Используются при разборе содержимого буфера данных:

//skip_nz - пропуск всех символов, пока не встретится 00h

//skip_nz2 - пропуск всех символов, пока не встретится 0000h

/////////////////////////////////////////////////////////////// Пропуски

VOID skip_nz(){

while(*ptr!=0){ptr++;}

ptr++;



}

VOID skip_nz2(){

while(*(LPWORD)ptr!=0){ptr++;}

ptr+=2;

}

//ИЗМЕНЕНИЕ СОЕДИНЕНИЯ

//Функция вызывается при выборе в списке соединений нового соединения

/////////////////////////////////////////////////////////////// Изменение соединения

VOID change_con(){

// Установка указателя на запись текущего соединения

INT sub_string=0;

INT cur_con=0;

ptr=dat_buffer;

while(*ptr!=1){

if(cur_con==current_con){break;}

if(*(LPWORD)ptr==0){

if(++sub_string==3){cur_con++;sub_string=0;}

ptr++;

}

ptr++;

}

skip_nz2();

//Формирование списка телефонов для данного соединения и установка его позиции

SendMessage(phon_window,CB_RESETCONTENT,0,0);

int number=0;

while(*ptr!=0){

SendMessage(phon_window,CB_ADDSTRING,0,(LPARAM)ptr);

number++;

skip_nz();

}

ptr++;

if(con_phone[current_con]>=number){con_phone[current_con]=0;}

SendMessage(phon_window,CB_SETCURSEL,(WPARAM)con_phone[current_con],0);

//Формирование списка логинов для данного соединения и установка его позиции

SendMessage(user_window,CB_RESETCONTENT,0,0);

number=0;

while(*ptr!=0){

SendMessage(user_window,CB_ADDSTRING,0,(LPARAM)ptr);

number++;

skip_nz();

skip_nz();

}

if(con_user[current_con]>=number){con_user[current_con]=0;}

SendMessage(user_window,CB_SETCURSEL,(WPARAM)con_user[current_con],0);

}

//АВАРИЙНОЕ ЗАВЕРШЕНИЕ ПРИЛОЖЕНИЯ

//Вызывается в случае, когда продолжение работы приложения невозможно

/////////////////////////////////////////////////////////////// Фатальный аборт

void fatal(int fatal_code){

LPCSTR fatal_text[]={

"Can't register main window class",

"Can't create main window",

"Can't open mycall.txt",

"Can't get size of mycall.txt",

"Can't allocate memory for mycall.txt",

"Can't read mycall.txt",

"Remote Access Service fatal error"};

MessageBox(NULL,fatal_text[fatal_code],"MyCall Error",MB_OK|MB_ICONERROR);

}

//В(Ы)КЛЮЧЕНИЕ ОРГАНОВ УПРАВЛЕНИЯ

//Меняет надпись на кнопке и активизирует списки в зависимости от того,



//находится приложение в режиме выбора или в режиме соединения

/////////////////////////////////////////////////////////////// В(ы)ключение органов управления

VOID disable_controls(BOOL disable){

SetWindowPos(main_window,NULL,NULL,NULL,win_width,win_height+(disable?12:0),SWP_NOMOVE|SWP_NOZORDER);

SendMessage(butt_window,WM_SETTEXT,0,(LPARAM)(disable?"HangUp":"Call"));

EnableWindow(conn_window,!disable);

EnableWindow(phon_window,!disable);

EnableWindow(user_window,!disable);

}

//ДОЗВОН

/////////////////////////////////////////////////////////////// Дозвон

VOID ras_dial(){

//Подготовка структуры RASDIALPARAMS

RASDIALPARAMS ras_dial_params;

ras_dial_params.dwSize=sizeof(RASDIALPARAMS);

ras_dial_params.szCallbackNumber[0]=0;

ras_dial_params.szDomain[0]=0;

ptr=dat_buffer;

for(int j=0;j!=current_con;j++){for(int k=0;k<3;k++){skip_nz2();}}

ras_dial_params.szEntryName[0]=0;

lstrcpy(ras_dial_params.szEntryName,(LPSTR)ptr);

skip_nz();

ras_dial_params.szPhoneNumber[0]=0;

lstrcpy(ras_dial_params.szPhoneNumber,(LPSTR)ptr);

skip_nz();ptr--;

while(*ptr==0){ptr++;}

for(j=0;j!=con_phone[current_con];j++){skip_nz();}

lstrcat(ras_dial_params.szPhoneNumber,(LPSTR)ptr);

skip_nz2();

for(j=0;j!=con_user[current_con];j++){skip_nz();skip_nz();}

ras_dial_params.szUserName[0]=0;

lstrcpy(ras_dial_params.szUserName,(LPSTR)ptr);

skip_nz();

ras_dial_params.szPassword[0]=0;

lstrcpy(ras_dial_params.szPassword,(LPSTR)ptr);

//Дозвон

ras_conn=NULL;

if(RasDial(0,0,&ras_dial_params,0,ras_dial_func,&ras_conn)){

ras_hangup();

fatal(FATAL_RAS);

ExitProcess(EXIT_RAS_ERROR);

}else{

online=TRUE;

}

}

//ФУНКЦИЯ КОНТРОЛЯ СОСТОЯНИЯ СОЕДИНЕНИЯ

/////////////////////////////////////////////////////////////// Контроль состояния соединения

VOID WINAPI ras_dial_func(UINT msg,RASCONNSTATE rasconnstate,DWORD error){

LPCSTR ras_state_text[]={

"OpenPort",

"PortOpened",

"ConnectDevice",

"DeviceConnected",



"AllDevicesConnected",

"Authenticate",

"AuthNotify",

"AuthRetry",

"AuthCallback",

"AuthChangePassword",

"AuthProject",

"AuthLinkSpeed",

"AuthAck",

"ReAuthenticate",

"Authenticated",

"PrepareForCallback",

"WaitForModemReset",

"WaitForCallback",

"Projected",

"StartAuthentication",

"CallbackComplete",

"LogonNetwork",

"SubEntryConnected",

"SubEntryDisconnected",

"Interactive",

"RetryAuthentication",

"CallbackSetByCaller",

"PasswordExpired",

"Connected",

"Disconnected",

"Unknown state"};

if(online){

//Показ состояния соединения в строке статуса

UINT ras_message_num=rasconnstate;

if(ras_message_num>=RASCS_Connected){ras_message_num-=RASCS_Connected-28;}

else{if(ras_message_num>=RASCS_Interactive){ras_message_num-=RASCS_Interactive-24;}}

if(ras_message_num>30){ras_message_num=30;}

SendMessage(stat_window,WM_SETTEXT,0,(LPARAM)ras_state_text[ras_message_num]);

//Если соединение установлено - запуск нити контроля

if(rasconnstate==RASCS_Connected){

CreateThread(NULL,0,ras_monitor_func,0,0,&ras_monitor_id);

//Если соединение не установлено - передача сообщения WM_USER для

//повтора дозвона

}else{

if(rasconnstate==RASCS_Disconnected){

PostMessage(main_window,WM_USER,0,0);

}

}

}

}

//НИТЬ МОНИТОРА РАЗРЫВА

// Каждые 200 мс опрашивает состояние установленного соединения.

//При обнаружении факта разъединения посылает сообщение WM_USER

//и завершается

//============================================================= Нить монитора разрыва

DWORD WINAPI ras_monitor_func(LPVOID param){

RASCONNSTATUS ras_connect_status;

while(online){

ras_connect_status.dwSize=sizeof(RASCONNSTATUS);

if(RasGetConnectStatus(ras_conn,&ras_connect_status)){break;}

if(ras_connect_status.rasconnstate==RASCS_Disconnected){break;}

Sleep(200);

}

PostMessage(main_window,WM_USER,0,1);

return 0;

}

//ПОЛОЖИТЬ ТРУБКУ

//Если происходит дозвон, или соединение установлено,

//дает команду на разрыв и ожидает, когда RAS ее выполнит

/////////////////////////////////////////////////////////////// Положить трубку

VOID ras_hangup(){

RASCONNSTATUS ras_connect_status;

online=FALSE;

if(ras_conn){

RasHangUp(ras_conn);

while(TRUE){

ras_connect_status.dwSize=sizeof(RASCONNSTATUS);

if(RasGetConnectStatus(ras_conn,&ras_connect_status)==ERROR_INVALID_HANDLE){break;}

Sleep(0);

}

ras_conn=0;

Sleep(1000);

}

}


Main.h для mycall (c++)


Это файл заголовков для приложения MyCall на C++. Этот файл в текстовом формате вместе со всеми остальными файлами, необходимыми для компиляции приложения MyCall, содержится в zip-файле mycallcb.zip (13192 байта). Имеется также Инструкция программиста.

Для получения комментариев щелкaйте по тексту или пользуйтесь групповым управлением:

if(dhtml){document.write("Все комментарии: [+][-]    Открывать: [несколько]");}

//Положение главного окна на экране по умолчанию (когда mycall.ini недоступен)

#define MAIN_WIN_DEFAULT_LEFT 100

#define MAIN_WIN_DEFAULT_TOP 100

//Максимальное количество позиций в списках

#define MAX_CON 16

//Размер файла mycall.ini

#define INI_FILE_LENGTH 4+1+MAX_CON*2

//Коды выхода приложения:

//EXIT_NORMAL: нормальное завершение работы

//EXIT_COMMON_ERROR: фатальная ошибка

//EXIT_OVERLOADED: экземпляр приложения уже существует

//EXIT_RAS_ERROR: ошибка Remote Access Service

#define EXIT_NORMAL 0

#define EXIT_COMMON_ERROR 1

#define EXIT_OVERLOADED 2

#define EXIT_RAS_ERROR 3

//Коды фатальных ошибок

#define FATAL_MAIN_CLASS_REG 0

#define FATAL_MAIN_CLASS_CREATE 1

#define FATAL_DAT_FILE_OPEN 2

#define FATAL_DAT_FILE_SIZE 3

#define FATAL_DAT_BUF_ALLOC 4

#define FATAL_DAT_FILE_READ 5

#define FATAL_RAS 6

//Имена используемых файлов

#define INI_FILE "mycall.ini"

#define DAT_FILE "mycall.txt"

//Объявления функций

LRESULT CALLBACK superprocedure (HWND,UINT,WPARAM,LPARAM);

VOID load_ini();

VOID save_ini();

BOOL load_data();

VOID change_con();

VOID fatal(INT);

VOID disable_controls(BOOL);

VOID ras_dial();

VOID ras_hangup();

VOID ras_control();

VOID WINAPI ras_dial_func(UINT,RASCONNSTATE,DWORD);

DWORD WINAPI ras_monitor_func(LPVOID);

//Дескрипторы окон и элементов управления

HWND main_window;

HWND conn_window;

HWND phon_window;

HWND user_window;

HWND butt_window;

HWND stat_window;

//Положение и размеры главного окна

INT main_win_left;

INT main_win_top;

LONG win_width;

LONG win_height;

//Текущее соединение

INT current_con;

//Массивы телефонов и логинов для текущего соещинения

INT con_phone[MAX_CON];

INT con_user[MAX_CON];

//Дескриптор установленного соединения

HRASCONN ras_conn;

//Идентификатор нити монитора разрыва

DWORD ras_monitor_id;

//Код регистрируемого глобального оконного сообщения "MyCallMessage"

UINT my_message;

//Флаг состояния приложения:

//FALSE: выбор соединения пользователем

//TRUE: выполняется дозвон или поддерживается коннект

BOOL online=FALSE;

//Буфер для mycall.txt

LPBYTE dat_buffer=0;

//Указатель внутри этого буфера

LPBYTE ptr;



Main.inc для mycall (ассемблер)


Это файл заголовков для приложения MyCall на ассемблере. Этот файл в текстовом формате вместе со всеми остальными файлами, необходимыми для компиляции приложения MyCall, содержится в zip-файле mycallab.zip (15913 байт). Имеется также Инструкция программиста.

Для получения комментариев щелкaйте по тексту или пользуйтесь групповым управлением:

if(dhtml){document.write("Все комментарии: [+][-]    Открывать: [несколько]");}

;КОНСТАНТЫ

//Положение главного окна на экране по умолчанию (когда mycall.ini недоступен)

MAIN_WIN_DEFAULT_LEFT=100

MAIN_WIN_DEFAULT_TOP=100

//Максимальное количество позиций в списках

MAX_CON=16

//Размер файла mycall.ini

INI_FILE_LENGTH=4+1+MAX_CON*2

//Коды выхода приложения:

//EXIT_NORMAL: нормальное завершение работы

//EXIT_COMMON_ERROR: фатальная ошибка

//EXIT_OVERLOADED: экземпляр приложения уже существует

//EXIT_RAS_ERROR: ошибка Remote Access Service

EXIT_NORMAL=0

EXIT_COMMON_ERROR=1

EXIT_OVERLOADED=2

EXIT_RAS_ERROR=3

//Коды фатальных ошибок

FATAL_MAIN_CLASS_REG=0

FATAL_MAIN_CLASS_CREATE=1

FATAL_DAT_FILE_OPEN=2

FATAL_DAT_FILE_SIZE=3

FATAL_DAT_BUF_ALLOC=4

FATAL_DAT_FILE_READ=5

FATAL_RAS=6

;ПРОТОТИПЫ ПОЛЬЗОВАТЕЛЬСКИХ ПРОЦЕДУР

;Необходимы в случаях, когда процедура вызывается оператором INVOKE,

;или когда ее адрес передается системной функции API в качестве параметра

fatal PROTO :DWORD

ras_dial_func PROTO :DWORD,:DWORD,:DWORD

ras_monitor_func PROTO :DWORD

;ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ

//Дескрипторы окон и элементов управления

main_window dd ?

conn_window dd ?

phon_window dd ?

user_window dd ?

butt_window dd ?

stat_window dd ?

//Положение и размеры главного окна

main_win_left dd ?

main_win_top dd ?

win_width dd ?

win_height dd ?

//Текущее соединение

current_con dd ?

//Массивы телефонов и логинов для текущего соещинения

con_phone db MAX_CON dup(?)

con_user db MAX_CON dup(?)

//Дескриптор установленного соединения

ras_conn dd ?

//Код регистрируемого глобального оконного сообщения "MyCallMessage"

my_message dd ?

//Флаг состояния приложения:

//FALSE: выбор соединения пользователем

//TRUE: выполняется дозвон или поддерживается коннект

online dd ?

//Буфер для mycall.txt

dat_buffer dd ?



Минимальная stub-программа


"Stub" в переводе с английского - "пень, обломок, огрызок". Это в литературном переводе. В нелитературный перевод углубляться не будем. Скажем только, что это то самое, что показывает компьютер человеку, попытавшемуся запустить из-под DOS приложение, написанное для Windows. Обычно он показывает сообщение: "This program cannot be run in DOS mode". А вы что подумали?

В связи с понятием "stub-программа" обычно возникают три вопроса:

Как она работает? Как сделать собственную stub-программу? Как уменьшить ее до размера, не раздражающего настоящего ассемблерщика?

Как она работает? Чтобы ответить на этот вопрос, следует заглянуть во внутренности формата PE-файла - стандартного исполняемого модуля Windows. Там мы обнаружим следующее:

PE-файл содержит в себе две программы, склеенные друг вслед за дружкой. Первая из них и есть stub-программа в обычном для MS-DOS exe-формате. Вторая - приложение для Windows. stub-программа, как и положено в MS-DOS, начинается в файле с нулевого смещения и первые два байта представляют собой знаменитые инициалы MZ, коими увековечил себя кто-то из корифеев Microsoft (не могу навскидку вспомнить. Кто знает - напишите) (16.05.01 написали. Михаил Орлов сообщил, что это был Mark Zbikovski. Спасибо им обоим.) Можно сказать, вырезал свое имя на каждом пне. С этого имени начинается стандартный заголовок exe-файла MS-DOS. windows-программа начинается с некоторого смещения, кратного параграфу (16 байт) и зависящему от размера stub-программы. Первые два байта windows-программы - это инициалы какого-то другого мужика со странным именем Portable Executable. Далее следует заголовок PE-файла и все остальные его элементы.

PE-формат - не единственный формат исполняемых модулей, который понимает Windows. Но мы не станем здесь рассматривать другие форматы, так как применительно к теме статьи это не имеет смысла.

Глупая DOS загружает exe-файл, обнаруживает в его начале привычный для себя заголовок и исполняет stub-программу, ничего не зная о наличии в файле еще чего-то, кроме слов "а пошел ты туда-то и туда-то".


Подозреваем, что не все компоновщики поддерживают эту опцию. Однако link.exe из состава MS Developer Studio, конечно же, поддерживает.

Написанную stub-программу используйте на этапе компоновки своего Windows-приложения. Для этого в командную строку компоновщика добавьте опцию /STUB:"filename.exe", где filename.exe, как вы можете догадаться - это имя вашей stub-программы, при необходимости с путем. Обнаружив эту опцию, компоновщик заменит стандартную stub-программу на вашу.

Здесь настоящего ассемблерщика подстерегает серьезное разочарование. Проблема в следующем. Обычно при компоновке exe-программ для DOS в исполняемом файле резервируется довольно большое место под таблицу перемещения. Например, в порядке вещей, если таблица перемещения вольготно располагается между смещениями 1ch (конец заголовка) и 200h (начало сегмента кода). Даже если ваша stub-программа не содержит ни одного фрагмента, подлежащего перемещению, все равно 486 байт чистых нулей будет бессмысленно вбацано в нежное девственное тело вашего Windows-приложения. Задачка, стоит ли избегать такого варварского разбазаривания дискового пространства и как это сделать, предлагается для самостоятельного решения. Кое-какие идеи читайте далее.

Минимальная stub-программа. Заглянув внутрь исполняемого модуля какого-нибудь Windows-приложения, вы обнаружите, что стандартная stub-программа чаще всего занимает 128 байт. Можно ли уменьшить этот размер и до какой величины? Отвечаем: можно, до 64 байт.

Возьмите любой редактор бинарных файлов и создайте с его помощью вот такой файл:

000000   4D 5A 00 00 01 00 00 00  02 00 00 00 FF FF 00 00

000010   40 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00

000020   B4 4C CD 21 00 00 00 00  00 00 00 00 00 00 00 00

000030   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

Присвойте ему имя, например, stub.exe и используйте в качестве stub-программы. А если лень набивать самому - можете скачать его отсюда.



Вот расшифровка содержимого файла stub.exe с комментариями:

смещение значение назначение комментарий
+0 5A4D Подпись exe-файла ('MZ')  
+2 0000 Длина последней неполной страницы образа, байт Игнорируется операционной системой
+4 0001 Длина образа, страниц (страница = 512 байт) Программа занимает менее одной страницы
+6 0000 Число элементов в таблице перемещения В этой программе перемещаемых элементов нет
+8 0002 Размер exe-заголовка, параграфов (параграф = 16 байт) Указывается размер базовой части заголовка. С учетом остальных значений параметров в данном случае означает, что исполняемый код в файле начинается со смещения 20h, и стартовый адрес находится в начале исполняемого кода
+0ah 0000 Минимум требуемой памяти за концом программы (параграфов) В данном случае смысла не имеет
+0ch FFFF Максимум требуемой памяти за концом программы (параграфов) Традиционно отводится вся доступная память
+0eh 0000 Сегментное смещение сегмента стека (для установки регистра ss)  
+10h 0040 Значение регистра sp (указателя стека) при запуске В этой программе стек не имеет значения
+12h 0000 Контрольная сумма исполняемого модуля Не используется
+14h 0000 Значение регистра ip (указателя команд) при запуске Стартовая точка совпадает с началом кодового сегмента
+16h 0000 Cегментное смещение кодового сегмента (для установки регистра cs)  
+18h 0040 Cмещение в файле 1-го элемента перемещения В этой программе ни одного элемента перемещения нет, а исполняемый код находится внутри заголовка, поэтому данное значение совпадает с концом программы
+1ah 0000 Номер оверлея Здесь не используется
+1eh 0000
0000
Резерв - 4 байта  
+20h 4CB4
21CD
Исполняемый код:
mov ah,4ch
int 21h
В данном случае программа просто возвращает управление операционной системе. Максимальный размер исполняемого кода для данных значений заголовка - 28 байт (смещения 20h...3bh). Обратите внимание, что исполняемый код с целью экономии размера программы находится в области, отведенной под exe-заголовок!
+3ch 0000
0000
Зарезервированное двойное слово Используется компоновщиком Windows-приложения для размещения смещения PE-заголовка
<


Вероятно, уменьшить размер stub- программы до величины, меньшей 64 байт, невозможно. А вот интересный вопрос: возможно ли вообще изъять stub-программу из модуля? Не спешите отвечать "нет"! 16-битные компоновщики от Microsoft имели в синтаксисе def-файла конструкцию вида STUB NONE, которая, согласно документации, предназначалась для использования при сборке dll-библиотек. В 32-битных компоновщиках эта возможность пропала. А жаль...

Вот уж в самом деле, "никогда не говори "никогда"! 24 июля 2001 года, в день падения 114-летнего рекорда жары в Москве, пришло письмо:

На вашем сайте assembler.ru в статье "Минимальная stub-программа" автором упомянуто, что создать stub короче 40h байт, вероятнее всего, невозможно.

Привожу пример stub'а длиной в 20h байт:

00000000 4D 5A 00 00 01 00 00 00 01 00 00 00 FF FF 00 00 MZ..............

00000010 B4 4C CD 21 00 00 00 00 40 00 00 00 00 00 00 00 .L.!....@.......

В отличие от примера, приведенного в статье, размер заголовка уменьшен до одного параграфа. В терминах статьи это означает, что stub-код располагается по смещению 10h от начала файла.

Очевидно, после такого "сокращения" области заголовка от 10h и выше продолжают использоваться. Но в данном случае это неважно, так как на месте 4CB4h находится initial SP, а на месте 21CDh - checksum. Ни то, ни другое роли не играют. К сожалению, stub-код ограничен в 4 байта, потому что уже следующий word - initial IP.

Далее, в файле со stub'ом в 20h байт смещение 3Ch от начала файла приходится на смещение 1Ch PE-заголовка. Здесь расположен dword, который, по-видимому, ни на что не влияет, так что можно спокойно ставить туда 00000020h:

00000020 50 45 00 00 4C 01 07 00 21 B1 97 36 00 00 00 00 PE..L...!..6....

00000030 00 00 00 00 E0 00 0E 01 0B 01 04 14 20 00 00 00 ............ ...

Такую штуку вряд ли поддерживают компоновщики (мне от них этого добиться не удалось), я лично правил EXE вручную. Зато работает.

Напоследок - пример тоже рабочего stub'а длиной в 18h байт - меньше, по-моему, некуда:

00000000 4D 5A 00 00 01 00 00 00 00 00 00 00 B0 21 CD 29 MZ...........!.)

00000010 B4 4C CD 21 0C 00 00 00 50 45 00 00 4C 01 07 00 .L.!....PE..L...

00000020 21 B1 97 36 00 00 00 00 00 00 00 00 E0 00 0E 01 !..6............

00000030 0B 01 04 14 20 00 00 00 00 2E 00 00 18 00 00 00 .... ...........

Из-под DOS он выводит восклицательный знак (21h).

Ну как, размер достоин настоящего ассемблерщика?

автор - Grief (soul_inspector@chat.ru)
<


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

Когда делается попытка запустить программу из-под DOS, загрузчик, определив по сигнатуре "MZ", что это исполняемый файл, считывает по смещению 08h размер exe-заголовка, а по смещению 14h стартовое значение регистра ip, чтобы узнать, где находится исполняемый код. В предложенном Grief'ом варианте stub'а длиной 20h он таким образом выясняет, что передавать управление следует на смещение 10h (просто выход в DOS посредством функции 4Ch), а в варианте длиной 18h - на смещение 0Ch (здесь перед выходом успевает еще напечататься восклицательный знак). И неважно, что занятые исполняемым кодом байты на самом деле являются полями exe-заголовка "Максимум требуемой памяти за концом программы", "Сегментное смещение сегмента стека", "Значение регистра sp" и "Контрольная сумма исполняемого модуля". Все эти поля в данном случае не влияют работу программы. Если же программа запускается из-под Windows,то загрузчик, опять же разобравшись с назначением файла, первым делом считывает двойное слово по смещению 03ch, чтобы узнать, где находится PE-заголовок. Так вот, здесь это смещение оказывается внутри самого PE-заголовка! В варианте "18h" оно попадает на поле, содержащее суммарный размер секций неинициализированных данных в образе приложения, а в варианте "20h" - на поле, содержащее размер секций кода. Использование этих полей в современных нам версиях Windows неочевидно. По одним источникам - они используются для первичного отведения памяти под приложение. По другим - не используются вообще. Во всяком случае, практическая проверка показывает, что приложения с таким stub'ом работают нормально. (Что будет дальше - пожалуй, неизвестно даже Microsoft'у.)

Конечно же, заставить какой-нибудь компоновщик создать исполняемый файл с предлагаемыми stub'ами не удастся. Такие трюки придется делать вручную, с помощью какого-нибудь бинарного редактора: переместить образ приложения внутри файла на нужное смещение, отредактировать exe-заголовок, PE-заголовок, заголовки секций - ну, в общем, кто знает формат - тот справится. (А лучше доверить эту рутину какой-нибудь маленькой программуле.)

Ну и напоследок, опровергнув свои же слова что "...меньше некуда...", Grief прислал вообще шедевр - вполне работающий stub длиной всего 0Ch!!! Разобраться, как он устроен, вы сможете самостоятельно, руководствуясь уже достаточно ясной идеей: в заголовках полным-полно никому не нужных полей, которые мы можем использовать для своих целей. Итак:

00000000 4D 5A 00 00 01 00 00 00 01 00 00 00 50 45 00 00 MZ..........PE..

00000010 4C 01 07 00 08 00 00 00 B0 21 CD 29 B4 4C CD 21 L........!.).L.!

00000020 E0 00 0E 01 0B 01 04 14 20 00 00 00 00 2E 00 00 ........ .......

00000030 18 00 00 00 00 E0 00 00 00 10 00 00 0C 00 00 00 ................




Минимальное приложение


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

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

Между тем вопрос о минимальной программе - совсем не простейший, и представляет отнюдь не академический интерес. Минимальная программа, очевидно, решает две задачи: (1)стартует и (2)завершается в конкретной рабочей среде. И то, и другое она должна делать корректно, с тем, чтобы:



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

Что здесь что и зачем?

.386 - директива ассемблера, определяющая набор команд процессора, который может быть использован в программе. Для приложений win32 необходимо использовать эту директиву или выше, в зависимости от того, собираетесь ли вы использовать возможности, предоставляемые процессорами последующих поколений .model flat,stdcall - сегментная директива ассемблера, определяющая сегментную модель приложения как плоскую, использующую соглашения о вызове процедур, принятые в win32. Именно такая сегментная модель должна всегда использоваться при написании приложений для win32 ExitProcess PROTO :DWORD - прототип функции API, выполняющей завершение приложения. (Поскольку сервис этой функции предоставляется dll-библиотекой kernel32.dll, то при сборке приложения хорошо бы не забыть подключить библиотеку импорта kernel32.lib) .code - сегментная директива ассемблера, определяющая начало сегмента кода WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show - начало тела стартовой процедуры. Следует обратить внимание на наличие директивы видимости (visibility) PUBLIC, которая позволит сборщику сделать процедуру доступной для операционной системы, дабы та смогла передать управление приложению. Параметры процедуры обсудим чуть ниже, правда, новость будет не очень приятной. ;... - здесь должна быть всякая прочая мелочь, которую вы хотели бы заставить делать ваше приложение invoke ExitProcess,0 - собственно вызов функции завершения приложения. Здесь код выхода 0, но он, естественно, может быть любым в пределах 32-разрядного целого. В полноценных приложениях нормой считается определение кода выхода в цикле обработки сообщений главного окна. WinMain ENDP - всякое тело должно иметь конец. Это он и есть. Правда, в силу наличия предыдущей строки, он в данной программе не достижим, но это и не требуется: здесь мы просто исполняем необходимую формальность. Кстати, настоящим ассемблерщикам должно быть приятно, что, опустив ненужную в связи с этим команду ret, мы сэкономили аж 4 байта кода! end - конец модуля.



Замечание 1. По поводу необходимости применения функции ExitProcess. Именно с ее помощью, а не посредством команды ret, как могли бы ожидать знатоки C/C++, должно завершаться приложение win32, написанное на ассемблере.

Если залезть внутрь runtime-библиотеки C/C++, то мы увидим, что именно так завершает работу приложения уже упоминавшаяся функция _WinMainCRTStartup. Но поскольку подключать runtime-библиотеку C/C++ к ассемблерной программе как-то нелогично (хотя и вполне возможно), мы должны вызвать ExitProcess "вручную". Только эта функция корректно завершает работу приложения, в частности, оповещая использовавшиеся приложением библиотеки DLL о необходимости декрементировать их счетчики занятости и, при обнулении последних, выгрузиться из памяти.

Замечание 2. А вот и обещанная плохая новость про параметры стартовой процедуры. Дело в том, что установкой их значений занимается - кто бы вы думали? - опять же _WinMainCRTStartup! И следовательно, в ассемблерных программах, где ее нет, при входе в WinMain параметры содержат:

hinst - не дескриптор текущего экземпляра приложения, а количество акций Microsoft у Билла Гейтса prev_hinst - не 0, а прогнозируемое значение NASDAQ после выхода на мировой рынок российской ОС BrokenWindows command_line - не указатель на командную строку, а традиционный народный российский указатель направления cmd_show - не предлагаемый начальный вид окна, а то, что обещал показать мировому сообществу Никита Сергеевич Хрущов (и даже, помнится, начал раздеваться)

Так что, воленс-ноленс, придется получать эту ценную информацию самостоятельно. Как говорится, клик хере.

Замечание 3. И все-таки приведенный текст приложения - не самый минимальный. Можно сделать его еще меньше, для чего следует убрать все параметры функции WinMain. Поскольку приложение собирается без runtime-библиотеки C/C++, это вполне допустимо, так как больше не с чем выполнять ее связывание. В результате объем исполняемого файла останется тем же, а вот объем исполняемого кода в нем (образа приложения) сократится аж до 7 байт. Про это стоит почитать еще.

Замечание 4. В этой статье рассматривается минимизация размера кода программы, но отнюдь не exe-файла, ее содержащего. Как известно, исполняемые файлы Windows обычно имеют так называемый PE-формат, и манипуляции с этим форматом дают некоторые возможности для сокращения размера файла. Например, можно вместо стандартной stub-программы использовать более компактную, собственной разработки.


Ms devstudio - среда разработки asm


Настоящие ассемблерщики - народ неприхотливый, хорошая находка для рачительной хозяйки. Едят, как правило, мало, и не особо разбираясь, что (одного моего друга жена, большая любительница животных, накормила как-то ради эксперимента педигрипалом. Очень хвалил). Место в помещении почти не занимают: не более двух кв.м вместе с компьютером. Линяют не чаще раза в год. В спячку, правда, не впадают, зато гон непродолжительный и тихий. В совокупности представляют собой прекрасный вторичный рынок для компьютеров 386 серии. Основой операционной среды предпочитают видеть кастрированный по самое ядро Norton Commander 5.0. Трансляцию запускают из командной строки (самые волевые с трудом заставляют себя написать batch-файл). Отладчик при разработке собственных приложений вообще не используют.

Поэтому Microsoft Developer Studio для них - как стадион "Уэмбли" для единственной выжившей после зимовки коровы симментальской породы из колхоза "Привет коммунизму": гораздо больше, чем нужно. И на такие его (MS DevStudio, а не "Уэмбли") мелкие недостатки, как отсутствие поддержки ассемблера, пенять не приходится.

Впрочем, к делу. Первый шаг - это подготовка MS DevStudio для работы с MASM:

Инсталлируйте, если не сделали этого до сих пор, MS Developer Studio в варианте Visual C++. Никаких особенностей здесь нет - все должно быть так же, как для программирования на C++ Найдите где-нибудь MASM 6.11с или более поздний. Чистый 6.11 (без "c") лучше не использовать: есть у него кое-какие проблемы. (В частности, выравнивает структуры максимум на 4, а в win32 часто требуется выравнивать на 8.) Сейчас щедрый американский дядюшка позволяет пользоваться MASM бесплатно, а совсем недавно приходилось покупать его за хорошие зеленые деньги. Есть он, например, в составе MSDN. Или его можно скачать из Сети (только не с ftp.microsoft.com. Рыбные места вы сможете найти в разделе ссылок нашего сайта). По минимуму из всего пакета потребуется один лишь модуль ml.exe размером около 400 Кбайт. Поместите ml.exe в папку исполняемых модулей пакета MS DevStudio (по умолчанию - C:\Program Files\DevStudio\VC\BIN).


Ваш пакет MS Developer Studio готов к разработке приложений win32 на MASM.

Любознательный читатель спросит: почему бы не использовать для разработки приложений inline-ассемблер, являющийся, как известно, неотъемлемым атрибутом C++. Вообще-то ничего невозможного на свете нет. Можно и отверткой в ухе ковыряться, только не очень глубоко. Но надо помнить, что inline-ассемблер предназначен для улучшения языка C++, а не для разработки самостоятельных приложений. Поэтому из него исключены большинство директив и, самое обидное, все макросредства, сильно помогающие облегчить процесс программирования.

Следующий шаг - это подготовка включаемых файлов:

Получите файл includes.zip

Распакуйте его и поместите файлы @struct.inc и windows.inc в папку включаемых файлов пакета MS DevStudio (по умолчанию - C:\Program Files\DevStudio\VC\INCLUDE).

Файл @struct.inc - это авторская отсебятина, которой в принципе можно и не пользоваться, а можно и наоборот, добавить в него что-нибудь свое, полезное. Что и для чего в этом файле, вы можете прочитать здесь.

С файлом windows.inc все гораздо сложнее. Это файл заголовков API, ассемблерный аналог той тучи h-файлов, которые содержатся в пакете Visual C++ и начинаются с файла windows.h (и, возможно, не только). Поскольку Microsoft официально не поставляет файл windows.inc (в отличие от windows.h), у ассемблерного программиста в связи с этим возникает большая-пребольшая проблема. Ее можно попытаться решить несколькими путями:

транслировать windows.inc из windows.h с помощью специальной программы-переводчика H2INC, поставляемой в составе пакета MASM 6.11+. Сразу скажем, что следует смело отправить эту надежду в мусоропровод. Продравшись сквозь частокол багов H2INC, вы обнаружите, что ее интеллекта хватает лишь на трансляцию тривиальнейших заголовочных файлов, к которым windows.h, безусловно, не относится. Вы утонете в бездне сообщений о некорректном переводе, и ваш сизифов труд по их обходу будет вознагражден получением совершенно негодного результата найти windows.inc (вариант - win.inc) в Интернете. Это гораздо более реально. Существует некоторое количество чудаков (возможно, это дети миллионеров, которым наплевать на необходимость зарабатывать на жизнь), которые тратят свое время на формирование и актуализацию этого файла и раздают его всем желающим. Некоторых из них вы найдете в разделе ссылок. Есть мнение, что пользоваться такими файлами настоятельно не рекомендуется. Представьте себе ситуацию к концу второго месяца возни этого доброго человека с каким-нибудь пятым по уровню вложенности заголовочным файлом в пирамиде windows.h. Возьмет, да и перепутает с устатку порядок описания членов какой-нибудь структуры. Или забудет указать ее выравнивание относительно умалчиваемого. Хорошо еще, если соответствующая функция API при использовании ее вами будет вываливаться с фатальной ошибкой вроде нарушения общей защиты. А может ведь и не вываливаться. И будете вы разбираться, почему глючит ваше приложение, дольше, чем добрый дядя переводил заголовочный файл описывать структуры, объявлять прототипы и делать все прочие действия, которые делает windows.h, внутри собственного проекта. Это еще более реальный вариант. Для этого вам потребуется оперативный доступ к содержимому windows.h из своего проекта (см.ниже). Каждый раз, как у вас возникнет необходимость обратиться к какой-нибудь функции API, вы должны будете найти в windows.h все, что касается ее вызова, и включить это в свой проект, предварительно переведя вручную на язык ассемблера. При наличии небольшого навыка такой перевод выполняется довольно быстро и проблем не вызывает делать все то же самое, но не в рамках текущего проекта, а в едином файле windows.inc, чтобы не повторять эти действия в последующих проектах. Со временем необходимость править windows.inc будет возникать у вас все реже и реже



Именно последний вариант и предлагается в качестве основного. Файл windows.inc, который мы вам предложили выше, взят из состава приложения MyCall. В нем только самая малая часть из того, что должно быть, и есть кое-что лишнее (RAS, например, в Visual C++ имеет собственный заголовочный файл). Но для старта этого достаточно.

Не забудьте только включить свой windows.inc в процедуру архивирования (надеемся, таковая у вас имеется). Будет обидно начинать все сначала после какой-нибудь очередной переинсталляции системы.

Ну вот, а теперь пора создать новый проект:

Запустите MS DevStudio Выберите в меню: File/New

В появившемся диалоге выберите вкладку Projects

В списке типов проектов выберите Win32 Application

В поле Location укажите каталог, в который предполагаете поместить папку проекта В поле Project Name укажите желаемое имя проекта. Учтите, что по умолчанию это имя будет присвоено исполняемому модулю и, кроме того, папке, в которой будут находиться файлы проекта на этапе его разработки. (Поле Location автоматически дополняется именем папки проекта.) По остальным элементам диалога убедитесь, что DevStudio собирается создавать для вашего проекта новое рабочее пространство (Create new workspace) для платформы Win32 Нажмите кнопку OK

Новый проект создан.

В рамках вновь созданного проекта первым делом необходимо создать файл с исходным текстом:

Нажмите кнопку New Text File или создайте текстовый файл посредством диалога, вызываемого через меню File/New

Сохраните вновь созданный файл командой меню File/Save As. При этом дайте файлу желаемое имя с расширением .asm, например, main.asm. В принципе, расширение тоже может быть любым, но лучше не отходить от общепринятых стандартов Подключите файл к проекту. Для этого, например, можно в окне Workspace на вкладке FileView щелкнуть правой кнопкой мыши по заголовку пока пустого списка файлов проекта и в появившемся контекстном меню выбрать пункт Add Files to Project. В файловом диалоге, чтобы увидеть наш новый файл, придется переключиться в режим Все Файлы.



А теперь самое важное. Файл когда-нибудь придется компилировать. Подготовиться к этому следует уже сейчас:

В окне Workspace на вкладке FileView щелкните правой кнопкой мыши по имени файла В появившемся контекстном меню выберите пункт Settings. Появится диалог Project Settings в режиме установок для файла В диалоге выберите вкладку Custom Build

В списке Settings for выберите пункт Win32 Debug

В текстовом поле Build command(s) введите новую команду:
ml.exe /c /coff /nologo /Zi /DDEBUG $(InputPath)

В текстовом поле Output file(s) введите текст:
.\$(InputName).obj

В списке Settings for выберите пункт Win32 Release

В поле Build command(s) введите новую команду:
ml.exe /c /coff /nologo $(InputPath)

В поле Output file(s) введите текст:
.\$(InputName).obj

Файл готов к компиляции с помощью модуля ml.exe. Обратите внимание, что пункты 5 и 6 касаются компиляции asm-файла для отладочного варианта проекта, и получаемый в этом случае объектный файл содержит отладочную информацию. Пункты же 8 и 9 касаются окончательного варианта проекта, готового к дистрибуции.

Наконец, наш файл должен иметь какое-нибудь корректное содержимое. Допустим, мы разрабатываем минимальное приложение. Учитывая, что кое-что из него уже имеется в файле @struct.inc, его содержимое может быть, например, таким:

include @struct.inc include c:\program files\devstudio\vc\include\windows.inc

.code WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show invoke ExitProcess,0 WinMain ENDP end
Вот теперь можно смело нажимать кнопку Compile (Ctrl+F7). Компиляция файла должна пройти без ошибок.

Обратите внимание, что имя включаемого файла windows.inc указано с полным путем. Это не случайно. В процессе разработки приложения вам придется очень часто обращаться к этому файлу, добавляя в него заголовочную информацию. В MS DevStudio это сделать очень просто, причем несколькими способами. Например, можно выделить имя файла и, щелкнув по нему правой кнопкой, выбрать пункт контекстного меню Open Document. Но удобнее все-таки подключить файл windows.inc к проекту "на постоянно", воспользовавшись правой кнопкой мыши в окне Workspace. Если вы удачно попадете по имени проекта или по какой-нибудь папке, вам станет доступна опция Add files to Project (Folder)... С ее помощью-то вы и подключите файл к проекту, и в дальнейшем простым двойным кликом сможете вызывать его на редактирование.



Теперь перейдем к сборке проекта:

В окне Workspace на вкладке FileView щелкните правой кнопкой мыши по заголовку списка файлов (он же имя проекта) В появившемся контекстном меню выберите пункт Settings. Появится диалог Project Settings в режиме установок для проекта В диалоге выберите вкладку Link

В списке Settings for выберите пункт Win32 Debug

В списке Category выберите пункт General

В поле Output file name вы можете при желании указать имя создаваемого исполняемого модуля, если вас не устраивает имя по умолчанию Установите флажок Ignore all default libraries. Таким образом вы откажетесь от подключения runtime-библиотеки C/C++ и сэкономите несколько десятков килобайт кода. Подробнее см. здесь и здесь. В списке Category выберите пункт Output

В поле Entry-point symbol укажите WinMain (в данном случае) - имя функции, являющейся точкой входа для проекта В списке Settings for выберите пункт Win32 Release

Проделайте все то же самое по п.п.6...9

Все, проект готов к сборке. Нажмите кнопку Build (F7) и убедитесь, что сборка проходит нормально. Можете также попробовать запустить полученный исполняемый модуль проекта и убедиться, что ничего не происходит: он выполняется и завершается слишком быстро и без каких-либо видимых признаков.

Выше мы обещали обеспечить удобный доступ к файлам заголовков API, чтобы можно было пользоваться ими как справочником при подготовке файла windows.inc. Для этого нам потребуется активизировать встроенное средство MS DevStudio под названием Browse Info. Работает оно очень просто. Достаточно щелкнуть по интересующему нас имени в исходном тексте программы правой клавишей мыши и выбрать пункт контекстного меню Go To Definition Of, как моментально загрузится заголовочный файл Visual C++, в котором это имя описано. В проектах на C/C++ активизация Browse Info происходит автоматически, при первой попытке выполнить указанное действие. А вот в проектах на ассемблере, к сожалению, этого нет. Но ничего страшного. Просто нужно немножко повозиться.



И наконец, маленькое замечание о зависимостях (Dependencies). Не забывайте о них. Например, файл main.asm, очевидно, зависит от windows.inc. И поскольку последний будет очень часто подвергаться изменениям, в том числе и вне текущего проекта, имеет прямой смысл эту зависимость описать. При этом в каждом случае, когда происходит изменение файла windows.inc, будет автоматически вызываться перекомпиляция файла main.asm. В MS DevStudio зависимости описываются очень просто (хотя и не очень удобно: приходится вводить имя файла вручную), гораздо проще, чем в заковыристых исторических make-файлах. Снова вызовите диалог Project Settings для файла main.asm. Найдите на вкладке Custom Build кнопку Dependencies и в появившеся диалоге укажите полное имя файла windows.inc.

Все, MS Developer Studio готово для ассемблерного программирования. Можно предъявить много всяких претензий по сервису и оформлению, но главное - это работает.


Mycall.rc для mycall


Это файл описания ресурсов для приложения MyCall. Этот файл вместе со всеми остальными файлами, необходимыми для компиляции приложения MyCall, содержится в zip-файлах mycallcb.zip (C++, 13192 байта) и в mycallab.zip (ассемблер, 15913 байт). Имеется также Инструкция программиста.

Для получения комментариев щелкaйте по тексту или пользуйтесь групповым управлением:

if(dhtml){document.write("Все комментарии: [+][-]    Открывать: [несколько]");}

//ВКЛЮЧАЕМЫЙ ФАЙЛ С ОПРЕДЕЛЕНИЯМИ КОНСТАНТ

//(cтандартный файл заголовков win32 из пакета MS Developer Studio)

#include "windows.h"

//ОБЪЯВЛЕНИЕ ИМЕН ИДЕНТИФИКАТОРОВ РЕСУРСОВ

//Обычно выносится в файл заголовков

//(по умолчанию в MS Developer Studio - resource.h)

//Содержится здесь по причине крайней простоты приложения.

#define IDD_DIALOG1 101

#define IDI_ICON1 103

#define IDC_COMBO1 1000

#define IDC_COMBO2 1001

#define IDC_COMBO3 1002

#define IDC_BUTTON1 1003

#define IDC_EDIT1 1004

//ОПИСАНИЕ ДИАЛОГА

//Следует обратить внимание на наличие определения CLASS.

//Обычно его применение не рекомендуется, так как оно перегружает стандартную

//обработку диалога и передает поток диалоговых сообщений процедуре окна,

//для которого зарегистрирован указанный класс. Однако в нашем случае, когда диалог

//используется в качестве главного окна приложения, это нормально.

IDD_DIALOG1 DIALOG DISCARDABLE 100,100,217,18

STYLE WS_CAPTION|WS_SYSMENU

CAPTION "MyCall"

CLASS "MainWindowClass"

FONT 8,"MS Sans Serif"

BEGIN

COMBOBOX IDC_COMBO1,2,2,43,120,CBS_DROPDOWNLIST|WS_VSCROLL

COMBOBOX IDC_COMBO2,48,2,58,120,CBS_DROPDOWNLIST|WS_VSCROLL

OMBOBOX IDC_COMBO3,108,2,68,120,CBS_DROPDOWNLIST|WS_VSCROLL

PUSHBUTTON "Call",IDC_BUTTON1,179,2,35,13

EDITTEXT IDC_EDIT1,2,17,215,12,ES_READONLY|NOT WS_BORDER

END

//ОПИСАНИЕ ИКОНКИ

IDI_ICON1 ICON DISCARDABLE "icon1.ico"



Обсуждение статьи "Зачем он нужен, этот ассемблер?"


Читайте также:

статью "Зачем он нужен, этот ассемблер?" дополнение Геннадия Майко

Нам приходят письма, в которых читатели излагают свои соображения, сомнения и возражения по теме, обсуждаемой в статье Зачем он нужен, этот ассемблер? Последнее мы получили совсем недавно от MemoBreaker'а. Автор излагает в нем мнение, во многом совпадающее с мнением других людей, чьи письма мы получали ранее и обсуждали в приватных беседах по е-мэйлу. И поскольку эта точка зрения изложена в письме MemoBreaker'а наиболее полно, мы с удовольствием и благодарностью публикуем его с разрешения автора:

Hello assembler.ru,

Вот прочитал я вашу статью: "зачем он нужен, этот ассемблер?". И возникло у меня несколько соображений по этому поводу...

Сразу отмечу, что сам я начинающий ассемблерщик (на PC, а вообще АСМ я знаю еще с Z80). Я не согласен с некоторыми утверждениями этой статьи:

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

1) Включать в список файлов, которые необходимо создавать самому, windows.inc нет смысла. Так как если особо не извращаться и писать на ЧИСТОМ MASM'е, то данный и другие файлы с описанием API уже есть в стандартной поставке. Писать не очень большие проги на масме гораздо удобнее и, по-моему, быстрее, нежели использовать монструозную Visual Studio.

2) Насчет размера файлов асма и С++ все в корне не верно! Если вы что-то сравниваете, то необходимо провести несколько сравнений: во-первых минимальное приложение на асме, выводящее только MessageBox, занимает, если не ошибаюсь полтора килобайта (меньше по-моему Винда не позволяет); во-вторых минимальное приложение, имеющее функцию WinMain, занимает 4608 байт(включая иконку, без оной - 3584); в-третьих нужно сравнивать и более серьезные проекты, причем, чем больше функций у программы, тем больше выигрыш по размеру при программировании на ассемблере. В результате если написать WordPad на ассемблере и на С++, то ассемблерная прога будет в несколько раз меньше, нежели СИшная.

3) Неудобство ассемблера заключается в том, что иногда простые ошибки очень сложно отловить, так, например, было с моей (и The Byte Reaper'а тоже) программой UIN2IP. Суть в том, что в MASM32 изначально были допущены ошибки во встроенной подпрограмме (из MASM32.lib) конвертации ASCII to DWORD, есть в масме и еще пара ошибочек, но менее существенных. В отношении ошибок С++ тоже есть в чем упрекнуть, бывали случаи, когда С++ сильно ругался на куски кода, которые ошибки (по крайней мере в теории) не имели. Хуже всего, когда на код ругается линкер - понять в чем ошибка становится гораздо сложнее.

4) Есть еще несколько применений ассемблера:

- на нем удобно писать такие вещи как VxD драйвера (не знаю возможно ли это на С++)

- ассемблер используется в критичных к скорости выполнения программах (можно, конечно, и INLINE, но как сами говорили "можно и отверткой в ухе...")

Только не надо говорить, что современные процессоры позволяют добиваться приемлемых скоростей и на С++, представьте, что вы пишете движок для рендеринга, но не используя ускоритель. В данном случае С++ отстанет от асма процентов на 50 точно (если писать под конкретный процессор на ассемблере). Другой пример, более реальный, - вы пишете программу, проводящую большое количество вычислений (тот же подбор пароля), вот здесь-то скорость и пригодится как нельзя лучше!

Ну вот в принципе и все претензии. Надеюсь что они будут рассмотрены и учтены :)

--

Best regards,

MemoBreaker

P.S. Кстати, по моему личному мнению лучшее место для ассемблера в современной индустрии программо-строения - различные .DLL , в них исчезают некоторые недостатки асма и проявляются достоинства:

- почти всегда отсутствует интерфейс, следовательно над ним не придется мучиться.

- необходима высокая скорость работы, для чего асм подходит лучше всего (пример: сжатие MP3 или MPEG; пользователь как всегда хочет побыстрее!)

- также возможна оптимизация под различные MMX, 3D-NOW!, SSE и т.д.


MemoBreaker в работает с пакетом MASM32 и считает его лучшим вариантом ассемблера для PC (в письме, говоря об ассемблере, он имеет в виду именно этот пакет). В качестве основы для рабочей среды он предпочитает не Visual Studio, а:

MASM32 Win32api.hlp Aditor v2.x (текстовый редактор) парочку утилит SoftIce

В аргументах MemoBreaker'а есть многое, с чем можно согласиться. Например, с тем, что MASM32 - действительно идеальное средство для разработки ассемблерных приложений под Windows. И входящий в его состав файл windows.inc, который весит сейчас уже больше 800 Кбайт - действительно освобождает программиста от необходимости создавать этого монстра самому, потому что сделан достаточно хорошо, со знанием специфики и, что самое главное, постоянно обновляется. И компилятор ml.exe, который входит в пакет - всегда самой последней версии. И это при всем при том, что Microsoft не имеет к MASM32 никакого отношения (ну разве что компилятор родом оттуда). Потому что MASM32 - это детище независимых разработчиков во главе с Iсzelion'ом.

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

Но некоторые положения статьи, которые вызывают возражения у наших корреспондентов, хотелось бы пояснить.

Собираясь ставить эксперимент по сравнению ассемблерной и сишной версий MyCall, мы изначально хотели наглядно показать преимущества ассемблерной программы. У нас совсем не было планов показывать, что никаких радикальных преимуществ нет, а вот очевидные недостатки (в первую очередь - большая трудоемкость) - имеются. Мало того, мы ведь вложили в этот эксперимент несколько больше, чем вы, дорогие читатели, в прочтение нашей статьи. И поэтому вы должны представить себе наше разочарование конечными результатами.

Проведенный эксперимент совершенно не претендует на роль истины в последней инстанции. MemoBreaker предлагает провести несколько сравнений: от минимального приложения до большого проекта, и только тогда делать выводы. Наверное, такой масштабный эксперимент дал бы много свежей пищи для размышлений. К сожалению, ничего подобного мы нигде не встречали ранее, да и вряд ли встретим когда-либо. По нашим наблюдениям, то, что сделали мы с MyCall - единственная реальная попытка подтвердить (или опровергнуть?) на практике так часто выдвигаемый тезис о прекрасной приспособленности ассемблера для создания приложений для Windows.



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

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

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

К сожалению, дар абстракции дан людям не в равной степени. Наверное, в этом проявляется рудиментарное наследие каменного века, когда одни индивидуумы абстрактно загоняли мамонтов в ловчие ямы, а другие вполне конкретно мочили несчастных сверху булыжниками и дубинами. В результате у первых больше развилось интуитивное правое полушарие головного мозга, а у вторых - рациональное левое. И теперь, по причине отсутствия мамонтов, первые занялись программированием на C++, а вторые - на ассемблере.

Еще раз сформулируем нашу позицию касательно размера приложения, выработанную в результате эксперимента с MyCall и на основании собственного опыта разработки приложений для Windows:

Разработка небольших приложений на ассемблере не дает ощутимых преимуществ по объему кода по сравнению с C++, а вернее, C, потому что в таких приложениях объектно-ориентированные возможности C++ обычно неприменимы. Пример тому - MyCall. Что касается больших приложений, то однозначно выявить преимущество ассемблера перед C++ по объему кода практически не представляется возможным. В таких приложениях существенную роль играют диктуемые языком принципы организации программ. Ассемблер предлагает программисту хаотический стиль, и приходится применять усилия и специальные приемы (см., например, статью Лептонный стиль программирования) для преодоления этого недостатка языка. C++, напротив, стимулирует формирование у программиста объектно-ориентированного стиля, и лишь неспособность или нежелание воспринять его приводит к отказу от этого стиля в пользу хаотического. А по-разному организованные программы становятся практически несравнимыми, даже если решают одну и ту же задачу. Еще более неочевидным становится гипотетическое преимущество ассемблера в объеме кода, если вспомнить, что приложения для Windows далеко не полностью состоят собственно из кода. Более того, в массе случаев код занимает в них меньшую часть. А гораздо больший объем занимают разнообразные данные: ресурсы, структуры, таблицы и прочее, прочее, прочее. Понятно, что данные в своем большинстве инвариантны к языку, и, следовательно, их наличие маскирует преимущество ассемблера в компактности кода. Плюс к сказанному: современные компиляторы C++ (в частности, Visual C++), при установленном флажке "Оптимизация по объему" дают практически оптимальный код, лишь в малой степени уступающий коду, написанному вручную на ассемблере. Более того, простая небрежность, лень или неполное знание ассемблерщика способны легко разрушить его призрачное преимущество. А вот глупому компилятору C++ такие человеческие слабости незнакомы - он оптимизирует код всегда.



Похожие аргументы мы высказываем и при рассмотрении преимуществ ассемблера в быстродействии:

Многозадачная операционная среда, каковой является Windows, распределяет ресурсы производительности компьютера отнюдь не по признаку того, на каком языке написано приложение. Вытесняющая многозадачность, не спрашивая желания нашего приложения, выделяет ему тот временной ресурс, который считает нужным, а остальное время отдает другим потокам, маскируя то преимущество в быстродействии, которое мог бы дать ассемблерный код. Развитый сервис API, освободивший ассемблерщика от огромного объема рутинной работы, играет в данном случае злую шутку: вызовы API обрабатываются с одной и той же скоростью вне зависимости от того, на каком языке написано приложение, и в силу этого также стирается преимущество в скорости ассемблерного кода. Такой же эффект производит и система сообщений, без которой не обходится ни одно уважающее себя оконное приложение. Ее обслуживает очень приличный кусок кода, локализованного вне приложения, и отнимающего у приложения свою немалую долю ресурса производительности. В случае разработки приложений с активным использованием графики большая часть гигантской работы по ее обслуживанию все больше ложится на "железо" - 2d и 3d ускорители, которым также безразлично, на чем написано наше приложение. Наконец, современные компиляторы C++ при установленном флажке "Оптимизация по времени" генерируют практически оптимальный код, который дает максимальную скорость так же независимо от настроения и знаний программиста, как и минимальный размер в случае оптимизации по размеру.

В подтверждение этих тезисов приведем письмо, полученное от Dmitry S. Bobrik (спасибо ему!) буквально во время написания этой статьи:

Hello!

Можете себе представить - компиленый Сишный код работает БЫСТРЕЕ написаного на Асме!

Единственное разумное объяснение - оптимизатор MSVC пишет код учитывая особенности архитектуры процессоров Пентиум (ну и выше...). А на Асме все то ручками, да ручками... обидно даже :-/

wbr, Dmitry

http://bcsoft.da.ru

http://home.tula.net/frazez

<


Дмитрий сообщил, что впервые об этом эффекте он прочитал в мэйл-листе sp-forth@egroups.com

И несколько замечаний в заключение:

Это не похороны ассемблера, хотя бы потому, что:

ему по-прежнему есть и будет место в разработке драйверов устройств ему по-прежнему есть и будет место в приложениях, монополизирующих ресурсы системы:

в играх в обработке потоков данных, в том числе в реальном времени в расчетах и т.д.

вы будете смеяться, но Windows - совсем не единственная операционная система

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




Ошибки при вызове функций api


Date: 17-24 ноября 1999
Newsgroup: microsoft.public.masm
Subject: How to call win32 API? (Как вызвать win32 API?)
Участвуют: Min
Xaphiosis
Randall Hyde
minwang@hotmail.com
void_s@ihug.com.au
rhyde@shoe-size.com
 
Organization: The Internet Group Ltd
 

if(dhtml){document.write("  Все сообщения: [+][-]    Открывать: [несколько]");}

[+] MinРабочая среда: VC6.0/MASM6.11/NT Server 4.0 .386 .MODEL flat, stdcall PUBLIC _start .DATA .CODE _start: INVOKE MessageBox, NULL, "ok", "test", MB_OK END _start Получаю сообщение: error A2004: symbol type conflict Также пробую MessageBoxA/MessageBoxW, бесполезно. Есть идеи?

Regards, Min [+] XaphiosisHehehehhhehehhehheheh ;)

Мне бы очень понравился такой синтаксис, но вы переоцениваете возможности ассемблера... если только вы не определили макрос MessageBox, вы не можете вызывать его с текстом в кавычках! И даже если вы сделали нечто подобное, вы не включили в строки завершающий ноль.

так что следовало бы поступить так: INVOKE MessageBox, NULL, addr msg_ok, addr msg_test, MB_OK Попробуйте.. Я гарантирую, что это будет работать...

Sincerely X. [+] MinСпасибо за ответ. Но оно по-прежнему не работает :( .386 .MODEL flat, stdcall include win.inc PUBLIC _start .DATA ALIGN DWORD msg_ok BYTE "ok",0 msg_test BYTE "test",0 .CODE _start: INVOKE MessageBox, NULL, addr msg_ok, addr msg_test, MB_OK END _start Сообщение об ошибке: "error A2004: symbol type conflict"

Regards, Min [+] XaphiosisAlright... Я быстренько откомпилировал ваш код на своей системе... Напоминаю, если вы забыли: OPTION CASEMAP:NONE включает чувствительность к регистру, иначе win.inc не работает

кроме того, после MessageBox, мне кажется, вы должны вызвать ExitProcess (правда, это не влияет на компиляцию)

Наконец, я думаю, следует проверить возможность того, что ваш win.inc дефектен (довольно сомнительно)

В любом случае, вот код, который я использовал: Попробуйте и убедитесь, что он работает ;) .386 .MODEL FLAT, STDCALL OPTION CASEMAP:NONE




include <my equivalent of win.inc = various files> PUBLIC _start

. DATA ALIGN DWORD msg_ok BYTE "ok",0 msg_test BYTE "test",0

.CODE _start: INVOKE MessageBox, NULL, addr msg_ok, addr msg_test, MB_OK call ExitProcess ;EXIT POINT END _start Sincerely, X. [+] MinThank you very much.

Оказывается, MASM611 содержит include-файл для win3.1! Поэтому я скачал пакет MASM32 - и все заработало.

Regards, Min [+] Randall HydeЕсли вам действительно нравится такой синтаксис, переходите на HLA (High Level Assembler). Он позволит вам писать код, подобный следующему: MessageBox( MB_OK, "test", "ok", NULL ); (параметры перечислены в обратном порядке, потому что HLA использует соглашения вызова Паскаля, а не C/C++. Вы можете использовать макрос для устранения этой проблемы, если она для вас существенна).

HLA будет автоматически размещать строки в памяти "только для чтения" и предоставлять вам адреса таких строк. Вы можете найти HLA на http://webster.cs.ucr.edu

Randy Hyde

 

Точка зрения assembler.ru изложена в статье Вызов функций API.


Параметры функции WinMain


Приложения для win32, написанные на ассемблере и собранные без runtime-библиотеки (как это сделать - см. статьи минимальное приложение и ms devstudio - среда разработки asm), работают в среде, несколько отличающейся от той, которая привычна программистам C/C++ и выше. Все дело в том, что runtime-библиотека в значительной мере занимается подготовкой этой среды. Вернее, не сама библиотека (это, конечно, не ее задача), а входящая в нее функция _WinMainCRTStartup. В частности, эта функция устанавливает значения четырех параметров функции WinMain, которую программист воспринимает как стартовую функцию приложения:

WinMain PROC PUBLIC hInstance,hPrevInstance,lpCmdLine,nCmdShow

Здесь:

hInstance - дескриптор текущего экземпляра данного приложения. Это значение используется в дальнейшем для взаимодействия с элементами рабочей среды, связанными с данным приложением: ресурсами, окнами, изображениями и многими другими. Как правило, надобность в знании этого дескриптора возникает почти сразу после запуска экземпляра приложения и сохраняется на протяжении всего срока его жизни. Этот дескриптор локален в рамках данного экземпляра приложения и не имеет смысла для других приложений и других экземпляров этого же приложения. hPrevInstance - должен был бы быть дескриптор предыдущего экземпляра данного приложения. Однако в win32 этот параметр не используется и всегда равен 0. Часто бывает нужно знать, существуют ли в момент запуска приложения другие его экземпляры, и в зависимости от их существования предпринимать какие-либо действия. (Например, отменить запуск нового экземпляра и вывести окно уже существующего экземпляра на передний план.) Так вот, hPrevInstance после гибели win16 стал для этого совершенно бесполезен. (См. статью взаимодействие экземпляров приложения.) lpCmdLine - указатель на командную строку. Сегодня большинство обычных пользователей практически не обращают внимания на архаичные возможности управления приложениями, предоставляемые командной строкой. Однако некоторые программисты все еще используют ее (в том числе и те, которые писали Проводник). Не факт, что командная строка вам понадобится, но знать, как получить к ней доступ - полезно. nCmdShow - параметр, описывающий, в каком виде рекомендуется создать главное окно приложения: скрытое, свернутое, нормальное, развернутое и т.д. Полезен в случаях, когда вызывающий процесс должен определять вид окна вызываемого процесса. Тогда nCmdShow следует использовать при первом вызове функции ShowWindow для главного окна приложения. Если же вы пишете самостоятельное приложение, то вполне допустимо проигнорировать этот параметр и показывать окно в том состоянии, которое вам больше нравится.


И вот все эти ценные и не очень параметры программисту на ассемблере недоступны. Сборка приложения без подключения runtime-библиотеки (например libc.lib) экономит от одного до нескольких десятков килобайт. Но тогда функция _WinMainCRTStartup не существует, а функция WinMain объявляется при сборке как точка входа. И следовательно, значения ее параметров неопределенны. Как же их получить?
Далее использован синтаксис вызова системных функций для masm6.1+
Дескриптор экземпляра приложения узнать очень просто:
invoke GetModuleHandleA,NULL
mov hInstance,eax
Возвращаемое функцией GetModuleHandleA в eax значение - и есть искомый дескриптор. Будучи настоящими ассемблерщиками, мы, как видите, сохраняем его в hInstance, чтобы не пропадали даром целых четыре байта памяти. Между прочим, этот дескриптор фактически представляет собой базовый адрес памяти для размещения образа приложения (image base в терминах формата PE-файла). Для самостоятельных приложений по умолчанию он обычно равен 400000h.
Следует обратить внимание на наличие суффикса A в имени функции, говорящего о том, что мы работаем с кодировкой ANSI. Если вы предпочитаете Unicode, то суффикс должен быть W. Это, так сказать, "ручной" выбор типа кодировки приложения на этапе компиляции. Поскольку функций API, имеющих две версии, большинство, то, в принципе, может иметь смысл автоматизация этого процесса, как это сделано, например, для C++ в MS Developer Studio. Ввести некую настроечную константу и, в зависимости от нее, подключать ту или иную версию функции.
Командную строку следует получать так:
invoke GetCommandLineA
mov lpCmdLine,eax
Возвращаемое в eax значение указывает на буфер в памяти, содержащий текст командной строки с завершающим нулем. Результат, правда, несколько отличается от того, который можно было бы получить из lpCmdLine, будь у нас runtime-библиотека. Функция GetCommandLineA возвращает командную строку, включая полный путь к исполняемому модулю приложения. Путь взят в кавычки, что необходимо, поскольку он может содержать пробелы. Например, командная строка может иметь такой вид:


"D:\myapp\Little Joke.exe" c: /format
Между прочим, lpCmdLine, буде оно нам доступно, содержало бы адрес в этом же самом буфере памяти, следующий за пробелом, которым завершается путь к исполняемому модулю.
Параметр показа главного окна приложения (если он вам действительно нужен) получить несколько труднее. Для этого необходимо воспользоваться фрагментом кода:
.data?
startup_info STARTUPINFO{}
.code
...
invoke GetStartupInfoA,offset startup_info
xor eax,eax
mov ax,startup_info.wShowWindow
mov nCmdShow,eax
...
Уникальная для каждого процесса структура STARTUPINFO заполняется вызывающим процессом, например, Проводником, перед запуском нашего приложения, при этом в нее заносится в том числе и значение параметра показа окна в виде члена wShowWindow, имеющего размер слова. С помощью этого кода мы считываем структуру STARTUPINFO, извлекаем из нее wShowWindow, приводим размер к двойному слову и записываем полученное значение в nCmdShow.
Таким образом, мы восстановили все, что потеряли, когда отказались от сборки ассемблерного приложения с runtime-библиотекой. Вернее, почти все, потому что runtime-библиотека делает чуть-чуть больше, чем установка параметров функции WinMain. Смайл.
И еще одно замечание. Раз уж мы выполняем сборку приложения без runtime-библиотеки, то ничто более не предъявляет к нашей пусковой функции каких-либо требований по ее именованию или числу аргументов. Мы можем назвать ее, например, my_main_function и лишить ее аргументов насовсем (если, допустим, они нам не понадобятся, или мы будем сохранять их значения как-нибудь по-другому), либо оставить только те, которые нам необходимы. Например:
my_main_function PROC PUBLIC inst_handle,command_string
Главное - объявить эту функцию точкой входа приложения с помощью опции /entry:"my_main_function" сборщика link.exe.

Шаблон оконного приложения


Каждый сайт, хоть немного причастный к программированию для Windows, считает своим долгом поместить на своих страницах шаблон оконного приложения. Assembler.ru в этом отношении ничем не хуже остальных. Почему бы и нет? Это настоящее, живое, работающее приложение, чего ж его стыдиться? Многим из нас его хватит, чтобы сказать себе с удовлетворением: "Достаточно, я в себя поверил", и вернуться к любимому третьему видеорежиму. Остальным же, возможно, оно понадобится еще много-много раз для того, чтобы начать с него новый проект, и, потратив полмесяца на его развитие, выяснить, что нужно было начинать с чего-то другого. Итак:

.386 .Model flat,stdcall

include windows.inc ;заголовочный файл API;///////////////////////////////////////////////////////////// windows.inc GetModuleHandleA PROTO :DWORD ;прототипы функций API LoadIconA PROTO :DWORD,:DWORD LoadCursorA PROTO :DWORD,:DWORD RegisterClassExA PROTO :DWORD CreateWindowExA PROTO :DWORD,:DWORD,:DWORD,:DWORD,\ :DWORD,:DWORD,:DWORD,:DWORD,\ :DWORD,:DWORD,:DWORD,:DWORD ShowWindow PROTO :DWORD,:DWORD GetMessageA PROTO :DWORD,:DWORD,:DWORD,:DWORD TranslateMessage PROTO :DWORD DispatchMessageA PROTO :DWORD ExitProcess PROTO :DWORD PostQuitMessage PROTO :DWORD DefWindowProcA PROTO :DWORD,:DWORD,:DWORD,:DWORD

WNDCLASSEXA STRUCT 8 ;структура класса окна cbSize dd ? style dd ? lpfnWndProc dd ? cbClsExtra dd ? cbWndExtra dd ? hInstance dd ? hIcon dd ? hCursor dd ? hbrBackground dd ? lpszMenuName dd ? lpszClassName dd ? hIconSm dd ? WNDCLASSEXA ENDS

POINT STRUCT 8 x dd ? y dd ? POINT ENDS

MSG STRUCT 8 ;структура сообщения hwnd dd ? message dd ? wParam dd ? lParam dd ? time dd ?

pt POINT{} MSG ENDS

NULL =0 ;разные константы IDI_APPLICATION =32512 IDC_ARROW =32512 COLOR_WINDOWFRAME =6 SW_SHOW =5 EXIT_ERROR =0ffffffffh WM_DESTROY =2

CS_VREDRAW =0001h CS_HREDRAW =0002h

WS_OVERLAPPED =00000000h ;константы стилей окна WS_CAPTION =00C00000h WS_SYSMENU =00080000h WS_THICKFRAME =00040000h WS_MINIMIZEBOX =00020000h WS_MAXIMIZEBOX =00010000h


WS_OVERLAPPEDWINDOW= WS_OVERLAPPED OR\ WS_CAPTION OR\ WS_SYSMENU OR\ WS_THICKFRAME OR\ WS_MINIMIZEBOX OR\ WS_MAXIMIZEBOX if(dhtml){document.all["include"].style.display="block";document.all["windows"].style.display="none";}

;///////////////////////////////////////////////////////////// WinMain .const app_name db "WinApp Template",0 ;имя приложения class_name db "WinApp Template Class",0 ;имя класса окна .data? win_class WNDCLASSEXA{} win_handle dd ? loop_message MSG{} .code ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::: WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show ;......................................................... регистрация класса окна mov win_class.cbSize,sizeof(WNDCLASSEXA) mov win_class.style,CS_HREDRAW OR CS_VREDRAW mov win_class.lpfnWndProc,offset win_procedure mov win_class.cbClsExtra,0 mov win_class.cbWndExtra,0 invoke GetModuleHandleA,NULL mov win_class.hInstance,eax invoke LoadIconA,NULL,IDI_APPLICATION mov win_class.hIcon,eax mov win_class.hIconSm,eax invoke LoadCursorA,NULL,IDC_ARROW mov win_class.hCursor,NULL mov win_class.hbrBackground,COLOR_WINDOWFRAME mov win_class.lpszMenuName,NULL mov win_class.lpszClassName,offset class_name invoke RegisterClassExA,offset win_class .if(!eax) jmp abort .endif ;......................................................... создание окна invoke CreateWindowExA,NULL,\ offset class_name,\ offset app_name,\ WS_OVERLAPPEDWINDOW,\ 100,100,300,200,\ NULL,\ NULL,\ win_class.hInstance,\ NULL .if(!eax) jmp abort .endif mov win_handle,eax ;он еще пригодится, только не сегодня ;......................................................... вывод окна на экран invoke ShowWindow,eax,SW_SHOW ;......................................................... инициализация ;здесь можно вставить все необходимые действия для первичной инициализации ;приложения (например, загрузку и разбор файла настроек) ;......................................................... цикл опроса очереди сообщений msg_loop: invoke GetMessageA,offset loop_message,NULL,0,0 .if(!eax) invoke ExitProcess,loop_message.wParam ;успешное завершение приложения .endif invoke TranslateMessage,offset loop_message invoke DispatchMessageA,offset loop_message jmp msg_loop ;......................................................... аварийное завершение abort: invoke ExitProcess,EXIT_ERROR WinMain ENDP



;///////////////////////////////////////////////////////////// Оконная процедура win_procedure PROC window_from,message,w_param,l_param .if(message==WM_DESTROY) invoke PostQuitMessage,0 ; .elseif(message==...) ;обработка остальных необходимых сообщений ; ... ; .elseif(message==...) ; ... ; .elseif(message==...) ; ... .else ;сообщение не обработано invoke DefWindowProcA,window_from,message,w_param,l_param ret .endif xor eax,eax ;сообщение обработано ret win_procedure ENDP

;############################################################# end

Вот, собственно, и все приложение. В нем реализованы фундаментальные концепции общего прикладного програмирования для Windows:

работа начинается с вынужденного объектно-ориентированного действия - громоздкой регистрации класса окна с помощью функции RegisterClassExA. Вы должны сделать это, даже если собираетесь создать всего одно окно с такими свойствами, потому что так повелел когда-то Уильям Гейтс Второй, известный в народе как просто Билл. Впрочем, вы можете избежать этой неприятной процедуры, если воспользуетесь при создании окна одним из предопределенных классов - BUTTON, COMBOBOX , EDIT , LISTBOX , MDICLIENT , SCROLLBAR , STATIC. С помощью, например, класса BUTTON вы можете сделать большую-пребольшую кнопку и нажимать ее день и ночь до умопомрачения, ожидая, пока из floppy-дисковода выскочит банан. А класс STATIC очень удобен для эмуляции забора: написанное на таком окне емкое трехбуквенное слово всегда напомнит вам о необходимости вернуться к повседневным заботам. если регистрация класса окна прошла успешно, можно создать само окно с помощью функции CreateWindowExA. Полученный при ее возврате в регистре eax дескриптор окна следует сохранить до лучших времен. Он потребуется очень часто для выполнения манипуляций с окном. созданное окно полностью функционально за исключением одной мелочи: его не видно. Может, и в самом деле его не стоит показывать, чтобы не позориться. А иногда это действительно не нужно, например, если использовать главное окно для общей диспетчеризации приложения, а для общения с пользователем создавать другие окна. Но если вам действительно нужно показать окно, то это делает функция ShowWindow следующий этап - ваш. Делайте что хотите. Можете пойти попить кофейку, а можете чего-нибудь попрограммировать. Но помните: пока еще в приложении обработка сообщений не идет, поэтому оно мертво. Свежесозданное окно торчит недвижимо на экране как символ стабильности внутриполитических процессов, и ничто не оставляет на нем следов, как на зубах Кристины Пугачевой/Орбакайте/Пресняковой/Орбакайте. и только цикл опроса очереди сообщений оживит ваше окно, и кнопочки в его заголовке начнут нажиматься, вызывая разные полезные эффекты, и границы его станут послушны движениям придавленной слева мыши, и много еще чего разного можно будет сделать с этим окном. потому что в вашем распоряжении теперь - оконная процедура win_procedure. Она принимает все и всяческие сообщения, адресуемые вашему окну всеми, кому не лень, и делает в ответ на них все, что вы пожелаете. В данном случае вы пожелали отправлять все эти сообщения обратно в систему, чтобы она сама делала с окном то, что повелел когда-то Билл. Единственное сообщение, на которое реагирует ваша оконная процедура - это WM_DESTROY, которое означает, что вам надоело любоваться окном, и вы нажали Alt+F4, или щелкнули по кнопочке с крестиком, или выбрали Close в системном меню. Да и поделом ему.

Если вы соберетесь откомпилировать приложение, вы можете сделать это с помощью MS Developer Studio, воспользовавшись нашими рекомендациями.

Только не забудьте удалить из исходного текста либо строку include windows.inc, либо содержимое файла windows.inc, если он у вас имеется в готовом виде.


Старт и завершение приложений


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

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

Ту же задачу, в принципе, решают и устаревшие функции WinExec и LoadModule. На самом деле на сегодняшний день они представляют собой всего лишь реализации той же функции CreateProcess.

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

Функция CreateProcess:

подготавливает для вновь создаваемого процесса рабочую среду (контекст, context), в которую входят: виртуальное адресное пространство, исполняемый код и данные, набор дескрипторов объектов, набор переменных окружения, базовый приоритет, минимальный и максимальный рабочие наборы страниц памяти в процессе подготовки контекста загружает PE-файл приложения. (Большинство из перечисленных выше элементов контекста определяются содержимым этого файла.) создает в рамках контекста процесса первичный поток (нить, thread) приложения и запускает его исполнение через точку входа приложения. Обычно точкой входа служит скрытая от прикладного программиста функция _WinMainCRTStartup, содержащаяся в runtime-библиотеке C/C++, подключаемой к приложению на этапе его сборки. В приложениях, написанных на ассемблере, без подключения runtime-библиотеки, точкой входа объявляется функция, подготовленная пользователем, обычно WinMain. (Подробнее...)


Функция _WinMainCRTStartup, если таковая имеется, подготавливает рабочую среду для runtime-библиотеки, а также значения параметров для функции WinMain, после чего передает управление функции WinMain.

И, наконец, функция WinMain делает все, что придумает прикладной программист.

Несколько пояснений про элементы контекста. Поскольку assembler.ru не ставит своей целью продублировать всю имеющуюся в природе документацию на Windows, то эти пояснения будут самыми общими.

виртуальное адресное пространство (virtual address space) - это набор адресов памяти от 0 до 0ffffffffh, доступных данному процессу. Каждый процесс имеет свое собственное виртуальное адресное пространство, одно для всех потоков процесса. Физически в каждый момент времени в оперативной памяти присутствует только небольшая часть из 4 Гбайт, доступных процессу, как правило, только то, что ему необходимо для работы. Для этого используется механизм страничной организации. Виртуальное адресное пространство разбито на страницы по 4 Кбайта. Страницы по мере необходимости загружаются в оперативную память или удаляются из нее. Это происходит автоматически, прозрачно для прикладного программиста исполняемый код и данные (executable code and data) - это совокупность кода и данных, загружаемых из соответствующих секций PE-файла приложения. Одни и те же фрагменты кода могут одновременно использоваться несколькими нитями процесса дескрипторы объектов (object handles) - это уникальный для данного процесса набор 32-битных идентификаторов, обеспечивающих обращение к самым разнообразным объектам рабочей среды: файлам, элементам графического интерфейса, процессам, потокам и пр. переменные окружения (environment variables) - это пришедший из DOS и UNIX набор текстовых пар имя=значение. Каждый процесс может иметь собственное окружение, либо пользоваться окружением вызвавшего его процесса базовый приоритет (base priority) - это значение в интервале от 0 до 31, определяющее приоритет каждого потока в многозадачной среде. В зависимости от базового приоритета распределяется процессорное время между всеми активными в данный момент потоками. Базовый приоритет формируется из класса приоритета, который присваивается каждому процессу и уровня приоритета, присваиваемого каждому потоку внутри процесса минимальный рабочий набор (minimum working set) - минимально необходимый для существования активного процесса набор страниц памяти максимальный рабочий набор (maximum working set) - наибольший набор страниц памяти, который может потребоваться активному процессу единовременно



В ОС Windows существует множество способов закончить работу приложения. В MSDN сказано, что это произойдет, если:

любой из потоков приложения вызовет функцию ExitProcess первичный поток приложения завершится командой ret Завершение первичного потока вызовом функции ExitThread в случае наличия других потоков не приведет к завершению приложения.

завершится (любым способом) последний из потоков приложения любой из потоков приложения вызовет функцию TerminateProcess консольное приложение получит от пользователя сигнал CTRL+C (CTRI+BREAK) завершится работа системы (shut down) или работа пользователя в системе (log off).

Надо сказать, что этот список не окончателен. Николай Критский (nkritsky@mail.ru) подсказал еще и такой экзотический, но вполне работоспособный вариант: функцией SetErrorMode отключить выдачу системных сообщений об ошибках, а затем искусственно создать такую ошибку (например, выполнив деление на 0). Возникшая при этом исключительная ситуация приведет к завершению работы приложения.

На самом деле перечисленное разнообразие - только кажущееся, так как некоторые из указанных вариантов - это всего лишь скрытый вызов все той же функции ExitProcess. Это касается и ret в первичном потоке, и CTRL+C для консольного приложения, и даже варианта Николая Критского. Фактически в этих вариантах управление передается в существующие по умолчанию runtime-модуль, процедуру поддержки консоли или обработчик исключительной ситуации, которые и вызовут функцию ExitProcess.

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

Все занятые приложением dll-библиотеки будут освобождены, то есть для каждой из них будет вызвана входная функция со параметрами отключения. При этом внутренние счетчики занятости библиотек будут декрементированы, и те библиотеки, счетчики которых достигнут 0, будут выгружены из системы. Будут закрыты все дескрипторы объектов, существовавшие в приложении Будет завершена работа всех потоков приложения Объект приложения перейдет в состояние "установлен", так что если кто-то в системе ждал завершения работы нашего приложения, сразу узнает об этом Все объекты потоков приложения также перейдут в состояние "установлен" Статус завершения процесса изменится со значения STILL_ACTIVE на значение, переданное функции ExitProcess

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

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

См. также статьи минимальное приложение и параметры функции WinMain.


@Struct.inc для mycall (ассемблер)


Это файл структурных макросов для приложения MyCall на ассемблере. Этот файл в текстовом формате вместе со всеми остальными файлами, необходимыми для компиляции приложения MyCall, содержится в zip-файле mycallab.zip (15913 байт). Имеется также Инструкция программиста.

Для получения комментариев щелкaйте по тексту или пользуйтесь групповым управлением:

if(dhtml){document.write("Все комментарии: [+][-]    Открывать: [несколько]");}

;Этот прием совместно с завершиющим ENDIF позволяет предотвратить

;ошибочное многократное включение файла @struct.inc в проект

IFNDEF @@struct_inc

@@struct_inc=1;

;Замена штатного написания структурных операторов MASM с лидирующей точкой

;на написание с лидирующим @. Повышает читаемость листингов:

;- зритильно выравнивает левый край записи, так как плотность символа @ сравнима

;с плотностью букв, чего не скажешь о точке

;- позволяет легче обнаруживать взглядом структурные операторы, так как символ @

;хорошо заметен в потоке текста

@if equ <.if>

@while equ <.while>

@break equ <.break>

@endif equ <.endif>

@endw equ <.endw>

@else equ <.else>

@elseif equ <.elseif>

@repeat equ <.repeat>

;Пара макросов, позволяющих сохранять в стеке произвольное количество регистров.

;Это очень удобно. Пара команд процессора pusha/popa, сохраняющая все регистры разом,

;применима далеко не всегда, так как восстанавливает все регистры, в том числе и те,

;содержимое которых следовало бы оставить без восстановления. А применять цепочки

;команд push и pop некрасиво - и так ассембленый листинг вытянут в длину.

;Предлагаемые макросы позволяют записывать сохраняемые регистры в строку.

;Причем порядок регистров в обоих макросах одинаков, например: @push eax,ecx,esi и

;@pop eax,ecx,esi. Это позволяет при наборе формировать команду @pop путем

;копирования строки команды @push с последующей заменой слова @push на @pop. Таким

;образом существенно снижается количество труднообнаружимых ошибок, вроде


;перепутывания порядка сохранения/восстановления регистров.

@push MACRO p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,pa,pb,pc,pd,pe,pf

FOR param,<p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,pa,pb,pc,pd,pe,pf>

IFNB <param>

push param

ENDIF

ENDM

ENDM

@pop MACRO p0,p1,p2,p3,p4,p5,p6,p7,p8,p9,pa,pb,pc,pd,pe,pf

FOR param,<pf,pe,pd,pc,pb,pa,p9,p8,p7,p6,p5,p4,p3,p2,p1,p0>

IFNB <param>

pop param

ENDIF

ENDM

ENDM

; Стандартный набор директив для прикладного программирования под win32.

;Вынесен сюда, чтобы не засорять основной листинг.

;ВНИМАНИЕ! Файл @struct.inc должен включаться первым!

.386

.Model flat,stdcall

;См. примечание к первому оператору в этом файле.

ENDIF

Варианты реализации макросов @push/@pop в версии Андрея Бордачева - в статье Макросы First и Second.




Вызов функций api


В этой статье речь идет именно о вызове системных функций API win32, а не об организации процедур в ассемблере вообще. Обсуждаемая здесь тема, следовательно, существенно уже, так как многие возможности языка при вызове функций API не применяются. Например, здесь нет необходимости обсуждать описание процедур, так как функции API написаны добрыми людьми из Misrosoft, и нам самим их писать не придется. Также не придется обсуждать вызов функций с переменным числом параметров и кое-что еще.

Вызов системных функций API win32 из программы на ассемблере подчиняется набору соглашений stdcall, ведущему свою родословную в части именования функций - от языка C, а в части передачи аргументов - от языка Pascal. С точки зрения прикладного программиста и с учетом специфики Windows и MASM эти соглашения заключаются в следующем:

регистр символов в имени функции не имеет значения. Например, функции с именами ILoveYou и iloveyou - это одна и та же функция. Здесь мы наблюдаем отличие от реализации MS Visual C++, где компилятор строго отслеживает совпадение регистра в именах используемых программистом функций с прототипами, содержащимися в системных заголовочных файлах. Сборщику же, как видим, регистр символов безразличен аргументы передаются вызываемой функции через стек. Если аргумент укладывается в 32-битное значение и не подлежит модификации вызываемой функцией, он обычно записывается в стек непосредственно. В остальных случаях программист должен разместить значение аргумента в памяти, а в стек записать 32-битный указатель на него. Таким образом, все передаваемые функции API параметры представляются 32-битными величинами, и количество байт, занимаемых в стеке для передачи аргументов, кратно четырем вызывающая программа загружает аргументы в стек последовательно, начиная с последнего, указанного в описании функции, и кончая первым. После загрузки всех аргументов программа вызывает функцию командой call за возвращение стека в исходное состояние после возврата из функции API отвечает сама эта вызываемая функция. Программисту заботиться о восстановлении указателя стека esp нет необходимости вызываемая функция API гарантированно сохраняет регистры общего назначения ebp, esi, edi. Регистр eax, как правило, содержит возвращаемое значение. Состояние остальных регистров после возврата из функции API следует считать неопределенным. (Полный набор соглашений stdcall регламентирует также сохранение системных регистров ds и ss. Однако для flat-модели памяти, используемой в win32, эти регистры значения не имеют.)


На системном уровне этот набор соглашений добавляется вот еще чем. Компилятор при формировании из исходного текста объектного файла добавляет к началу имени функции символ подчеркивания, а к концу - выражение вида @n, где n - десятичное число, равное количеству байт, занятому в стеке под аргументы функции. Так формируется технологическое имя, позволяющее осуществить связывание не только по имени вызываемой функции, но и по количеству ее аргументов. Благодаря ему при сборке обнаруживаются ошибки программиста в случае, когда он задал для вызываемой функции неправильное число аргументов. Конечно, этому сервису далеко до строгого контроля типов C++, но от огромного количества трудноустранимых ошибок он все-таки оберегает.

Соблюдение перечисленных соглашений обеспечивается компилятором автоматически. Для этого необходимо включить в начало исходного файла комбинацию директив:

.386
model flat,stdcall

Директиву .386 можно заменить на более высокую в зависимости от того, на какой процессор вы рассчитываете. Директива model определяет сегментную модель приложения. Для всех приложений win32 она должна иметь именно такой вид, как показано здесь.

Прежде чем переходить к рассмотрению примеров, следует обсудить еще одно обстоятельство. Оно связано с тем, что Windows, благодаря прозорливости ее создателей, поддерживает два типа кодировки текстовых символов - архаичную восьмибитную ANSI и перспективную шестнадцатибитную Unicode. Из-за этого программистам Microsoft пришлось для всех функций API, так или иначе работающих с текстовыми символами, писать по два программных варианта. Тот, который работает с кодировкой ANSI, имеет суффикс A, например, CreateFileA. Тот, который работает с кодировкой Unicode, имеет суффикс W, например, CreateFileW.

Обычно приложение ориентируется на работу только с одной из этих двух кодировок. Выбор производится программистом на этапе компиляции путем указания значения для специальной настроечной константы. В MS Visual C++, например, это константа UNICODE. Если она определена, то вызов функции CreateFile в тексте программы при компиляции незаметно для программиста преобразуется в вызов функции CreateFileW, в противном случае - в CreateFileA.



В программах на ассемблере, конечно, можно реализовать тот же механизм. А можно, руководствуясь девизом "handmade forever", делать это вручную.

Основываясь на вышесказанном, приведем пример вызова какой-нибудь функции API. Допустим, это будет уже упоминавшаяся функция CreateFile. Вот ее описание для C++, как оно дано в Platform SDK:

HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDistribution,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);

Фрагмент программы для кодировки ANSI, открывающий для чтения уже существующий файл, может выглядеть, например, так:

.data
file_name DB "MyFile.dat",0
.code
...
push NULL
push FILE_ATTRIBUTE_NORMAL
push OPEN_EXISTING
push NULL
push 0
push GENERIC_READ
push offset file_name
call CreateFileA
...

;имя открываемого файла

;hTemplateFile
;dwFlagsAndAttributes
;dwCreationDistribution
;lpSecurityAttributes
;dwShareMode
;dwDesiredAccess
;lpFileName

Но многим гораздо больше понравится тот же самый фрагмент так, как он выглядит в MASM 6.1+:

CreateFileA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.data
file_name DB "MyFile.dat",0
.code
...
invoke CreateFileA,offset file_name,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL
...

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


Взаимодействие экземпляров приложения


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

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

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

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

Например, при разработке приложения MyCall пришлось столкнуться с такой проблемой. Как известно, Remote Access Service (RAS), работающий в асинхронном режиме, запускается в отдельном потоке (нити), отличном от основного потока приложения. Также не менее хорошо известно, что для контроля процесса установления соединения в RAS используется callback-функция (например, RasDialFunc), которую работающий RAS вызывает при необходимости сообщить приложению о своем состоянии. Так вот, если у программиста имеется потребность передать из RasDialFunc какое-либо пользовательское сообщение окну приложения, то он может побояться воспользоваться для этой цели функцией SendMessage, полагая, что два потока, одновременно запускающие одну и ту же оконную процедуру в одном контексте - это пролог к катастрофе. Между тем такое действие вполне допустимо и безопасно, так как умная Windows, прежде чем передать управление оконной процедуре, проверяет, в каком потоке создавалось соответствующее окно, и при необходимости переключает потоки. При этом передающий поток замораживается на время обработки сообщения принимающим потоком.


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

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

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

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

Яркий пример третьей стратегии реализован в Word. Если экземпляр приложения уже запущен, и пользователь выполняет двойной щелчок по какому-нибудь файлу .doc в Проводнике, то запускаемый при этом второй экземпляр Word передает реквизиты загружаемого файла первому экземпляру и завершается, а первый экземпляр загружает файл в новое окно документа.



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

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

Поговаривают, что не всегда все было так трагично. Во времена наших предков, когда "Докторская" стоила 2.20, а Билл был еще совсем бедным со своими двумя десятками гигабаксов, заработанных на DOSе, это делалось очень просто. В win16 имел смысл параметр hPrevInstance функции WinMain. Достаточно было убедиться, что он ненулевой - и приложение знало, что оно не одиноко на этом свете. Но когда разрабатывалась win32, кто-то из главных системщиков Microsoft лопухнулся, а потом, когда все всплыло, на специальном совещании было принято решение прикрыть провинившегося и объяснить все произошедшее какими-нибудь благообразными словами, вроде: "Мы так и хотели с самого начала, а hPrevInstance сохранили для совместимости снизу вверх." Так или иначе - но hPrevInstance теперь бесполезен. А следует пользоваться средствами межпроцессного взаимодействия (Interprocess Communications), которые, к чести Microsoft, реализованы в Windows великолепно.

Способов дать знать второму экземпляру о существовании первого можно придумать много. Один из них прямо дается в MSDN в статье WinMain:

hPrevInstance

Identifies the previous instance of the application. For a Win32-based application, this parameter is always NULL. If you need to detect whether another instance already exists, create a named mutex using the CreateMutex function. If the GetLastError function returns ERROR_ALREADY_EXISTS, another instance of your application exists (it created the mutex).



Идентифицирует предыдущий экземпляр приложения. Для Win32-приложений этот параметр всегда равен NULL. Если вам необходимо определить, существует ли уже экземпляр приложения, создайте именованный объект mutex, используя функцию CreateMutex function. Если функция GetLastError вернет ERROR_ALREADY_EXISTS, другой экземпляр вашего приложения существует (так как он уже создал mutex).

Вот этой самой идеей и воспользуемся, слегка модифицировав ее:

.386 .Model flat,stdcall

include windows.inc ;заголовочный файл API;///////////////////////////////////////////////////////////// windows.inc OpenMutexA PROTO :DWORD,:DWORD,:DWORD ;прототипы функций API CreateMutexA PROTO :DWORD,:DWORD,:DWORD ExitProcess PROTO :DWORD

SYNCHRONIZE =00100000h ;константы типов доступа STANDARD_RIGHTS_REQUIRED =000f0000h MUTANT_QUERY_STATE =0001h MUTANT_ALL_ACCESS =STANDARD_RIGHTS_REQUIRED OR SYNCHRONIZE OR MUTANT_QUERY_STATE MUTEX_ALL_ACCESS =MUTANT_ALL_ACCESS

NULL =0 ;разные константы FALSE =0 TRUE =1 if(dhtml){document.all["include"].style.display="block";document.all["windows"].style.display="none";}

;///////////////////////////////////////////////////////////// WinMain EXIT_OK =0 ;коды выхода EXIT_ALREADY_EXIST =1 .const check_inst_mutex_name db "Check Instant Mutex 0001",0 ;имя mutex .data? check_inst_mutex dd ? ;дескриптор mutex .code ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::: WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show ;......................................................... проверка повторного запуска mov check_inst_mutex,0 invoke OpenMutexA,MUTEX_ALL_ACCESS,FALSE,offset check_inst_mutex_name .if(!eax) ;если mutex не существует - создать его и запустить приложение invoke CreateMutexA,NULL,TRUE,offset(check_inst_mutex_name) mov check_inst_mutex,eax .else ;если mutex существует - прервать работу приложения invoke ExitProcess,EXIT_ALREADY_EXIST .endif ;......................................................... тело приложения ;... ;... ;весь остальной текст приложения ;... ;......................................................... завершение работы .if(check_inst_mutex!=0) ;уничтожить объект mutex invoke ReleaseMutex,check_inst_mutex .endif invoke ExitProcess,EXIT_OK WinMain ENDP ;############################################################# end



Как видим, логика работы очень проста. Первым делом пытаемся открыть mutex с заданным именем.

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

Если mutex открыть не удается, значит запускаемый нами экземпляр приложения уникален, и мы смело можем продолжат его работу. Только перед этим создадим mutex, чтобы не дать запускаться последующим экземплярам.

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

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

А вот Сергей AKA The Byte Reaper (http://neptunix.narod.ru) совершенно справедливо обратил наше внимане на то, что приведенный выше пример реализации второй стратегии сильно перегружен формальностями и реверансами в сторону MSDN. Полностью сохраняет ту же функциональность, но гораздо приятнее взору настоящего ассемблерщика предложенный Сергеем вот какой вариант:

;=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= invoke CreateMutexA,NULL,0,offset(check_inst_mutex_name) invoke GetLastError .if(eax==ERROR_ALREADY_EXISTS) invoke ExitProcess,EXIT_ALREADY_EXIST .endif ;=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Идея проста предельно. Если создаваемый mutex уже присутствует в системе, то функция GetLastError вовращает код ERROR_ALREADY_EXISTS. Кстати, функция CreateMutex возвращает при этом вполне валидный дескриптор существующего mutex'а, но нас это совершенно не должно интересовать. Мы просто заканчиваем работу приложения - и все.

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



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

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

Это полностью работающее приложение. В его основу положен шаблон оконного приложения. Фрагменты, отвечающие за обеспечение уникальности экземпляра приложения и за вывод окна работающего приложения на первый план, выделены бледно-желтым фоном. Содержимое файла windows.inc не приводится (то, что он должен собой представлять, показано в предыдущем примере. Недостающие элементы следует добавить самостоятельно, по аналогии, пользуясь заголовочными файлами для C/C++).

.386 .Model flat,stdcall

include windows.inc ;заголовочный файл API

;///////////////////////////////////////////////////////////// WinMain EXIT_OK =0 ;коды выхода EXIT_ERROR =1 EXIT_ALREADY_EXIST =2

QUESTION_PRIME_HWND =1 ;запрос идентификатора окна ANSWER_PRIME_HWND =2 ;ответ идентификатора окна

.data? win_class WNDCLASSEXA{} ;структура класса главного окна main_window dd ? ;дескриптор главного окна check_inst_mutex dd ? ;дескриптор mutex'а second_instance dd ? ;признак второго экземпляра check_inst_message dd ? ;идентификатор регистрируемого сообщения loop_message MSG{} ;сообщение для цикла обработки .const app_name db "Single Instance Application",0 class_name db "Single Instance Application Class",0 check_inst_mutex_name db "Check Instant Mutex 0001",0 check_inst_message_name db "Check Instant Message 0001",0 .code ;::::::::::::::::::::::::::::::::::::::::::::::::::::::::: WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show ;......................................................... регистрация класса окна mov win_class.cbSize,sizeof(WNDCLASSEXA) mov win_class.style,CS_HREDRAW OR CS_VREDRAW mov win_class.lpfnWndProc,offset win_procedure mov win_class.cbClsExtra,0 mov win_class.cbWndExtra,0 invoke GetModuleHandleA,NULL mov win_class.hInstance,eax invoke LoadIconA,NULL,IDI_APPLICATION mov win_class.hIcon,eax mov win_class.hIconSm,eax invoke LoadCursorA,NULL,IDC_ARROW mov win_class.hCursor,NULL mov win_class.hbrBackground,COLOR_WINDOWFRAME mov win_class.lpszMenuName,NULL mov win_class.lpszClassName,offset class_name invoke RegisterClassExA,offset win_class .if(!eax) jmp abort .endif ;......................................................... создание окна invoke CreateWindowExA,NULL,\ offset class_name,\ offset app_name,\ WS_OVERLAPPEDWINDOW,\ 100,100,300,200,\ NULL,\ NULL,\ win_class.hInstance,\ NULL .if(!eax) jmp abort .endif mov main_window,eax ;.............................................. зарегистрировать глобальное сообщение mov check_inst_message,0 invoke RegisterWindowMessageA,offset check_inst_message_name .if(eax) mov check_inst_message,eax .endif ;......................................................... проверить повторный запуск mov check_inst_mutex,0 invoke OpenMutexA,MUTEX_ALL_ACCESS,FALSE,offset check_inst_mutex_name .if(!eax) mov second_instance,FALSE invoke CreateMutexA,NULL,TRUE,offset check_inst_mutex_name mov check_inst_mutex,eax .else mov second_instance,TRUE .if(check_inst_message) invoke SendMessageA,HWND_BROADCAST,check_inst_message,QUESTION_PRIME_HWND,NULL



.endif invoke ExitProcess,EXIT_ALREADY_EXIST .endif ;......................................................... вывод окна на экран invoke ShowWindow,main_window,SW_SHOW ;......................................................... инициализация ;здесь можно вставить все необходимые действия для первичной инициализации ;приложения (например, загрузку и разбор файла настроек) ;......................................................... цикл опроса очереди сообщений msg_loop: invoke GetMessageA,offset loop_message,NULL,0,0 .if(!eax) ;завершение работы .if(check_inst_mutex!=0) ;освободить mutex invoke ReleaseMutex,check_inst_mutex .endif invoke ExitProcess,loop_message.wParam .endif invoke TranslateMessage,offset loop_message invoke DispatchMessageA,offset loop_message jmp msg_loop abort: invoke ExitProcess,EXIT_ERROR WinMain ENDP

;///////////////////////////////////////////////////////////// Оконная процедура win_procedure PROC hwnd,message,w_param,l_param mov eax,message ;................................................... обработка регистрируемого сообщения .if(eax==check_inst_message) ;если получено зарегистрированное сообщение .if(check_inst_message) .if(second_instance) ;если это второй экземпляр .if(w_param==ANSWER_PRIME_HWND) ;если в сообщении получен дескриптор окна invoke SetForegroundWindow,l_param ;вывести это окно на первый план jmp worked .endif .else ;если это первый экземпляр .if(w_param==QUESTION_PRIME_HWND) ;если запрашивается дескриптор окна mov eax,hwnd ;послать ему дескриптор главного окна .if(eax==main_window) invoke SendMessageA,HWND_BROADCAST,check_inst_message,ANSWER_PRIME_HWND,main_window jmp worked .endif .endif .endif .endif jmp noworked ;........................................................ закрытие приложения .elseif(eax==WM_DESTROY) invoke PostQuitMessage,EXIT_OK jmp worked ;........................................................ обработка остальных сообщений ; .elseif(message==...) ; ... ; .elseif(message==...) ; ... ; .elseif(message==...) ; ... ;........................................................ сообщение не обработано .else noworked: invoke DefWindowProcA,hwnd,message,w_param,l_param ret ;........................................................ сообщение обработано .endif worked: xor eax,eax ret win_procedure ENDP ;############################################################# end



Общая идея такова:

регистрируется класс главного окна и создается само окно с тем, чтобы вновь запускаемое приложение могло принимать и обрабатывать сообщения с помощью оконной процедуры win_procedure регистрируется глобальное сообщение, которое впоследствии должно использоваться для обмена информацией между экземплярами приложения выполняется проверка наличия уже работающего экземпляра приложения с помощью объекта mutex если уже работающий экземпляр приложения существует - ему посылается инициирующее сообщение с параметром QUESTION_PRIME_HWND. Поскольку дескриптор окна этого экземпляра неизвестен, сообщение посылается широковещательно (HWND_BROADCAST).

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

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

См. также интересный, эффективный и простой вариант решения рассматриваемой задачи, предложенный Геннадием Майко.




Взаимодействие экземпляров приложения (вариант)


Этот материал предоставлен Геннадием Майко. Он дополняет публикацию "Взаимодействие экземпляров приложения", в которой assembler.ru излагал суть проблемы и рассматривал некоторые способы ее решения. Вариант, предложенный Геннадием, изящен и предельно прост в реализации. Приносим ему свою благодарность. Итак, слово автору:

 Добрый день,

Хотел бы предложить еще один вариант (на мой взгляд, весьма эффективный) проверки того, что приложение, написанное под Win32, было уже запущено. Этот пример навеян очень интересной и полезной книгой Jeffrey Richter "Advanced Windows" (если я не ошибаюсь, есть ее перевод на русский язык).

Идея состоит в следующем. Для приложения или DLL можно определить некоторую секцию (сегмент) как "разделяемую" (shared). Это значит, что ее содержимое будет использоваться совместно всеми экземплярами приложения. Этим можно воспользоваться для проверки факта запуска приложения во второй и более раз.

Определим в некотором отдельном сегменте переменную Cnt и проинициализируем ее 1 (или любым другим ненулевым значением). В самом начале работы приложение проверяет значение этой переменной и, если оно равно 1, присваивает ей 0. Понятно, что эта операция должна быть атомарной (во-первых, к этой переменной могут обращаться несколько программ одновременно, во-вторых, мы можем работать на многопроцессорной машине). Это можно сделать, например, с помощью команды XCHG или LOCK CMPXCGH (в последнем случае необходимо обязательно использовать префикс LOCK и определить в программе директиву .486).

Если значение этой переменной равно 1, значит программа работает первый раз. Если значение этой переменной равно 0, была запущено вторая (или более) копия приложения.

Для того, чтобы объявить некоторую секцию разделяемой, необходимо добавить к опциям компоновщика следующее: link /section:,RWS Здесь ключевым является 'S' (SHARED) в списке атрибутов сегмента.

В приложении к этому письму Вы можете найти проект для Visual C++ 6.0 с соответствующим примером (я воспользовался Вашими рекомендациями для его создания). Текст программы с соответствующими комментариями находится в файле ms.asm. В каталоге Debug есть исполняемый файл ms.exe. Запустив его один раз и открыв Task Manager, мы можем найти Image Name ms.exe в списке Processes. При втором, третьем и т.д. запуске этой программы новых процессов ms.exe в этот список не добавляется. Если в программе заменить команду jnz на jmp и пересобрать проект, то при втором и т.д. запусках добавляются новые процессы ms.exe. Обратите внимание, что запущенная программа будет работать бесконечно, поэтому, чтобы ее окончить, необходимо принудительно закончить соответствующий процесс с помощью Task Manager'a.

Работа программы проверена на двухпроцессорной машине под Windows NT 4.0.


Этот материал предоставлен Сергеем (AKA The Byte Reaper) http://www.neptunix.com. Он дополняет публикацию "Взаимодействие экземпляров приложения", в которой assembler.ru излагал суть проблемы и рассматривал некоторые способы ее решения. Вариант, предложенный Сергеем, в отличие от нашего, решает одну вполне конкретную задачу: предотвращает повторный запуск приложения, то есть реализует вторую стратегию из трех перечисленных в статье.

 Здравствуйте. Прочитал данную статью и ее обсуждение и решил предложить еще один вариант, может быть, не такой элегантный, как оба предыдущих, но все-таки работающий, короткий и примененный мною в нашей с MemoBreaker'ом программе UIN2IP.

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= secAttrib SECURITY_ATTRIBUTES <> ;... ;... ;... mov secAttrib.nLength,SIZEOF secAttrib mov secAttrib.lpSecurityDescriptor,NULL mov secAttrib.bInheritHandle,TRUE invoke CreateMutex,ADDR secAttrib,1,ADDR mutex invoke GetLastError .if eax > 0 ; Вообще-то правильнее было бы сверять еах ; c ERROR_ALREADY_EXISTS, ну да ладно... invoke ExitProcess,0 ; Если такой мьютекс уже существует - .endif ; завершаем приложение =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Не совсем, конечно, использование по прямому назначению, но чем не синхронизация? ;)

Всего наилучшего, Сергей AKA The Byte Reaper

Действительно, самая что ни на есть синхронизация. Проверено под Windows 98 и NT - действительно, второй экземпляр приложения не запускается. Под Windows 95 не проверялось, но, скорее всего, это решение работать не будет, так как параметр secAttrib в вызове функции CreateMutex в этой ОС игнорируется. Впрочем, сейчас это уже не актруально: 95-я неумолимо становятся историей.




Упомянутый в письме проект находится в файле ms.zip размером 8851 байт.

Вот исходный текст приложения (ms.asm):

; Простое приложение, которое может быть запущено только один раз. ; Однажды стартовав, работает бесконечно. Для завершения: ; NT: Откройте Task Manager. На вкладке Processes выберите ms.exe. ; Нажмите кнопку End Process. ; 95/98: Нажмите Ctrl+Alt+Del. Выберите в списке приложение Ms. ; Нажмите кнопку End Task. ; При построении проекта в командную строку link.exe следует ; включить опцию /SECTION:SHS,RWS

.386 .Model flat,stdcall

; Разделяемый сегмент SHS SEGMENT Cnt dd 1 ; Флаг: 1 - работает первый экземпляр приложения; ; 0 - запущен второй (и последующие) экземпляры. SHS ENDS

; Код .code WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show xor eax,eax ; eax = 0 xchg eax,Cnt ; eax Cnt. Атомарный обмен eax и Cnt or eax,eax ; проверить флаг jnz L_Cont ; переход к коду для первого экземпляра

; Код для второго (и последующих) экземпляров

ret ; Завершение второго (и последующих) экземпляров

; Код для первого экземпляра приложения

L_Cont: jmp L_Cont ; Бесконечный цикл WinMain ENDP

end

Несколько комментариев от assembler.ru:

1. Опция командной строки компоновщика link.exe /SECTION:name,[E][R][W][S][D][K][L][P][X] позволяет принудительно назначать атрибуты секциям PE-файла. В данном случае секции, образованной из сегмента SHS, устанавливаются атрибуты R (доступна для чтения), W (доступна для записи), S (разделяемая). Атрибут S означает, что все процессы, запущенные с помощью одного и того же исполняемого файла ("image" в терминах PE-файла), получат общий доступ к области памяти, содержащей переменную Cnt. Всякое изменение этой переменной одним процессом будет наблюдаться другими процессами.

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



Ms. exe демонстрирует одну интересную особенность операционной среды. Как видим, разделяемая секция, содержащая инициализированные данные, при загрузке приложения инициализируется только в случае, когда загружается первый экземпляр приложения. При загрузке же последующих экземпляров инициализации переменных не происходит, а они получают значения, установленные к этому моменту предыдущими экземплярами приложения. Это логично, и странно было бы, если бы было по-другому, но как же все-таки производители операционных систем умудряются все предусмотреть?

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

Немного найдется пользователей, которые способны запустить два экземпляра приложения один за другим с достаточно малым интервалом времени так, чтобы они успели вступить в конфликт при обращении к переменной Cnt. Однако настоящий ассемблерщик, конечно же, обязательно поставит префикс lock.

Про префикс lock Геннадий представил следующее пояснение. Как известно, этот префикс имеет смысл в многопроцессорных системах. Он блокирует на время выполнения команды, перед которой стоит, доступ к памяти со стороны других процессоров. Так вот, команда xchg гарантированно формирует сигнал LOCK# вне зависимости от того, имеется или нет при ней префикс lock (см. "Intel Architecture Software Developer's Manual. vol.2: Instruction Set Reference"). А вот команда cmpxchg такого сигнала не формирует, поэтому в случаях, когда в многопроцессорных системах имеется опасность одновременного доступа нескольких процессоров к одним и тем же данным, применение префикса для нее обязательно.

3. Рассматриваемый способ обеспечения уникальности экземпляра приложения имеет органически присущее ему отличие от способов, основанных на средствах межпроцессного обмена. Если скопировать ms.exe в другую папку и попытаться запустить обе копии приложения, мы обнаружим, что обе они успешно запустятся, потому что это разные image'ы, и разделения секции SHS между ними не происходит.



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

4. Разделение сегмента между экземплярами приложения можно использовать не только для обеспечения уникальности экземпляров, но и для реализации сколь угодно сложного обмена информацией между ними (третья стратегия), ведь мы можем добавить в разделяемый сегмент любой набор необходимых нам для этого переменных. Это даже удобнее, чем File Mapping, так как не требует специальной поддержки программой, не связано с использованием файловой системы, и к этим переменным можно обращаться по именам, а не по смещениям.

А вот здесь уж точно без атомарности не обойтись. Точнее, без средств межпроцессного взаимодействия. Еще точнее - без средств синхронизации процессов. И уж совсем точно - без объектов mutex и/или event. Потому что, передавая друг другу данные, процессы должны обеспечить согласованный доступ к разделяемой памяти, то есть сообщать друг другу о готовности к передаче и приему данных и не допускать обращения к неготовым или изменяемым в данный момент данным.

В целом решение, предложенное Геннадием, следует признать очень интересным и совершенно функциональным. Еще раз большое ему спасибо.


Windows.inc для mycall (ассемблер)


Это файл windows.inc, который содержит константы и прототипы функций API, используемые в приложении MyCall. Подробнее... Этот файл в текстовом формате вместе со всеми остальными файлами, необходимыми для компиляции приложения MyCall, содержится в zip-файле mycallab.zip (15913 байт). Имеется также Инструкция программиста.

Для получения комментариев щелкaйте по тексту или пользуйтесь групповым управлением:

if(dhtml){document.write("Все комментарии: [+][-]    Открывать: [несколько]");}

;КОНСТАНТЫ

FALSE=0

TRUE=1

NULL=0

SW_SHOW=5

ERROR_INVALID_HANDLE=6

WM_COMMAND=0111h

WM_MOVE=0003h

WM_DESTROY=0002h

WM_CLOSE=0010h

WM_SETTEXT=000ch

WM_USER=0400h

CB_ADDSTRING=0143h

CB_GETCURSEL=0147h

CB_SETCURSEL=014eh

CB_ERR=0ffffffffh

CB_RESETCONTENT=014bh

CBN_SELCHANGE=1

BN_CLICKED=0

SWP_NOSIZE=0001h

SWP_NOMOVE=0002h

SWP_NOZORDER=0004h

SM_CXSCREEN=0

SM_CYSCREEN=1

MB_OK=0

MB_ICONERROR=10h

IDC_ARROW=32512

COLOR_WINDOW=5

HWND_BROADCAST=0ffffh

GENERIC_READ=80000000h

GENERIC_WRITE=40000000h

OPEN_EXISTING=3

CREATE_ALWAYS=2

FILE_ATTRIBUTE_NORMAL=00000080h

INVALID_HANDLE_VALUE=0ffffffffh

GMEM_FIXED=0h

EWX_REBOOT=2

EWX_FORCE=4

DLGWINDOWEXTRA=30

;ПРОТОТИПЫ ФУНКЦИЙ API

GetModuleHandleA PROTO :DWORD

LoadIconA PROTO :DWORD,:DWORD

ExitProcess PROTO :DWORD

LoadCursorA PROTO :DWORD,:DWORD

RegisterClassExA PROTO :DWORD

MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD

CreateDialogParamA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD

GetLastError PROTO

GetCurrentProcess PROTO

ShowWindow PROTO :DWORD,:DWORD

TranslateMessage PROTO :DWORD

DispatchMessageA PROTO :DWORD

GetMessageA PROTO :DWORD,:DWORD,:DWORD,:DWORD

DefWindowProcA PROTO :DWORD,:DWORD,:DWORD,:DWORD

PostQuitMessage PROTO :DWORD

DestroyWindow PROTO :DWORD

RegisterWindowMessageA PROTO :DWORD

SendMessageA PROTO :DWORD,:DWORD,:DWORD,:DWORD

PostMessageA PROTO :DWORD,:DWORD,:DWORD,:DWORD

SetForegroundWindow PROTO :DWORD

CreateFileA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD


CloseHandle PROTO :DWORD

ReadFile PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD

WriteFile PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD

GetSystemMetrics PROTO :DWORD

SetWindowPos PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD

GetWindowRect PROTO :DWORD,:DWORD

GetDlgItem PROTO :DWORD,:DWORD

GetFileSize PROTO :DWORD,:DWORD

GlobalAlloc PROTO :DWORD,:DWORD

GlobalFree PROTO :DWORD

RasHangUpA PROTO :DWORD

Sleep PROTO :DWORD

CreateThread PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD

EnableWindow PROTO :DWORD,:DWORD

lstrcpy PROTO :DWORD,:DWORD

lstrcat PROTO :DWORD,:DWORD

RasDialA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD,:DWORD

RasGetConnectStatusA PROTO :DWORD,:DWORD

ExitWindowsEx PROTO :DWORD,:DWORD

;ЭКВИВАЛЕНТЫ ТИПОВ ДАННЫХ WINDOWS

; Удобны для сохранения синтаксиса вызова функций API, соответствующего

;документации программиста для Windows. Лидирующий символ @ требуется из-за того,

;что имена некоторых типов данных Windows совпадают с ключевыми словами ассемблера

;(например, DWORD).

@LONG equ dd

@UINT equ dd

@WNDPROC equ dd

@int equ dd

@HINSTANCE equ dd

@HICON equ dd

@HCURSOR equ dd

@HBRUSH equ dd

@LPCSTR equ dd

@HWND equ dd

@WPARAM equ dd

@LPARAM equ dd

@DWORD equ dd

;СТУКТУРЫ WINDOWS

WNDCLASSEX STRUCT 8

cbSize @UINT ?

style @UINT ?

lpfnWndProc @WNDPROC ?

cbClsExtra @int ?

cbWndExtra @int ?

hInstance @HINSTANCE ?

hIcon @HICON ?

hCursor @HCURSOR ?

hbrBackground @HBRUSH ?

lpszMenuName @LPCSTR ?

lpszClassName @LPCSTR ?

hIconSm @HICON ?

WNDCLASSEX ENDS

POINT STRUCT 8

x @LONG ?

y @LONG ?

POINT ENDS

MSG STRUCT 8

hwnd @HWND ?

message @UINT ?

wParam @WPARAM ?

lParam @LPARAM ?

time @DWORD ?

pt POINT {}

MSG ENDS

RECT STRUCT 8

left @LONG ?

top @LONG ?

right @LONG ?

bottom @LONG ?

RECT ENDS

;ДАННЫЕ REMOTE ACCESS SERVICE

RAS_MaxEntryName=256

RAS_MaxPhoneNumber=128

RAS_MaxCallbackNumber equ RAS_MaxPhoneNumber

UNLEN=256

PWLEN=256

CNLEN=15

DNLEN=CNLEN

RASDIALPARAMS STRUCT 4

dwSize @DWORD ?

szEntryName db (RAS_MaxEntryName+1)dup(?)



szPhoneNumber db (RAS_MaxPhoneNumber+1)dup(?)

szCallbackNumber db (RAS_MaxCallbackNumber+1)dup(?)

szUserName db (UNLEN+1)dup(?)

szPassword db (PWLEN+1)dup(?)

szDomain db (DNLEN+1)dup(?)

RASDIALPARAMS ENDS

RASCS_PAUSED=1000h

RASCS_DONE=2000h

RASCS_OpenPort=0

RASCS_PortOpened=1

RASCS_ConnectDevice=2

RASCS_DeviceConnected=3

RASCS_AllDevicesConnected=4

RASCS_Authenticate=5

RASCS_AuthNotify=6

RASCS_AuthRetry=7

RASCS_AuthCallback=8

RASCS_AuthChangePassword=9

RASCS_AuthProject=10

RASCS_AuthLinkSpeed=11

RASCS_AuthAck=12

RASCS_ReAuthenticate=13

RASCS_Authenticated=14

RASCS_PrepareForCallback=15

RASCS_WaitForModemReset=16

RASCS_WaitForCallback=17

RASCS_Projected=18

RASCS_StartAuthentication=19

RASCS_CallbackComplete=20

RASCS_LogonNetwork=21

RASCS_SubEntryConnected=22

RASCS_SubEntryDisconnected=23

RASCS_Interactive=RASCS_PAUSED

RASCS_RetryAuthentication=RASCS_PAUSED+1

RASCS_CallbackSetByCaller=RASCS_PAUSED+2

RASCS_PasswordExpired=RASCS_PAUSED+3

RASCS_Connected=RASCS_DONE

RASCS_Disconnected=RASCS_DONE+1

RAS_MaxDeviceType=16

RAS_MaxDeviceName=128

RAS_MaxPhoneNumber=128

RAS_MaxEntryName=256

RASCONNSTATUS STRUCT 4

dwSize @DWORD ?

rasconnstate @DWORD ?

dwError @DWORD ?

szDeviceType db (RAS_MaxDeviceType+1)dup(?)

szDeviceName db (RAS_MaxDeviceName+1)dup(?)

RASCONNSTATUS ENDS

RASCONN STRUCT 4

dwSize @DWORD ?

hrasconn @DWORD ?

szEntryName db (RAS_MaxEntryName+1)dup(?)

szDeviceType db (RAS_MaxDeviceType+1)dup(?)

szDeviceName db (RAS_MaxDeviceName+1)dup(?)

RASCONN ENDS


Зачем нужен ассемблер - дополнение Геннадия Майко


Читайте также:

статью "Зачем он нужен, этот ассемблер?" обсуждение статьи с MemoBreaker'ом

Если вы уже прочитали статью Зачем он нужен, этот ассемблер? и ее обсуждение с MemoBreaker'ом, то должны помнить, что речь там идет о том, имеет или нет смысл писать на ассемблере прикладные программы для Windows. И если этот вопрос (без сомнения, важный для некоторой маргинальной части программистского поголовья, к которой причисляем себя и мы), по-прежнему остается без ответа, то сопряженный с ним вопрос "А имеет ли смысл писать на ассемблере системные программы" особых сомнений ни у кого не вызвал. Все участники обсуждения время от времени делали реверансы в сторону драйверов устройств, обработчиков потоков данных и т.п., говоря, что, дескать, вот в них-то ого-го, йэх, ну и ну, ух ты, то есть ассемблеру самое и место. Никаких более веских аргументов, правда, привести не пытались, но консенсуса достигли.

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

Добрый день!

Прочитав вновь поднятую тему "о нужности" ассемблера, хотел бы добавить свои личные комментарии.

1. На мой взгляд, использование ассемблера полезно и оправдано в качестве "дополнения" к используемому (выбранному, навязанному) языку или среде програмирования. Я бы еще раз хотел подчеркнуть – дополнение, а не противопоставление.

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

- Микросхема MPEG-encoder'a выдавала поток данных не в такой последовательности, как было принято для данного процессора (проще говоря, порядок байт был big-endian, а не little-endian). В этом случае одна команда BSWAP рассматривалась как одна из 3-х других алтернатив вот такому, на мой взгляд, некрасивому решению, которое предлагалось на С или С++:

*ptData = ((*ptData & 0xFF) << 24) | ((*ptData & 0xFF00) << 8) | ((*ptData & 0xFF0000) >> 8) | ((*ptData & 0xFF000000) >> 24);

- Эта же микросхема генерировала длинную последовательность нулей в конце MPEG-потока данных, за сигнатурой, соответствующей окончанию этого потока. Эти нули необходимо было исключить. Использование команды SCASD также рассматривалось в качестве одной из 2-х альтернатив простейшего линейного поиска.

- В приложении и библиотеках программы для управления платой MPEG-encoder'a, написанной для Windows NT4, очень широко и успешно использовалась функция InterlockedExchangeAdd (по сути, на ней базировалось вся синхронизация взаимной работы нескольких потоков). При переносе этой программы под Windows 95 оказалось, что там этой функции нет (!). Реализация собственной функции с таким же названием и синтаксисом, написанной на ассемблере с использованием команды XADD, рассматривалась в качестве основной альтернативы полной переработки всего кода синхронизации.

Для себя я сделал вывод, что ассемблер является одним из вспомогательных инструментов разработки программ. Знание его не является обязательным, но оно желательно, так как это есть еще одна "степень свободы" при решении программистских задач.

2. Я знаю, по крайней мере, несколько микросхем, которые являются "micro-code driven". То есть внутри есть несколько очень специализированных процессоров, для которых необходимо писать программы. Здесь применение языка ассемблера весьма оправдано. Я знаю, по крайней мере, одну попытку написания и использования компилятора языка С для таких микросхем, которая (естественно) завершилась неудачей. Срок "жизни" такой микросхемы весьма недолог для того, чтобы разрабатывать, поддерживать и использовать какой-то язык высокого уровня при ее программировании.

Я понимаю, что это не имеет явного отношения к программированию на ассемблере для X86 и пример не очень соответствует обсуждаемой теме, но, по крайней мере, слово "ассемблер" в нем присутствует :)

3. Несколько комментариев к высказаным в статье областям применения ассемблера:

> - на нем удобно писать такие вещи как VxD драйвера (не знаю возможно ли это на С++)

Мне приходилось писать драйвера и на ассемблере, и на С, и на С++. Как по мне, так их "удобнее" писать на С++, но это дело вкуса и "религии".

Я могу (по памяти) привести некоторую информацию об этих проектах. Все они были написаны "с нуля" (from the scratch). Она может быть полезна при сравнении использования ассемблера и языков высокого уровня.

- Драйвер и библиотека (DLL) для специализированной платы (8 контроллеров RS-232/485, таймер, контроллер клавиатуры, несколько вспомогательных регистров), языки: asm для драйвера, С для библиотеки. Операционная система Windows 3.11/95. Система "задышала" через 6 месяцев, еще 9 месяцев система модернизировалась (были изменения в "железе" и требованиях к функциональности).

- Драйвер, библиотека и приложения для платы MPEG-encoder'a, языки: С для драйвера, С++ для библиотеки и приложения. Операционная система Windows NT4. Система "задышала" через 4 месяца, еще 2 месяца ушло на "доводку". При изменении "железа" адаптация занимала пару недель.

- Драйвер, библиотека и приложения для другой платы MPEG-encoder'a, языки: С++ для драйвера, библиотеки и приложения. Операционная система Windows NT4/2000. Система "задышала" через 3 месяца, еще 1 месяц ушел на "доводку". Драйвер изначально поддерживал работу с несколькими функционально однотипными платами и в нем использовалось и наследование, и абстрактные классы, и виртуальные функции, и статические элементы классов, т.е. практически полный набор методов ООП.

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

>- ассемблер используется в критичных к скорости выполнения программах

> (можно, конечно, и INLINE, но как сами говорили "можно и отверткой в ухе...")

Я думаю, что MPEG-encoder является "критичной к скорости выполнения" программой и она действительно требует очень много времени (например, 3 платы MPEG-encoder'ов (установленных на одном компьютере), в которых мультиплексирование аудио- и видео-данных проводилось в программе, а не в микросхеме, при одновременной работе занимали более 75 % времени работы процессоров). Но у нас не возникало даже идеи переписать все это на ассемблере! Я думаю, причина этого – требуемые очень большие временнЫе ресурсы разработки. Система просто не успела бы выйти на рынок; с другой стороны, предложения типа "поставьте мощный компьютер" не вызывали у клиентов никаких возражений.

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


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

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

Мы на 100% согласны с этими выводами, и поэтому, как раз сейчас приступая к новому прикладному проекту, уже окончательно определились с базовым языком. Конечно же, это будет ассемблер.


Зачем он нужен, этот ассемблер?


Читайте также:

обсуждение этой статьи c MemoBreaker'ом дополнение Геннадия Майко

Вопрос о том, имеет ли смысл заниматься разработкой приложений для Windows на ассемблере, довольно часто возникает в разных дискуссиях, форумах и конференциях Usenet. Как правило, обсуждающие высказывают при этом точки зрения, основанные на личных впечатлениях, которые не могут убедить никого, кроме их самих. Спектр мнений лежит в интервале от "в эпоху объектно-ориентированного программирования, на котором зиждется вся Windows, это полная дурость" и до "программировать на ассемблере под Windows легче, чем на C++".

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

Потому что этот вопрос не имеет объективного, независимого от каждого конкретного человека, ответа. Ответ на него лежит в области психологии, мироощущения.

Скажите пожалуйста, почему объектно-ориентированное программирование, имея уже довольно долгую историю, так и не заняло на сегодняшний момент подобающего ему первого и единственного места? Оно продвигается в жизнь программистским авангардом, в первую очередь Microsoft, и благодаря этим усилиям явочным порядком все-таки постепенно завоевывает мир. Некоторые из его элементов, как, например, инкапсуляция, стали очевидной нормой. Но 90% программистов, честно перебрав свой повседневный инструментарий, вряд ли найдут в нем, например, перегрузку операций, наследование, абстрактные классы, несмотря на то, что этим темам посвящена большая часть любого учебника по C++. Как следствие - перспективные технологии вроде COM встречают брюзжание и неприятие со стороны широких масс. Один руководитель серьезного коллектива разработчиков ответил на вопрос, почему его люди до сих пор пишут в стиле Фортрана, примерно так: "Некогда фигней заниматься, дел полно" (при том, что на старте в этот момент находился крупный проект, который по своим характеристикам как нельзя лучше ложился на COM).


Все дело в том, что далеко не все люди смотрят на мир объектно-ориентированно. Как не все мы одинаково годимся в поэты, диспетчеры АЭС, киллеры и мотогонщики, так не всем нам дан Богом дар абстракции. Проектируя приложение и принимая принципиальные решения, или выбирая, как построить конкретный фрагмент программы, многие видят своим мысленным взором не мир людей с характерными для каждого цветом глаз, размером кулака и профессией, а все ту же классическую фоннеймановскую архитектуру ЭВМ: АЛУ, ЗУ и прочее. Возможно, такое положение вещей определилось исторически, и когда вымрет пара-тройка поколений выпускников ФПМ и на Земле не останется ни одного кейса на винтиках, детская болезнь программирования будет навсегда забыта.

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

Что касается личностных характеристик, то попробуйте воспользоваться тестом. Шутка, конечно, но, как положено, с долей шутки.

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

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

Например, популярный у авторов учебников пример со студенческой группой. Вершиной иерархии в этом примере является класс Student, имеющий базовый набор свойств и методов. Нижележащие уровни иерархии составляют наследующие классы - староста группы, участник научного общества, член сборной университета по бейсболу и прочее. Эта иерархия поставлена с ног на голову относительно того, что мы наблюдаем в реальной жизни. Иерархии реальной жизни основаны не на общих наборах свойств у их членов, а наоборот, на различиях. Староста группы, несомненно, выше в реальной иерархии, чем рядовой студент. Миллионер выше представителя среднего класса, хоть у обоих и по две ноги и две руки.



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

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

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

В массовом сознании существует лишь один по-настоящему универсальный инструмент - C/C++. Всякий программист, работая с другими языками, постоянно имеет ввиду существование этого самого профессионального из всех профессиональных инструментов. То есть как бы ощущает себя не до конца профессионалом.

То есть выбор не так уж велик. Либо ты владеешь C/C++, и ты абсолютный профессионал, либо нет - и ты как бы немножко ненастоящий программист.

А уж осознав это, и даже, может быть, став мастерами C/C++, люди вспоминают об ассемблере. А еще чаще о нем вспоминают новички, желающие стать профессионалами и только-только выбирающие свой путь. Вот почему так часто возникают вопросы: имеет ли смысл программировать на ассемблере.

Есть и еще одно обстоятельство, стимулирующее интерес к ассемблеру. Под Windows на нем действительно программировать много легче, чем под DOS. Самое главное, благодаря flat-модели ушла в прошлое возня с сегментами памяти и сегментными регистрами. А сервис API делает теперь многое из той рутины, на которую уходила раньше большая часть рабочего времени программиста.



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

Итак, приложение называется MyCall и представляет собой простейший пользовательский интерфейс для Remote Access Service, то есть звонилку. Реализовано на C++ и на ассемблере. Имеются исходные тексты:

Вариант на C++:

main.cpp

main.h

Вариант на ассемблере:

main.asm

main.inc

@struct.inc

windows.inc

Общие для обоих вариантов:

mycall.rc

Инструкция программиста

Начнем сравнение с трудозатрат программиста. Непосредственно сравнить время разработки не представляется возможным, так как порядок разработки был такой: сначала была написана реализация на C++, а затем, пользуясь уже отработанными решениями - на ассемблере. Можно отметить только субъективное ощущение, что на ассемблере приложение писалось бы несколько дольше, процентов на 20.

Зато можно сравнить объективные показатели.

Объем исходного текста на C++ - 14 Кбайт, на ассемблере - 17,5 Кбайта. Во втором случае пришлось сделать почти на 20% больше ударов по клавишам. Туннельного синдрома, конечно, не заполучил, но ведь и приложеньице-то масенькое.

Число строк исходного текста на C++ - 384, на ассемблере - 693. Почти в два раза больше приходится скроллить листинг вверх-вниз.

Кроме того, для реализации на ассемблере пришлось тратить время на составление файла системных заголовков windows.inc.

В целом можно сделать вывод, что общие трудозатраты на разработку небольшого приложения в хаотическом стиле на ассемблере на 20-30% больше, чем на C/C++. Что касается больших проектов, то все зависит от того, насколько правильно будет организована работа и насколько принимаемые технические решения будут соответствовать задаче. То, что для C++ и для ассемблера эти решения будут разными - очевидно, поэтому заранее трудно сказать, какие из них ускорят разработку, а какие замедлят. Можно ведь и со всей мощью ООП утонуть в соблюдении формальных правил стиля, диктуемого языком, а можно и в изначально хаотическом языке найти такие приемы, которые дадут многократный прирост производительности труда. Тем более что в среде Windows объектно-ориентированное программирование становится вполне доступным ассемблеру (например, COM. Недавно в одной из конференций прозвучало мнение, что с COM работать на ассемблере проще, чем на C++. Спорно, но поспорить есть о чем.). Но a priori следует ожидать, что разработка проекта на C++ будет несколько менее трудоемка, чем на ассемблере.



Теперь сравним результаты разработки, то есть готовые приложения. Это гораздо интереснее. Допустим, вы shareware-программист. Тогда вы должны знать, что сегодня почти невозможно придумать что-нибудь новенькое, аналогов чему не нашлось бы на бесчисленных download-серверах. Чаще всего разработанная вами программа попадает в категорию, где уже лежит десяток-другой ее близких и дальних родственников. И еще вы должны знать, что, каким бы завлекательным и подробным ни было описание приложения, первый взгляд, который бросает пользователь, играющий роль буриданова осла(по Стругацким - барана), падает на графу "размер".

Об ассемблере ходят слухи, что он позволяет писать самые компактные из компактных программы. Так ли это на самом деле?

Размер исполняемого модуля mycall.exe в реализации на C++ - 8704 байта. На ассемблере - 8704 байта.

Разочарование! Никакого выигрыша!

Стоп. Давайте сначала вспомним, что речь идет о PE-файле. И имеет смысл заглянуть внутрь него: так ли уж все одинаково в том и другом случае, как кажется на первый взгляд.

имя
секции
назначение
секции
размер в файле размер в памяти
C++(speed) C++(size) asm C++(speed) C++(size) asm
.text код 1000h (4096) 0e00h (3584) 0e00h (3584) 0f22h (3874) 0c84h (3204) 0c54h (3156)
.rdata константы нет нет 0400h (1024) нет нет 0315h (789)
.data переменные 0400h (1024) 0400h (1024) 0 042ch (1068) 042ch (1068) 05c1h (1473)
.idata данные импорта 0400h (1024) 0400h (1024) 0400h (1024) 03eeh (1006) 03eeh (1006) 03eah (1002)
.rsrc ресурсы 0800h (2048) 0800h (2048) 0800h (2048) 0648h (1608) 0648h (1608) 0648h (1608)
В этой таблице перечислены все секции PE-файла приложения в каждой из трех реализаций: на C++ с оптимизацией по времени выполнения, на C++ с оптимизацией по размеру и на ассемблере.

Кстати, размер исполняемого модуля в реализации C++ с оптимизацией по времени выполнения несколько больше - 9216 байт. Но при программировании приложений для Windows такую оптимизацию использовать обычно нет смысла, так как многозадачная архитектура и механизм сообщений отнимают куда больше временного ресурса, чем может дать выигрыш от нее.



Вот что видно из этой таблицы:

размер кода в реализации на ассемблере немного меньше, чем в реализации на C++(size) и существенно, на 20%, меньше, чем в реализации C++(speed) различия в размере всех остальных секций не принципиальны

А равенство размеров исполняемых модулей для C++(size) и ассемблера объясняется тем, что размеры секций в PE-файле выравниваются на ближайшую большую величину, по умолчанию - 200h байт.

Итак, пора подвести итог вышесказанному:

Разработка приложения на ассемблере несколько более трудоемка, чем на C++. Однако нельзя сказать, что это различие настолько велико, что C++ имеет безусловное преимущество Размер получаемого приложения на ассемблере сравним с размером приложения на C++ при условии, что приложение на C++ компилировалось с оптимизацией по размеру кода. Размер получаемого приложения на ассемблере примерно на 20% меньше, чем размер приложения на C++, компилированного с оптимизацией по скорости выполнения. Однако этот выигрыш получается только за счет кода.

Решать, стоит ли заниматься разработкой приложений на ассемблере, должен каждый для себя сам. Существенных объективных преимуществ в разработке приложений для Windows перед C++ ассемблер не дает.