Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Все, что вы не знали, но хотели бы узнать о Delphi №13


Выпуск №13

Раздел: Язык Программирования Delphi

Подраздел:
Взаимодействие с другими языками (часть 2)

Этот подраздел может показаться немного скучным, но не все же программированию быть интересным и занимательным?

Ассемблер - вещь необходимая, хотя и неприятная для большинства программистов.

Этот подраздел разбит на три части, но, по-моему, он того стоит.

Уважаемый подписчик,

О чем будет следующий раздел - решать вам.

Варианты:

VCL

Системные функции и Winapi

Базы данных

Работа с файловой системой

Репортинг, работа с принтером

Работа с сетью, Интернетом, протоколами

Работа с графикой, мультимедиа

 

Ваши предложения высылайте на

formyreferal@rambler.ru

В этом выпуске:

Использование ассемблера в Дельфи
   -Об основах ассемблерных процедур
  -Замечания о синтаксисе
  


 
Об основах ассемблерных процедур  

 
 

Глава 1: Об основах ассемблерных процедур
 

Когда вы захотите добавить хороший и успешный ассемблерный код внутрь проекта на Дельфи, вам потребуется вызвать BASM процедуру, передать в нее переменные и принять обратно результат. Для этого вы должны знать, как работают соглашения о передаче параметров. На первом шаге мы обсудим основное: подпрограммы на ассемблере, получают некоторые переменные для обработки, возвращают впоследствии результаты. Позже, в других главах, мы обсудим, как вызывать другие функции, методы и процедуры из ассемблерного кода с помощью регистров и стека.

 

1.1. Где размещать ассемблерный код
 

Ассемблерные инструкции размещаются внутри блока asm...end. Эти блоки могут появляться внутри процедур и функций обычного кода, но я настоятельно не рекомендую поступать таким образом. Гораздо лучше изолировать их в отдельной функции или процедуре. Вставка asm блока внутри обычной процедуры создает сложности для компилятора Паскаля и код становится не эффективным с точки зрения производительности. Переменные, которые обычно передаются через регистры, в этом случае будут передаваться через стек или перезагрузку. Также, это заставляет компилятор адаптировать собственный код к вашему вставленному коду, что делает механизм оптимизации менее эффективным. Так, что это становится правилом помещать ассемблерный код в отдельную процедуру или функцию. Кроме того - это вопрос проектирования. Читабельность и управляемость вашего кода становится выше, если он помещен в свой собственный блок.
Часто, ассемблерный код ассоциируется со скоростью. Поэтому циклы вы также должны по возможности организовывать внутри ассемблерного кода. Это не сложно, а иначе вы просто потеряете множество времени за счет постоянного вызова.
 
 function CriticalCode(...): ...; register;  
 
 asm  
 
  
 ...  
 
 {Here comes your assembler code}  
 
 ...  
 
  
 end;  
 
  

 
 procedure Example;  
 
 var  
 
  
 I: Integer;  
 
  
 begin  
 
  
 I:=0;  
 
 ...  
 
 while I < NumberOfTimes do begin  
 
  
 CriticalCode(...);  
 
 Inc(I);  
 
  
 end;  
 
 ...  
 
  
 end;  
 
  
 
  
Вы должны сделать так:

 
 function CriticalCode(...): ...; register;  
 
 Asm  
 
  
 ...  
 
 mov ECX,{NumberOfTimes}  
 
  
 @@loop:  
 
  
 ...  
 
 {Остальной код}  
 
 ...  
 
 dec ECX  
 
 jnz @@loop  
 
 ...  
 
  
 end;  
 
  
 
 procedure Example;  
 
 begin  
 
  
 ...  
 
 CriticalCode(...);  
 
 ...  
 
  
 end;  
 
  
 
  
Использование цикла в обратном направлении позволяет просто проверять флаг установки нуля после команды dec. Если же цикл начинать с нуля, то потребуется больше на одну команду сравнения с конечным значением, каждый раз при проходе цикла.

 
 mov ECX,0  
 
  
 @@loop:  
 
  
 ...  
 
 inc ECX  
 
 cmp ECX,{NumberOfTimes}  
 
 jne @@loop  
 
  
 
  
Другая возможность – это вычесть значение NumberOfTimes из 0 и затем увеличивать переменную цикла, пока она не станет равной нулю. Этот метод обычно используется, когда переменная цикла также является индексом в таблице или массиве в памяти, поскольку механизм кэширования работает лучше, чем при доступе в прямом направлении. Это можно сделать так:

 
 xor ECX,ECX  
 
  
 sub ECX,{NumberOfTimes}  
 
  
 @@loop:  
 
  
 ...  
 
 inc ECX  
 
 jnz @@loop  
 
  
 
  
Помните, что в этом случае базовый регистр или адрес, должен указывать на конец массива, вместо его начала.
(1) В данных главах мы специально указываем соглашение о вызове. На самом деле указание register избыточно, так как соглашением по умолчанию является передача параметров через регистры, это сделано исключительно для читабельности (или как дополнительный комментарий) и как напоминание читателю, что параметры передаются через регистры.

 

1.2. Код входа/выхода и сохранение регистров
 

Компилятор автоматически генерирует необходимый код входа и выхода из ассемблерных подпрограмм.
Код входа выглядит так:
 
 push EBP  
 
 mov  EBP,ESP  
 
 sub  ESP, {Размер стека для локальных переменных}  
 
  
А код выхода так:
 
 mov ESP,EBP  
 
 pop EBP  
 
 ret {Размер стека резервированный для параметров}  
 
  
Однако, если ваша процедура не имеет никаких локальных переменных или параметров на стеке, то компилятор не делает кода входа/выхода, за исключением инструкции ret.
Код входа сначала сохраняет текущее значение регистра EBP на стеке, поскольку его требуется восстановить при выходе. Затем, устанавливает значение EBP, как базу для доступа к параметрам и локальным переменным, которые также размещаются на стеке. Более подробно мы обсудим этот механизм позже.
Код выхода сначала освобождает память, распределенную для локальных переменных, путем подстройки указателя стека, а затем восстанавливает регистр EBP в его предыдущее состояние и производит возврат в вызвавшую программу. Для всех соглашений, исключая cdecl, процедура сама очищает стек, путем соответствующего варианта инструкции ret, Для соглашения cdecl очисткой стека занимается вызвавшая программа. Снова, все это мы рассмотрим подробнее в дальнейшем.
Внутри вашей функции или процедуры, содержимое регистров EAX, ECX, EDX можно полностью изменять и нет необходимости возвращать их в исходное состояние, кроме того, регистр EAX или его часть часто используется для возврата результата. Если вы изменяете, другие регистры общего назначения (EBX, ESI, EDI), то вы обязаны восстановить их первоначальное состояние до выхода из процедуры. То же самое относится и к регистрам ESP и EBP. Вы также не должны никогда изменять содержимое сегментных регистров (ds, es и ss указывают на один и тот же сегмент; cs имеет свое собственное значение; fs используется Windows и gs резервирован).
Регистр ESP указывает на верхушку стека, а EBP указывает на текущий фрейм стека и генерируется по умолчанию компилятором как код входа. Поскольку каждая инструкция pop и push изменяет содержимое регистра ESP, то его использование не является хорошей идеей для доступа к стеку. Для этих целей зарезервирован регистр EBP.
И в дополнение к регистрам, вы также должны сохранять состояние флага направления. При входе в функцию флаг направления сброшен и если вы его изменяете, то вы должны сбросить его до выхода из функции, сделать это можно с помощью инструкции cld.
И наконец, вы также должны очень осторожно относиться к управляющему слову сопроцессора. Поскольку оно позволяет менять режим точности и округления, а также маскировать определенные исключения, то это может драматически изменить результат вычислений в вашей программе. Если у вас возникла нужда в изменении управляющего слова, то постарайтесь восстановить его значение как можно быстрее. Если вы используете типы Comp или Currency, то не уменьшайте точность!

 

1.3. Передача информации через регистр
 

При использовании соглашения по умолчанию, передачу через регистры, Дельфи может передавать до двух методов или до трех параметров через регистры процессора. Это означает, что нет необходимости генерировать фрейм стека для передачи параметров. Не все типы могут быть переданы через регистры, а только те, которые помещаются полностью в регистр. Поэтому, многие сложные типы передаются или через стек, или через память, и вместо самих типов передается указатель через регистр. Это означает, что любой тип может быть передан через регистр, как указатель, за исключением указателей методов, которые всегда передаются как два 32-разрядных указателя, размещенных на стеке.
Текущее поколение процессоров, для которых написана данная статья, обычно называются как Intel Pentium процессоры, имеет регистры шириной в 32 бита. Когда передаваемая информация не полностью использует регистр (для типов слово и байт), то используется, только часть регистра, байты используют младшие восемь бит, например al и для слов младшее слово, например ax. Указатели всегда 32-битные (по крайней мере, пока не появятся 64-битные процессоры) и занимают весь регистр полностью, например eax. В случае переменных типа байт и слово оставшаяся часть регистра не определена и вы не должны делать никаких предположений относительно его содержимого. Например, при передаче байта в функцию, через регистр al, остальные 24 бита регистра eax не определены и вы, конечно, не можете рассчитывать на то, что они равны нулю. Вы просто можете использовать инструкцию and для очистки оставшихся бит.
 
 and EAX,$FF {беззнаковый байт в AL, очистка старших 24 бит}  
 
  
или
 
 and EAX,$FFFF {беззнаковое слово в AX, очистка старших 16 бит}  
 
  
Когда вы передаете знаковые параметры (ShortInt или SmallInt), вы должны расширить их с учетом знака. Для расширения знака для байтового параметра до двойного слова, вы должны использовать две инструкции:
 
 cbw  {расширение al до ax}  
 
 cwde {расширение ax до EAX}  
 
  
Для демонстрации, напишите следующую тестовую подпрограмму:
 
 function Test(Value: ShortInt ): LongInt; register;  
 
 asm  
 
  
 
 end;  
 
  
Разместите кнопку и метку на форме и поместите следующий код в обработчик OnClick:
 
 var  
 
  
 I : ShortInt ;  
 
  
 begin  
 
  
 I := -7;  
 
 Label1.Caption := IntToStr(Test(I));  
 
  
 end;  
 
  
Запустите проект и нажмите кнопку. Тестовая процедура принимает параметр типа ShortInt  через al. И возвращает результат типа integer через регистр EAX, который возвращает результат неизменным. Вы можете просто считать, что EAX имеет неизмененное значение при возврате. Теперь изменим функцию следующим образом и запустим проект снова на выполнение:
 
 function Test(Value: ShortInt ): LongInt; register;  
 
 asm  
 
  
 cbw  
 
 cwde  
 
  
 end;  
 
  
Единственным соглашением, согласно которому регистры используются для передачи параметров, это соглашение с ключевым словом register, которое также является соглашением по умолчанию. Все другие соглашения используют стек для передачи параметров в функции или процедуры. И конечно, если вы передаете более двух или трех параметров, то стек также используется для передачи оставшихся параметров. В заключение, некоторые параметры всегда передаются через стек - это указатели методов, которые в действительности состоят из двух 32-битных указателей и параметров с плавающей запятой. Обзор можно найти в таблице 2.
 

1.4. Передача информации через стек
 

При использовании соглашения по передаче параметров через регистры и передаче более двух указателей метода или трех параметров, остальные передаются через стек. Другие соглашения также используют стек для передачи параметров.
Обычно, для доступа к параметрам на стеке вы должны обращаться к ним через адресацию с помощью регистра EBP. Код входа по умолчанию, который генерирует компилятор, устанавливает регистр EBP на данный фрейм. Таким образом, использование EBP с нужным смещением позволяет иметь доступ к параметрам на стеке и к локальным переменным. Посмотрим на пример с использованием соглашения о вызове pascal.
Данное соглашение помещает параметры слева, направо. Для примера, в следующем объявлении:
 
 function Test(First, Second, Third: Integer): Integer; pascal;  
 
  
мы имеем три 32-битных параметра типа integer, и каждый параметр помещается на стек следующим образом:
 
 First  
 
 Second  
 
 Third  
 
 ESP ->  
 
  
Инструкция вызова добавляет адрес возврата на стек, и стек теперь выглядит следующим образом:
 
 First  
 
 Second  
 
 Third  
 
 ESP ->  
 
 Return Address  
 
  
Компилятор автоматически генерирует код входа (см. главу 1.2) для сохранения текущего значения регистра EBP и затем копирует регистр ESP в EBP для доступа к фрейму стека:
 
 First  
 
 Second  
 
 Third  
 
 Return Address  
 
 EBP, ESP->  
 
 Previous EBP  
 
  
В данной точке, мы имеем доступ к параметрам на стеке, как смещение относительно регистра EBP. Поскольку адрес возврата находится на стеке между текущей верхушкой стека и действительными параметрами, мы можем получить их следующим образом:
 
 First=   EBP + $10   (EBP + 16)  
 
 Second=   EBP + $0C   (EBP + 12)  
 
 Third=   EBP + $08   (EBP + 8)  
 
   
В действительности, вы будете просто ссылаться на них по их именам, компилятор сам рассчитает необходимые смещения самостоятельно. Так, для выше описанного случая, напишите:
 
 mov EAX,First  
 
  
Компилятор превратит их в следующий код:
 
 mov EAX,[EBP+0x10]  
 
  
Это избавляет вас от самостоятельного расчета смещений и делает код более читабельным. Так что вы должны использовать имена везде, где только возможно (практически всегда), вместо расчета их вручную смещений.
Данные, переданные через стек, всегда занимают 32 бита, даже если вы передаете байт, оставшиеся биты просто не определены.
 

1.5. Локальные переменные
 

Так же как и в обычной процедуре, вы можете использовать локальные переменные для хранения временных значений. Они объявлятся - с помощью директивы var и размещаются на стеке. Компилятор генерирует необходимый код пролога, для резервирования необходимого места на стеке, вместе с параметрами и обеспечивает доступ по имени. Вы помните, что функция объявляется следующим образом:
 
 function Test(First, Second, Third: Integer): Integer; pascal;  
 
  
Для временного хранения объявим переменную типа integer, для этого сделаем следующее объявление:
 
 var  
 
  
 MyTemp: Integer;  
 
  
В результате компилятор сгенерирует следующий код и выделит место на стеке. Обратим внимание, что фрейм стека будет выглядеть следующим образом без использования локальных переменных.
 
 First  
 
 Second  
 
 Third  
 
 Return Address  
 
 EBP, ESP->  
 
 Previous EBP  
 
  
Для создания места на стеке для локальной переменной MyTemp, компилятор добавляет инструкцию push, теперь стек выглядит следующим образом:
 
 First  
 
 Second  
 
 Third  
 
 Return Address  
 
 EBP ->  
 
 Previous EBP  
 
 ESP ->  
 
 ? (MyTemp)  
 
  
Для адресации этих локальных переменных опять же используется смещение относительно регистра EBP. Для примера, переменная MyTemp доступна через EBP-4 (напомню: регистр EBP инициализируется компилятором в коде входа). И еще раз, нет необходимости рассчитывать это смещение вручную, достаточно использовать имя переменной MyTemp:
 
 mov EAX,MyTemp  
 
  
будет оттранслировано в следующий код:
 
 mov EAX,[EBP-4]  
 
  
Содержимое переменных не инициализируется при входе, и вы не должны делать никаких предположений об их начальных значениях. Поэтому вы должны сами проинициализировать их до использования:
 
 mov MyTemp,0  
 
  
Так же, вы должны с осторожностью объявлять и использовать локальные переменные. Они добавляют лишнюю нагрузку по созданию места на стеке и на освобождение при выходе. Еще важно то, что доступ к основной памяти гораздо медленнее, чем доступ к регистрам процессора. Так, что пытайтесь использовать регистры везде, где это возможно, для хранения временных переменных, вместо использования локальных переменных. В обычном коде, при включенной оптимизации, компилятор так же пытается использовать регистры для локальных переменных.
 

1.6. Возврат информации через регистры процессора
 

В большинстве случаев (зависит от типа результата), функция возвращает результат через регистры процессора. Напомним, что в отличие от передачи параметров в функцию, где в большинстве соглашений используют стек, для возврата результата, все соглашения используют регистры для допустимых типов!
Таблица 3 содержит обзор о вариантах возврата результатов. В большинстве случаев, результат возвращается через регистр EAX или FP(0). Особый случай, когда в результате возвращается длинная строка или другой тип, возвращаемый через указатель. В случае длинных строк, динамических массивов, больших множеств, вариантов и больших записей, переданных через параметр с директивой var, используется 32-битный указатель на результат. Так, где же хранится действительное содержимое результата (например, длинных строк)? Ответ на это в том, что вы должны выделить место в куче, заполнить его данными, и вернуть указатель на эту область памяти через переменную Result. Заметим, что, для множеств, записей и массивов, которые могут разместиться в регистре, переменная Result возвращает их через регистр. Только для длинных строк, вариантов и множеств, записей и массивов, которые занимают свыше 32 бит, переменная Result возвращает указатель на дополнительный указатель, размещенный функцией, аналогично директиве var параметра (мы рассмотрим директиву параметра var в главе 1.9).
Не беспокойтесь, если что-то сейчас не понятно, позже мы рассмотрим эти типы подробнее.
Теперь поясним это на примере. Функция PlusMinusLine возвращает длинную строку, состоящую из последовательности плюсов и минусов, для формирования строки. Например, когда вы напишите так S:=PlusMinusLine(9), то S должна получить значение: "-+-+-+-+-".
Декларация функции следующая:
 
 function PlusMinusLine(L: Integer): AnsiString; register;  
 
  
Функция принимает один параметр: длину строки символов (L). Поскольку мы используем соглашение по умолчанию, то параметр передается через регистр EAX. Функция должна вернуть длинную строку, Что в действительности означает указатель на область памяти, содержащей нашу строку. Вы можете использовать переменную Result для обращения к этой области, но поскольку ее поведение аналогично var, то в этом случае @Result эквивалентно регистру EDX (второй параметр отдельной функции передается через регистр EDX, при использования соглашения register)! Подробности мы рассмотрим в главе 1.9. EDX не содержит самого указателя, но указатель на область памяти для этого указателя! Тем не менее, пока еще не распределена память для нашей длинной строки. Будем использовать функцию NewAnsiString из модуля system для размещения памяти в куче и установке длины строки. Функция NewAnsiString устанавливает длину новой строки, которая передается через регистр EAX и возвращает адрес этой строки в том же регистре. Если же мы не вызовем функцию NewAnsiString (или другую функцию или процедуру, которая выделит память в куче для нашей длинной строки), то переменная Result не будет содержать действительного указателя и мы можем получить ошибку доступа (access violation) если попытаемся использовать его.
 
 function PlusMinusLine(L: Integer): AnsiString; register;  
 
 asm  
 
  
 push EDI  
 
 push ESI  
 
 push EBX  
 
 mov ESI,EDX {Указатель памяти на Result}  
 
 mov EBX,EAX {EBX хранит длину параметра}  
 
 call System.@NewAnsiString  
 
 mov EDI,EAX {EDI используется для заполнения строки}  
 
 mov [ESI],EDI  
 
 mov ECX,EBX  
 
 shr ECX,2 {обрабатываем по 4 байта за раз}  
 
 test ECX,ECX  
 
 jz @@remain  
 
 mov EAX,'+-+-'  
 
  
 @@loop:  
 
  
 mov [EDI],EAX  
 
 add EDI,4  
 
 dec ECX  
 
 jnz @@loop  
 
  
 @@remain: {заполняем оставшие байты, если length/4 не ноль}  
 
  
 mov ECX,EBX  
 
 and ECX,3  
 
 jz @@ending  
 
 mov EAX,'+-+-'  
 
  
 @@loop2:  
 
  
 mov BYTE PTR [EDI],al  
 
 shr EAX,8  
 
 inc EDI  
 
 dec ECX  
 
 jnz @@loop2  
 
  
 @@ending:  
 
  
 mov EAX,ESI {для совместимости: возврат указателя через EAX}  
 
 pop EBX  
 
 pop ESI  
 
 pop EDI  
 
  
 end;  
 
  
Для облегчения понимания, данного примера пришлось пожертвовать некоторой эффективностью. Для ускорения за раз одновременно обрабатывается по 4 байта для заполнения строки. Тем не менее, можно сделать еще быстрее, если использовать указатель в EDI на конец строки и использовать отрицательный счетчик в ECX, постепенно увеличивая его до нуля и используя его как индекс ([EDI+ECX*4]), что также сделало бы не нужным увеличение регистра EDI после каждой итерации. Это прекрасный повод для читателя переписать эту функцию данным образом и сравнить результаты выполнения. Также, может быть, вы захотите уменьшить количество циклов для еще большей эффективности. Например, обрабатывать по 8 байт за каждую итерацию, уменьшив этим количество переходов.
Как видим, возврат информации через регистры не всегда самый простой путь, особенно для структурных переменных типа длинных строк.
 

1.7. Возврат информации через стек процессора
 

Даже если на первый взгляд кажется странным возвращать результат через стек, в некоторых случаях это единственный путь для возврата результата. Например, если результат не помещается в регистр или не помещается на стек сопроцессора. Вы должны думать об коротких строках, записях и множествах, которые не помещаются в регистр. Но мы не говорим о таких типах данных, которые возвращаются, как указатель на результат и не говорим о результатах, передаваемые как var параметр. В предыдущей главе, мы уже обсуждали эти принципы. Тем не менее, когда размер результата не известен заранее, как для длинных строк до их создания, некоторые типы занимают фиксированное количество байт в памяти, и место для их размещения может быть выделено компилятором еще до вызова функции. Это то, что в действительности применимо для записей, статических массивов и больших множеств, также и для коротких строк.
Допустим, что у нас есть запись TMyRecord, объявленная следующим образом:
 
 type  
 
  
 TMyRecord = record  
 
  
 A: Integer;  
 
 B: Double;  
 
 C: Integer;  
 
  
 end;  
 
  
Компилятор знает, что эта запись занимает 16 байт в памяти. Поэтому, мы можем объявить функцию, которая возвращает запись как результат, объявление будет выглядеть так:
 
 function MyFunction(I: Integer): TMyRecord; register;  
 
  
Как отмечено в главе 1.6, переменная Result передается в функцию как дополнительный var параметр. Поэтому регистр EDX хранит указатель на результат. Но в отличие от примера с AnsiString, память для результата уже выделена компилятором до входа в функцию, если быть точным, то на стеке. Поэтому нам не нужно самостоятельно выделять память в куче. Достаточно заполнить эту память, которую компилятор резервировал для этой цели. Надо быть только острожным и не выйти за пределы отведенной памяти! Если же это произойдет, то будет разрушен стек и как результат - повисание программы или ошибка доступа (access violation).
В связи с тем, что память уже выделена компилятором, и регистр EDX содержит указатель на эту память (в действительности это стековая память), мы можем просто использовать регистр EDX для заполнения результата:
 
 mov [EDX],EAX  
 
  
Мы заполнили первое двойное слово (член записи A) содержимым регистра EAX. Заметим, что в данном случае мы не заботились о расчете смещения, относительно регистра EDX, для доступа к нужному члену записи. Но вы должны все-таки написать так, чтобы позволить компилятору сделать эту работу за вас:
 
 mov [Result].A,EAX  
 
  
Это то же самое, что и выше, но компилятор знает, что Result - это указатель, хранящийся в регистре EDX, и вы можете использовать более ясную точечную нотацию для адресации членов записи. Компилятор сам рассчитает смещение. Строго рекомендуется использовать именно точечную нотацию везде, где только возможно.
 

1.8. Возврат информации через стек сопроцессора
 

Функции, которые возвращают результат с плавающей запятой, просто должны возвращать результат в ST(0). Ниже вы найдете пример. Помните, что сопроцессор обрабатывает внутри все числа, как 10-байтные расширенные числа. Указывая формат результата (single, double, extended, comp, currency, и т.д.) мы только указываем, как число будет записано в ST(0) и затем в память. В примере функции CalcRelatMass показано это. Передаются два параметра, масса и скорость тела и производится расчет относительной массы, согласно теории относительности. Оба параметра: масса (m) и скорость (v), передаются в функцию как double, результат так же возвращается как double.
 
 function CalcRelatMass(m,v: Double): Double; register;  
 
 const  
 
  
 LightVelocity: Integer = 299792500;  
 
  
 asm  
 
  
 {Расчет относительной массы по следующей фформуле:   
 
 Result = m / Sqrt(1-vІ/cІ), где c = скорость света,  
 
 m масса и v скорость движения объекта}  
 
  
 
 fild LightVelocity  
 
 fild LightVelocity  
 
 fmulp {расчет cІ}  
 
 fld v  
 
 fld v  
 
 fmulp {расчет vІ}  
 
 fxch  
 
 fdivp {vІ/cІ}  
 
 fld1  
 
 fxch  
 
 fsubp {ST(0)=1-(vІ/cІ)}  
 
 fsqrt {корень ST(0)}  
 
 fld m  
 
 fxch  
 
 fdivp {деление массы на корень результата}  
 
  
 end;  
 
  
Оба параметра, m и v, передаются в функцию через стек. Поскольку они типа double, то они занимают по 8 байт стека, который выглядит следующим образом:
 
 EBP+0x10m  
 
 EBP+0x08v  
 
 EBP+0x04адрес возврата  
 
 EBP ->предыдущий EBP  
 
   
Вы никогда не должны забывать, что внутри сопроцессора они обрабатываются как 10-байтные числа с плавающей запятой. Результат остается в ST(0) (верхушка стека математического сопроцессора). Это извлекается кодом, который вызвал функцию.
Вы можете изменять точность и режим округления вычислений путем изменения контрольного слова процессора. Хотя вы точно знаете, что делаете, но это не поощряется, поскольку смена контрольного слова влияет на все вычисления для всего вашего приложения. Проблему обостряет то, что некоторые DLL также изменяют контрольное слово. Это иногда может привести к непредсказуемым результатам или различным результатам в зависимости от Операционной Системы, на которой запускается программа или в зависимости от того, какие версии DLL реально используются. Как заметил Robert Lee в одном из сообщений в группе новостей, вы должны особенно избегать этого, путем загрузки контрольного слова из глобальной переменной Default8087CW (объявлена в модуле System) до выполнения важных процедур.
Так же очень важно полное понимание природы чисел с плавающей запятой при использовании их внутри вашего кода. Я написал отдельную статью, в которой обсуждается основы. Статья доступна на моих страницах по Дельфи на сайте http://www.optimalcode.com/Guido/fpv.html.

 

1.9. Передача параметров по зничению и ссылке
 

Имеется огромная разница между передачей параметров по значению и по ссылке (через директиву var). Например, следующее объявление функции:
 
 function MyFunction(I: Integer): Integer; register;  
 
  
Значение параметра I будет передано через регистр EAX (см. таблицу 2 для обзора, как параметры разного типа передаются в функцию/процедуру). Например, когда I=254, EAX подобен $000000FE. Но следующее объявление:
 
 function MyFunction(var I: Integer): Integer; register;  
 
  
передаст не значение I (254 в нашем примере), а указатель на местонахождение, где переменная I записана в памяти (например, $0066F8BC) и этот указатель будет помещен в регистр EAX! При передаче параметра с помощью ключевого слова var, вы всегда передаете 32-битный указатель на переменную (который естественно помещается в регистр соглашения register).
Посмотрим на простой пример: допустим, мы желаем, чтобы наша функция вернула сумму целочисленного числа и 12 (Конечно, это очень бессмысленный пример, он нужен просто для демонстрации), передадим параметр по значению (функция вернет результат в регистре EAX):
 
 function MyFunction(I: Integer): Integer; register;  
 
 asm  
 
  
 add EAX,12  
 
  
 end;  
 
  
В случае же передачи по ссылке, мы должны написать так:
 
 function MyFunction(var I: Integer): Integer; register;  
 
 asm  
 
  
 mov EAX,[EAX] {Загрузить значение параметра I через указатель}  
 
 add EAX,12  
 
  
 end;  
 
  
При использовании директивы const правила те же, как для переменных, передаваемых по значению. Например, для объявления:
 
 function MyFunction(const I: Integer): Integer; register;  
 
  
Регистр EAX будет содержать значение I, а не указатель.
Как мы обсуждали в главе 1.6, длинные строки, динамические массивы, варианты, большие множества и записи возвращаются с помощью дополнительного var параметра. Позже, в других главах, мы обсудим эти типы более детально.

 
Замечания о синтаксисе  

 

Глава 2: Замечания о синтаксисе
 

В этой главе, мы рассмотрим синтаксические требования, которые вам требуется знать. Если вы использовали отдельный ассемблер, то вы заметили, что встроенный ассемблер в Дельфи 1-5 поддерживает только относительно небольшой набор возможностей языка. Эта ситуация была улучшена с выходом Дельфи 6, теперь компилятор распознает набор MMX, SIMD и SSE инструкций (так же и Enhanced 3D для AMD CPU, но данная статья сфокусирована только на Intel, и мы не будем обсуждать это в дальнейшем). С другой стороны, это также дает возможность использовать некоторые OP конструкции внутри ассемблерного кода.
 

2.1. Инструкции и команды
 

Ваш код на ассемблере состоит из нескольких выражений. Каждая инструкция состоит как минимум из одной команды. В большинстве случаев, вам потребуется использовать от одного до нескольких операндов. Операнды разделяются символом запятой. Также в инструкции могут использоваться префиксы (например, rep или lock). Наконец, инструкция может включать метку (смотрите ниже рассуждения о метках).
Примеры допустимых инструкций:
 
 cdq {только команда}  
 
 bswap EAX {команда и один операнд}  
 
 mov EAX,[ESI] {команда и два операнда}  
 
 imul EAX,ECX,16 {команда и триа операнда}  
 
 rep movsd {префикс и коаднда }  
 
  
 @@Start: rep stosd {локальная метка, префикс и команда }  
 
  
Разрешено помещать несколько инструкций в одной строке, разделяя их точкой с запятой, Но я настоятельно не рекомендую так делать. Это сильно снижает читабельность вашей программы, и не добавляет при этом никакой эффективности, повышения скорости или каких-либо других преимуществ. При использовании по одной инструкции в строке не требуется ставить точку с запятой в конце строки (как это требуется для обычного Паскаль кода).
Комментарии могут быть добавлены в конце строки, но не могут размещаться внутри инструкции.
 

2.2. Набор команд
 

Встроенный ассемблер Дельфи 2-5 поддерживает только подмножество команд процессора Intel 80486 (документация по Дельфи 3 вообще утверждает, что только 80386, но дополнительные инструкции процессора 80486, например bswap, xadd, cmpxchg, fstsw ax, и другие в действительности распознаются и обрабатываются корректно). Тем не менее, специфические команды Pentium, например cpuid или условные перемещения из Pentium Pro, PII и PIII, не распознаются встроенным ассемблером в этих версиях. В Дельфи 6, поддержан полный набор команд от Pentium I до IV. Включая специальные расширения MMX, SSE и другие. Это действительно серьезное улучшение, поскольку в более ранних версиях приходилось их кодировать вручную с помощью инструкций db (см. ниже). Это было довольно неприятно, так как эти инструкции особо интересны для специальных случаев.
Если вы желали использовать эти инструкции в Д2-Д5, то должны были вставлять их вручную с помощью серии инструкций db. Ясно, что вы не только должны были быть очень осторожны при вставке их в код, избегая ошибок, но и также особо комментировать эти строки. Со следующей ссылки вы можете загрузить .pas, который содержит исходный текст класса TCPUID, в котором интенсивно используется ассемблер, и в котором инструкция cpuid закодирована с помощью инструкций db. Нажмите здесь
для загрузки cpuinfo.pas с сайта автора или с текущего каталога в формате cpuinfo.zip.
Вы должны проштудировать исходный код cpuinfo.pas, обратив особое внимание на функцию GetCPUIDResult, которая написана полностью на basm. Программа вызывает cpuid для различных уровней ID, которые поддержаны и заполняет запись типа TCPUIDResult полученной информацией. Данный тип записи используется в методах класса TCPUID. Заметим, что все поля записи TCPUIDResult адресуются через их имена, вместо расчета смещения. Компилятор сам рассчитывает смещение, так что если структура записи будет изменена, то код будет продолжать работать корректно.
Заметим, что команда cpuid уничтожает содержимое всех нормальных регистров, так что требуется особая осторожность при работе с ними. При этом так же сбрасываются все конвейеры, и ожидается окончание работы всех оставшихся инструкций, поэтому вы не должны использовать это в критических ситуациях. После выполнения инструкции cpuid, все нормальные регистры, включая EAX и другие, будут изменены.
Полное описание набора команд процессоров можно найти на сайте фирмы Intel http://developer.intel.com. Как я заметил во введении, данная статья посвящена только процессорам фирмы Intel не только, поскольку они самые распространенные, но и потому что они являются стандартом де-факто для набора команд процессора и сопроцессора. Некоторые другие производители имеют в составе своих процессоров дополнительные команды, но поскольку они присутствуют только в их процессорах, вы не должны пытаться их использовать, чтобы ваши приложения могли работать на более широком спектре систем. Другим решением является иметь различные варианты критичных по времени кусков кода, оптимизированные для различных процессоров. В начале вашей программы вы должны проверить тип процессора и установить глобальный флаг, который будет указывать, какую версию использовать.
Аналогичный вопрос: какой минимальный набор инструкций должен быть в вашей программе, что бы она могла работать на 80486 или более ранних процессорах. Конечно, 80486 и более старые процессоры уже устарели и, как минимум, стоит ориентировать вашу программу на Intel Pentium Plain или выше. Тем не менее, если выбрать более новую модель, как базис, например Pentium II и выше, то вы игнорируете многие компьютеры с Pentium Plain и Pentium MMX, которые еще в ходу. Если вы выберите минимум как Pentium II, то вы сможете получить преимущества от дополнительных инструкций, таких как условные перемещения. Так же, в данном случае проще написать кусок кода, который будет поддерживать все платформы. Если вы решили включить поддержку Pentium Plain и MMX CPU, чтобы быть более осведомленным в других вещах, таких как парность команд, различные особенности по предсказанию переходов и т.д. Все это можно изучить в деталях в превосходном руководстве от Agner Fog на сайте http://www.agner.org/assem/, но давайте начнем, ниже несколько основных правил.
 

2.2.1. Не используйте комплексные команды
 

В большинстве случаев, комплексные строковые инструкции очень медленны и должны быть заменены оптимизированным циклом с простыми инструкциями. Без префикса rep, комплексные инструкции вообще за пределами вопроса. Только при определенных специфических условиях инструкции rep movsd и rep stosd могут быть быстрее, но только при условии, что оба адреса, приемник и источник, выровнены на границу восьми байт и при этом не должно быть конфликтов в кэше, и в свете того, что в данный момент Дельфи не дает возможности управлять выравниванием, вы не должны их использовать.
 

2.2.2. Используйте 32-битные алгоритмы, везде, где только возможно
 

Если только невозможно иначе, то вы не должны использовать инструкции, которые оперируют словами, более правильно использовать те, которые работают с переменными типа двойное слово. Байтовый доступ еще может иногда использоваться, но остерегайтесь использовать операции со словами. Ниже пример использования 32-битного алгоритма для поиска символа в строке. Идея основана на генерации уникального значения, если и только если символ найден. Поскольку пример обрабатывает строку по четыре байта за раз, то это значительно быстрее, чем обработка по одному байту за раз, несмотря на дополнительное усложнение, поскольку требуется обрабатывать сразу четыре байта.
 
 function ScanStrForChar(C: Char; S: String): Integer; register;  
 
 asm  
 
  
 push EBX  
 
 push EDI  
 
 push ESI  
 
 test EDX,EDX  
 
 jz @notfound  
 
 mov ESI,[EDX-4]  
 
 test ESI,ESI  
 
 jz @notfound  
 
 add ESI,EDX  
 
 mov ah,al  
 
 mov di,ax  
 
 shl EDI,16  
 
 or di,ax  
 
 mov EAX,EDX  
 
 lea EDX,[EAX+3]  
 
  
 @L1:  
 
  
 cmp EAX,ESI  
 
 ja @notfound  
 
 mov EBX,[EAX]  
 
 xor EBX,EDI  
 
 add EAX,4  
 
 lea ECX,[EBX-$01010101]  
 
 not EBX  
 
 and ECX,EBX  
 
 and ECX,$80808080  
 
 jz @L1  
 
 mov EBX,ECX  
 
 shr EBX,16  
 
 test ECX,$8080  
 
 jnz @L2  
 
 mov ECX,EBX  
 
  
 @L2:  
 
  
 lea EBX,[EAX+2]  
 
 jnz @L3  
 
 mov EAX,EBX  
 
  
 @L3:  
 
  
 shl cl,1  
 
 sbb EAX,EDX  
 
 inc EAX  
 
  
 @ending:  
 
  
 pop ESI  
 
 pop EDI  
 
 pop EBX  
 
 ret  
 
  
 @notfound:  
 
  
 xor EAX,EAX  
 
 jmp @ending  
 
  
 end;  
 
  
В зависимости от длины обрабатываемых строк, функция может быть быстрее стандартной функции pos раза в два. Заметим, что должна прочитать от одного до трех символов в конце строки, но в текущей версии компилятор всегда размещает строки по модулю четыре, так что, это не приносит проблем, но нельзя гарантировать, что в следующих версиях это будет так же. Вы можете устранить эту проблему, путем расчета остатка символов в конце строки по модулю четыре и обработать остаток побайтно. Вы потеряете некоторое быстродействие, но выиграете в надежности. Заметим, что компилятор добавляет одну или несколько инструкций ret после jmp @ending. Но они никогда не будут выполнены, поскольку мы включили инструкцию ret сами.
Вы должны избегать подобных вещей, поскольку если нужен фрейм стека, то вы должны будете сами написать код выхода (см. главу 1.2, где рассмотрены коды входа и выхода). В вышеприведенном примере нет фрейма стека, так что нет нужды и в коде выхода. Вы можете избежать этой проблемы, путем добавления инструкции jmp для условия, когда символ не найден, это просто пропустит установку результата в ноль, если строка пустая или символ не найден. После этого пример будет выглядеть так:
 
 ...   
 
 shl cl,1  
 
 sbb EAX,EDX  
 
 inc EAX  
 
 jmp @ending  
 
  
 @notfound:  
 
  
 xor EAX,EAX  
 
  
 @ending:  
 
  
 pop ESI  
 
 pop EDI  
 
 pop EBX  
 
  
 end;  
 
  
Но, в этом случае вы будете вынуждены всегда добавлять дополнительную инструкцию jmp в ваш алгоритм, который немного от этого замедлится. Если вы обрабатывает достаточно длинные строки, то это почти незаметно на общем процессе обработке, и добавленный код может быть субъектом для оптимизации в дальнейшем, когда это станет важным.
 

2.2.3. Избегайте деления
 

Деление, как правило, более медленное - заменяйте его сдвигами или умножением с соответствующим значением.
 

2.2.4. Замечания по особым инструкциям
 

Битовые инструкции (bt, btc, bts, btr) должны по возможности заменяться на инструкции and, or, xor и test, когда приоритетом является скорость.
Избегайте инструкции wait. На старых процессорах инструкция wait была нужна для синхронизации доступа к памяти и уверенности, что сопроцессор был готов к выполнению операции. На процессорах Pentium это абсолютно лишнее. Единственная причина использования инструкции wait это отлов исключения из предыдущей инструкции. Сейчас большинство инструкций сопроцессора, отлавливают исключение без инструкции wait (исключая fnclex и fninit), вы можете опускать инструкцию wait в большинстве случаев. Если бит исключения устанавливается, то следующая инструкция с плавающей запятой отлавливает это. Если вы хотите быть уверенным, что любые, необслуженные исключения, были обработаны до окончания процедуры, то вы можете добавить инструкцию wait после критического по времени куска, что обработает все необслуженные исключения.
 

 

...Продолжение в следующем выпуске

 

Сайт рассылки Здесь

Так же можете посетить несколько сайтов для заработка в Интернете:

Hit&Host

 

Raskrutim.ru

 

WmSearch

 


В избранное