Основное предназначение этой статьи, заполнить пробелы в оригинальной
документации по Borland Delphi Developer, при этом весь программный код, а так
же теория, полность совместимы со всеми версиями Delphi.
Основное направление статьи, это познакомиться с использованием ассемблера в
Object Pascal. Однако, не будем пропускать и те аспекты программирования,
которые будут требовать пояснения для конкретных примеров, приведённых в этой
статье.
Использование Ассемблера в Борландовком Delphi
Перед тем, как
начать, хотелось бы определиться с уровнем знаний, необходимых для нормального
усвоения данного материала. Необходимо быть знакомым со встроенными средствами
отладки в Delphi. Так же необходимо иметь представление о таких терминах как тип
реализации (instantiation), null pointer и распределение памяти. Если в чём-то
из вышеупомянутого Вы сомневаетесь, то постарайтесь быть очень внимательны и
осторожны при воплощении данного материала на практике. Кроме того, будет
обсуждаться только 32-битный код, так что понадобится компилятор не ниже Delphi
2.0.
Зачем использовать Ассемблер?
На мой
взгляд, Object Pascal, это инструмент, позволяющий генерировать быстрый и
эффективный код, однако использование ассемблера в некоторых случаях позволяет
решать некоторые задачи более эффективно. За всю работу с Delphi, я пришёл к
выводу, что использование низкоуровневого кода необходимо в двух случая.
(1) Обработка большого количества данных. Nb. В данный случай не входит
ситуация, когда используется язык запроса данных.
(2) В высокоскоростных подпрограммах работы с дисплеем. Nb. Имеется ввиду
использование простых процедур на чистом паскале, но никак не внешних библиотек
и DirectX.
В конце статьи мы рассмотрим примеры, которые явно отражают значимость этих
критериев, а так же не только когда и где использовать ассемблерные вставки, но
и как включать такой код в Delphi.
Что такое Ассемблер?
Надеюсь, что Все
читатели этой статьи имеют как минимум поверхностное представление о работе
процессора. Грубо говоря, это калькулятор с большим объёмом памяти. Память, это
не более чем упорядоченная последовательнось двоичных цифр. Каждая такая цифра
является байтом. Каждый байт может содержать в себе значение от 0 до 255, а так
же имеет свой уникальный адрес, при помощи которого процессор находит нужные
значения в памяти. Процессор так же имеет набор регистров (это можно расценить
как глобальные переменные). Например eax,ebx,ecx и edx, это универсальные
32-битные регистры. Это значит, что самое большое число, которое мы можем
записать в регистр eax, это 2 в степени 32 минус 1, или 4294967295.
Как мы уже выяснили, процессор манипулирует значениями регистров. Машинный
код операции прибавления 10 к значению регистра eax будет выглядеть следующим
образом
05/0a/00/00/00
Однако, такая запись
абсолютно не читабельна и, как следствие, не пригодна при отладке программы. Так
вот Ассемблер, это простое представление машинных команд в более удобном виде.
Теперь давайте посмотрим, как будет выглядеть прибавление 10 к eax в
ассемблерном представлении:
add eax,10 {a := a +
10}
А вот так выглядит вычитаение значения ebx из eax
sub eax,ebx {a := a - b }
Чтобы сохранить значние,
можно просто поместить его в другой регистр
mov eax,ecx
{a := c }
или даже лучше, сохранить значение по определённому адресу в
памяти
mov [1536],eax {сохраняет значение eax по адресу
1536}
и конечно же взять его от туда
mov
eax,[1536]
Однако, тут есть важный момент, про который забывать не желательно. Так как
регистр 32-битный(4 байта), то его значение будет записано сразу в четыре ячейки
памяти 1536, 1537, 1538 и 1539.
А теперь давайте посмотрим, как компилятор преобразует действия с переменными
в машинный код. Допустим у нас есть строка
Count := 0;
Для компилятора это означает, что надо просто запомнить значение.
Следовательно, компилятор генерирует код, который сохраняет значение в памяти по
определённому адресу и следит, чтобы не произошло никаких накладок, и обзывает
этот адрес как 'Count'. Вот как выглядит такой код
mov
eax,0
mov Count,eax
Компилятор не может использовать строку
типа
mov Count,0
из-за того, что как минимум
один параметр инструкции должен являться регистром.
Если посмотреть на
строку
Count := Count + 1;
то её ассемблерное
представление будет выглядеть как
mov eax,Count
add
eax,1
mov Count,eax
Для переменных, тип которых отличается от
целого, всё усложняется. Однако, рассмотрим эту тему немного позже, а сейчас
предлагаю закрепить теорию практическими примерами.
Итак, рассмотрим первый пример. Сразу извинюсь за тривиальность, но с чего-то
надо начинать.
function Sum(X,Y:integer):integer;
begin
Result := X+Y;
end;
А вот так будет выглядеть оперция сложения двух целых чисел на
ассемблере:
function Sum(X,Y:integer):integer;
begin
asm
mov eax,X
add eax,Y
mov
Result,eax
end;
end;
Этот код прекрасно работает, однако он не даёт нам преимущества в скорости, а
так же потерялось восприятие кода. Но не стоит огорчаться, так как те немногие
знания, которые Вы почерпнули из этого материала, можно использовать с большей
пользой. Допустим, нам необходимо преобразовать явные значения Red,Green, и Blue
в цвета типа TColor, подходящие для использования в Delphi. Тип TColor описан
как 24-битный True Colour хранящийся в формате целого числа, то есть четыре
байта, старший из которых равен нулю, а далее по порядку красный, зелёный,
синий.
function GetColour(Red,Green,Blue:integer):TColor;
begin
asm
{ecx будет содержать значение
TColor}
mov ecx,0
{начинаем с красной
компоненты}
mov eax,Red
{необходимо
убедиться, что красный находится в диапазоне
0<=Red<=255}
and eax,255
{сдвигаем значение красного в правильное
положение}
shl eax,16
{выравниваем
значение TColor}
xor ecx,eax
{проделываем тоже самое с зелёным}
mov eax,Green
and eax,255
shl eax,8
xor ecx,eax
{и тоже самое с синим}
mov eax,Blue
and
eax,255
xor ecx,eax
mov Result, ecx
end;
end;
Заметьте, что я использовал несколько бинарных операций. Эти операции также
определены непосредственно в Object Pascal.
Продолжение следует ...