Assembler для Windows


Глава 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. Пример использования процедуры из внешнего модуля. Используется транслятор BORLAND C++ 5.0.

Трансляция модулей на 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.

Явное указание параметров в заголовке процедуры приводит:

  1. К имени процедуры в объектном модуле автоматически добавляется @8 (а не @0).
  2. Ассемблер автоматически управляется со стеком.

Трансляция.

  1. В проект Visual C++ включается уже объектный модуль, откомпилированный первоначально с помощью ml.exe.
  2. Если Вы хотите включить в проект ассемблерный текст. Вам придется для него указать способ трансляции, т.е. командную строку для программы 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);
}

Рис. 3.7.5. Консольная программа на C++ вызывает процедуру (Рис. 3.7.6), определенную в ассемблерном модуле, которая, в свою очередь, работает в GUI-режиме.

// файл 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

Рис. 3.7.8. Ассемблерный модуль для программы простейшего калькулятора, компонуемый с Си-модулем на Рис. 3.7.7.

Трансляция.

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.

Рис. 3.7.10. Пример использования директивы ASM и команд сопроцессора в программе на языке Паскаль (Delphi 5.0).

#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;
}

Рис. 3.7.11. Пример использования директивы ASM и команд сопроцессора в программе на языке Си (Borland C++ 5.0).

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

Рис. 3.7.13. Пример программы на языке ассемблера, осуществляющей вызов динамической библиотеки на Рис. 3.8.12.

Трансляция.
MASM32:

ml /c /coff /DMASM setup.asm
link /subsystem:windows setup.obj
TASM32:
tasm32 /ml setup.asm
tlink32 -aa setup.obj

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