Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции КПиЯП.docx
Скачиваний:
50
Добавлен:
20.09.2019
Размер:
3.8 Mб
Скачать

Inline-ассемблер в Delphi

во многом похож на inline-ассемблер Visual C++. Поэтому, чтобы не повторяться, мы рассмотрим некоторые моменты вобщем, без подробностей. Ассемблерные вставки в Delphi размещаются между asm и end, например:

{обычный код на Pascal}

Writeln('Hello!');

{вставка на ассемблере}

asm

mov al,1

mov bx,2

end;

{и снова обычный код на Pascal}

Writeln('Bye!');

Inline-ассемблер Delphi поддерживает все инструкции вплоть до Pentium 4 и AMD Athlon. Также можно использовать инструкции AMD 3DNow! для AMD K6 и AMD Enhanced 3DNow! для AMD Athlon. К сожалению, поддержки 64-разрядного кода нет, так что про Itanium и x64 можно забыть.

По синтаксису inline-ассемблер Delphi в основном похож на MASM. Например, поддерживаются выражения MASM и локальные метки. Также разрешено использование offset для глобальных переменных, объявленных в программе (смотри листинг 6). Как и в MASM, можно использовать глобальные метки, но они должны быть объявлены в секции label (смотри листинг 7).

Есть и отличия от MASM. К примеру, комментарии в ассемблерных вставках должны быть обязательно в стиле Delphi, в операторах безусловного перехода нельзя использовать short и т.п. Все эти отличия описаны в документации, которая идет с Borland Developer Studio (файл Reference.pdf).

Как и в inline-ассемблере Visual C++, в inline-ассемблере Delphi нельзя определять переменные и структуры. Однако можно использовать константы, переменные и записи, уже определенные в программе (смотри листинг 8). При написании кода на inline-ассемблере Delphi нужно соблюдать те же предосторожности, что и в inline-ассемблере Visual C++. Пожалуй, единственная разница заключается в том, что нельзя изменять регистры edi, esi, esp, ebp и ebx. А вот регистры eax, ecx и edx можно изменять свободно.

Обычно на inline-ассемблере Delphi пишутся функции. При написании функций следует учитывать некоторые особенности. Во-первых, все параметры, занимающие в памяти больше 4 байтов, передаются в функцию так, как если бы перед ними стояли директивы var (даже если эти директивы не указаны). Во-вторых, результат функции должен возвращаться в eax.

Типичная функция выглядит примерно так:

{умножение X*Y}

function LongMul(X, Y: Integer): Longint;

asm

mov eax,X

imul Y

end;

OBJ-модули

Мы не будем подробно останавливаться на OBJ-модулях, их упоминание здесь – скорее дань традиции. В славные времена MS DOS OBJ-модули были чуть ли не единственным способом использовать одновременно несколько языков программирования.

Если кто не в курсе, OBJ-модуль – это файл с расширением .obj. В OBJ-модуле в специальном формате содержится машинный код, обычно - набор каких-то полезных функций. Функции из OBJ-модулей можно вызывать из программ на Visual C++ и Delphi. Создавать OBJ-модули можно с помощью компиляторов тех же Visual C++ и Delphi. Таким образом, OBJ-модуль, написанный на Visual C++, теоретически может использоваться в программах на Delphi и наоборот.

Но это теоретически: на практике форматы OBJ-модулей, генерируемых Visual C++ и Delphi, несовместимы между собой. Кроме того, в Visual C++ и Delphi по умолчанию приняты разные соглашения о передаче параметров в функции. Это значит, что компиляторы Visual C++ и Delphi по-разному вызывают функции из OBJ-модулей и генерируют для этого принципиально разный машинный код. Отсюда возникает масса всяких нюансов, описание которых занимает не одну страницу (если кого-то интересуют подробности, можно посмотреть книгу В. Юрова «Assembler. Учебник»).

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

Межпрограммное взаимодействие

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

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

Memory-mapped файл – это, по сути, именованный участок оперативной памяти, к которому может получить доступ любой работающий процесс. Приведем две программы, на примере которых рассмотрим основные методы работы с memory-mapped файлами. Первая программа – MemoryMapped1, будет создавать memory-mapped файл и записывать в него строчки, введенные с клавиатуры. Вторая программа – MemoryMapped2, будет считывать эти строчки из memory-mapped файла и выводить их на монитор. Обе программы будут консольными.

Начнем с первой программы. Сначала напишем ее на Visual C++. Откроем Visual Studio и создадим новое консольное приложение MemoryMapped1. Сразу же отключим в настройках компилятора использование UNICODE. Пропишем в MemoryMapped1.cpp заголовочные файлы windows.h (чтобы можно было без проблем использовать Windows API) и stdio.h:

// заголовочные файлы

#include <windows.h>

#include <stdio.h>

Затем впишем в main() следующий код:

int main()

{

// создаем memory-mapped файл

HANDLE hFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,\

PAGE_READWRITE, 0, 1024, LPTSTR("DigitalPoem"));

Этот код вызывает API CreateFileMapping, которая создает memory-mapped файл. Коротко остановимся на параметрах, передаваемых в CreateFileMapping. Первый параметр – хендл ранее открытого файла на диске. Никакого файла на диске мы не открывали, поэтому мы передаем здесь INVALID_HANDLE_VALUE. Второй параметр – указатель на структуру SECURITY_ATTRIBUTES, в которой мы можем установить права доступа к нашему memory-mapped файлу. Ничего особого мы устанавливать не собираемся, поэтому второй параметр у нас 0. Третий параметр – защита (protection) memory-mapped файла. Мы хотим читать и писать memory-mapped файл, поэтому переедем константу PAGE_READWRITE. Четвертый и пятый параметр – двойные слова. Вместе они определяют одно 64-разрядное число — размер memory-mapped файла в байтах. Нам хватит одного килобайта, поэтому четвертый параметр (который определяет старшее двойное слово в значении размера) у нас равен 0, а пятый параметр (младшее двойное слово в значении размера) равен 1024. И, наконец, пятый параметр «DigitalPoem» — уникальное имя нашего memory-mapped файла.

Если API CreateFileMapping завершилась успешно, в hFile будет находиться хендл созданного файла. В случае неудачи в hFile будет 0. Чтобы отследить это, вставляем в нашу программу проверку:

// ошибка?

if(hFile==0)

{

// да, ошибка — выведем сообщение и закончим работу

printf("Error! CreateFileMapping returns NULL.\n");

return 0;

}

После того, как memory-mapped файл создан, его надо отобразить в адресное пространство приложения. Для этого используется API MapViewOfFile. Код следующий:

// отобразим memory-mapped файл с полным доступом

LPVOID pFileContent = MapViewOfFile(hFile,\

FILE_MAP_ALL_ACCESS, 0, 0, 0);

Опять коротко остановимся на параметрах, которые мы передаем в MapViewOfFile. Первый параметр – хендл memory-mapped файла. Вторым параметром у нас передается FILE_MAP_ALL_ACCESS. Это значит, что мы хотим получить полный доступ к отображенному файлу. Третий и четвертый параметры определят 64-разрядное смещение отображаемого участка относительно начала файла. Поскольку мы хотим отображать файл с начала (с нулевого смещения), у нас нули. Ну, и пятый параметр – число типа DWORD, которое определяет, сколько байт нужно отобразить. Мы передаем 0 – это значит, что нам нужно все.

Если MapViewOfFile завершилась успешно, в pFileContent будет смещение отображаемого файла. В случае неудачи там будет NULL. Отслеживаешь, и если MapViewOfFile вернула NULL, то закрываешь хендл memory-mapped файла с помощью API CloseHandle и выходишь:

// memory-mapped файл отображен?

if(pFileContent==NULL)

{

// нет, открыть memory-mapped файл не удалось

printf("Error! MapViewOfFile returns NULL.");

// закрываем хендл memory-mapped файла и выходим

CloseHandle(hFile);

return 0;

};

Дальше организуем цикл, который читает строки с клавиатуры. В качестве буфера для этих строк используется наш memory-mapped файл. Цикл выглядит следующим образом:

// инициализируем счетчик сообщений

int i = 1;

// будем передавать через memory-mapped файл сообщения

// до тех пор, пока пользователь не введет «stop»

while(strcmp((char*)pFileContent, "stop")!=0)

{

// ожидаем ввода

printf("Message #%d: ", i);

scanf("%1023s\0", (char*)pFileContent, 1024);

// увеличиваем счетчик сообщений

i++;

};

Когда программа завершает работу, прекращаем отображение memory-mapped файла с помощью API UnmapViewOfFile и закрываем его хендл:

// закрываем отображение и хендл memory-mapped файла

UnmapViewOfFile(pFileContent);

CloseHandle(hFile);

// выходим

return 0;

}

На Delphi MemoryMapped1 реализуется точно так же. Чтобы не повторяться, прокомментируем принципиальные моменты.

Открываешь Borland Delphi и создаешь новое консольное приложение. Сохраняешь его под именем MemoryMapped1. Затем прописываешь в начале файла MemoryMapped1.dpr:

{$APPTYPE CONSOLE}

// использовать Windows API

uses Windows;

Затем прописываешь все необходимые переменные:

var

// хендл memory-mapped файла

hFile: THandle;

// указатель на отображение memory-mapped файла в памяти

pFileContent: pointer;

// счетчик количества отображений

i: integer = 1;

// строка, в которую будет осуществляться ввод

s: string;

Затем создаешь memory-mapped файл и проверяешь, успешно ли завершилась CreateFileMapping:

begin

// создаем memory-mapped файл

hFile:= CreateFileMapping(INVALID_HANDLE_VALUE, nil,

PAGE_READWRITE, 0, 1024, 'DigitalPoem');

// memory-mapped файл открыт?

if(hFile=0) then

begin

// нет, открыть memory-mapped файл не удалось

writeln('Error! MapViewOfFile returns nil.');

exit;

end;

Отображаешь memory-mapped файл в память и проверяешь, получилось ли:

// отобразим memory-mapped файл с полным доступом

pFileContent:= MapViewOfFile(hFile,

FILE_MAP_ALL_ACCESS, 0, 0, 0);

// memory-mapped файл отображен?

if pFileContent=nil then

begin

// нет, открыть memory-mapped файл не удалось

writeln('Error! MapViewOfFile returns nil.');

// закрываем хендл memory-mapped файла и выходим

CloseHandle(hFile);

exit;

end;

Организуешь цикл передачи в memory-mapped файл введенных с клавиатуры строк:

// будем передавать через memory-mapped файл сообщения

// до тех пор, пока пользователь не введет "stop"

while s<>'stop' do

begin

// читаем строку с клавиатуры

write('Message #', i, ': ');

readln(s);

// записываем строку в memory-mapped файл

CopyMemory(pFileContent, pchar(s), length(s));

// увеличиваем счетчик сообщений

inc(i);

end;

И после всего:

// закрываем отображение и хендл memory-mapped файла

UnmapViewOfFile(pFileContent);

CloseHandle(hFile);

end.

Все, теперь можно начинать писать программу MemoryMapped2, которая будет считывать строчки из memory-mapped файла и выводить их в консоль. Сначала рассмотрим соответствующий код на Visual C++.

Снова открываешь Visual Studio и создаешь консольное приложение MemoryMapped2. Выделяешь исходный код MemoryMapped1 и копируешь в файл MemoryMapped2.cpp. Затем целиком удаляешь цикл ввода строк, начинающийся со строчки while(strcmp((char*)pFileContent, "stop")!=0), и вместо него вписываешь цикл чтения строк из memory-mapped файла:

// считываем из memory-mapped файла сообщения, пока

// не считаем "stop"

while(strcmp((char*)pFileContent, "stop")!=0)

{

// в memory-mapped файле есть строка?

if(strcmp((char*)pFileContent, "")!=0)

{

// да — выводим сообщение

printf("Incoming message #%d: %s\n",\

i, (char*)pFileContent);

// очищаем (заполняем нулями) буфер

memset(pFileContent, '\0', 1024);

// увеличиваем счетчик сообщений

i++;

}

};

Вот и все. Как видишь, единственное отличие MemoryMapped2 от MemoryMapped1 в том, что чтение из memory-mapped файла, а не запись в него. Аналогично, чтобы получить MemoryMapped2 на Delphi, в исходном коде MemoryMapped1.pas достаточно заменить цикл записи строк, который начинается с while s<>'stop' do, на цикл чтения:

// считываем из memory-mapped файла сообщения, пока

// не считаем "stop"

while s<>'stop' do

begin

// в memory-mapped файле есть строка?

if (byte(pFileContent^)<>0) then

begin

// да — выводим сообщение

s:=string(pchar(pFileContent));

writeln('Incoming message ',i,': ', s);

// очищаем (заполняем нулями) буфер

FillMemory(pFileContent, 1024, 0);

// увеличиваем счетчик сообщений

inc(i);

end;

end;

Конечно, это не единственный способ «подружить» между собой две программы. Есть еще DDE, именованные каналы, сокеты, COM, DCOM... Но это намного сложнее и требует больше кода.

#include <stdio.h>

char format[] = "%s %s\n";

int main()

{

__asm{

mov eax, offset format ; заносим в eax смещение строки format

mov bl, byte ptr [eax+2] ; теперь в bl третий байт из строки format

}

}

__asm{

mov ecx, 10 ; заносим в ecx число 10

jmp short lab ; принудительный короткий переход на lab

xor ecx, ecx ; обнуление ecx (никогда не произойдет из-за предыдущего

; короткого перехода)

lab: dec ecx ; декремент ecx

cmp ecx, 0 ; сравниваем с 0

jne lab ; если не равно 0, переходим на метку lab

}

#include <stdio.h>

char format[] = "%s %s\n";

int main()

{

char i = 1;

int j;

__asm{

mov eax, offset format ; заносим в eax смещение строки format

mov bl, byte ptr [eax] ; заносим в bl первый символ строки

mov i, bl ; копируем bl в переменную i

mov j, 1 ; заносим в переменную j единицу

inc j ; инкрементируем j

}

}

#include <stdio.h>

struct my_str1{

int mem1;

char both;

};

struct my_str2{

int mem2;

char both;

};

int main()

{

my_str1 str1;

my_str2 str2;

__asm{

lea eax, str1 ; заносим в eax адрес str1

; поскольку both есть и в str1 и в str2, для доступа к

; str1.both нужно явно указывать str1

mov [eax]str1.both, 1

; поскольку mem1 есть только в str1, для доступа к str1.mem

; указывать str1 не обязательно

mov [eax].mem1, 1

}

}

#include <stdio.h>

int main()

{

int i;

__asm{

; установим eax в 1

mov eax,1

}

// выведем значение eax

printf("EAX=1\n");

__asm{

; присвоим значение eax переменной i

mov i, eax

}

// выведем значение eax и убедимся, что оно

// не совпадает с предыдущим

printf("EAX=%d", i);

}

program Example;

{$APPTYPE CONSOLE}

var

myVar: integer;

begin

asm

mov myVar, -1

mov eax, offset myVar {offset с глобальной переменной}

mov bl, byte ptr [eax+2]{выражение MASM}

@@1: {локальная ссылка}

dec eax

cmp bl, 0

jl @@1

end;

end.

program Example2;

{$APPTYPE CONSOLE}

label lab1; {объявляем глобальную метку}

begin

asm

jmp lab1 {безусловный переход на глобальную метку}

mov eax, 1

lab1:

end;

end.

program Example3;

{$APPTYPE CONSOLE}

label lab1;

var

a: record

a_mem: integer;

a_mem2: char;

end;

i: integer;

begin

asm

{работаем с переменной i}

mov eax, 1

mov i, eax

{работаем с записью a}

mov eax, offset a

{при обращении к a_mem запись a указывать обязательно

и именно в таком формате}

mov [eax+a].a_mem, 1

end;

end.