Рассылка закрыта
При закрытии подписчики были переданы в рассылку "Сетевой адаптер: осваиваем Интернет" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
Все, что вы не знали, но хотели бы узнать о Delphi №13
Выпуск №13 Раздел: Язык Программирования Delphi Подраздел: Этот подраздел может показаться немного скучным, но не все же программированию быть интересным и занимательным? Ассемблер - вещь необходимая, хотя и неприятная для большинства программистов. Этот подраздел разбит на три части, но, по-моему, он того стоит. Уважаемый подписчик, О чем будет следующий раздел - решать вам. Варианты: VCL Системные функции и Winapi Базы данных Работа с файловой системой Репортинг, работа с принтером Работа с сетью, Интернетом, протоколами Работа с графикой, мультимедиа
Ваши предложения высылайте на В этом выпуске: Использование ассемблера в Дельфи
Глава 1: Об основах ассемблерных процедур 1.1. Где размещать ассемблерный код Часто, ассемблерный код ассоциируется со скоростью. Поэтому циклы вы также должны по возможности организовывать внутри ассемблерного кода. Это не сложно, а иначе вы просто потеряете множество времени за счет постоянного вызова.
(1) В данных главах мы специально указываем соглашение о вызове. На самом деле указание register избыточно, так как соглашением по умолчанию является передача параметров через регистры, это сделано исключительно для читабельности (или как дополнительный комментарий) и как напоминание читателю, что параметры передаются через регистры. 1.2. Код входа/выхода и сохранение регистров Компилятор автоматически генерирует необходимый код входа и выхода из ассемблерных подпрограмм. Код входа выглядит так:
Код входа сначала сохраняет текущее значение регистра 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. Передача информации через регистр Текущее поколение процессоров, для которых написана данная статья, обычно называются как Intel Pentium процессоры, имеет регистры шириной в 32 бита. Когда передаваемая информация не полностью использует регистр (для типов слово и байт), то используется, только часть регистра, байты используют младшие восемь бит, например al и для слов младшее слово, например ax. Указатели всегда 32-битные (по крайней мере, пока не появятся 64-битные процессоры) и занимают весь регистр полностью, например eax. В случае переменных типа байт и слово оставшаяся часть регистра не определена и вы не должны делать никаких предположений относительно его содержимого. Например, при передаче байта в функцию, через регистр al, остальные 24 бита регистра eax не определены и вы, конечно, не можете рассчитывать на то, что они равны нулю. Вы просто можете использовать инструкцию and для очистки оставшихся бит.
1.4. Передача информации через стек Обычно, для доступа к параметрам на стеке вы должны обращаться к ним через адресацию с помощью регистра EBP. Код входа по умолчанию, который генерирует компилятор, устанавливает регистр EBP на данный фрейм. Таким образом, использование EBP с нужным смещением позволяет иметь доступ к параметрам на стеке и к локальным переменным. Посмотрим на пример с использованием соглашения о вызове pascal. Данное соглашение помещает параметры слева, направо. Для примера, в следующем объявлении:
Данные, переданные через стек, всегда занимают 32 бита, даже если вы передаете байт, оставшиеся биты просто не определены. 1.5. Локальные переменные
1.6. Возврат информации через регистры процессора Таблица 3 содержит обзор о вариантах возврата результатов. В большинстве случаев, результат возвращается через регистр EAX или FP(0). Особый случай, когда в результате возвращается длинная строка или другой тип, возвращаемый через указатель. В случае длинных строк, динамических массивов, больших множеств, вариантов и больших записей, переданных через параметр с директивой var, используется 32-битный указатель на результат. Так, где же хранится действительное содержимое результата (например, длинных строк)? Ответ на это в том, что вы должны выделить место в куче, заполнить его данными, и вернуть указатель на эту область памяти через переменную Result. Заметим, что, для множеств, записей и массивов, которые могут разместиться в регистре, переменная Result возвращает их через регистр. Только для длинных строк, вариантов и множеств, записей и массивов, которые занимают свыше 32 бит, переменная Result возвращает указатель на дополнительный указатель, размещенный функцией, аналогично директиве var параметра (мы рассмотрим директиву параметра var в главе 1.9). Не беспокойтесь, если что-то сейчас не понятно, позже мы рассмотрим эти типы подробнее. Теперь поясним это на примере. Функция PlusMinusLine возвращает длинную строку, состоящую из последовательности плюсов и минусов, для формирования строки. Например, когда вы напишите так S:=PlusMinusLine(9), то S должна получить значение: "-+-+-+-+-". Декларация функции следующая:
Как видим, возврат информации через регистры не всегда самый простой путь, особенно для структурных переменных типа длинных строк. 1.7. Возврат информации через стек процессора Допустим, что у нас есть запись TMyRecord, объявленная следующим образом:
В связи с тем, что память уже выделена компилятором, и регистр EDX содержит указатель на эту память (в действительности это стековая память), мы можем просто использовать регистр EDX для заполнения результата:
1.8. Возврат информации через стек сопроцессора
Вы можете изменять точность и режим округления вычислений путем изменения контрольного слова процессора. Хотя вы точно знаете, что делаете, но это не поощряется, поскольку смена контрольного слова влияет на все вычисления для всего вашего приложения. Проблему обостряет то, что некоторые DLL также изменяют контрольное слово. Это иногда может привести к непредсказуемым результатам или различным результатам в зависимости от Операционной Системы, на которой запускается программа или в зависимости от того, какие версии DLL реально используются. Как заметил Robert Lee в одном из сообщений в группе новостей, вы должны особенно избегать этого, путем загрузки контрольного слова из глобальной переменной Default8087CW (объявлена в модуле System) до выполнения важных процедур. Так же очень важно полное понимание природы чисел с плавающей запятой при использовании их внутри вашего кода. Я написал отдельную статью, в которой обсуждается основы. Статья доступна на моих страницах по Дельфи на сайте http://www.optimalcode.com/Guido/fpv.html. 1.9. Передача параметров по зничению и ссылке
Посмотрим на простой пример: допустим, мы желаем, чтобы наша функция вернула сумму целочисленного числа и 12 (Конечно, это очень бессмысленный пример, он нужен просто для демонстрации), передадим параметр по значению (функция вернет результат в регистре EAX):
Как мы обсуждали в главе 1.6, длинные строки, динамические массивы, варианты, большие множества и записи возвращаются с помощью дополнительного var параметра. Позже, в других главах, мы обсудим эти типы более детально.
Глава 2: Замечания о синтаксисе 2.1. Инструкции и команды Примеры допустимых инструкций:
Комментарии могут быть добавлены в конце строки, но не могут размещаться внутри инструкции. 2.2. Набор команд Если вы желали использовать эти инструкции в Д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. Не используйте комплексные команды 2.2.2. Используйте 32-битные алгоритмы, везде, где только возможно Если только невозможно иначе, то вы не должны использовать инструкции, которые оперируют словами, более правильно использовать те, которые работают с переменными типа двойное слово. Байтовый доступ еще может иногда использоваться, но остерегайтесь использовать операции со словами. Ниже пример использования 32-битного алгоритма для поиска символа в строке. Идея основана на генерации уникального значения, если и только если символ найден. Поскольку пример обрабатывает строку по четыре байта за раз, то это значительно быстрее, чем обработка по одному байту за раз, несмотря на дополнительное усложнение, поскольку требуется обрабатывать сразу четыре байта.
Вы должны избегать подобных вещей, поскольку если нужен фрейм стека, то вы должны будете сами написать код выхода (см. главу 1.2, где рассмотрены коды входа и выхода). В вышеприведенном примере нет фрейма стека, так что нет нужды и в коде выхода. Вы можете избежать этой проблемы, путем добавления инструкции jmp для условия, когда символ не найден, это просто пропустит установку результата в ноль, если строка пустая или символ не найден. После этого пример будет выглядеть так:
2.2.3. Избегайте деления 2.2.4. Замечания по особым инструкциям Избегайте инструкции wait. На старых процессорах инструкция wait была нужна для синхронизации доступа к памяти и уверенности, что сопроцессор был готов к выполнению операции. На процессорах Pentium это абсолютно лишнее. Единственная причина использования инструкции wait это отлов исключения из предыдущей инструкции. Сейчас большинство инструкций сопроцессора, отлавливают исключение без инструкции wait (исключая fnclex и fninit), вы можете опускать инструкцию wait в большинстве случаев. Если бит исключения устанавливается, то следующая инструкция с плавающей запятой отлавливает это. Если вы хотите быть уверенным, что любые, необслуженные исключения, были обработаны до окончания процедуры, то вы можете добавить инструкцию wait после критического по времени куска, что обработает все необслуженные исключения.
...Продолжение в следующем выпуске
Сайт рассылки Здесь Так же можете посетить несколько сайтов для заработка в Интернете: |
В избранное | ||