Глава 15. Использование ассемблера с языками высокого уровня.

Какой он нации, сказать не знаю смело: на всех языках говорит...

М.Ю. Лермонтов Маскарад.

Одним из удобств современных языков высокого уровня является встроенный ас­семблер, который предоставляет возможность использовать язык ассемблера непос­редственно в тексте программ на языке Си и Паскале. Это делается путем введения специальных ассемблерных блоков, с помощью ключевого слова ASM. Такой встро­енный ассемблер довольно полно описывается в соответствующих руководствах по программированию, и мы не будем этим заниматься. Кстати сказать, встроенный ас­семблер, как правило, отражает не все команды микропроцессора. Предмет же нашего разговора лежит в области классического интерфейса между различными языками, который основывается на существовании промежуточных файлов трансляции, кото­рые называются объектными модулями (см. главу 13 и конец главы 1).

Вопрос стыковки различных языков друг с другом довольно интересен и сложен. Заинтересованного читателя отошлю к источнику [16]. Мы сужаем проблему и рас­сматриваем стыковку только ассемблера с языками высокого уровня. В качестве тако­вых берутся три языка (точнее, три компилятора): TURBO С++ (BORLAND), TURBO PASCAL (BORLAND) и QUICK BASIC42 (MICROSOFT). Сказанное здесь, однако, в значительной степени будет относиться и к другим компиляторам, предполагающим интерфейс с языком ассемблера. Получив некоторые навыки, в дальнейшем Вы само­стоятельно сможете использовать язык ассемблера с другими языками, предваритель­но просмотрев документацию по этим языкам.

I.

На Рис. 15.1 представлен модуль, процедура которого CLRSCR может быть вызвана из программ на языках высокого уровня. свою очередь, этой процедуры вызывается процедура (или функция), находящаяся в основной программе. Эти программы представ­лены на Рис. 15.2,15.3,15.4. Приведу краткую инструкцию по объединению модулей.

1. Для того чтобы объединить указанный модуль с программой на языке BASIC (Рис. 15.2), удалите предварительно из него все подчеркивания. Далее оба мо­дуля транслируются независимо и объединяются при помощи стандартного компоновщика LINK.EXE.

2. В случае программы на языке Паскаль следует вначале оттранслировать мо­дуль. Транслятор Турбо Паскаля сам производит компоновку, поэтому далее достаточно выполнить команду: ТРС MOD2/В.

42   Речь, конечно, вдет о компиляторе языка BASIC.

3. Транслятор с языка Си++ автоматически вызывает компоновщикТЬГЫК.ЕХЕ. При трансляции используйте модель LARGE. Если же Вы хотите проводить компоновку отдельно, то следует правильно указать все объектные модули и библиотеки (см. конец главы Рис. 15.13).

EXTRN J?R:FAR CODE SEGMENT

ASSUME CS:CODE

PUBLIC CLRSCR _CLRSCR PROC FAR    ;процедура очистки экрана

PUSH AX

PUSH BX

PUSH CX

PUSH ES

MOV AX,0B800H     /полагаем этот адрес видеобуфера

MOV ES,AX

MOV CX,2000

XOR BX,BX

LOO:

MOV ES: [BX] ,0700H

ADD BX, 2

LOOP LOO

POP ES

POP CX

POP BX

POP AX

CALL FAR  PTR _PR RET

_CLRSCR ENDP CODE ENDS

end

Рис. 15.1. Модуль на языке ассемблера, который может быть подключен к модулям наРис. 15.2, 15.3, 15.4. Для подсоединения к модулю на языке В ASIC следуетубрать

все подчеркивания.

DECLARE SUB CLRSCR CALL CLRSCR

end

SUB PR STATIC

PRINT "ПРОЦЕДУРА В ПРОГРАММЕ НА QUICK BASIC"

Е N D S U Б

Puc. 15.2. Программа на языке QUICKBASIC.

{интерфейсный модуль MODI.PAS  на языке  Турбо Паскаль}

UNIT ..... ;

MODI;

INTERFACE

PROCEDURE _PR; PROCEDURE _CLRSCR;

IMPLEMENTATION

{$L BLOK}

PROCEDURE EXTERNAL;

PROCEDURE _PR;

BEGIN

WRITELN('ПРОЦЕДУРА В  ПРОГРАММЕ  HA TURBO PASCAL1);

END;

END.

{основной модуль M0D2.PAS  на  языке  TURBO PASCAL} USES MODI;

BEGIN

_CLRSCR;

END.

Puc. 15.3. Модули на языке Турбо Паскаль. #INCLUDE VOID

VOID

{

CHAR 3[Зб]="ПРОЦЕДУРА В ПРОГРАММЕ НА TURBO CI++\N"; PUTS (S);

}

VOID

{

CLRSCR();

}

Рис. 15.4. Программна языке Си.

Я думаю, что читатель без особого труда разберется в программах на Рис. 15.1 -Однако есть некоторые нюансы, о которых стоит поговорить. Мне не хотелось выдавать некоторые исчерпывающие правила. Попробуем порассуждать о том, поче­му может не получиться компоновка таких программ. В данном разделе мы не касаем­ся вопроса передачи параметров и использования общих данных.

Итак, Вы написали два модуля (конечно, их может быть больше) - один на языке ассемблера, а другой на языке высокого уровня. Как положено, все общие процедуры

Вы отметили как PUBLIC и extrn43. Однако программа не идет. Либо появляется ошибка при компоновке, либо ошибка проявляется во время работы программы. Про­блема может быть в следующем:

1. Неправильно осуществляется вызов процедуры, находящейся в другом модуле.

2. Неправильно осуществляется возврат из такой процедуры.

Это ключевые моменты - третьего не дано. Рассмотрим вначале пример на Пас­кале. Замечу, что согласно описанию Турбо Паскаля на ассемблерный код наклады­ваются жесткие требования по поводу имен кодовых сегментов. Кодовый сегмент может иметь имена CODE, CSEG или оканчиваться на_ТЕХТ. Это условие было, естественно, выполнено. Причем компилятор не требует указания ни типов, ни клас­сов этих сегментов. Компилятор не формирует самостоятельных сегментов (в отли­чие от Си и Бейсика), а объединяет их с модулями, где данные процедуры были объяв­лены. Согласование вызовов и возвратов осуществлено тем, что, во-первых, в ас­семблерном модуле и процедура _PR, и процедура _CLRSCR имеют атрибут FAR, во-вторых, в модуле MODI .PAS обе эти процедуры указаны в разделе INTERFACE. Проделаем теперь такой эксперимент: уберем объявление процедуры _PR из интер­фейсного раздела. Трансляция программы завершается успешно, однако возврата из процедуры _PR не происходит. Выход из этой процедуры происходит по RETN, а не по RETF, как раньше. Вызов же производится длинным CALL. Вы можете испра­вить ассемблерный модуль, заменив атрибуты FAR у PR на NEAR. После этого все пойдет нормально. Похожая ситуация возникнет в том случае, если мы объявим CLRSCR в главном модуле. Теперь вызов к ней будет близким, но возврат дальним.

Здесь аналогично проблема разрешается изменением в ассемблерном коде атрибута

y_CLRSCR (с FAR на NEAR).

В случае с Си ситуация сложнее, но и гибче одновременно. Работая с моделями памяти MEDIUM и LARGE, Вы можете брать любые имена для кодовых сегментов в ассемблерных модулях. При этом, естественно, все вызовы должны быть длинными. В программе на Си при этом не обязательно указывать атрибуты (FAR или NEAR), т.к. они берутся по умолчанию. Измените модель на SMALL, и Вы не сможете скомпоно­вать программу. Для того чтобы решить проблему, вспомним, что модель SMALL име­ет всего один кодовый сегмент. Кроме того, в этой модели используются близкие вы­зовы. С учетом этого измените в ассемблерном коде все атрибуты с FAR на NEAR, a заголовок сегмента измените на_ТЕХТ SEGMENT BYTE PUBLIC 'CODE'. Дело в том, что именно такой сегмент (такого имени, типа и класса) формируется транслято­ром Си. При компоновке оба сегмента, естественно, объединятся. Вы можете объеди­нить сегменты и в модели LARGE. Если Вы объедините два сегмента, коды которых работают только друг с другом, то сможете смело изменить все атрибуты в ассемблер­ном коде с FAR на NEAR. He забудьте при этом в программе на языке Си в соответ­ствующих объявлениях поставить атрибут NEAR. Для того чтобы выяснить, какие сегменты и как следует объединять, используйте ключ -S при трансляции. Вы получи­

В языках высокого уровня вместо PUBLIC и EXTERN используются другие средства.те ассемблерный код из программы на Си, просматривая который, Вы узнаете все, что необходимо, из первых рук.

Ситуация с Бейсиком похожа на предыдущую ситуацию, но проще. Используйте любое имя для ассемблерного сегмента и дальние переходы44. Успех будет сопутство­вать Вам.

П.

В модуле на языке ассемблера Вы можете определять и хранить данные. Разумеет­ся, данные можно поместить как непосредственно в кодовый сегмент, так и создать для этого отдельный сегмент данных. Однако из языка высокого уровня доступ к этим

данным будет закрыт.

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

Доступ к данным в программе на языке Турбо Паскаль можно получить через сег­менты CONST (константы), или сегмент, оканчивающийся на_ОАТА(инициализиро-ванные переменные), или через сегменты с именами DATA, DSEG и сегменты с име­нами, оканчивающимися Ha_BSS. На Рис. 15.5показан пример, демонстрирующий сказанное. В основном модуле на Паскале задается строковая переменная, а печатает­ся в модуле на ассемблере. Обратите внимание, как задан сегмент данных и внешняя переменная STR. Заметим, что здесь понадобилась информация о том, в каком форма­те Турбо Паскаль хранит строковые переменные (последовательность кодов ASCII, перед которой стоит длина строки).

На Рис. 15.6представлен аналогичный пример для языка Си++. Различие заклю­чается лишь в формате строковых переменных. Для языка Си признаком конца строки служит символ с кодом 0. При компоновке используйте модель LARGE или MEDIUM.

С Бейсиком дела обстоят несколько сложнее. Оказывается, компилятор языка Бей­сик фирмы Микрософт не генерирует внешние имена для переменных. Поэтому бес­смысленно пытаться определить внешние имена в ассемблерном модуле, соответству­ющие переменным в модуле на Бейсике. Однако можно обойтись и без этого, если знать, в каком сегменте располагаются переменные. Для Бейсика это сегмент с име­нем BC DATA и классом BC VARS (тип выравнивания у сегмента данных обычно WORD). Конечно, трудно искать вслепую, но если знать порядок инициализации пе­ременных и их формат, то это вполне допустимый подход. Можно упростить ситуа­цию, если использовать ключевое слово COMMON. Переменные, определенные с по­мощью этого слова, помещаются в сегмент с этим именем и классом BLANK. Теперь достаточно определить с помощью COMMON те переменные, которые мы хотим ме­нять в ассемблерном модуле, а дальше уже дело Рис. иллюстрирует ска­занное выше. В ассемблерном модуле происходит изменение значений двух целочис­ленных переменных АА и ВВ.

44 В языке BASIC MICROSOFT для вызовов используются только дальние адреса.

/модуль на  языке ассемблера DATA  SEGMENT  WORD 'DATA' EXTRN STR:NEAR DATA ENDS PUBLIC PRINT; CODE SEGMENT

ASSUME   CS:CODE,    DS:DATA

PRINT

PROC

FAR вывода

 

PUSH

DS

 

PUSH

AX

 

PUSH

CX

 

PUSH

BX

 

MOV

AX,SEG STR

 

MOV

DS, AX

 

XOR

CH,CH

 

MOV

PTR

 

MOV

BX,OFFSET DS:STR+1

 

MOV

AH, 2

LOO:

 

 

 

MOV

DL,DS:[BX]

 

INT

21H

 

INC

BX

 

LOOP

LOO

 

POP

BX

 

POP

CX

 

POP

AX

 

POP

DS

 

RET

 

PRINT

ENDP

 

CODE

ENDS END

 

1 на языке UNIT MODI;

INTERFACE VAR

STR:STRING;

PROCEDURE PRINT; IMPLEMENTATION

{$L BLOK}

PROCEDURE PRINT; EXTERNAL;

END.

{модуль 2 на языке Паскаль}

USES MODI;

BEGIN

STR: = 'ПЕЧАТАЕМ ИЗ МОДУЛЯ НА ЯЗЫКЕ АССЕМБЛЕРА1 ; PRINT;

END.

Рис. 15.5. Доступ к общим данным при компоновке модулей на языке ассемблера и на

языке Turbo Pascal.

;модуль на   языке ассемблера _DATA  SEGMENT   BYTE   PUBLIC 'DATA' EXTRN _STR:BYTE _DATA ENDS PUBLIC _PRINT CODE SEGMENT

ASSUME CS:CODE,   DS:_DATA _PRINT  PROC  FAR     ; процедура  вывода  строки на экран

PUSH DS

PUSH AX

PUSH BX

MOV    AX,SEG _STR MOV    DS, AX

MOV     BX,OFFSET DS:_STR MOV    AH,2

LOO:

END:

CMP BYTE PTR

JZ _END

MOV DL, DS : [BX]

INT 21H

INC BX

JMP SHORT LOO

POP BX

POP AX

■ POP     DS . RET

_PRINT ENDP CODE ENDS

END

/*модуль на языке Си*/ EXTERN VOID    PRINT(VOID) ;

CHAR STR[ ] ="ПЕЧАТАЮ ИЗ МОДУЛЯ НА ЯЗЫКЕ АССЕМБЛЕРА.

VOID MAIN(VOID)

{

PRINT () ;

}

Рис. 15.6. Доступ к общим данным при компоновке модулей на языке ассемблера и на

языке Турбо Си++.

/модуль на   языке ассемблера COMMON  SEGMENT  BYTE   PUBLIC    1 BLANK' COMMON ENDS PUBLIC INASM

CODE SEGMENT

ASSUME  OS:CODE INASM   PROC FAR PUSH DS PUSH AX MOV AX,COMMON MOV DS,AX

MOV WORD PTR DS:[0],1234 /переменная AA MOV WORD  PTR DS:[2],456 /переменная BB

POP AX

pop ds RET

INASM ENDP CODE ENDS

END

•модуль на языке Бейсик

DECLARE SUB INASM

COMMON   SHARED AA AS INTEGER

COMMON   SHARED BB AS INTEGER

AA=23

BB=990

PRINT AA, BB

CALL INASM

PRINT AA,BB

END

Puc. 15.7. Доступ к общим данньм при компоновке модулей на языке ассемблера и на

языке Бейсик Микрософт.

III.

Перейдем к вопросу о передаче параметров в процедуру и из нее. Этот вопрос не­плохо был разобран в главе 13, теперь закрепим пройденное. Если мы обратимся к ас­семблерному коду программы, написанной скажем на Си (это несложно сделать, ис­пользуя параметр S), то легко выявить общность в структуре процедур. Эта общая струк­тура изображена на Рис. 15.8. Структура стека во время выполнения данной процедуры изображена на Рис. 15.9. Обращаю Ваше внимание на то, что при выходе из процедуры в стеке еще остаются параметры (если они были). В случае языка Паскаль или Бейсик возврат нужно осуществлять по RET К, где К - размер области параметров. В Си об этом заботиться не надо, т.к. по выходу из функции транслятор автоматически освобождаетстек (SP-K). Когда Вы пишите процедуру для компоновки с программой на языке высо­кого уровня, Вам следует придерживаться изложенного подхода.

Доступ к параметрам и локальным переменным осуществляется через регистр ВР. Легко видеть, что в случае дальней процедуры на область параметров указывает [ВР+6], а на область локальных переменных (в Ваших процедурах на языке ассемблера их скорее всего не будет) [ВР-2] (на первое слово). В случае близкой процедуры на пара­метры будет указывать [ВР+4]. Запомните, что в любом случае перед выходом из про­цедуры должны быть восстановлены 4 регистра - DS, SS, SP, ВР.

Параметры могут передаваться по значению и по указателю. Указатель при этом может быть как длинным, так и коротким. Длинный указатель соответствует паре сег-ментхмещение, а короткий - равен смещению (предполагается, что берется сегмент, на который в данный момент показывает DS).

ИМЯ_РРчОС  PROC FAR

PUSH ВР

MOV   BP, SP

SUB   SP, PA3M. JIOK.   ; выделяем место для локальных

;переменных

нужные

{выполняем необходимые действия} {восстанавливаем регистры}

ADD локальные переменные

POP ВР RET

ИМЯ^РИОС ENDP

Рис. 15.8. Общая структура процедуры в языке высокого уровня.

■Младшие адреса

Сохраненные значения регистров

Область  локальных переменных

значение BP

Адрес возврата

Область параметров

-SP

BP

Старшие адреса

Рис. 15.9. Структура стека во время работы процедуры.

Хочу заметить, что команда SUB SP,PA3M._JIOK. перемещает указатель стека так, что рост стека не может испортить локальные переменные или параметры. Возможна, однако, и другая альтернатива: указатель стека поместить не в меньшие, а в большие адреса. Здесь, однако, есть опасность, что указатель стека будет указывать на какие-либо данные, которые оказались в стеке при вызове процедуры, из которой была выз­вана данная. В этом случае компилятор должен иметь информацию об объеме исполь­зованного уже стека.

Теперь, получив некоторый теоретический заряд, мы займемся примерами. Пер­вый пример будет касаться языка Паскаль. В Паскале возможна передача как по ссыл­ке, так и по значению. Параметры переменные всегда передаются по ссылке. Парамет­ры значения передаются по значению, если их длина не превышает 4 байта. Парамет­ры значения длиной свыше 4 байт передаются по ссылке на некоторую локальную область, куда этот параметр предварительно должен быть помещен. Порядок протал­кивания в стек параметров процедуры - слева на право. Например, если вызывается процедура SUM(A,B:WORD), то вначале в стек будет помещен параметр А, а затем В. Что касается ссылок, то в Паскале передаваемые в процедуру ссылки всегда длинные.

;ассемблерный модуль PUBLIC PRO TEXT SEGMENT

ASSUME CS: TEXT

 

PRO PROC

 

 

PUSH

BP

 

MOV

BP, SP

 

PUSH

AX

 

PUSH

DX

 

PUSH

DS

 

PUSH

BX

 

LDS

ВХ,[BP+4]

• *

MOV

DL,[BX]

 

MOV

2

* *

INT

21H

r

MOV

BYTE  PTR    [BX] , 65

* * t

LDS

BX,[BP+8]

. *

f

MOV

DL,[BX]

* *

INT

21H

. *

POP

BX

 

POP

DS

 

POP

DX

 

POP

AX

 

POP

BP

 

RET,

8

 

PRO ENDP.

 

 

_TEXT ENDS

 

 

END

 

 

{модуль на языке Турбо Паскаль} VAR

А,В:BYTE;

I:LONGINT; {$L BLOK}

PROCEDURE PRO (VAR А, В: BYTE) ; EXTERNAL; BEGIN

A:=65; B:=66; PRO(A,B);

WRITELN;

WRITELN(CHR(A),CHR(B));

END.

Puc. 15.10. Пример передачи параметров в процедуру на ассемблере из программы

на языке Паскаль.

Рассмотрим пример на Рис. 15.10. Здесь два параметра из программы на Паскале передаются в процедуру PRO, расположенную в ассемблерном модуле. Вызов проце­дуры близкий, однако, параметры передаются по длинной ссылке. Параметр В меня­ется в процедуре PRO, что "чувствует" основная программа: в первой строке будет напечатано ВА, тогда как во второй строке - АА. Если теперь параметры А и В будут параметрами значениями, то это потребует некоторого изменения процедуры PRO. Строки, отмеченные звездочкой, следует заменить на

MOV DL, [ВР+4] MOV АН, 2 INT 21H

MOV BYTE PTR [ВР+4],65

MOV DL, [ВР+б] INT 21H

В конце программы следует поставить RET 4, т.к. отсутствует команда передачи в стек байта - вместо этого передается целое слово. В этом случае программа не "почув­ствует" изменения параметра В.

Работа с функциями на Паскале аналогична работе с процедурами. Здесь следует толькознать,какикудадолжновозвращатьсязначениефункции.Вдокументациипо программированию на Паскале, Вы без труда найдете эти сведения.

Обратимся к Бейсику. Здесь необходимо запомнить следующие правила:

1. В языке Бейсик Микрософт по умолчанию все параметры передаются по близ­кой ссылке.

2. Параметр передается по значению, если вызываемая подпрограмма объявлена при помощи оператора DECLARE и к параметру применено ключевое слово BYVAL. Использование ключевого слова CALLS заменяет эту принимаемую по умолчанию установку, и параметры передаются по удаленной ссылке. При­менение к параметру ключевого слова SEG также приводит к тому, что он будет передаваться поудаленной ссылке.

;ассемблерный модуль PUBLIC INASM EXTRN  INBAS:FAR CODE SEGMENT

ASSUME CS:CODE

PROC

FAR

PUSH

BP

MOV

BP,SP

PUSH

AX

PUSH

BX

PUSH

DS

LDS

BX,[BP+6]

ADD

WORD PTR

LDS

BX,[BP+10]

ADD

WORD PTR [BX] , 10

PUSH

WORD PTR [BP+10]

PUSH

WORD PTR [BP+6]

CALL

INBAS

POP

DS

POP

BX

POP

AX

POP

BP

RET

8

INASM ENDP CODE ENDS END

на языке

DECLARE  SUB   INASM(SEG A1%,SEG A2%)

A%=13:B%=2623 PRINT A%,B%

CALL   INASM(A%,B%)

END

SUB  INBAS(X1%,X2%) PRINT X1%,X2%

END SUB

Пример       параметрами, между процедурой па ассемблере и программой на Бейсике.

Рассмотрим пример на Рис. В этом примере производится обмен парамет-

рами между основной программой, написанной на Бейсике, и процедурой, написан­ной на ассемблере. Если в процедуру на ассемблере параметры пересылаются по длин­ной ссылке, то в процедуру INBAS они приходят (предварительно увеличенные на 10)

по короткой ссылке. Вызов процедур INASM и INBAS также длинный.

В Си ситуация в целом похожая. Надо только помнить, что последовательность пе­редачи параметров здесь обратная - параметры вталкиваются в стек справа налево. Па­раметр может быть как значением, так и ссылкой - в зависимости отего типа. Ссылка может быть как далекой, так близкой. По умолчанию все зависит от выбранной модели памяти. Например, для модели ЗМАЬЬссьшки будут близкими, адля модели large -дальними. Изменить тип ссылки можно при помощи ключевых слов NEARh far.

;ассемблерный модуль public _inasm extrn   _inci:far code segment

assume   cs: code i        dw 0 J       DW 0 _inasm proc far

push bp

mov bp,sp

push ds

push bx

push cs

pop ds

mov  ax, [bp+6] shr   ax, 1 mov i,ax mov j, 10000

push ds

mov    ax,offset i push ax .

push ds

mov    ax,offset j push ax

call _inci ;в ax значение функции

ADD sp,8 /восстановили стек

pop bx pop ds pop bp ret inasm endp

code ends

END

/*модуль на языке Си*/ extern   int   inasm(int i); int inci(int *  i, int * j) ;

VOID MAIN(VOID) . {

PRINTF("%D",INASM(30000));

}

int *j)

{

RETURN((*I)+(*J));

}

Пример обмена параметрами между процедурой на ассемблере и программой на Бейсике.

Параметр в программе на Рис. 15.12 проходит ряд превращений. Вначале он по­сылается как значение в процедуру на ассемблере. Там это значение делится на 2 и пересылается по далекой ссылке вместе с еще одним параметром в функцию, находя­щуюся в модуле на Си. В этой функции значения складываются, и результат отсылает­ся обратно. Наконец, оттуда это значение опять переходит в основной модуль и там печатается. Напомню, что значение функции размером, не превышающим слово, воз­вращается через регистр АХ (это справедливо и для других языков). Напомню также, что восстановление стека в Си происходит после возвращения из процедуры. Разли­чие в порядке передачи параметров в языках Си и, скажем, Паскаль является доста­точно существенным. Вдумайтесь: при передаче параметров в процедуру в языке Пас­каль процедура получает доступ вначале к последнему параметру. Другими словами, она должна "знать", сколько параметров ей передается. В языке Си все наоборот: Вы не заботитесь заранее, сколько параметров предполагается передать. Этому же спо­собствует тот факт, что очистка стека от параметров происходит не в процедуре (она же не "знает" реальное состояние стека), а в основной программе.

В Паскале существует такое понятие, как вложенные процедуры. Отличие вызова вложенной процедуры от обычной процедуры заключается лишь в том, что в стек вме­сте с параметрами проталкивается значение регистра ВР. Таким образом вложенная процедура получает доступ к локальным переменным вьізьітющей процедуры. С точки зрения программирования вложенные процедуры, на мой взгляд, не представляют никакого удобства.

IV.

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

Несколько слов следует сказать о том, какие регистры следует сохранять в ассемб­лерных процедурах, написанныхдля вызова их из языков высокого уровня. Как Вы уже поняли, сохранять необходимо ВРв силутой роли, которую он играет. Также очевидно, что нельзя портить регистры DS и SS. Для языка Паскаль и Бейсик список сохраняемых регистров этим и ограничивается. Для языка Си всегда существовал режим компиля­ции, когда для хранения переменных могут использоваться и регистры. Для этих целей используются два регистра SI и DI. Вот о них и следует также позаботиться, если Вы разрешаете компилятору хранить переменные в регистрах.

V.

В этом разделе несколько противоречивым выглядит то, что использование биб­лиотечных процедур языков высокого уровня в языке ассемблера вполне возможно, а иногда и удобно. Ниже приводится пример вызова стандартной библиотечной функ­ции языка Си: _CLRSCR() - очистки экрана из программы на языке ассемблера. Заме­тим, что ничего от фирмы BORLAND, кроме, разумеется, библиотеки, использовать

необязательно.

/программа CLR.ASM EXTRN _CLRSCR:FAR

PUBLIC _MAIN ;для связи с модулем COL.OBI

DATA SEGMENT DATA ENDS

STK   SEGMENT STACK

DB  100 DUP(?) STK ENDS

_TEXT   SEGMENT   BYTE   PUBLIC 'CODE'

ASSUME   CS :_TEXT,DS:_DATA,SS:STK

_MAIN   PROC FAR

CALL   FAR   PTR _CLRSCR   /вызов функции   очистки экрана / выход из  программы,   можно   просто RET MOV АН,4СН

INT 21H

_MAIN ENDP _TEXT ENDS

END

Рис. 15.13. Программа на ассемблере, использующая стандартную функцию языка Си.

После трансляции данной программы мы получим объектный модуль CLR.OBJ. Следующая правильно скомпоновать его. Для компоновки используем

стандартную библиотеку Си CL.LIB (дальняя модель) и объектный модуль COL.OBJ,

необходимый для создания загрузочного модуля (вначале работают процедуры из

COL.OBJ, а затем передается управление на_МАШ). В командной строке пишем: LINK COL+CLR. На вопрос о библиотеке указываем CL. После этого программа очистки экрана будет получена.

./программа PUTS.ASM EXTRN _PUTS:FAR

PUBLIC _MAIN /для связи с модулем COL.OBJ

_DATA   SEGMENT   BYTE   PUBLIC 'DATA' /данная строка   будет напечатана S DB   'Проверка.', О

_DATA ENDS

STK   SEGMENT STACK

DB   100 DUP(?) STK ENDS

_TEXT   SEGMENT  BYTE   PUBLIC 'CODE'

ASSUME CS:_TEXT,DS:_DATA,SS:STK

_MAIN   PROC FAR /адрес  выводимой строки PUSH DS LEA   AX, S

PUSH AX

CALL   FAR  PTR _PUTS   /вызов функции  очистки экрана стек

pop dx pop dx

;выход из  программы,   можно просто RET MOV АН,4СН

INT  21H '

_MAIN ENDP _TEXT ENDS

END

Пример с передачей параметров.

Пример на Рис. 15.14 характерен тем, что вызывается функция с передачей пара­метров. В функцию передается адрес строки. Согласно принятому в Си соглашению освобождение стека происходит не в вызванной функции, а после возврата из нее. Последовательность двух команд POP DX и предназначена для этой цели.

VI.

Повсеместная популярность языков баз данных побуждает меня дать хотя бы крат­кое описание использования ассемблера в этой области. Для определенности буду рас­сматривать семейство DBASE, причем вего командном, ане компилируемом варианте.

Основой использования ассемблера в базах данных служат две команды:

LOAD имя    *указывается имя двоичного файла,    по умолчанию

*расширение BIN CALL имя   [WITH параметры]

Имя в команде CALL должно совпадать с именем загружаемого файла. Всего мо­жет быть загружено несколько файлов, и каждый получит свою область памяти. Осво­бодить память можно с помощью команды RELEASE. Структура ассемблерного мо­дуля совпадает со структурой СОМ-файлас той лишь разницей, что вместо ORG 100Н следует поставить ORG 0. На передаваемые в модуль параметры указывает DS:[BX]. Если содержимое ВХ равно 0, то это означает, что параметров нет. Возврат из ассемб­лерного модуля должен быть длинным. Не пытайтесь в ассемблерном модуле менять структуруданных, т.к. можно испортить значение некоторых переменных.

Ниже на Рис. 15.15 представлен ассемблерный модуль, а также фрагмент програм­мы, вызывающей этот модуль.

CODE SEGMENT ORG 0

ASSUME CS:CODE CMP BX, 0

JZ     EXIT       ;нет параметров MOV AH,2L1: MOV DL,DS:[BX] CMP DL,0

JZ     EXIT        ;конец строки? ' INT 21H INC BX

JMP  SHORT LI /--передать управление   исполняющей системе--

EXIT:

RETF

CODE ENDS END

а) ассемблерный модуль AA='Печать  строки.'

LOAD WWW *вызов модуля WWW. BIN

CALL WWW WITH AA

б) фрагмент программы вызова ассемблерного модуля

Рис. 15.15. Пример ассемблерпогомодуля и вызова его на языке FOXBASE.

При разборе примера на Рис. 15.15 следует обратить внимание на формат строки в языке FOXBASE. Как видим, формат совпадает с форматом строк языка Си.

VII.

Одним из моих любимых занятий является исследование того, как отдельные фраг­менты на языке высокого уровня преобразуются в ассемблерный код. Поверьте, это одно из самых поучительных и полезных занятий. Наиболее удобно работать в этом плане с Турбо Си. Здесь у компилятора имеется опция, которая заставляет его генери­ровать ассемблерный код. Впрочем, можно поступить и по-другому: откомпилировать программу с отладочной информацией, а потом использовать стандартный отладчик (Турбо DEBUGER для компиляторов фирмы BORLAND и CODEVIEWдля Микро-совтовских компиляторов). Рассмотрим несколько примеров.

Многие считают, что использование переменных типа BYTE (UNSIGNED CHAR) позволяет несколько оптимизировать работу программы (в пику использованию пере­менных типа INT). Рассмотрим, всегда ли так бывает. Ниже представлен фрагмент

программы на Турбо Си.

INT 1=1; WHILE (К=10) { С[1]='Н' ; I + + ;

}

Что же произойдет при преобразовании его в ассемблерный код? У Турбо Си имеется опция: использовать регистровые переменные тогда, когда это возможно. Это существен­ный момент. Переменная I будет помещена в регистр Б! Смотрим ассемблерный код.

MOV SI,1 ;INT 1=1

JMP SHORT LI

L2 :

MOV BYTE PTR [BP+SI-10] , 72 ;C[I] = 'H'

INC SI ;I++

LI:

CMP SI, 10

JLE SHORT L2

Ассемблерный фрагмент довольно прозрачен и в комментарии не нуждается. Что же произойдет, если мы I определим как UNSIGNED CHAR (т.е. BYTE). Смотрим фрагмент.

L2 :

MOV JMP

MOV MOV LEA ADD MOV

CL, 1

SHORT

;UNSIGNED CHAR 1=1

AL, CL

AH, О

DX,W0RD PTR  [BP-10] AX, DX BX, AX

MOV BYTE PTR SS:[BX],72 ;C[I]='H'

INC CL ; I++

LI;

CMP CL,10

JBE SHORT L2

Ну что, Вы удивлены? Вот именно! Использование байтовой переменной в дан­ном случае увеличило код программы и, очевидно, увеличило время ее работы. Отме­ним теперь использование регистровых переменных и посмотрим ассемблерный фраг­мент для случая INT 1= 1.

L2:

L1:

MOV WORD PTR  [ВР-12],1 ;INTI=1

JMP SHORT LI

LEA AX,WORD PTR [BP-10]

MOV BX,WORD PTR   {BP-12]

ADD BX,AX

MOV BYTE PTR SS: [BX], 72 ;C[I]='H*

INC WORD PTR  [BP-12] ; I++

CMP WORD PTR  {BP-12], 10

JLE SHORT L2

Ниже представлен фрагмент для UNSIGNED CHAR 1= 1.

;UNSIGNED CHAR 1=1

L2:

LI

MOV BYTE PTR [BP-11],1

JMP SHORT LI

MOV AL,BYTE   PTR [BP-11]

MOV AH, 0

LEA DX,WORD  PTR [BP-10]

ADD AX,DX

MOV

MOV BYTE PTR SS:[BX],72 /CtlH'H1

INC BYTE PTR  [BP-11] ;I++

CMP BYTE PTR  [BP-11], 10

JBE SHORT L2

Как видите, в этом случае фрагменты с точки зрения оптимальности достаточно близки друг к другу. Всеже я бы предпочел опять выбрать случай INT 1= 1. Примерно также обстоит дело в случае аналогичной программы на Паскале. Определение пере­менной I как байтовой величины приводит к некоторому усложнению результирую­щего кода.

Еще один интересный пример. В книжках по Си часто приходится читать, что использование указателей предпочтительнее, чем индексов. Забегая вперед, хочу со­гласиться с этим утверждением и представить доказательство этому положению. Рас­смотрим два фрагмента на Си.

Фрагмент 1. LONG С[10] ; INT I;

FOR (1=0;  К10;   I+ + ) С[1]=1;

Фрагмент 2. LONG С[10] ; LONG  *   S ; INT I ; S=&C[0];

FOR(I=0; K10;      + ) *(S++)=1;

Ниже представлены ассемблерные коды, соответствующие циклам этих фрагментов.

Для фрагмента!.

LI :

XOR JMP

MOV

MOV

SHL

LEA

ADD

MOV

MOV

INC

CMP JL

DX, DX SHORT LI

BX,DX CL,2

BX,CL

AX,WORD PTR BX,AX

;получить ; значение ; индекса [BP-40]

добавить

к базовому адресу

WORD PTR SS:[BX+2],0 WORD PTR SS:[BX],1

DX

DX, 10

SHORT L2

Для фрагмента 2.

L2 :

XOR AX,AX

JMP SHORT LI

LES BX, DWORD PTR [BP-4]

MOV WORD PTR ES:[BX+2],0

MOV WORD PTR ES:[BX],1

ADD WORD   PTR [ВР-4],4     /увеличить значение указателя

;сразу на 4

INC АХ

L1:

СМР АХ, 10

JL SHORT L2

Как можно заметить, ассемблерный фрагмент во втором случае более эффекти­вен. И здесь все ясно: указатель непосредственно определяет элемент, а индекс, преж­де чем получить элемент, на который он указывает, должен быть подвергнут раду пре­образований. В данном примере индекс вначале умножается на 4, а затем добавляется к базовому адресу. Я полагаю, что из уже приведенных фрагментов, читатель должен извлечь еще одну мораль: на ассемблере программу можно сделать гораздо короче.

Ассемблер вошел в мою кровь, когда мне приходилось программировать на малень­ких компьютерах. Согласитесь, что если на компьютере всего 64 килобайт оперативной памяти, то о языках высокого уровня говорить не приходится. Впоследствии жизнь зас­тавила меня программировать на разных языках. мой взгляд, в языке программиро­вания высокого уровня должен быть соблюден баланс между удобством программиро­вания и пониманием того, как работает программа на низком уровне. Поясню свою мысль. Рассмотрим такое понятие, как "указатель". Оно является основополагающим для тако­го языка, как Си. Однако стоило ли отделять его от понятия адреса. Адрес можно запи­сать в различных формах, но все равно это будет адрес. Но, введя понятия указателя, я думаю, авторы чисто психологически посчитали разумным различать указатели по тому, на какой объект они направлены (переменные различныхтипов, функции, структуры и т.д.). Конечно, они полагали удобным, когда компилятор сможет контролировать типы входных параметров. Писать программы для программиста средней руки, возможно, стало легче, но зато затруднило понимание механизма работы самой программы. Амежду тем нет никакой разницы между адресом строки байти адресом функции. Посути, все, что мы имеем в памяти, это лишь цепочки байт.

В умных книжках по Си много говорится на тему, как эффективнее программиро­вать. При этом часто смешиваются такие понятия, как эффективный код и эффектив­ный текст. Мы же, как читатель уже догадывается, печемся в основном об эффектив­ности кода. Рассмотрим несколько простых примеров.

1. Чем отличается условие if(!a) от условия if(a=:::0).Прочтя умные книжки, наивный читатель решит, что первое условие более эффективно. Ничуть не бывало - они совершенно одинаковы. Если переменная "а" будет помещена в регистр, ну, скажем, DI, то проверка условия сведется к командам:

or di,di

jne   11    и т.д.

причем и в том, и в другом случае. Но второй случай явно предпочтительнее, т.к. улучшает читаемость программы. Воттак, "зри в корень", - говаривал Козь­ма Прутков.

2. Недавно прочел еще одну удивительную вещь. Автор предлагает, где это воз­можно, заменять оператор «if» оператором «?». Ну, скажем, в таком случае:

if(j==2)  i=j;  else i=k;      на      i=(j==2)?(j) : (k) ;

Наивный автор не понимает, что во втором случае фактически записана функция,

и компилятор сгенерирует код, в котором результат вначале будет помещен в регистр АХ, а уже потом в переменную. Так что первая запись более эффективна и по коду, и по записи - так легче читается.

3. Утверждается, что лучшее сравнение - сравнение с О. А здесь я вынужден со­гласиться. Дело в том, что сравнение с О осуществляется командой or (or si,si и

т.п.), а другие сравнения - посредством команд стр. Естественно, первое срав­нение осуществляется быстрее. Поэтому, как утверждается, в циклических опе­раторах параметр цикла лучше уменьшать.

Краткий справочник.

Более полное описание Вы сможете найти в руководствах по соответствующим

языкам.

Передача параметров из функций в языках высокого уровня.

Турбо Си:

Структура длиной в І байт помещается в AL.

Структура длиной в 2 байта - в АХ. Четырехбайтовая структура - в DX:AX.

Числа типа FLOAT, DOUBLE и LONG через TOS (см. руководство по программи­рованию на Си) или ST(0).

Структуры в 3 байта или более 5 байт - возвращается указатель на них: в АХ для модели SMALL и в ОХ:АХдля остальных моделей.

Турбо Паскаль:

Структура длиной в 1 байт помещается в AL. Структура длиной в 2 байта - в АХ. Четырехбайтовая структура - в DX:AX.

Структура типа REAL передается в ОХ:ВХ:АХлибо в ST(0), если используется сопроцессор.

Результат типа POFNTER передается в DX:AX.

Для результатов функций типа STPJNG вызывающая программа помещает указа­тель на временную область памяти до помещения любых параметров, и функция воз­вращает строковое значение в эту временную память. Функция не должна удалять указатель.