Основное предназначение этой статьи, заполнить пробелы в
оригинальной документации по 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.
|