ГЛАВА 23. РЕДАКТИРОВАНИЕ АССЕМБЛЕРНОГО КОДА. Процедуры и функции, написанные на ассемблере могут быть связаны с программами и модулями Turbo Pascal с помощью директивы компилятора $L. Исходный файл на ассемблере должен быть ассемблирован в объектный файл (.OВJ) с помощью Turbo Assembler. С программой или модулем можно связать несколько объектных файлов с помощью нескольких директив $L. Процедуры и функции, написанные на ассемблере, должны быть объявлены в программе или модуле на Паскале, как external, например: function LoCase(Ch: Char): Char; external; В соответствующей исходной программе на ассемблере, все процедуры и функции должны быть расположены в сегменте с именем CODE или CSEG, или в сегменте, чье имя оканчивается _TEXT и имена внешних процедур и функций должны быть в директиве PUBLIC. Вы должны быть уверены, что процедура или функция на ассемблере соответствует ее определению на Паскале по модели вызова (NEAR или FAR), числу параметров, типу параметров и типу результата. Исходный файл на ассемблере может объявлять инициализированные переменные в сегменте с именем CONST или в сегменте, чье имя оканчивается _DATA и неинициализированные переменные в сегменте с именем DATA или DSEG, или в сегменте, чье имя оканчивается _BSS. Такие переменные являются локальными в программе на Ассемблере и к ним нельзя обратиться из программы или модуля на Паскале. Однако, они размещаются в том же сегменте, что и глобальные переменные Паскаля и к ним можно обратиться через сегментный регистр DS. Ко всем процедурам, функциям и переменным, объявленным в программе или модуле на Паскале и объявленным в секции interface используемых модулей, можно обратиться из программы на Ассемблере через директиву EXTRN. Конечно, Вы должны использовать корректный тип в описании EXTRN. Когда в директиве $L появляется объектный файл, Turbo Pascal преобразует этот файл из перемещаемого формата объектного модуля Intel (.OBJ) в свой внутренний перемещаемый формат. Это преобразование возможно только при соблюдении правил: - Все процедуры должны быть размещены в сегменте с именем CODE или CSEG, или в сегменте, чье имя оканчивается на _TEXT. Все инициализированные локальные переменные должны размещаться в сегменте с именем CONST или сегменте с именем, оканчивающимся на _DATA. Все неинициализированные локальные переменные должны быть размещены в сегменте с именем DATA или DSEG, или в сегменте с именем, оканчивающимся на _BSS. Все другие сегменты игнорируются, также, как и директива GROUP. Описания сегментов могут указывать выравнивание BYTE или WORD; при редактировании кодовые сегменты всегда выравнены на байт, а сегменты данных всегда выравнены на слово. Описание сегментов могут указывать PUBLIC и имя класса, но они игнорируются. - Turbo Pascal игнорирует все данные для сегментов, отличных от сегментов кода (CODE, CSEG или xxxх_TEXT) и сегменты инициализированных данных (CONST или xxxx_DATA). Когда объявляется переменные в сегменте неинициализированных данных (DATA, DSEG или xxxx_BSS), всегда используйте знак (?) для указания значений, например: Count DW ? Buffer DB 128 DUP(?) - Однобайтовые ссылки к EXTRN символам недопустимы. Это означает, что операторы HIGH и LOW не могут быть использованы с EXTRN символами. Turbo Assembler и Turbo Pascal. Turbo Assembler (TASM) позволяет просто писать программы на ассемблере и связывать их с Вашими программами на Turbo Pascal. Turbo Assembler обеспечивает простую сегментацию, модели памяти и языковую поддержку для программистов на Turbo Pascal. Использование TPASCAL с директивой .MODEL устанавливает соглашения о вызовах Паскаля, определяет имена сегментов, делает PUSH BP и MOV BP,SP и также устанавливает возврат через POP BP и RET N (где N число байтов параметров). Директива PROC позволяет Вам определять ваши параметры в том же порядке, как они определены в Вашей программе на Паскале. Если Вы определяете функцию, которая возвращает строку, помните, что директива PROC имеет опцию RETURNS, которая позволяет Вам обращаться к указателю на временную строку в стеке без учета числа байт параметров, добавленных к оператору RET. Пример кода с использованием директив .MODEL и PROC: .MODEL TPASCAL .CODE MyProc PROC FAR I: Byte, J: Byte RETURNS Result: DWORD PUBLIC MyProc les DI,Result ; получить адрес временной строки mov AL,I ; получить первый параметр I mov BL,J ; получить второй параметр J ... ret Описание функции на Паскале: function MyProc(I, J: Char): string; external; Примеры программ на Ассемблере. Здесь представлен пример модуля, который реализует две программы обработки строк на Ассемблере. Функция UpperCase преобразует все символы в строке в заглавные. Функция StringOf возвращает строку символов заданной длины. unit Strings; interfase function UpperCase(S: String): String; function StringOf(Ch: Char; Count: Byte): String; implementation {$L STRS} function UpperCase; external; function StringOf; external; end. Программа на Ассемблере, реализующая функции UpperCase и StringOf, показана ниже. Она должна быть ассемблирована в файл Strs.OBJ до компиляции модуля Strings. Заметим, что программы используют дальнюю модель вызова, так как они объявлены в интерфейсной части модуля. CODE SEGMENT BYTE PUBLIC ASSUME CS:CODE PUBLIC UpperCase, StringOf ; объявление функций ; function UpperCase(S: String): String; UpperRes EQU DWORD PTR [BP+10] UpperStr EQU DWORD PTR [BP+6] UpperCase PROC FAR push bp ; сохранить bp mov bp,sp ; установить стек push ds ; сохранить ds lds si,UpperStr ; загрузить адрес строки les di,UpperRes ; загрузить адрес результата cld lodsb ; загрузить длину строки stosb ; копировать в результат mov cl,al ; длину строки в CX xor ch,ch jcxz U3 ; пропустить, если строка пустая U1: lodsb ; загрузить символ cmp al,'a' ; пропустить если не 'a'..'z' jb U2 cmp al,'z' ja U2 sub al,'a'-'a' ; преобразовать в заглавные U2: stosb ; запомнить в результат loop U1 ; цикл для всех символов U3: pop ds ; востановить ds pop bp ; востановить bp ret 4 ; удалить параметр и возврат UpperCase ENDP ; procedure StringOf(var S :string; Ch : char; Count: byte) StrOfs EQU DWORD PTR [BP + 10] StrOfChar EQU BYTE PTR [BP + 8] StrOfCount EQU BYTE PTR [BP + 6] StringOf Proc FAR push bp ; сохранить bp mov bp,sp ; установить стек les di,StrOfRes ; загрузить адрес результата mov al,StrOfCount ; загрузить счетчик cld stosb ; сохранить длину mov cl,al ; счетчик в CX xor ch,ch mov al, StrOfChar ; загрузить символ rep STOSB ; сохранить строку символов pop bp ; восстановить bp ret 8 ; удалить параметр и возврат StringOf ENDP CODE ENDS END Для того чтобы ассемблировать пример и откомпилировать модуль, используйте команды: TASM STRS TPC strings Следующий пример показывает, как программа на Ассемблере может обращаться к программам и переменным Паскаля. Программа Numbers читает до 100 целых значений и затем вызывает процедуру на Ассемблере, чтобы проверить диапазон каждого из этих значений. Если значение выходит из диапазона, процедура на Ассемблере вызывает процедуру на Паскале для его печати. program Numbers; {$L CHECK} var Buffer: array[1..100] of Integer; Count: Integer; procedure RangeError(N: Integer); begin WriteLn('Range error: ', N); end; procedure CheckRange(Min, Max: Integer); external; begin Count := 0; while not EOF and (Count < 100) do begin Count := Count + 1; ReadLn(Buffer[Count]); {закончится, когда пользователь введет CTRL-Z или после 100 итераций} end; CheckRange(-10,10); end. Программа на Ассемблере, реализующая процедуру CheckRange, приведена ниже. Она должна быть ассемблирована в файл Check.OBJ до компиляции программы Numbers. Заметим, что процедура использует ближнюю модель вызова, так как объявлена в программе. DATA SEGMENT WORD PUBLIC EXTRN Buffer: WORD, Count: WORD; ;Переменные Паскаля DATA ENDS CODE SEGMENT BYTE PUBLIC ASSUME CS: CODE, DS: Buffer EXTRN RangeError: NEAR ;реализован в Паскале PUBLIC CheckRange ;реализован здесь CheckRange PROC NEAR mov bx,sp ;получить указатель параметров mov ax,ss:[bx+4] ;загрузить Min mov dx,ss:[bx+2] ;загрузить Max xor bx,bx ;очистить индекс данных mov cx,Count ;загрузить Count jcxz SD4 ;пропустить если 0 SD1: cmp Buffer[BX],AX ;слишком мал? jl SD2 ;да, перейти cmp Buffer[BX],DX ;слишком велик? jle SD3 ;нет, перейти SD2: push ax ;сохранить регистр push bx push cx push dx push Buffer[BX] ;передать значение в Паскаль CALL RangeError ;вызвать процедуру Паскаля pop dx ;восстановить регистры pop cx pop bx pop ax SD3: inc BX ;перейти к следующему элементу inc BX loop SD1 ;цикл для каждого элемента SD4: ret ;возврат CheckRange ENDP CODE ENDS END Пример на Turbo Assembler. Здесь представлена версия предыдущей программы на Ассемблере, которая показывает преимущества применения Turbo Assembler при стыковке с Паскалем: .MODEL TPASCAL ;модель кода Турбо-Паскаля LOCALS @@ ;определить префикс локальных меток .DATA ;сегмент данных EXTRN Buffer: WORD, Count: WORD; ;Переменные Паскаля .CODE ;сегмент кода EXTRN RangeError: NEAR ;реализован в Паскале PUBLIC CheckRange ;реализован здесь ChechRange Proc NEAR Min : WORD, Max : WORD mov ax,Min ;загрузить Min в ax mov dx,Max ;загрузить Max в dx xor bx,bx ;очистить индекс данных mov cx,Count ;загрузить Count jcxz @@4 ;пропустить если 0 @@1: cmp ax,Buffer[BX] ;слишком мал? jg @@2 ;да, перейти на @@2 cmp dx,Buffer[BX] ;слишком велик? jge @@3 ;нет, перейти на @@3 @@2: push ax ;сохранить регистр push bx push cx push dx push Buffer[BX] ;передать значение в Паскаль call RangeError ;вызвать процедуру Паскаля pop dx ;восстановить регистры pop cx pop bx pop ax @@3: inc BX ;перейти к следующему элементу inc BX loop @@1 ;цикл для каждого элемента @@4: ret ;возврат CheckRange ENDP END Заметим, что .MODEL TPASCAL Turbo Assembler автоматически генерирует код входа до первой инструкции и код выхода для RET. Встроенный машинный код. Для очень коротких программ на Ассемблере удобно применять оператор или директиву Inline. Они позволяют Вам вставить инструкции машинного кода прямо в текст программы или модуля вместо использования объектного файла. Оператор Inline. Оператор Inline состоит из зарезервированного слова Inline и следующих за ним одного или более элементов, разделенных слэшами (/) и заключенных в скобки: inline(10/$2345/Count+1/Data-OffSet); Синтаксис оператора Inline: ┌──────┐ ┌─┐ ┌──────────────┐ ┌─┐ оператор Inline ───Ў│inline├──Ў│(├────Ў│элемент inline├─┬─Ў│)├──Ў └──────┘ └─┘ ° └──────────────┘ │ └─┘ │ ┌─┐ │ └───────┤/│ў────────┘ └─┘ Каждый элемент оператора Inline состоит из возможного указателя размера, < или > и константы или идентификатора переменной идущими за 0 или более указателями смещения (см. синтаксис ниже). Указатель смещения состоит из + или - с константой. элемент inline │ ┌────────┐ └─┬───────────Ў│сonstant│─────────────────────────────────────Ў │ ° └────────┘ ° │ ┌─┐ │ │ ├─Ў│<├──┤ │ │ └─┘ │ │ │ ┌─┐ │ │ ├─Ў│<├──┘ │ │ └─┘ │ │ ┌────────────────────────┐ │ └─Ў│идентификатор переменной├─┬──────────────────────────┘ └────────────────────────┘ │ ° │ ┌────┐ ┌─────────┐ │ └──Ў│знак├─Ў│константа├─┬┘ ° └────┘ └─────────┘ │ └─────────────────────┘ Каждый элемент оператора Inline генерирует 1 байт или 1 слово кода. Значения вычисляются из значения первой константы или смещения идентификатора переменной, к которому добавлено/вычтено значение каждой из констант, которые следуют за ним. Элемент Inline генерирует 1 байт кода, если он состоит только из констант и если их значения внутри 8-и битового диапазона (0..255). Если значение выходит за 8-и битовый диапазон, или если элемент Inline ссылается к переменной - генерируется код длиной в одно слово (меньший значащий байт стоит первым). Операторы < и > могут быть использованы для того, чтобы перекрыть автоматический выбор размера, описанный ранее. Если элемент Inline начинается с оператора <, только меньший байт значения будет кодироваться, даже если это 16-и битовое значение. Если элемент Inline начинается с оператора >, то будет кодироваться слово, даже если наибольший байт равен 0. Например, оператор Inline(<$1234/>$44); генерирует 3 байта кода: $34,$44,$00. Значение идентификатора переменной в элементе Inline представляет собой смещение адреса переменной внутри ее базового сегмента. Базовой сегмент глобальной переменной - переменной объявленной на внешнем уровне в программе или модуле или типированной константы - это сегмент данных, к которому обращаются через регистр DS. Базовый сегмент локальной переменной - переменной, объявленной внутри текущей подпрограммы - это сегмент стека. В этом случае переменная смещена относительно регистра BP, который автоматически ссылается на сегмент стека. Примечание: Регистры BP, SP, SS и DS должны быть сохранены оператором Inlile; все остальные регистры могут изменяться. Следующий пример оператора Inline генерирует машинный код для запоминания заданного числа слов данных в заданной переменной. Процедура FillWord запоминает Count слов в значении Data в памяти, начиная с первого байта Dest. procedure FillWord(var Dest; Count, Data: Word); begin Inline( $C4/$BE/Dest/ {LES DI, Dest[BP]} $8B/$8E/Count/ {MOV CX, Count[BP]} $8B/$86/Data/ {MOV AX, Data[BP]} $FC/ {CLD} $F3/$AB); {REP STOSW} end; Оператор Inline может быть свободно смешан с другими операторами в операторной части блока. Директива Inline. Директива Inline позволяет Вам писать процедуры и функции, вместо которых вставляется данная последовательность инструкций машинного кода, в том месте, где они вызываются. Они подобны макросам в Ассемблере. Синтаксис директивы Inline такой же как и оператора Inline: ┌────────────────┐ директива Inline ───Ў│оператор inline│ └────────────────┘ Когда вызывается обычная процедура или функция (включая и те, которые содержат операторы Inline), компилятор генерирует код, который помещает параметры (если они есть) в стек и затем генерирует инструкцию CALL для вызова процедуры или функции. Однако, когда вы вызываете Inline процедуру или функцию, компилятор вместо генерации CALL вставляет код из этой директивы Inline. Короткий пример двух процедур Inline: procedure DisableInterrupts; Inline($FA); {CLI} procedure EnableInterrupts; Inline($FB); {STI} Когда вызывается DisableInterrupts - генерируется 1 байт кода - инструкция CLI. Процедуры и функции, объявленные с директивой Inline, могут иметь параметры; однако, к параметрам нельзя обращаться по имени (но к другим переменным можно). Также из-за того, что такие процедуры и функции в действительности макро, в них нет автоматического входного и выходного кода, и не должно быть инструкции возврата. Следующая функция умножает два целых значения, создавая результат типа LongInt: function LongMul(X, Y: Integer): Longint; Inline ( $5A/ {POP AX; POP X} $5E/ {POP DX; POP Y} $F7/$EA); { IMUL DX; DX : AX = X * Y} Заметьте отсутствие входного и выходного кода и пропуск инструкции выхода. Здесь они не требуются, так как эти 4 байта вставляются в остальной код при вызове LongMul. Директива Inline применяется только для очень коротких процедур и функций ( < 10 байт). Так как Inline процедуры и функции - это макро, они не могут использоваться как аргументы оператора @ и функций Addr, Ofs и Seg.