http://sulfurzona.com/
News
Service
Magazine
Software (Battle City Game, Wallpaper manager, Superpad, VG-NOW, Puzzle Game, Netler Internet Browser, ..)
Dune Game (Dune III, Dune IV, Cheats, Forum, ..)
Games free
Turbo Pascal (Assembler, Docs, Sources, Debbugers, ..)
Books (Docs for developers)
Forum
Guest book
Компьютерная диагностика двигателя автомобиля (адаптер К-линии)Компьютерная диагностика двигателя автомобиля (адаптер К-линии)
 
 
 
 
 
 

Паскаль для новичков (часть 39)

 

Специальные asm-идентификаторы

 
Паскаль для новичковПри программировании на ассемблере помимо доступа к переменным может понадобиться получить доступ к сегменту данных и кода программы, например, в процедуре обработчика прерывания. Для этих целей предусмотрены два идентификатора @Data и @Code соответственно для сегмента данных и кода. Эти идентификаторы следует использовать совместно с оператором SEG
 
asm
 mov ax, SEG @Data
 mov ax, SEG @Code
end;
 
Такие комбинации компилируются в инструкцию загрузки константного значения соответствующего номера сегмента в регистр AX, что позволяет, например, восстановить значение сегментного регистра DS, если его значение было утеряно. Необходимо знать, что стандартная функция DSEG возвращает не номер сегмента данных, а текущее значение сегментного регистра DS.
  
Во встроенном ассемблере имеется ещё один идентификатор @Result, который может применяться только в функциях и служит для передачи результата во временную переменную для хранения результата, расположенную в стеке. Следующий пример иллюстрирует, как можно изменять и получать значение результата функции внутри asm-оператора:
 
function ClearWord : word;
begin
asm
 xor AX, AX {обнуляем регистр AX}
 mov @Result, AX
 mov AX, @Result
end;
end;
 
Немного по-другому обстоит дело с возвращением результата типа String. Дело в том, что применительно к типу String идентификатор @Result подразумевает адрес памяти не самой переменной, а адрес указателя на переменную, поэтому следует извлечь адрес сегмент:смещение и загрузить его в регистровую пару, удобную для доступа к области строки, например, в ES:DI. Затем индексировать регистром DI обращение к памяти, где хранится результат строчного типа.
 
function GetStr : string;
begin
asm
 MOV DI, word ptr @Result
 MOV AX, word ptr @Result+2
 MOV ES, AX
 MOV AL, 2   {длина строки будет 2 символа}
 MOV ES:[DI], AL
 INC DI   {переходим к следующему байту строки}
 MOV AL, 'A'      {первый символ будет ‘A’}
 MOV ES:[DI], AL
 INC DI {переходим к следующему байту строки}
 MOV AL, 'B'      {второй символ будет ‘B’}
 MOV ES:[DI], AL
end;
end;
 
Первые три строки из данного примера можно заменить одной, выполняющей ту же операцию загрузки указателя в регистровую пару ES:DIсегмент:смещение, причём в ES сегмент, а в DI соответственно смещение. При этом регистр AX не задействован.
Тогда код функции GetStr станет короче:
 
function GetStr : string;
begin
asm
 LES DI, @Result
 MOV AL, 2  
 MOV ES:[DI], AL
 INC DI  
 MOV AL, 'A'     
 MOV ES:[DI], AL
 INC DI 
 MOV AL, 'B'     
 MOV ES:[DI], AL
end;
end;
 
Бывает, что логика кода функции строится на поэтапном формировании результата, при чём каждый последующий этап базируется на значении результата, полученном при выполнении предыдущего этапа. Но в переменную результата можно было только записывать, указывая идентификатор функции в левой части присваивания, так как указание оного в правой части оператора присваивания расценивалось бы компилятором как рекурсивный вызов. Чтобы как-то выйти из данной ситуации приходилось объявлять локальную переменную такого же типа, что и тип результата функции. При этом увеличивался расход стека, что при использовании рекурсии заметно снижало количество возможных рекурсивных вызовов.
 
В этом щекотливом деле неожиданно на помощь пришла гибкость встроенного ассемблера. Спрашивается, зачем выделять лишнюю память под временную переменную для хранения результата, когда перед вызовом функции память под результат уже была выделена. Особенно это касается функций, возвращающих результат строчного типа. Так, например, достаточно объявить переменную типа указатель на строку, в данном случае MyResult, затем при помощи короткого ассемблерного оператора адрес памяти в стеке, зарезервированной под результат, загрузить в объявленный указатель MyResult и в дальнейшем, при помощи разыменования указателя, легко и свободно выполнять с результатом все действия, какие только заблагорассудится. При этом значение результата функции всегда будет определёно, несмотря на то, что в теле функции нет ни единого оператора присваивания с идентификатором функции в левой части.
 
function GetStr : string;
var MyResult : ^string;
begin
asm
 mov AX, word ptr @Result
 mov word ptr MyResult, AX
 mov AX, word ptr @Result+2
 mov word ptr MyResult+2, AX
end;
MyResult^ := '10';
MyResult^ := MyResult^ + '40';
end;
 
Хотя это и не эффективно, но аналогичным образом можно поступить и с функциями, возвращающими результат любого другого типа. Такой метод доступа к результату, скорее всего, может лишь сделать код функции нагляднее, поскольку с точки зрения экономии стека для таких типов как целые и вещественные, такой метод убыточен. При этом получить смещение переменной в памяти можно инструкцией LEA AX,@Result, а сегмент из сегментного регистра SS.
 
function GetWord : word;
var MyResult : ^word;
begin
asm
    LEA AX, @Result
    MOV word ptr MyResult, AX
    MOV AX, SS
    MOV word ptr MyResult+2, AX
end;
MyResult^ := 10;
MyResult^ := MyResult^ + 50;
end;
 
Идентификатор MyResult я применил для наглядности, хотя для большего удобства следует использовать просто идентификатор Result, не путать со стандартным @Result.
 

Ограничения

 
Следует отметить ряд ограничений. В выражениях asm-оператора недопустимо использовать следующие конструкции языка Turbo Pascal:
а) стандартные функции и процедуры, такие как Writeln, Chr;
б) специальные массивы Mem, MemW, MemL, Port, PortW;
в) константы строковых, вещественных и множественных типов;
г) inline-подпрограммы;
д) нелокальные метки;
е) специальный идентификатор @Result за пределами тела функции.
 

Asm-подпрограммы

 
Помимо asm-операторов Turbo Pascal позволяет создавать подпрограммы полностью на языке ассемблера. Для этого достаточно заголовок подпрограммы завершить служебным словом assembler и тело подпрограммы должно быть оформлено в виде единственного asm-оператора без Begin..End. При этом, если такая подпрограмма не имеет входных параметров и локальных переменных, то компилятор не отводит память в стеке и код для этого не формирует. К тому же ассемблерные функции возвращают результат в регистрах процессора и, таким образом, эффективность кода за счёт таких ассемблерных подпрограмм увеличивается. Так, функции 8-битных типов (Byte, Shortint, Char, Boolean и перечислимые) должны возвращать результат в регистре AL. Функции 16-битных типов (Integer, Word) должны возвращать результат в регистре AX. Функции 32-битных типов (Longint, Pointer) должны возвращать результат в регистровой паре DX:AX, где DX – старшее слово, AX – младшее слово. Функции вещественного типа Real должны возвращать результат в регистрах DX:BX:AX. Функции, использующие обращения к математическому сопроцессору i80x87, с типом результата Single, Double, Extended, Comp возвращают его в регистре ST(0) сопроцессора. Исключением являются ассемблерные функции, возвращающие результат строчного типа, так как для результата такой функции резервируется память в стеке длиной 256 байт, где первый байт – это длина строки. Следующая ассемблерная функция переводит значение типа байт в текстовую строку с представлением в шестнадцатеричной системе:
 
function ByteToHex( Decimal : byte ) : string; assembler;
 
 
 
 
 
const HexTable : string = ('0123456789ABCDEF');
asm
 LES DI, @Result
 MOV AL, 2 {строка длиной 2 символа}
 MOV ES:[DI], AL
 INC DI
 LEA BX, HexTable
 INC BX {пропускаем байт длины строки}
 MOV AL, Decimal
 SHR AL,4 {берём старшие 4 бита}
 XLAT
 MOV ES:[DI], AL
 INC DI
 MOV AL, Decimal
 AND AL, $0F {берём младшие 4 бита}
 XLAT
 MOV ES:[DI], AL
end;
 
При компиляции ассемблерных подпрограмм компилятор формирует код для загрузки значений фактических параметров в локальные переменные только для формальных параметров, размер которых равен 1, 2 и 4 байтам (типы byte, shortint, char, boolean, integer, word, longint, pointer, перечислимые и ограниченные). В случае передачи в качестве параметра строки, записи или массива, следует рассматривать данный параметр как var-параметр, то есть параметр-переменная. Так в функции GetLength из следующего примера, которая возвращает длину строки, параметр Str подразумевается как var-параметр.
 
type TArrByte = array [0..64000] of byte;
var   S : string;
        Screen : ^TArrByte;
 
function GetLength(Str : string):byte; assembler;
asm
 LES DI, Str
 MOV AL, ES:[DI] {длину строки в результат}
end;
 
procedure FastMove( var Src, Dst; Count : word ); assembler;
asm
 PUSH DS
 MOV CX, Count
 CMP CX,0
 JZ @End
 LDS SI, Src
 LES DI, Dst
 REP MOVSB
@End:
 POP DS
end;
 
begin
S := 'H e l l o ! ';
Screen := Ptr( $0B800, 0 );
FastMove( S[1], Screen^, GetLength( S ) );
end.
 
Данный пример при выполнении процедуры FastMove выводит строку S на экран. Экран представлен в виде массива Screen^, расположенного в экранной области видеопамяти. Так как данная процедура получает бестиповые параметры Src и Dst, то ссылка на строку указана как S[1], чтобы вывод строки начинался с её первого символа, а не с символа длины. В процедуре FastMove инструкция REP MOVSB эквивалента строкам:
 
@Next:
 MOV AL, DS:[SI]
 MOV ES:[DI], AL
 INC SI
 INC DI
 LOOP @Next
 
Хочу обратить особое внимание на то, что вопреки рекомендациям не изменять значение регистра DS, в данной процедуре всё же его значение изменяется инструкцией LDS SI,Src. Чтобы такая дерзость с нашей стороны не повлекла за собой сбоев, код процедуры заключён в комбинацию инструкций PUSH DS…POP DS. Таким образом, удаётся восстановить исходное значение регистра DS, равное истинному номеру сегмента данных.
  
Если необходимо экономить стек, то есть другой способ восстановить значение регистра DS. Для этого можно в этом же примере убрать команду PUSH DS, а вместо команды POP DS поставить инструкции MOV AX,SEG @Data; MOV DS,AX. В последнем способе для восстановления значения регистра DS задействован регистр AX.
  
Есть третий способ, он использует стек, но сразу же его выравнивает и не использует других регистров. Для его реализации достаточно в примере убрать команду PUSH DS, а вместо команды POP DS поставить инструкции PUSH Seg @Data; POP DS. При этом должна быть установлена директива компиляции {$G+}.
  
Так же вопреки рекомендациям не изменять значение регистра BP, всё-таки это можно делать, если необходимо серьёзно оптимизировать код подпрограммы.
  
В практике программиста могут возникать случаи, когда некоторый код оперирует с большим количеством параметров, но при этом должен выполнять свою работу весьма быстро. Параметры, как правило, размещаются в памяти (это же переменные). Каждое обращение к памяти занимает уйму времени, так как вступает в силу фактор производительности системной памяти компьютера, а она, как правило, на порядок медленнее, чем процессор. Даже при наличии кэш-памяти может наблюдаться медлительность, так как один параметр может быть размещён в начале сегмента данных, другой в его конце, за пределами кэшируемой страницы. Третий параметр может быть расположен в стеке, а четвёртый и вовсе в видеопамяти. При таком разбросе адресов, вероятнее всего будут быстро доступны только данные из стека и некоторые данные из сегмента данных. Поэтому, машинные операции регистр-регистр выполняются на много быстрее, чем операции регистр-память. Следовательно, для того, чтобы такой код быстро работал, необходимо разместить все необходимые параметры в свободных регистрах процессора. Благо их не мало: AX, BX, CX, DX, SI, DI, но даже шести регистров может оказаться мало и тогда приходится высвобождать регистр BP, чтобы можно было использовать и его.
  
В данном случае необходимо знать, что все обращения к параметрам и локальным переменным внутри подпрограммы осуществляются адресацией через регистр BP. Следовательно, если его значение изменить, то последующее чтение значения переменной может дать ошибочное значение, а запись в переменную может привести к сбою работы компьютера, причём это может произойти не сразу, а с задержкой, и место такой ошибки будет трудно выявить. Поэтому очень важно перед кодом, где значение регистра BP будет изменено, загрузить все используемые локальные переменные в регистры, сохранить в стеке значение BP командой PUSH BP, и только затем загрузить данные в регистр BP. После того, как некоторые действия с регистром BP выполнены, следует восстановить его значение из стека инструкцией POP BP, иначе все последующие обращения к локальным переменным будут аварийными, и стек останется не выровненным.
  
Следует отметить ещё одну деталь. При указании ассемблерной инструкции RET для выхода из подпрограммы, компилятор формирует машинную команду возврата из подпрограммы в соответствии с директивой компиляции {$F+}/{$F-}, так как служебные слова FAR и NEAR применять к ассемблерным подпрограммам невозможно. По умолчанию ассемблерная подпрограмма рассматривается компилятором как NEAR и любое указание инструкции RET компилируется в машинную команду RETN (ближний возврат), иначе в машинную команду RETF (дальний возврат). Однако я хочу предостеречь от необдуманного использования инструкции RET, поскольку её можно безопасно использовать лишь в подпрограммах, которые не используют стек, то есть, не имеют входных параметров и локальных переменных, и не возвращают результат строчного типа. Иначе откомпилированная инструкция RET при выполнении её процессором может повлечь за собой непредсказуемые действия. В любом случае подпрограмма будет успешно завершена, если указать в её теле перед служебным словом End метку, например, с именем @End и, в случае необходимости прервать выполнение подпрограммы, выполнить условный переход Jxx @End или безусловный переход JMP @End. В качестве примера может служить выше указанная процедура FastMove.
 

Представление данных

 
Встроенный ассемблер позволяет указывать в инструкциях непосредственные данные, представленные в десятичной, двоичной или шестнадцатеричной системе счисления. При указании данных в десятичном виде достаточно просто указать цифры. Если данные в двоичном виде, то следует указать каждый бит в виде единицы или нуля и завершить список бит символом ‘b’ (латинская “би”). Следует отметить, что если старшие биты числа нулевые, то достаточно указать значащие младшие биты. При указании данных в шестнадцатеричном виде следует цифры предварять символом ‘$’ (доллар) как это принято в Паскале, либо в конце числа поставить символ ‘h’ (латинская “эйч”) как это принято в ассемблере. Если шестнадцатеричное число в самом старшем разряде содержит букву, а не цифру, то эту букву следует предварить нулём. Необходимо помнить, что цифры числа любой системы счисления справа означают младшие разряды, а слева старшие. Вот примеры, где в фигурных скобках указаны эквивалентные десятичные значения:
 
mov AL, 11110000b {240}
mov AL, 1111B        {15}
mov BX, 1111111111111111b {65535}
mov CX, 10          {10}
mov CX, 10h        {16}
mov CX, $8          {8}
mov AX, 0A000h {40960}
 
Продолжение следует…
 
© Владислав Демьянишин
 
Литература та же
 
На нашем сайте можно не только бесплатно скачать игры, но и документацию и книги по программированию на MIDLetPascal, Turbo Pascal 6, Turbo Pascal 7, Borland Pascal, по программированию устройств Sound Blaster, Adlib, VESA BIOS, справочник Norton Guide и много другой полезной информации для программистов, включая примеры решения реальных задач по созданию резидентных программ. Предлагаю также посетить Марья искусница - сайт о рукоделии (http://mariya-iskusnica.ru).
 

Журнал > Программирование > Паскаль для новичков (Turbo Pascal, Assembler) > Паскаль для новичков (часть 39): Специальные asm-идентификаторы
 
 
 
15
 
ВКонтакте
Facebook
 
 
 
На главную страницу На предыдущую страницу На начало страницы
 
 




Украина онлайн Рейтинг@Mail.ru Рейтинг Сайтов YandeG Rambler's Top100