<<
>>

Каркасное Windows-приложение на C/C++

Обсуждение вопросов программирования для Windows на ассемблере начнем с об- суждения программы на языке C/C++. Не нужно удивляться такому подходу — «цель оправдывает средства». Нам необходимо, во-первых, понять общие принци- пы построения оконных Windows-приложений.
Во-вторых, разобраться с тем, ка- кие средства ассемблера при этом используются. Добиваться этих целей без пред- варительного обсуждения нецелесообразно. Сделаем это мягко и ненавязчиво, через рассказ о построении минимального Windows-приложения на языке C/C++. В ходе его разработки мы введем необходимую терминологию и сможем больше внима- ния уделить логике работы Windows-приложения, а не деталям его реализации. После этого мы с относительной легкостью разработаем эквивалентное приложе- ние на ассемблере. Приступая к разработке первого (и не только) Windows-приложения, важно понимать, что сам язык программирования мало влияет на его общую структуру. Это обстоятельство, кстати, и позволит нам чуть позже с относительной легкостью сменить инструментальное средство разработки Windows-приложений с C/C++ на ассемблер.
Минимальное приложение Windows состоит из трех частей: * главной функции; . цикла обработки сообщений; оконной функции. Выполнение любого оконного Windows-приложения начинается с главной функ- ции. Она содержит код, осуществляющий настройку (инициализацию) приложе- ния в среде Windows. Видимым для пользователя результатом работы главной функции является появление на экране графического объекта в виде окна. Послед- ним действием кода главной функции является создание цикла обработки сообще- ний. После его создания приложение становится пассивным и начинает взаимо- действовать с внешним миром посредством специальным образом оформленных данных — сообщений.
Обработка поступающих приложению сообщений осуществ- ляется специальной функцией, называемой оконной. Оконная функция уникаль- на тем, что может быть вызвана только из операционной системы, а не из приложе- ния, которое ее содержит (функция обратного вызова). Тело оконной функции имеет определенную структуру, о которой мы поговорим далее. Таким образом, Windows-приложение, как минимум, должно состоять из трех перечисленных эле- ментов. В листинге 16.1 приведен вариант минимального приложения на языке C/C++.

Листинг 16.1 (продолжение)

Рассмотрим более подробно суть действий, выполняемых каждым из трех эле- ментов Windows-приложения. В листинге 16.1 видно, что минимальное Windows- приложение на языке C++ состоит из двух функций: главной — WinMain и окон- ной — WindowProc. Цель WinMain — сообщить системе о новом для нее приложении, его свойствах и особенностях. Для достижения этой цели WinMain выполняет опре- деленную последовательность действий. 1. Определяет и регистрирует класс окна приложения. 2. Создает и отображает окно приложения зарегистрированного класса. 3. Создает и запускает цикл обработки сообщений для приложения. 4. Завершает программу при получении оконной функцией соответствующего сообщения. Оконная функция получает все сообщения, предназначенные данному окну, и обрабатывает их, возможно, вызывая для этого другие функции. Видимая часть работы каркасного приложения заключается в создании нового окна на экране. Оно отвечает всем требованиям стандартного окна Windows-при- ложения, то есть его можно развернуть, свернуть, изменить размер, переместить в другое место экрана и т. д. Для этого, как вы увидите, нам не придется написать ни строчки кода, а нужно будет всего лишь удовлетворить определенные требова- ния, предъявляемые к приложениям со стороны Windows.

Мы не будем больше обсуждать код листинга 16.1, так как это довольно по- дробно и полно сделано в других источниках. Взамен этого мы пойдем по другому пути — заглянем за «фасад» приведенного Windows-приложения и посмотрим на работу, выполняемую компилятором по формированию соответствующего испол- няемого кода. Причем сделать это целесообразно на двух этапах: в процессе фор- мирования объектного кода и после формирования загрузочного модуля. В процессе формирования объектного модуля компилятор преобразует исход- ный текст на языке C/C++ в эквивалентный текст на языке ассемблера. В контек- сте нашего обсуждения это достаточно ценная информация. Для того чтобы полу- чить такой текст, необходимо в командной строке компилятора задать специальный ключ ДА (см. make-файл в каталоге дацной главы с программой рrglб_l.срр среди файлов, прилагаемых к книге1): с! /FA ... рrgl6_l.срр Полученный текст на ассемблере ценен тем, что в нем каждой строке исходного текста программы на C/C++ сопоставляется текст на ассемблере. В листинге 16.2 приведен фрагмент этого файла (рrдlб_l.аsm), а полный его текст находится в ка- талоге \Lеssопlб\рrдlб_l\с среди файлов, прилагаемых к книге.
Листинг 16.2. Фрагмент ассемблерного представления исходного файла рrд16_1.срр

Lf вяоdц '■ tf⅜зЗеssэwэiвisuεji dшL aid анома цвэ хвэ цsпd [dqэ]$Зsцdl~Уld анома 'хвэ вэ[ 9Х вяоdхо ∙‘ εuбzu эс _ _ хвэ 'хвэ 9SЭq 9Т⅜VэSвssэц}Ээ dШL aid анома 11во хвэ цsпd [dqз]$Sswdi aid аЖЖI 'хвэ вэх О цsпd О цsпd О цsпd ■■ZIL6Z1% щ вяоdхэ : t?⅞морu iмэ⅜εрd∩^^dшL^^aid анома ЦВD хвэ цsпd [dqэ]$рuмq~aid аНОМа 'хвэ лот Zf вяоdхэ ·' 8⅞морu l;⅛⅝моl|S dшl—aid анома Цво хвэ цsпd [dqэ]$рuмч~aid анома 'хвэ лот хεэqsпd [dqэ]$моqsршэu^aid анома 'хвэ лот lf вяоdхэ хвэ ' [dqэ]$рuмч^d∙l∙d аюма лош 8frйvхзморui∙мэqвэjэ dшi aid (Ж)М Цво О qsпd морu!∙мssεгэzs: vаvdi⅞ёiморu iмssεiэzsi : i\∩d ХНSННО Цsпd 0U6гэS$:iVПd 1ЗSddО Чsпd Н0000iэ00:гs659sεх чsпd H00000008' 8Ч9εцтε- цsпd нооооооо8∙ 8ч9ε8чччхε- чsпa H00000008 ·' 8Ч9ε8Чεεхε- чsпd Н00000008: 899ε8чεчiε- чsпd О цsпd О цsпd хвэ цsпd [dqэ]$qsuiч~aid ачома 'хвэ лот о цsпd fj£ вяоdхэ '■ :SвZ6Zl$ И вяоdхэ ■ f69бгч$ ■ duit ХРЭ ;XPЭ JОХ тsхоdхэ ! εоεбεч$ эпt ХРЭ ⅛э хтз'ххэ хzлош t?@vхэssε\ЭJЭяsLЗэу^dшL">аd анома II ^ хвэ цsпd [dqэ] $Т,эм~aid ачома 'хвэ вэ[ ZZ вяоdiЭ '■ хвэ ∙ [гε+dqэ]_$iэsГ}ыd анома лош tr⅞iqээ[qо>|эоqsqэ9 duн aid анома цвэ О цsпd 81 вяоdхэ : хвэ ■ [t-г+dqэ]$iэм^aid анома лош 8йvuоэiрεоη dun aid а⅜iома uеэ 0 цsпd (эинэжиоttоdи)Z'9\.

jниiоиi/ θdθi/дiлiθоов вн иинθжоi/иdu-sморui/\л эинвИεоQ'91. вавi/j ZL£

продолжение &

Листинг 16.2 (продолжение) pop ebx leave ret 16 ;ОООООО1 ОН ?Wi пdоwРrос@@YGJРАUНWND_@@IIJ@Z ЕNDР;WiпdоwРrос _TEXT ENDS . END Беглый взгляд на код листинга 16.2 показывает, что он оформлен как полно- ценная программа на ассемблере. Код программы был сгенерирован компилято- ром Visual C++ 6.0 от Microsoft, поэтому транслятор МАSМ может сделать из него загрузочный модуль без дополнительного редактирования. Для этого компания Microsoft даже подготовила специальный включаемый файл listing.іпс (он нахо- дится в каталоге пакета VC++ 4.0:. .\Мsdеv\iпсludе, а также среди файлов, прилага- емых к книге). Таким образом, имея исходный файл Windows-приложения на языке C/C++, можно получить текст на языке ассемблера. Теоретически, на его основе впослед- ствии можно сформировать функционально эквивалентный исполняемый модуль. Более того, если задаться такой целью, то не составит большого труда переделать его для обработки транслятором TASM 5.0. Но не все так просто. Готов расстроить читателя, но мы изложили очень упро- щенный подход к разработке Windows-приложения на ассемблере. Полученный код можно рассматривать лишь как схему (шаблон) такого приложения. Реально все несколько сложнее. Прежде чем мы объясним имеющиеся проблемы, прове- дем еще один эксперимент — дизассемблируем исполняемый модуль программы рrglб_l.срр. Причем сделать это нужно тем дизассемблером, который «понимает» интерфейс Win32 API. В данной книге для этой цели был использован дизассемб- лер IDA. Дизассемблированный с его помощью файл можно сохранить как лис- тинг (.1st) и как исходный текст ассемблера (.asm). Это говорит о том, что IDA также пытается по загрузочному модулю построить файл, пригодный для повтор- ной трансляции. В начале дизассемблированного им текста даже приводятся ре- комендации по использованию соответствующих параметров транслятора TASM. В контексте нашего изложения наибольший интерес представляет файл лис- тинга (.1st), формируемый IDA. Анализ его содержимого позволяет изнутри по- смотреть на результаты процесса формирования исполняемого модуля трансля- тором и редактором связей. Файл листинга в своей левой части содержит колонку с адресными смещениями команд. Все метки и символические имена в дизассем- блированном тексте формируются с учетом этих смещений, поэтому данный файл удобно использовать для анализа. Файл листинга IDA имеет большой размер, поэтому привести весь его текст в книге невозможно, да это и не нужно. Для нас интерес представляют лишь от- дельные фрагменты этого файла (листинг 16.3). Полностью Дизассемблированный код программы рrglб_l.срр находится среди фалов, прилагаемых к книге. Листинг 16.3. Фрагменты дизассемблированного кода каркасного Windows-приложения (дизассемблер IDA) ; Этот файл сгенерирован интерактивным дизассемблером(IDА) ; Copyright (с) 1997 by DаtаRеsсuе sрrl,

Листинг 16.3 (продолжение)

Текст листинга 16.3 дает богатую пищу для размышлений. Его обсуждение нуж- но начать с последней строки. По правилам ассемблера, в последней директиве программы END должна стоять метка первой исполняемой команды. В нашем слу- чае это метка start. Если теперь посмотреть на место в тексте, куда она ссылается, то мы увидим, что в данной строке содержится директива PROC, обозначающая на- чало процедуры с именем start. Можно сделать вывод, что исполнение программы начинается именно с этого места. Обратите внимание на то, что в теле процедуры start содержатся многочисленные вызовы функций.

В листинге 16.3 показаны только вызовы функций API, но если вы посмотрите полный вариант листинга 16.3, находящийся среди файлов к книге, то увидите, что в нем содержатся вызовы множества других функций. Названия этих функ- ций начинаются символом подчеркивания (_), в отличие от названий функций API, которым предшествует префикс переопределения сегмента, например: ds:GеtVеrsiоп. Задержимся немного и посмотрим внимательно на листинг 16.1, в ча- стности, нас интересуют функции, к которым идет обращение. Видно, что в исход- ном тексте Windows-приложения, написанном на C/C++, нет и намека на какие- либо функции, за исключением функций API. Более того, вызов многих из функций API, которые присутствуют в дизассемблированном тексте, отсутствует в листин- ге 16.1. Отсюда следует естественный вывод о том, что их вставил компилятор C/C++ и, очевидно, они предназначены для инициализации среды, в которой бу- дет функционировать программа. А так ли они необходимы? Забегая вперед, ска- жем, что нет. Но и отказаться от них нельзя, так как они фактически «навязывают- ся» нам компилятором C/C++. В этом и состоит одна из главных причин того, что исполняемые модули на ассемблере по размеру в несколько раз меньше функцио- нально эквивалентных модулей на языках высокого уровня. Можно сказать, что приложение, написанное на ассемблере, не содержит избыточного кода. Отметьте еще один характерный момент относительно функций API. Кодовый сегмент ис- полняемого модуля содержит только команды дальнего перехода к этим функци- ям. Сами же функции находятся в отдельном файле — библиотеке DLL. В конце листинга 16.3 содержится таблица, в которой описано местонахождение всех им- портируемых функций API. Проследим за действиями, которые производит код исполняемого модуля, фор- мируемый компилятором Visual C++ для инициализации Windows-приложения. Для этого откройте файл рrglб_l.lst с дизассемблированным текстом программы на C/C++, находящийся среди прилагаемых к книге файлов, и переместитесь на начало процедуры start. 1. Вызов функции API GetVersion[15] для определения текущей версии Windows и не- которой другой информации о системе. В настоящее время существует более информативная версия данной функции — GеtVеrsiопЕх. 2. Инициализация кучи. Используется как для динамического выделения памя- ти функциями языков C/C++, так и для организации ввода-вывода на низком уровне. 3. Вызов функции API GetCommandLineAдля получения указателя на командную строку, с помощью которой было вызвано данное приложение. 4. Вызов функции API GetEnvironmentStringsA для получения указателя на блок с переменными окружения. 5. Инициализация глобальных переменных периода выполнения. 6. Вызов функции API GеtStаrtuрIпfоАдля заполнения структуры, поля которой определяют свойства основного окна приложения. 7. Вызов функции API GеtМоdulеНапdlеАдля получения базового адреса, по кото- рому загружен данный исполняемый модуль. 8. Вызов функции WiпМа1п. Как можно судить по способу вызова и внутреннему названию, _WinMain@16 не является функцией API. Это локальная функция данного исполняемого модуля. Территориально она расположена в начале диз- ассемблированного текста (см. листинг 16.3). Позже обсудим ее подробнее. Главный вывод приведенных рассуждений — текст процедуры start исполняе- мого модуля не соответствует исходному тексту программы на C/C++. В него до- бавлены локальные функции и функции Win32 API, которые вызываются в ис- полняемом модуле, например GetVersion, GetCommandLineA и т. д. Это и есть так называемый стартовый код программы. Если продолжить изучение этого стар- тового кода в файле рrglб_l.lst, то можно увидеть вызов функции WiпМаiп@lб. Найдем теперь тело этой функции в тексте. Даже беглое сравнение функции_WiпМаiп@lб (см. листинг 16.3) и функции WinMain (см. листинг 16.1) показывает, что мы нашли место, где содержится код ассемблера, функционально эквивалентный коду на C/C++. В частности, хорошо видно, что в_____ ^^^Маiп@lб вызываются именно те функции (и никакие другие), что и в функции WinMain программы на C/C++. Мы не будем сейчас обсуждать порядок и цель вызова каждой из этих функ- ций, как это делалось для стартовой процедуры, по той причине, что следующий наш шаг — это создание программы на языке ассемблера. В процессе ее разработки мы и рассмотрим эти вопросы достаточно подробно. Сейчас нам важно сделать вывод, что функция WiпМаiп не имеет прямого отношения к функциям Win32 API. Эта функция используется лишь для того, чтобы компилятор мог сгенерировать код, выполняющий инициализацию приложения. Кстати, если вы внимательно посмотрите на листинг 16.3, то без труда обнару- жите другие фрагменты кода исполняемого файла, прямо соответствующие тек- сту исходного файла на C/C++. Например, в качестве упражнения найдите текст оконной функции. В заключение обсуждения обратите внимание на код завершения программы: 004012C6 call _WiпМаiп@16 004012СВ push eax 004012CC call _ехit Видно, что для завершения приложения вызывается процедура______ exit. Код, ко- торый она содержит, является обязательным для корректного завершения любого Windows-приложения. Более подробно мы его обсудим при разработке каркасно- го Windows-приложения на языке ассемблера.

<< | >>
Источник: В. И. Юров. Assembler. Учебник для вузов. 2-е изд. 2003

Еще по теме Каркасное Windows-приложение на C/C++:

  1. М.Руссинович, Д.Соломон. Внутреннее устройство Microsoft Windows (главы 1–4), 2005
  2. ПРИЛОЖЕНИЯ
  3. Приложения
  4. ПРИЛОЖЕНИЕ
  5. Приложение
  6. Приложение 1.
  7. Приложение 3.
  8. Приложение 4.
  9. Приложение 5.
  10. ПРИЛОЖЕНИЕ 1