Глава 7. Использование ассемблера с языками высокого уровня
Данная глава посвящена вопросам использования ассемблера с языками высокого уровня. К сожалению, многие современные программисты, не зная языка ассемблера или не зная, как его использовать с языками высокого уровня, лишены мощного и гибкого инструмента программирования. Я бы сказал так: специалист по программированию на любом языке программирования должен владеть ассемблером как вторым инструментом. Это похоже на то, что изучение европейских языков в идеале должно предваряться изучением основ латыни.
Вообще, стыковка ассемблера с языками высокого уровня зиждется на трех китах: согласование имен, согласование параметров, согласование вызовов. Остановимся сначала на последнем "ките", т.е. на согласовании вызовов.
Согласование вызовов. В операционной системе MS DOS вызываемая процедура могла находиться
либо в том же сегменте, что и команда вызова, тогда вызов назывался близким (NEAR)
или внутрисегментным, либо в другом сегменте, тогда вызов назывался дальним (FAR)
или межсегментным. Разница заключалась в том, что адрес в первом случае формировался из двух
байт, а во втором - из четырех байт. Соответственно, возврат из процедуры мог быть либо близким
(RETN), т.е. адрес возврата формировался на основе двух байт, взятых из стека,
либо дальним (RETF), и в этом случае адрес формировался на основе четырех байт,
взятых опять же из стека. Ясно, что вызов и возврат должны быть согласованы друг с другом. В
рамках единой программы это, как правило, не вызывало больших проблем. Но вот когда необходимо
было подключить или какую-то библиотеку, или объектный модуль, здесь могли возникнуть трудности.
Если в объектном модуле возврат осуществлялся по RETN, вы должны были компоновать
объектные модули так, чтобы сегмент, где находится процедура, был объединен с сегментом, откуда
осуществляется вызов. Вызов в этом случае, разумеется, должен быть близким (см. [1]). Если же
возврат из процедуры осуществлялся по команде RETF, то и вызов этой процедуры
должен быть дальним. При этом при компоновке вызов и сама процедура должны были попасть в
разные сегменты. Данная проблема усугублялась еще и тем, что ошибка обнаруживалась не при
компоновке, а при исполнении программы. С этим были связаны и так называемые модели памяти в
языке Си, что также было головной болью многих начинающих программистов. Если, кстати, Вы
посмотрите на каталог библиотек Си для DOS, то обнаружите, что для каждой модели памяти там
существовала своя библиотека. Сегментация памяти приводила в Си еще к одной проблеме - проблеме
указателей, но это уже совсем другая история. В Турбо Паскале пошли по другому пути. Там
приняли, что в программе должен существовать один сегмент данных и несколько сегментов кода.
Если же Вам не хватало одного сегмента для хранения данных, то предлагалось использовать
динамическую память. При переходе к Windows мы получили значительный подарок в виде плоской
модели памяти. Теперь все вызовы по типу являются близкими, т.е. осуществляющимися в рамках одного
огромного сегмента. Тем самым была снята проблема согласования вызовов, и мы более к этой проблеме
обращаться не будем.
Согласование имен. Согласование вызовов, как мы убедились, снято с повестки дня, а вот согласование имен год от года только усложнялось. Кое-что Вы уже знаете. Транслятор MASM, как известно, добавляет в конце имени @N, где N количество передаваемых в стек параметров. То же делает и компилятор Visual C++. Т.о. трудности возникают уже при согласовании двух ассемблерных модулей. В этом смысле TASM является более гибким компилятором, т.к. при желании в конце любого имени можно добавить @N, тем самым согласовав имена.
Другая проблема - подчеркивание перед именем. Транслятор MASM генерирует подчеркивание автоматически, если в начале программы устанавливается тип вызова stdcall (Standard Call, т.е. стандартный вызов). Транслятор TASM этого не делает, следовательно, при необходимости это нужно делать прямо в тексте программы, что, на мой взгляд, является положительным моментом. Интересно, что между фирмами Borland и Microsoft здесь полное разночтение.
Еще одна проблема - согласование заглавных и прописных букв. Как Вы помните, при трансляции с помощью TASM мы используем ключ /ml как раз для того, чтобы различать буквы прописные и заглавные. Транслятор MASM делает это автоматически. Как известно, и в стандарте языка Си с самого начала предполагалось различие между заглавными и прописными буквами. В Паскале же прописные и заглавные буквы не различаются. В этом есть своя логика: Турбо Паскаль и Delphi не создают стандартных объектных модулей, зато могут подключать их. При создании же динамических библиотек туда помещается имя так, как оно указано в заголовке процедуры.
Наконец последняя проблема, связанная с согласованием имен, - это уточняющие имена в Си++. Дело в том, что в Си++ возможна так называемая перегрузка. Это значит, что одно и тоже имя может относиться к разным функциям. В тексте программы эти функции отличаются друг от друга по количеству и типу параметров и типу возвращаемого значения. Поэтому компилятор Си++ автоматически делает в конце имени добавку - так, чтобы разные по смыслу функции различались при компоновке. Разумеется, фирмы Borland и Microsoft и тут не пожелали согласовать свои позиции и делают в конце имени совершенно разные добавки. Обойти эту проблему не так сложно, нужно использовать модификатор EXTERN "С" (см. примеры далее).
Согласование параметров. В таблице ниже представлены основные соглашения по передаче параметров в процедуру. Заметим в этой связи, что во всех наших ассемблерных программах мы указывали тип передачи параметров как stdcall. Однако, по сути, это никак и нигде не использовалось - так передача и извлечение параметров делалась нами явно, без помощи транслятора. Когда мы имеем дело с языками высокого уровня, это необходимо учитывать и знать, как работают те или иные соглашения.
Таблица, представляющая соглашения о вызовах
Соглашение | Параметры | Очистка стека | Регистры |
---|---|---|---|
Pascal (конвенция языка Паскаль) | Слева направо | Процедура | Нет |
Register (быстрый или регистровый вызов) | Слева направо | Процедура | Задействованы три регистра (EAX,EDX,ECX), далее стек |
Cdecl (конвенция С) | Справа налево | Вызывающая программа | Нет |
Stdcall (стандартный вызов) | Справа налево | Процедура | Нет |
Таблица довольно ясно объясняет соглашения о передаче параметров, и здесь более добавить нечего.
Остановлюсь еще на весьма важном моменте - типе возвращаемых функцией данных. С точки зрения ассемблера здесь все предельно просто: в регистре EAX возвращается значение, которое может быть либо числом, либо указателем на некую переменную или структуру. Если возвращаемое число типа WORD, то оно содержится в младшем слове регистра EAX. Однако имея дело с Си, Вам надо очень аккуратно обращаться с такой проблемой, как преобразование типов. Преобразование типов — это целая наука, на которой мы не можем останавливаться в данной книге.
I
В данном разделе рассматривается простой модуль на языке ассемблера, содержащий процедуру, копирующую одну строку в другую. Мы подсоединяем этот модуль к программам, написанным на языке Си и Паскаль, с использованием трех трансляторов: Borland C++ 5.02, Visual C++ 6.0, Delphi 5.0.
1) Borland C++ 5.0
Функцию, вызываемую из модуля, написанного на языке ассемблера, мы объявляем при помощи модификаторов extern "С" и stdcall. Поскольку модуль на языке ассемблера транслируется с помощью транслятора TASM, проблемы с подчеркиванием не возникает. Тип вызова stdcall предполагает, что стек освобождается в вызываемой процедуре. В ассемблерном модуле вызываемая процедура должна быть дополнительно объявлена при помощи директивы PUBLIC.
// файл copyc.cpp #include <windows.h> #include <stdio.h> extern "C" _stdcall COPYSTR(char *, char *); void main() { char s1[100]; char *s2="Privet!"; printf("%s\n",(char *)COPYSTR(s1, s2)); ExitProcess(0); } ; файл copy.asm .386P ; эта процедура будет вызываться из внешнего модуля PUBLIC COPYSTR ; плоская модель .MODEL FLAT, stdcall _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; процедура копирования одной строки в другую ; строка, куда копировать [EBP+08Н] ; строка, что копировать [EBP+0CН] ; не учитывает длину строки, куда производится копирование COPYSTR PROC PUSH EBP MOV EBP,ESP MOV ESI,DWORD PTR [EBP+0CH] MOV EDI,DWORD PTR [EBP+08H] L1: MOV AL,BYTE PTR [ESI] MOV BYTE PTR [EDI],AL CMP AL,0 JE L2 INC ESI INC EDI JMP L1 L2: MOV EAX,DWORD PTR [EBP+08H] POP EBP RET 8 COPYSTR ENDP _TEXT ENDS END
Трансляция модулей на 3.7.1.
1-й способ.
В начале TASM32 /ml copy.asm
Затем в проект Borland C++ включить файл copy.obj.
2-й способ.
В проект включить сразу файл copy.asm, тогда транслятор автоматически вызовет
TASM32.EXE.
3-й способ. Трансляция из командной строки. Предварительно готовим командный файл copy.
Содержимое файла:
copyc.cpp copy.objДалее вызываем bcc32 @сору.
4-й способ. В командном файле вместо copy.obj помещаем файл copy.asm.
2) Visual C++ 6.0
Текст на языке C++ не изменится, а вот ассемблерный текст придется изменить. Дело здесь в том, что транслятор Си автоматически добавит в конец вызываемой функции @8. В этом случае пришлось отказаться от нашей обычной детализации программы и положится на транслятор MASM. Для этого потребовалось явно указать параметры при объявлении процедуры. Как только это было сделано, транслятор взял часть работы на себя, кроме того, к имени COPYSTR он, естественно, добавил @8. Лишний раз подчеркну, что при таком объявлении не нужно явно устанавливать регистр EBP и освобождать стек - за нас все делает транслятор.
; файл proc.asm .386P .MODEL FLAT, stdcall PUBLIC COPYSTR ; плоская модель _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; процедура копирования одной строки в другую ; строка, куда копировать [EBP+08H] ; строка, что копировать [EBP+0CH] ; не учитывает длину строки, куда производится копирование ; явное указывание параметров COPYSTR PROC str1:DWORD, str2:DWORD MOV ESI,str2 ; DWORD PTR [EBP+0CH] MOV EDI,str1 ; DWORD PTR [EBP+08H] L1: MOV AL,BYTE PTR [ESI] MOV BYTE PTR [EDI],AL CMP AL,0 JE L2 INC ESI INC EDI JMP L1 L2: MOV EAX,DWORD PTR [EBP+08H] RET COPYSTR ENDP _TEXT ENDS END
Puc. 3.7.2. Модуль на языке ассемблера для компоновки с помощью пакета Visual C++ 6.0.
Комментарий к программе на Рис. 3.7.2.
Явное указание параметров в заголовке процедуры приводит:
- К имени процедуры в объектном модуле автоматически добавляется @8 (а не @0).
- Ассемблер автоматически управляется со стеком.
Трансляция.
- В проект Visual C++ включается уже объектный модуль, откомпилированный первоначально с помощью ml.exe.
- Если Вы хотите включить в проект ассемблерный текст. Вам придется для него указать способ трансляции, т.е. командную строку для программы ML.EXE.
3) Delphi 5.0
Транслятор Delphi также вносит незначительные нюансы в данную проблему. Для сегмента кода нам придется взять название CODE. Из-за того, что строки Паскаль понимает несколько иначе, чем, скажем, Си, в качестве строк мне пришлось взять символьный массив. Впрочем, оператор writeln оказался довольно интеллектуальным и понял все с полуслова. Обратите внимание, что директива stdcall используется и здесь.
{ программа COPYD.PAS, использующая ассемблер путем подключения объектного модуля } program Project2; uses SysUtils; {$APPTYPE CONSOLE} {$L 'copy.OBJ'} procedure COPYSTR(s1,s2 : PChar); stdcall; EXTERNAL; var s1,s2 : array[1..30] of char; begin s2[1] ='P'; s2[2] ='r'; s2[3] ='i'; s2[4] ='v'; s2[5] ='e'; s2[6] ='t'; s2[7] = char(0); COPYSTR(addr(s1[1]),addr(s2[1])) ; Writeln(s1); end. ; файл proc.asm .386P .MODEL FLAT, stdcall PUBLIC COPYSTR ; плоская модель CODE SEGMENT DWORD PUBLIC USE32 'CODE' ; процедура копирования одной строки в другую ; строка, куда копировать [EBP+08Н] ; строка, что копировать [EBP+0CH] ; не учитывает длину строки, куда производится копирование COPYSTR PROC PUSH EBP MOV EBP,ESP MOV ESI,DWORD PTR [EBP+0CH] MOV EDI,DWORD PTR [EBP+08H] L1: MOV AL,BYTE PTR [ESI] MOV BYTE PTR [EDI],AL CMP AL,0 JE L2 INC ESI INC EDI JMP L1 L2: MOV EAX,DWORD PTR [EBP+08H] POP EBP RET 8 COPYSTR ENDP CODE ENDS END
Puc. 3.7.3. Пример подключения объектного модуля к программе на Delphi.
Трансляция.
Вам достаточно только подготовить объектный модуль (с помощью транслятора TASM32) и указать его в
директиве {$L copy.obj} - далее транслятор сделает все сам.
II
В этом разделе используется другой тип вызова - быстрый, или регистровый. В соответствии с таблицей в начале главы, этот тип вызова предполагает, что три первых параметра будут передаваться в регистрах (EAX,EDX,ECX), а остальные в стеке, как и в случае соглашения stdcall. При этом если стек был задействован, освобождение его возлагается на вызываемую процедуру. Есть еще один нюанс. В случае быстрого вызова транслятор Си добавляет к имени значок @ спереди, что мы естественно учитываем в ассемблерном модуле.
#include <windows.h> #include <stdio.h> // файл ADDC.cpp // объявляется внешняя функция сложения четырех целых чисел extern "C" _fastcall ADDD(DWORD, DWORD, DWORD, DWORD); void main() { DWORD a,b,c,d; a=1; b=2; c=3; d=4; printf("%lu\n",(DWORD *)ADDD(a,b,c,d)); ExitProcess(0); } ; файл add.asm .386P .MODEL FLAT, stdcall PUBLIC @ADDD ; плоская модель _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; процедура возвращает сумму четырех параметров ; передача параметров регистровая ; первые три параметра в регистрах EAX,EDX,ECX ; четвертый параметр в стеке, т.е. [ЕРВ+08Н] @ADDD PROC PUSH EBP MOV EBP,ESP ADD EAX,ECX ADD EAX,EDX ADD EAX,DWORD PTR [EBP+08H] POP EBP RET 4 @ADDD ENDP _TEXT ENDS END
Puc. 3.7.4. Пример регистрового соглашения вызова процедуры.
Трансляция модулей на Рис. 3.7.4.
tasm32 /ml add.asmВключаем add.obj в проект addc (Borland C++ 5.0)
III
В данном разделе я показываю, что вызываемая ассемблерная процедура может содержать не только какие-то вспомогательные процедуры, но и работу с функциями API и ресурсами. Причем, ресурсы, разумеется, являются для всех модулей проекта общими. Можно иметь несколько файлов, задающих ресурсы, но главное, чтобы не совпадали имена и идентификаторы.
#include <windows.h> #include <stdio.h> // объявляется внешняя функция extern "C" _stdcall DIAL1(); void main() { DIAL1(); ExitProcess(0); }
// файл dialforc.rc // определение констант #define WS_SYSMENU 0x00080000L // элементы на окне должны быть изначально видимы #define WS_VISIBLE 0x10000000L // бордюр вокруг элемента #define WS_BORDER 0x00800000L // при помощи TAB можно по очереди активизировать элементы #define WS_TABSTOP 0x00010000L // текст в окне редактирования прижат к левому краю #define ES_LEFT 0x0000L // стиль всех элементов на окне #define WS_CHILD 0x40000000L // запрещается ввод с клавиатуры #define ES_READONLY 0x0800L #define DS_3DLOOK 0x0004L // определение диалогового окна DIAL1 DIALOG 0, 0, 240, 100 STYLE WS_SYSMENU | DS_3DLOOK CAPTION "Диалоговое окно с часами и датой" FONT 8, "Arial" { CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_READONLY, 100, 5, 130, 12 } ; файл dialforc.inc ; константы ; сообщение приходит при закрытии окна WM_CLOSE equ 10h ; сообщение приходит при создании окна WM_INITDIALOG equ 110h ; сообщение приходит при событии с элементом на окне WM_COMMAND equ 111h ; сообщение от таймера WM_TIMER equ 113h ; сообщение посылки текста элементу WM_SETTEXT equ 0CH ; прототипы внешних процедур EXTERN SendDlgItemMessageA:NEAR EXTERN _wsprintfA:NEAR EXTERN GetLocalTime:NEAR EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN SetTimer:NEAR EXTERN KillTimer:NEAR ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DD ? MSMESSAGE DD ? MSWPARAM DD ? MSLPARAM DD ? MSTIME DD ? MSPT DD ? MSGSTRUCT ENDS ; структура данных дата-время DAT STRUC year DW ? month DW ? dayweek DW ? day DW ? hour DW ? min DW ? sec DW ? msec DW ? DAT ENDS ; файл dialforc.asm .386P ; плоская модель .MODEL FLAT, stdcall include dialforc.inc PUBLIC DIAL1 ;--------------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 TIM DB "Дата %u/%u/%u Время %u:%u:%u",0 STRCOPY DB 50 DUP (?) DATA DAT <0> _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' DIAL1 PROC PUSH EBP MOV EBP,ESP ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA MOV [HINST], EAX ; создать диалоговое окно PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA CMP EAX,-1 ;------------------------------ POP EBP RET DIAL1 ENDP ;------------------------------ ; процедура окна ; расположение параметров в стеке ; [EВР+014Н] ;LPARAM ; [EВР+10Н] ;WAPARAM ; [EВР+0CH] ;MES ; [EВР+8] ;HWND WNDPROC PROC PUSH EBP MOV EBP,ESP PUSH EBX PUSH ESI PUSH EDI ;-------------------- CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 ; здесь реакция на закрытие окна ; удалить таймер 1 PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer ; удалить таймер 2 PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL KillTimer ; закрыть диалог PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog JMP FINISH L1: CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE L2 ; здесь начальная инициализация ; установить таймер 1 PUSH 0 ; параметр = NULL PUSH 1000 ; интервал 1 с. PUSH 1 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer ; установить таймер 2 PUSH OFFSET TIMPROC ; параметр = NULL PUSH 500 ; интервал 0.5 с. PUSH 2 ; идентификатор таймера PUSH DWORD PTR [EBP+08H] CALL SetTimer JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_TIMER JNE FINISH ; отправить строку в окно PUSH OFFSET STRCOPY PUSH 0 PUSH WM_SETTEXT PUSH 1 ; идентификатор элемента PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA FINISH: POP EDI POP ESI POP EBX POP EBP MOV EAX,0 RET 16 WNDPROC ENDP ;-------------------------------------------- ; процедура таймера ; расположение параметров в стеке ; [EВР+014Н] ; LPARAM - промежуток запуска Windows ; [EВР+10Н] ; WAPARAM - идентификатор таймера ; [EВР+0CH] ; WM_TIMER ; [EВР+8] ; HWND TIMPROC PROC PUSH EBP MOV EBP,ESP ; получить локальное время PUSH OFFSET DATA CALL GetLocalTime ; получить строку для вывода даты и времени MOVZX EAX,DATA.sec PUSH EAX MOVZX EAX,DATA.min PUSH EAX MOVZX EAX,DATA.hour PUSH EAX MOVZX EAX, DATA.year PUSH EAX MOVZX EAX,DATA.month PUSH EAX MOVZX EAX,DATA.day PUSH EAX PUSH OFFSET TIM PUSH OFFSET STRCOPY CALL _wsprintfA ; восстановить стек ADD ESP,32 POP EBP RET 16 TIMPROC ENDP _TEXT ENDS END
Puc. 3.7.6. Пример использования в ассемблерном модуле API-функций и ресурсов.
Трансляция.
1-й способ.
tasm32 /ml dialforc.asm brcc32 dialforc.rcДалее включаем в проект для программы на Рис. 3.7.5 файлы dialforc.obj и dialforc.res.
2-й способ.
Отличается от предыдущего тем, что в проект включается файл dialforc.rc, а не
dialforc.res. Разумеется, определения констант оттуда следует убрать, т.к. они имеются
в подключаемых Си head-файлах.
IV
Здесь рассматривается пример простейшего калькулятора. Для ассемблера часто сложно найти библиотеки с определенными процедурами, писать же все самому - не хватит времени. Предлагаемая схема очень проста. По сути, программа на языке Си (или другом языке высокого уровня) является неким каркасом. Она вызывает процедуру на языке ассемблера, которая и выполняет основные действия. Кроме того, в Си-модуле можно поместить и другие процедуры, которые легче написать на Си и которые будут вызываться из ассемблера. Здесь как раз и приводится такой пример. В Си-модуле я поместил процедуры, преобразующие строки в вещественные числа, выполняющие над ними действия и затем преобразующие результат опять в строку.
// calcc.срр #include <windows.h> #include <stdio.h> // вызов главной ассемолернои процедуры extern "С" _stdcall MAIN1(); // сложить extern "С" _stdcall void sum(char *, char *, char *); // вычесть extern "C" _stdcall void su(char *, char *, char *); // умножить extern "C" _stdcall void mu(char *, char *, char *); // разделить extern "C" _stdcall void dii(char *, char *, char *); int WINAPI WinMain (HINSTANCE hThisInst, HINSTANCE hPrevInst, LPSTR lpszArgs, int nWinMode) { MAIN1(); return 0; } extern "C" _stdcall void sum(char * s1, char * s2, char * s) { float f1,f2,f; f1=atof(s1); f2=atof(s2); f=f1+f2; sprintf(s,"%f",f); strcat(s," +"); return; } extern "C" _stdcall void su(char * s1, char * s2, char * s) { float f1,f2,f; f1=atof(s1); f2=atof(s2); f=f1-f2; sprintf(s,"%f",f); strcat(s," -"); return; } extern "C" _stdcall void mu(char * s1, char * s2, char * s) { float f1,f2,f; f1=atof(s1); f2=atof(s2); f=f1*f2; sprintf(s,"%f",f); strcat(s," *"); return; } extern "C" __stdcall void dii(char * s1, char * s2, char * s) { float f1,f2,f; f1=atof(s1); f2=atof(s2); if(f2!=0) { f=f1/f2; sprintf(s,"%f",f); strcat(s," /"); } else strcpy(s,"Ошибка деления"); return; }
Рис. 3.7.7. Си-модуль для программы простейшего калькулятора, компонуемый с ассемблерным модулем на Рис. 3.8.8.
// calc.rc // определение констант // стили окна #define WS_SYSMENU 0x00080000L #define WS_MINIMIZEBOX 0x00020000L #define DS_3DLOOK 0x0004L #define ES_LEFT 0x0000L #define WS_CHILD 0x40000000L #define WS_VISIBLE 0x10000000L #define WS_BORDER 0x00800000L #define WS_TABSTOP 0x00010000L #define SS_LEFT 0x00000000L #define BS_PUSHBUTTON 0x00000000L #define BS_CENTER 0x00000300L #define DS_LOCALEDIT 0x20L #define ES_READONLY 0x0800L // идентификаторы кнопок #define IDC_BUTTON1 101 #define IDC_BUTTON2 102 #define IDC_BUTTON3 103 #define IDC_BUTTON4 104 DIAL1 DIALOG 0, 0, 170, 110 STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_3DLOOK CAPTION "Пример простейшего калькулятора" FONT 8, "Arial" { CONTROL "", 1, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 9, 8, 128, 12 CONTROL "", 2, "edit", ES_LEFT | WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 9, 27, 128, 12 CONTROL "", 3, "edit", ES_LEFT | WS_CHILD | ES_READONLY | WS_VISIBLE | WS_BORDER | WS_TABSTOP, 9, 76, 127, 12 CONTROL "+", IDC_BUTTON1, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 11, 48, 15, 14 CONTROL "-", IDC_BUTTON2, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 34, 48, 15, 14 CONTROL "*", IDC_BUTTON3, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 56, 48, 15, 14 CONTROL "/", IDC_BUTTON4, "button", BS_PUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 80, 48, 15, 14 } ; calc. inc ; константы ; сообшение приходит при закрытии окна WM_CLOSE equ 10h WM_INITDIALOG equ 110h WM_COMMAND equ 111h WM_GETTEXT equ 0Dh WM_SETTEXT equ 0Ch ; прототипы внешних процедур EXTERN ExitProcess:NEAR EXTERN GetModuleHandleA:NEAR EXTERN DialogBoxParamA:NEAR EXTERN EndDialog:NEAR EXTERN SendDlgItemMessageA:NEAR ; структуры ; структура сообщения MSGSTRUCT STRUC MSHWND DWORD ? MSMESSAGE DWORD ? MSWPARAM DWORD ? MSLPARAM DWORD ? MSTIME DWORD ? MSPT DWORD ? MSGSTRUCT ENDS ; модуль calc.asm .386P ; поская модель .MODEL FLAT, stdcall include calc.inc EXTERN sum:NEAR EXTERN su:NEAR EXTERN mu:NEAR EXTERN dii:NEAR PUBLIC MAIN1 ; директивы компоновщику для подключения библиотек includelib c:\tasm32\lib\import32.lib ;------------------------------------------------ ;сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' MSG MSGSTRUCT <?> HINST DD 0 ; дескриптор приложения PA DB "DIAL1",0 S1 DB 50 DUP (0) S2 DB 50 DUP (0) S DB 50 DUP (0) _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; процедура, вызываемая из Си-модуля MAIN1 PROC ; получить дескриптор приложения PUSH 0 CALL GetModuleHandleA MOV [HINST], EAX ;------------------------------------------- PUSH 0 PUSH OFFSET WNDPROC PUSH 0 PUSH OFFSET PA PUSH [HINST] CALL DialogBoxParamA ;------------------------------------------- RET MAIN1 ENDP ; процедура окна ; расположение параметров в стеке ; [EBP+014Н] ; LPARAM ; [EBP+10Н] ; WAPARAM ; [EBP+0CH] ; MES ; [EBP+8] ; HWND WNDPROC PROC PUSH EBP MOV EBP,ESP ;------------------------------------------- CMP DWORD PTR [EBP+0CH],WM_CLOSE JNE L1 PUSH 0 PUSH DWORD PTR [EBP+08H] CALL EndDialog MOV EAX,1 JMP FINISH L1: CMP DWORD PTR [EBP+0CH],WM_INITDIALOG JNE L2 ; здесь заполнить окна редактирования, если надо JMP FINISH L2: CMP DWORD PTR [EBP+0CH],WM_COMMAND JNE FINISH CMP WORD PTR [EBP+10H],101 JNE NO_SUM ; первое слагаемое PUSH OFFSET S1 PUSH 50 PUSH WM_GETTEXT PUSH 1 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; второе слагаемое PUSH OFFSET S2 PUSH 50 PUSH WM_GETTEXT PUSH 2 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; сумма PUSH OFFSET S PUSH OFFSET S2 PUSH OFFSET S1 CALL sum ; вывести сумму PUSH OFFSET S PUSH 50 PUSH WM_SETTEXT PUSH 3 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA JMP FINISH NO_SUM: CMP WORD PTR [EBP+10H],102 JNE NO_SUB ; уменьшаемое PUSH OFFSET SI PUSH 50 PUSH WM_GETTEXT PUSH 1 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; вычитаемое PUSH OFFSET S2 PUSH 50 PUSH WM_GETTEXT PUSH 2 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; разность PUSH OFFSET S PUSH OFFSET S2 PUSH OFFSET S1 CALL su ; вычислить разность PUSH OFFSET S PUSH 50 PUSH WM_SETTEXT PUSH 3 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA JMP FINISH ; NO_SUB: CMP WORD PTR [EBP+10H],103 JNE NO_MULT ; первый множитель PUSH OFFSET S1 PUSH 50 PUSH WM_GETTEXT PUSH 1 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; второй множитель PUSH OFFSET S2 PUSH 50 PUSH WM_GETTEXT PUSH 2 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; произведение PUSH OFFSET S PUSH OFFSET S2 PUSH OFFSET S1 CALL mu ; вывести произведение PUSH OFFSET S PUSH 50 PUSH WM_SETTEXT PUSH 3 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA JMP FINISH ; NO_MULT: CMP WORD PTR [EBP+10H],104 JNE FINISH ; делимое PUSH OFFSET S1 PUSH 50 PUSH WM_GETTEXT PUSH 1 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; делитель PUSH OFFSET S2 PUSH 50 PUSH WM_GETTEXT PUSH 2 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA ; деление PUSH OFFSET S PUSH OFFSET S2 PUSH OFFSET S1 CALL dii ; вывести результат деления PUSH OFFSET S PUSH 50 PUSH WM_SETTEXT PUSH 3 PUSH DWORD PTR [EBP+08H] CALL SendDlgItemMessageA JNE FINISH FINISH: MOV EAX,0 POP EBP RET 16 WNDPROC ENDP _TEXT ENDS END
Трансляция.
tasm32 /ml calc.asm brcc32 calc.rcЗатем в проект к программе calcc.cpp (на Рис. 3.8.7) добавим файлы calc.obj и calc.res.
Рис. 3.7.9. Пример работы программы-калькулятора (Рис. 3.7.7-3.7.8).
V
Теперь поговорим о встроенном ассемблере. Это весьма мощное средство. Надо только иметь в виду, что встроенные ассемблеры часто несколько отстают от обычных ассемблеров в части поддержки новых команд микропроцессоров. Это вполне объяснимо, так как разработка новой версии пакета, скажем C++ Builder, требует гораздо больше времени, чем пакета TASM. В примерах на Рис. 3.7.10 и Рис. 3.7.11 мы используем команды арифметического сопроцессора.
program Project2; {$APPTYPE CONSOLE} uses SysUtils; var d:double; function soproc(f:double): double; var res:double; begin asm FLD f FSIN FSTP res end; soproc:=res; end; begin d:=-pi; while (d<=pi) do begin writeln(d:10:2,'-',soproc(d):10:2); d:=d+0.1; end; end.
#include <windows.h> #include <stdio.h> double soproc(double f); void main() { double w=-3.14; while(w<=3.14) { printf("%f- %f\n", w, soproc(w)); w=w+0.1; } ExitProcess(0); } double soproc(double f) { double d; asm { FLD f FSIN FSTP d } return d; }
VI
В заключение я хочу привести пример того, как динамическая библиотека, созданная на Delphi, может быть использована в программе на языке ассемблера. Некоторое время назад я нашел алгоритм, реализованный на Delphi, который помещает ярлык программы на рабочий стол и одновременно делает пункт в меню Программы (Пуск). Я сам на Delphi не пишу, но времени на переписывание алгоритма на Си у меня не было, и я из программы сделал динамическую библиотеку, которой теперь с успехом пользуюсь при создании различных инсталляционных пакетов.
Данный пример предполагает посылки в процедуру четырех параметров: две ссылки на программы, название ярлыка на рабочем столе и название пункта меню. Вы можете при желании расширить возможности данной процедуры. Если Вы посмотрите на ассемблерный модуль, то увидите, что никаких особых отличий вызова данной динамической библиотеки нет.
library lnk; uses SysUtils, Classes, Windows, ShlObj, ActiveX, ComObj, Registry, syncobjs; procedure setup(prog:PChar; uns: PChar; jar:PChar; menu:PChar); stdcall; var MyObject : IUnknown; MySLink : IShellLink; MyPFile : IPersistFile; FileName : String; Directory : String; WFileName : WideString; MyReg : TRegIniFile; ps1,ps2,sn,path,s :string; nb:dword; handle : integer; l:DWORD; f: text; begin MyObject := CreateComObject(CLSID_ShellLink); MySLink := MyObject as IShellLink; MyPFile := MyObject as IPersistFile; FileName := prog; path := ExtractFilePath(FileName); with MySLink do begin SetArguments(''); SetPath(PChar(FileName)); SetWorkingDirectory(PChar(ExtractFilePath(FileName))); end; // вначале ярлык на экране MyReg : = TRegIniFile.Create('Software\MicroSoft\Windows\CurrentVersion\Explorer'); Directory := MyReg.ReadString('Shell Folders','Desktop',''); WFileName := Directory+'\'; WFileName := WFileName+jar; WFileName := WFileName+'.lnk'; MyPFile.Save(PWChar(WFileName),False); ps1:=string(WFileName); // создание ярлыка в главном меню Directory :=MyReg.ReadString('Shell Folders','Programs','')+'\'; Directory:=Directory+menu; WFileName := Directory+'\'; WFileName := WFileName+jar; WFileName := WFileName+'.lnk'; CreateDir(Directory); ps2:=Directory+'\'; MyPFile.Save(PWChar(WFileName),False); //************************************ MyObject:= CreateComObject(CLSID_ShellLink); MySLink:= MyObject as IShellLink; MyPFile := MyObject as IPersistFile; FileName := uns; path := ExtractFilePath(FileName); with MySLink do begin SetArguments(''); SetPath(PChar(FileName)); SetWorkingDirectory(PChar(ExtractFilePath(FileName))); end; WFileName := Directory+'\'; WFileName := WFileName+'UNFILES.lnk'; MyPFile.Save(PWChar(WFileName),False); MyReg.Free; // создать файл, куда будет помещена // нужная для дальнейшей инсталляции информация sn:=path+'perebros.lnk'; AssignFile(f,sn); rewrite(f); writeln(f,ps1); writeln(f,ps2); close(f); end; //********************* Procedure DLLMain(r:DWORD); begin end; exports setup; begin DLLProc:=@DLLMain; DLLMain(dll_Process_Attach); end.
Рис. 3.7.12. Пример динамической библиотеки, написанной на Delphi.
; файл setup.asm .386P ; плоская модель .MODEL FLAT, stdcall ; константы ; прототипы внешних процедур IFDEF MASM ; MASM EXTERN GetProcAddress@8:NEAR EXTERN LoadLibraryA@4:NEAR EXTERN FreeLibrary@4:NEAR EXTERN ExitProcess@4:NEAR EXTERN MessageBoxA@16:NEAR includelib c:\masm32\lib\user32.lib includelib c:\masm32\lib\kernel32.lib ELSE includelib c:\tasm32\lib\import32.lib EXTERN GetProcAddress:NEAR EXTERN LoadLibraryA:NEAR EXTERN FreeLibrary:NEAR EXTERN ExitProcess:NEAR EXTERN MessageBoxA:NEAR GetProcAddress@8 = GetProcAddress LoadLibraryA@4 = LoadLibraryA FreeLibrary@4 = FreeLibrary ExitProcess@4 = ExitProcess MessageBoxA@16 = MessageBoxA ENDIF ;-------------------------------------------- ; сегмент данных _DATA SEGMENT DWORD PUBLIC USE32 'DATA' TXT DB 'Ошибка динамической библиотеки',0 MS DB 'Сообщение',0 LIBR DB 'LNK.DLL',0 HLIB DD ? PAR1 DB "C:\PROG\FILES.EXE",0 PAR2 DB "C:\PROG\UNINST.EXE",0 PAR3 DB "Универсальный поиск",0 PAR4 DB "Программа универсального поиска",0 NAMEPROC DB 'setup',0 _DATA ENDS ; сегмент кода _TEXT SEGMENT DWORD PUBLIC USE32 'CODE' ; [EBP+10Н] ; резервный параметр ; [EBP+0CH] ; причина вызова ; [EBP+8] ; идентификатор DLL-модуля START: ;загрузить библиотеку PUSH OFFSET LIBR CALL LoadLibraryA@4 CMP EAX,0 JE _ERR MOV HLIB,EAX ;получить адрес PUSH OFFSET NAMEPROC PUSH HLIB CALL GetProcAddress@8 CMP EAX,0 JNE YES_NAME ; сообщение об ошибке _ERR: PUSH 0 PUSH OFFSET MS PUSH OFFSET TXT PUSH 0 CALL MessageBoxA@16 JMP _EXIT YES_NAME: PUSH OFFSET PAR4 PUSH OFFSET PAR3 PUSH OFFSET PAR2 PUSH OFFSET PAR1 CALL EAX ; закрыть библиотеку ; библиотека автоматически закрывается также ; при выходе из программы PUSH HLIB CALL FreeLibrary@4 ; выход _EXIT: PUSH 0 CALL ExitProcess@4 _TEXT ENDS END START
Трансляция.
MASM32:
ml /c /coff /DMASM setup.asm link /subsystem:windows setup.objTASM32:
tasm32 /ml setup.asm tlink32 -aa setup.obj
Заключая данную главу, отмечу, что мы здесь рассмотрели далеко не все возможные случаи и проблемы
стыковки ассемблера с языками высокого уровня. Разобрав, однако, предложенные примеры, Вы сможете
самостоятельно решить все подобные задачи.