Изучаем ассемблер в Delphi. Часть 1
Автор: Ian Hodger
Основное предназначение этой статьи, заполнить
пробелы в оригинальной документации по 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.
Продолжение следует ...
|