    Глава 17 

    Оптимизация с помощью ассемблера 

    Несмотря на преимущества таких языков высокого уровня, как С и C++, 
ассемблер продолжает играть важную роль в программировании. Только с помощью 
ассемблера программист может общаться с компьютером на равных, т.е. языком 
процессора, который часто называют машинным языком. 
    Большинство программ Borland C++ можно запускать на выполнение сразу же 
после компиляции, в них нет необходимости использовать ассемблер. Однако 
пришли времена, когда даже небольшие вкрапления низкоуровневого кода могут 
значительно усовершенствовать высокоуровневое приложение, написанное на C++. 
Ассемблер поможет вам: 

    •   оптимизировать отдельные критические операторы, от которых зависит 
производительность программы; 
    •   получить доступ к оборудованию компьютера на уровне физических устройств; 
    •   реализовать процедуры обработки прерываний. 

    Этим сфера применения ассемблера не ограничивается, однако в этих областях 
его использование особенно эффективно. Borland C++ предлагает три способа 
использования ассемблера совместно с программами, написанными на C++. 

    •   Компиляция программы в файл с ассемблерным текстом. 
    •   Вставка встраиваемых операторов asm в текст программы. 
    •   Написание внешних модулей, ассемблируемых с помощью Turbo Assembler (TASM). 

    В этой главе рассматриваются все три возможности, и для каждой приводится 
отдельный пример. Turbo Assembler поставляется Borland отдельно. 

    Анатомия функций языка С 

    При использовании ассемблера вам приходится выполнять задачу, обычно 
решаемую компилятором и состоящую в преобразовании операторов языков С и C++ в 
машинный код. Выполнение обязанностей компилятора не только дает вам 
неограниченные возможности управления конечными результатами, но и требует 
большой ответственности. Написание программ на ассемблере напоминает стряпню с 
атомами и молекулами вместо яиц и муки. Пишущих на ассемблере можно назвать 
программистами-химиками. Можно смешивать инструкции процессора любым способом, 
и только от вас зависит, насколько съедобным будет результат. 
    

    438 30 

    ЗАМЕЧАНИЕ

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

    Перед тем как окунуться с головой в написание собственных функций на 
ассемблере, полезно взглянуть на код, который генерирует Borland C++. Вы 
можете также воспользоваться этим способом для исследования результатов 
компиляции. С помощью опции -В автономного компилятора программа транслируется 
в текстовый файл, содержащий инструкции ассемблера. Для программы с именем 
MYPROG.CPP команда Ьсс -В myprog создаст файл MYPROG.ASM, содержащий 
результаты компиляции в форме ассемблерного текста. После компиляции ВСС 
запускает TASM для ассемблирования MYPROG.CPP в объектный код, после чего 
удаляет файл MYPROG.ASM. Вы также можете вставить строку: 

    «pragma inline 

в программный модуль для достижения аналогичного результата. (Директива 
#pragma задает команду специальную для компилятора.) 
    В прошлом единственной причиной использования опции -В было 
ассемблирование встраиваемых операторов на ассемблере в программах на С и C++. 
Теперь компилятор имеет свой собственный встроенный ассемблер, опция -В 
устарела и используется исключительно для старых программ. Можно также 
использовать -В вместе с опцией -Ename, где name — альтернативный ассемблер, 
например Microsoft Macro Assembler (MASM). 
    Еще одна уместная и потенциально более важная опция -S также генерирует 
вывод на ассемблере, но не запускает TASM и не удаляет созданный файл с 
расширением .ASM. Инструкции, содержащиеся в этом файле, помогут изучить 
процесс преобразования компилятором высокоуровневых операторов в 
низкоуровневый машинный код. Вы можете также модифицировать ассемблерный текст 
перед ассемблированием, например, для того, чтобы поэкспериментировать с 
низкоуровневой оптимизацией. Для программы MYPROG.CPP введите bcc -S myprog, 
чтобы создать файл MYPROG.ASM. Отредактируйте или просто просмотрите этот 
файл, затем с помощью TASM.EXE, доступного по текущим маршрутам (обычно он 
находится в каталоге C:\BC45\BIN), завершите компиляцию командой bcc myprog .asm. 

    ЗАМЕЧАНИЕ 

    Из интегрированной среды сгенерировать файл с ассемблерным текстом нельзя. Для 
использования описанных приемов следует запускать автономный компилятор из 
командной строки DOS. (Компилятор интегрированной среды может обрабатывать 
встраиваемые операторы asm. Он только не может их создавать.) 


    Функции, которые возвращают void 

    Скомпилируйте с опцией -S программу, содержащую простую функцию языка С, 
которая возвращает void. Вы должны будете ввести с клавиатуры прописную S. 
Найдите листинг 17.1 и из приглашения DOS задайте команду bcc -S voidf. Затем 
загрузите результирующий файл VOIDF.ASM в текстовый редактор. 
    Не считая нескольких вспомогательных строк в начале сгенерированного 
ассемблерного файла (в этих строках объявляются макросы, компонуются сегменты 
программ, а также присутствует отладочная информация), в листинге 17.2 
демонстрируется результирующий файл VOIDF.ASM, содержащий команды ассемблера. 

    ЗАМЕЧАНИЕ

    Я получал операторы ассемблера, приведенпмс ниже, с помощью Borland C++ 
3.1. В версии 4.5 создается более "чистый" код (например, в нем отсутствуют 
ненужные инструкции jmp, подобные той, что присутствует в строке 17), однако 
цель данного раздела — продемонстрировать, как можно исследовать операторы 
программы на ассемблере, а не документировать генерациию кода компилятором. 

    Листинг 17.1.  VOIDF.CPP (скомпилируйте эту программу ь опцией -S) 

    1.   #include <stdio.h> 
    2: 
    3    void f(), 
    4 
    5:   main() 
    6'   { 
    7      f(); 
    8:     return 0;
    9-   } 


    Глава 17. Оптимизация с помощью ассемблера  439 31 

    10: 
    11:	void f() 
    12:	{ 
    13:	   int x = 123; 
    14:	   printf("x == %d\n",  x); 
    15:	} 
    
    
    ЗАМЕЧАНИЕ 

    Нe стоит вводить или ассемблировать файл VOIDKASM. Вы можете создать свою 
собственную немодифиднрованную копию с помощью компиляции листинга 17.1 с 
опцией -S. На диске файл VOIDFM.ASM (лишнее М означает модификацию) содержит 
листинг 17.2. Я добавил поясняющие комментарии справа в большинстве строк. 

    Листинг 17.2. VOIDFM.ASM (снабженный комментариями файл на ассемблере VOIDF.CPP) 

    1:	_ТЕХТ segment byte public 'CODE'	; Начало сегмента кода 
    2:	;
    3:	; main() 
    4:	; 
    5:	assume cs:_TEXT	;Директива ассемблера 
    6:	_main proc near	;Начало функции main() 
    7:	push bp	;Сохранить указатель базы 
    8:	mov bp, sp	;Задать bp равным sp 
    9:	; 
    10:	;{ 
    11:	;f(); 
    12: 	;
    13:	call near ptr @f$qv	; Вызвать функцию f() 
    14: 	;
    15:	;return 0; 
    16: 	;
    17:	xor ax, ax	; Задать возвращаемое в- ах значение равным 0 
    18:	jmp short §1058	; Бесполезная инструкция! 
    19:	91958: 
    20: 	;
    21:	;} 
    22: 	;
    23:	pop bp	Восстановить bp 
    24:	ret	Вернуться в место вызова 
    25:	_main endp	Конец функции main() 
    26:	; 
    27:	; void f(void) 
    28:	; 
    29:	assume cs:_TEXT	Директива ассемблера 
    30:	@f$qv proc near	Начало функции f() 
    31:	push bp	Сохранить регистр bp 
    32:	mov bp.sp	Задать bp равным sp 
    33:	sub sp,2	Зарезервировать стек для х 
    34: 	;
    35:	;{ 
    36.	;int x = 123; 
    37: 	;
    38:	mov word ptr [bp-2],123	; Присвоить 123 целому х 
    39: 	;
    40:	;printf("x == %d\n", x); 
    41:	; 
    42:	push word ptr [bp-2]	Запомнить значение х 
    43:	mov ax, offset DGROUP:s@	Запомнить адрес строки s, 
    44:	push ax	;хранящейся в сегменте данных 
    45:	call near ptr _printf	;Вызвать функцию printf() 
    46:	pop ex	;Удалить элемент из стека 
    47:	pop ex	;Удалить элемент из стека 
    48: 	;
    49:	;} 
    50: 	;
    51:	mov sp.bp	; Очистить указатель стека 
    52:	pop bp	; Восстановить регистр bp 
    53:	ret	; Вернуться в место вызова 
    54:	ef$qv endp 
    55:	?debug С Е9 .	; Отладочная информация 
    56:	?debug С FA00000000	; для Turbo Debugger 

    
   440     Часть IV. Разработка приложений 

    57:	_TEXT ends 
    58:	_DATA segment word public 'DATA'	;Глобальный сегмент данных 
    59:	s@ label byte			;Метка строки для printf() 
    60:	db 'x == %d'			;строка для printf() 
    61:	db 10				;Символ новой строки 
    62:	db 0				;Нуль, завершающий строку 
    63:	DATA ends	Конец сегмента 
    64:	TEXT segment byte public 'CODE'	;Сегменты поддерживаются в 
    65:	_TEXT ends			;требуемом порядке 
    66:	public _main			;Экспорт имени jnain 
    67:	public @f$qv			;Экспорт функции f() 
    68:	extrn _printf: near			;Импорт функции printf() 
    69:	_s@ equ s@			;В этом коде не используется            
    70:	end				;Конец листинга

    В сгенерированном тексте демонстрируются ключевые инструкции, которыми 
следует пользоваться при написании своих собственных функций на ассемблере. 
Функция main() начинается в строке б с директивы ргос, которая присваивает 
метку _main этой ячейки памяти с ближним адресом. (В других моделях памяти 
адрес может быть дальним.) В ассемблере, такие глобальные имена, как main, 
имеют лидирующий знак подчеркивания (_main). В строке 7 значение регистра bр 
сохраняется в стеке, затем ему присваивается значение регистра sp. Регистр bр 
адресует элементы, запомненные в стеке, и обычно внутри функции 
инициализируется значением стекового указателя. 
    В строке 13 вызывается функция программы f(), заданная ближним адресом метки 
@f$qv. Компилятор сам создает подобные метки в случае необходимости. Обратите 
внимание, что исходной текст C++ отражается в комментариях строк 9-12. Эти 
комментарии приводятся над соответствующим участком ассемблерного кода. 
    В строках 17-19 реализуется оператор возврата из функции main(). Операция 
исключающего ИЛИ (хог), выполняемая над регистром ах в качестве аргументов, 
очищает этот регистр, устанавливая нулевое возвращаемое значение (строка 15). 
Инструкция jmp в строке 17, очевидно, бесполезна — она приведет к простому 
переходу к следующей инструкции. Это демонстрирует, что в результат работы 
компилятора могут иногда вкрасться бессмысленные инструкции. В критических 
случаях вы можете оптимизировать такие операторы, переписав их на ассемблере 
(как — вы узнаете дальше в этом разделе). 
    Наконец, функция main() завершается в строках 23, 24, восстанавливая
сохраненное первоначальное значение bр (отменяя воздействие строки 7) и 
возвращаясь в место вызова функции main() — начальный код запуска, который 
компонуется вместе со скомпилированным модулем при создании результирующих 
исполняемых файлов с расширением .ЕХЕ. 
    Теперь мы подошли к функции f() (строки 30-54). Поскольку остальные функции 
тоже могут воспользоваться регистром bр, первым делом необходимо запомнить его 
в стеке (строка 31). После этого для адресации в этой функции локальных 
переменных и параметров (в данном случае они отсутствуют) регистру bр 
присваивается значение стекового указателя sp (строка 32). Точно такие же 
действия имели место в начале функции main(). Большинство функций начинаются 
аналогичным образом. 
    В строке 33 стековый указатель "понижается" на два, тем самым 
резервируется указанное количество байтов в стеке для локальной целой 
переменной х, принадлежащей функции f() (строка 36). Инструкция mov в строке 38 
запоминает литеральное значение 123 в этом зарезервированном участке памяти. 
Обратите внимание на использование bр для адресации локальных переменных в 
операторах. Далее следует оператор вызова функции printf() в строках 42—47. В 
коде значение переменной х записывается в стек вместе с адресом строки s, 
хранящейся в сегменте данных программы. Это литеральная строка, использованная 
в модуле VOIDF.CPP (строки 14 глистинга 17.1 и 40 листинга 17.2). Компилятор 
собирает все такие строки и запоминает их в сегменте данных программы. Он 
также изобретает метки, подобные s, для адресации безымянных объектов. 
    Запомнив соответствующие аргументы в стеке, функция далее вызывает функцию 
printf() в строке 45. По соглашению имя этой библиотечной функции на ассемблере 
звучит _printf. По возвращении из функции printf() две инструкции pop в строках 
46 и 47 удаляют аргументы, которые запоминались в стеке перед вызовом, этот 
механизм известен как соглашение вызова языка С. 

    ЗАМЕЧАНИЕ 

    В соглашении вызова языка С в скомпилированной программе аргументы 
запоминаются в стеке перед вызовом функции, после чего вызывающий функцию 
обязан удалить аргументы из стека во избежание его переполнения. В 
альтернативном соглашении вызова языка Pascal (используемом, например, в 
функциях Microsoft Windows) аргументы из стека удаляет вызванная функция.
Соглашение вызова языка С позволяет использовать переменное число аргументов в 
функции, но засоряет объектный код лишними инструкциями извлечения из стека. 
Соглашение вызова языка Pascal уменьшает размер кода, однако позволяет 
передавать функции только фиксированное число аргументов. 


    Глава 17. Оптимизация с помощью ассемблера     441 

    Функция f() завершается в строках 51-53, сначала устанавливая стековый 
указатель равным Ьр, чем освобождается зарезервированный участок стека, 
выделяющийся в строке 33. С этого момента целая переменная х прекращает свое 
существование. В строке 52 восстанавливается сохраненный регистр Ьр, после 
чего инструкция ret осуществляет возврат в место вызова функции f(). 
    Наряду с другими действиями, оставшиеся строки задают отладочную 
информацию (строки 55, 56), запоминают в сегменте данных программы (строки 
59-62) символьную строку, использующуюся при вызове функции printfO, 
экспортируют глобальные имена (строки 66, 67) и импортируют внешнюю 
библиотечную функцию printfO (строка 68). 
    Даже в этом простом примере за кулисами выполняется огромная работа! 
Следует понять, что "раскопки" в скомпилированном тексте требуют большого 
труда. Необходимо быть уверенным в том, что результат будет стоит приложенных 
усилий, прежде чем пуститься в подобное путешествие по нижнему уровню вашей 
программы. 

    Функции, возвращающие значения	

    Есть несколько способов, с помощью которых функция возвращает значение: 
слова передаются в регистре ах, байты — в al; дальние указатели передаются 
функциями в паре регистров dx:ax, в dx содержится сегмент, в ах — смещение 
адреса; вещественные значения передаются в стеке математического сопроцессора 
80x87; структуры — непосредственно в стеке; массивы — по адресу. 
    Существует много вариантов схем возврата значения. Для точного определения 
того, как обрабатываются отдельные типы возвращаемых значений, скомпилируйте с 
опцией -S короткий пример и изучите результирующий ассемблерный код. 
Бессмысленно запоминать множество правил возврата значений, которые могут 
измениться в будущих реализациях компилятора. Всегда следует изучить 
действительный способ возврата значения на примере функции, перед тем как 
делать какие-либо умозаключения по поводу того, как работает" низкоуровневый код. 

    Функции — члены класса 

    Объекты классов и их функции-члены усложняют ассемблерный текст. Взгляните 
на пример программы с простым классом (листинг 17.3). Скомпилируйте его с 
помощью команды bcc -S classf и исследуйте результирующий ассемблерный код в 
файле CLASSF.ASM. 

    Листинг 17.3.  CLASSF.СРР (скомпилируйте с опцией -S) 

    1:   «include <iostream.h> 
    2: 
    3: class TClass { 
    4: public: 
    5:    int x; 
    6:    void SetX(int n); 
    7:    int GetX(); 
    8: }; 
    9: 
    10: main() 
    11: { 
    12:   TClass q;
    13: 
    14:  q.SetX(123); 
    15:  cout « "q.GetXO == " « q.GetX() «  '\n'; 
    16:     return 0; 
    17:   }
    18: 
    19: void TClass::SetX(int n) 
    20: { 
    21:  x = n; 
    22: } 
    23: 
    24: int TClass::GetX() 
    25: { 
    26:    return x; 
    27: } 

    В классе TClass в строке 38 объявляется один глобальный член (int x) и две 
функции-члена для чтения и записи значения х. В ассемблерном коде функция-член 
SetX() после компиляции в версии 3.1 выглядит следующим образом: 

    push bp		;Сохранить Ьр 
    mov bp, sp	;Адресовать стек указателем bp 
    push si		;Сохранить si, используемый далее 
    mov si,word ptr  [bp+4]      ;Адресовать объект через 'this' с" помощью si 

    
    442     Часть IV. Разработка приложений 

    mov	ax,word ptr [bp+6]	;Присвоить параметр функции регистру ах 
    mov	word ptr [si],ax	;Присвоить ах члену объекта х 
    pop	si	;Восстановить si 
    pop	bp	;Восстановить bp 
    ret	;Вернуться в место вызова 

    Использование версии 4 5 даст аналогичные результаты  Как обычно, в 
инструкциях регистр bp запоминается в стеке, после чего он устанавливается 
равным стековому указателю sp для адресации значений в стеке, в данном случае 
параметра int n (строка 19 в листинге 173)  После сохранения регистра si в 
программе с помощью bp регистр si устанавливается равным указателю this  
передаваемому всем нестатическим функциям-членам класса   Указатель this 
ссылается на объект, для которого в операторе вызывалась нестатическая 
функция-член, в данном случае на объект q, объявленный в функции main() в 
строке 12  После инициализации регистра si значением this в программе 
регистру ах присваивается значение [Ьр+6], которое ссылается на параметр n  
Затем в программе это значение присваивается по адресу word ptr [si], 
ссылающемуся на первое слово объекта q (адресованному регистром si)   Наконец, 
в программе перед возвратом из функции восстанавливаются регистры si и bp 
    Нелишним будет напоминание о том, что нестатические функции-члены класса 
получают указатель this на объект, для которого в операторе вызывались эти 
функции В программах на ассемблере вы можете воспользоваться указателем this 
для определения местоположения объекта в памяти 

    Написание встраиваемого кода BASM 

    Теперь, после исследования нескольких примеров программ на 
ассемблере, можно начинать думать о написании своих программ  Вы можете 
действовать двумя способами   Первый заключается в использовании 
встроенного ассемблера (BASM) для вставки встраиваемых ассемблерных 
операторов в текст вашей программы 
    Второй состоит в написании внешних модулей для их отдельного 
ассемблирования в объектные файлы с помощью TASM и последующей компоновки 
вместе с вашей скомпилированной программой   В этом разделе поясняется работа 
с BASM   Это самый простой способ совместного использования ассемблера с программами, 
написанными на С и С++.
    Несмотря на впечатляющую легкость использования,  в  BASM  отсуствуют
некоторые  расширенные свойства TASM.

   •  Отсутствуют макросы.
   •  Нельзя ассемблировать специфические для процессоров 80386 н 80486 инструкции.
   •  Не распознается идеальный режим — расширенный ассемблерный синтаксис TASM 
   •  Имеется ограниченный набор ассемблерных директив для резервирования памяти.

    Если вы не можете обойтись без этих средств, следует воспользоваться 
автономным ассемблером TASM Однако имейте в виду, что вы можете воспользоваться 
компиляторами С и C++ для выхода из большинства ситуаций в которых  необходимы 
автономные ассемблерные программы с расширенным набором команд TASM.
Например, наверное лучше резервировать пространство для переменных с помощью 
объявлений С и C++, на которые затем можно ссылаться по именам в ассемблерных 
операторах. Однако, если вам нужны инструкции 80386  или вы предпочитаете пользоваться 
идеальным режимом, следует использовать TASM.

ПРЕДУПРЕЖДЕНИЕ

    Если в ваших ассемблерных вставках используются инструкции 80186 или 80286, при 
компиляции с помощью BCC используйте опцией -1.  Если же в ваших операторах используются 
инструкции защищенного режима 80286, воспользуйтесь опцией -2.


      Написание операторов asm

    Операторы asm исполняют один или несколько байтов инструкций процессора 
непосредственно в скомпилированную программу. Следует полъзоваться операторами asm 
для вставки небольших участков ассемблерного кода, например для оптимизации  ключевых 
операторов или для доступа к регистрам компьютера  Общий вид оператора asm следующий:

    asm мнемоника [операнды] [;]

   Первым идет ключевое слово asm за ним следует оператор ассемблера (мнемоника), 
подобный mov или shl. Затем следуют операнды, например одно или несколько имен регистров 
или адрес для ссылки  Завершает оператор необязательный символ точки с запятой. Оператор 
может также завершаться символом новой строки. В отличие от языка автономного ассемблера, 
точка с запятой не начинает комментарий. Например, в модуле TASM следующий оператор 
завершается комментарием:

    mov ax, cx  ; Задать ax  значение  cx
    
    
    Глава 17. Оптнмшгирш с мшвщмо ассемблера	443 

    Эквивалентный оператор BASM выглядит несколько иначе: 

    asm mov ах,  сх;	/* Задать ах равным сх */ 

    Для документирования оператора asm следует использовать комментарии С или 
C++. Точка с запятой завершает оператор, однако ее присутствие необязательно. 
Вы можете написать оператор следующим образом: 

    asm mov ах,  сх	/* Задать ах равным сх */ 

    Можно также использовать комментарии C++: 

    asm mov ах,  сх	// Задать ах равным сх 

    Несколько операторов asm следует заключать в фигурные скобки. Каждый 
оператор asm — это оператор С, и правила, касающиеся составных операторов, 
относятся к asm в такой же степени, как и к if, while и друг гим конструкциям: 

    asm { 
    pushbp	// Запомнить Ьр в стеке 
    mov bp,  sp                  // Задать Ьр равным sp 
    ........ // Вставить другие операторы asm в этом месте 
    pop bp	// Восстановить Ьр 

    Можно воспользоваться точкой с запятой для разделения нескольких 
операторов в одной строке, что сохранит немного места в тексте вашей 
программы. Этот популярный прием следует использовать для запоминания и 
извлечения из стека необходимых регистров: 
    
    asm { 
    push ax; push bx; push ex; push dx 
    .... // Вставить другие операторы в этом месте 
    pop dx; pop сх; pop bx; pop ax; 
    } 

    Глобальные операторы asm, не принадлежащие ни одной из функций, 
ассемблируются в сегмент данных программы. Существует не так уж много 
ситуаций, в которых данный прием по-настоящему необходим, однако, если вы 
хотите вставить участок кода в сегмент данных, вы можете это сделать. Просто 
пишите ваши операторы asm вне реализаций функций. 
    Локальные операторы asm, которые располагаются внутри функций, 
ассемблируются BASM в поток кода в той же позиции. Компилятор предотвращает 
большинство конфликтов, возникающих между результатами BASM и близлежащими 
операторами C++, однако, чтобы быть уверенным в успехе, имеет смысл сравнивать 
исходный текст и результаты компиляции с помощью опции -S. 

    Оптимизация функций 

    Основная причина использования ассемблера в программах совместно с C++ — 
повышение производительности. Сначала с помощью ваших собственных тестов или 
даже лучше, с помощью тестов Turbo Profiler определите функции, выполнение 
которых отнимает большую часть общего времени выполнения. Оптимизация таких 
критических функций может значительно повысить производительность вашей программы. 

    ЗАМЕЧАНИЕ 

    Фредерик П. Брукс в книге The Mythical Man-Month (Мифический 
человеко-месяц) выдвигает предположение о том, что все проблемы быстродействия 
можно решить переводом от одного до пяти процентов программ в оптимизированный 
язык ассемблера. Оптимизация же остальных программ даст незначительные результаты. 

    Рассмотрим простой пример — функцию MySquare(), возвращающую квадрат целого значения: 

    int MySquare(int n) { 
    return n * n; 
    } 

    Скомпилируем программу с этой функцией с помощью опции -S. В результате 
получим следующий ассемблерный код (с добавленными поясняющими комментариями): 

	push	bp	;Сохранить регистр указателя базы 
   	mov	bp, sp	;Адресовать стек регистром Ьр 
	mov	bx, word ptr [bp+4]	;Записать int n в bx 
	mov	ax, bx	;Переслать bx в ах 
	Imul	bx	;Умножить ах на bx 
	jmp	short @2@58	;Перейти к следующей команде 
@2@58: 
	pop	bp	Восстановить регистр Ьр 
	ret		Вернуться в место вызова 


    444     Часть IV. Разработка приложений 

    Этот код имеет два очевидных недостатка. Во-первых, целое п (размещенное в 
стеке по смещению Ьр+4) записывается в регистр Ьх и тут же в ах. Почему бы не 
переслать п непосредственно в регистр ах? Во-вторых, инструкция jmp 
бессмысленно переходит к следующему же адресу, якобы пропуская код, который, 
возможно, существовал бы в этом месте при других обстоятельствах. 
    Скомпилированный код изобилует подобными недостатками, кажущимися, на 
первый взгляд, грубыми ошибками, которые компетентный программист, пишущий на 
ассемблере, никогда бы не допустил. Однако имейте в виду, что если функция 
MySquare() не обладает статусом критической, эти расточительные инструкции, 
вероятно, не оказывают ощутимого влияния на быстродействие программы. 
    С другой стороны, если функция MySquare() занимает большую часть общего 
времени выполнения программы, оптимизация функции для достижения максимальной 
производительности даст приличный выигрыш. Руководствуясь ассемблерным кодом, 
сгенерированным компилятором, вы можете переписать функцию с использованием 
операторов asm, как показано в листинге 17.4. 

    Листинг 17.4.  INLINE.CPP (оптимизация функции MySquare() с помоцыо 
операторов asm) 

    1:   #include <stdio.h> 
    2: 
    3:   int MySquare(int n); 
    4: 
    5:   main() 
    6:   { 
    7:     int i; 
    8:     int array[10]; 
    9: 
   10:     for (i = 0;  i < 10;  i++) 
   11:       array[i] = MySquare(i); 
   12:     for (i = 0;   i < 10;  i++) 
   13:        printf("array[%d] == %d\n",  i,  array[i]); 
   14:     return 0; 
   15:   } 
   16: 
   17:   // Предотвращение предупреждения "Function should return value" 
   18:  #pragma warn -rvl 
   19: 
   20:   int MySquare(int n) 
   21:   { 
   22:   /* 
   23:     return n * n; 
   24:   */ 
   25:    asm { 
   26:       mov ax, word ptr n 
   27:       imul bx 
   28:     } 
   29:   } 
   30: 
   31:   // Восстановление предыдущего состояния предупреждения 
   32:   #pragma warn +rvl 

    В строках 22-24 первоначальный оператор умножения заключен в скобки 
комментария. При оптимизации ваших собственных функций никогда не удаляйте 
первоначальный код. Когда-нибудь в будущем вам может понадобиться перенести 
программу на другую компьютерную систему, которая, возможно, не будет оснащена 
процессором 80x86. В этом случае вы просто удалите скобки комментария и 
операторы asm для преобразования программы обратно в С или C++. 
    В строках 25-29 приводится оптимизированное тело функции на ассемблере. 
Первый оператор пересылает параметр а в регистр ах. Вам не надо самим 
вычислять смещения (имеется в виду использование адресных выражений наподобие 
[Ьр+4]). Вы можете сослаться на любой параметр по имени переменной, и ВASM 
вставит соответствующий адрес. 
    В строке 28 регистр ах умножается на Ьх, результат этой операции (и 
функции в целом) хранится в регистрах dx (старшее слово) и ах (младшее слово). 
Очевидно, поскольку модифицированная функция не выполняет возврат явным 
образом, излишняя инструкция jmp также уничтожается. 
    К несчастью, компилятор теперь выдает предупреждение, что в функции 
MySquare() отсутствует оператор return. Несмотря на безобидность, появление 
этого предупреждения раздражает, и вы можете его выключить с помощью директивы 
#pragma warn, как показано в строке 18. Директива 

    #pragma warn -rvl 

выключает предупреждение, а директива 

    #pragma warn +rvl 


    Глава 17. Оптимизация с помощью ассемблера     445 

    включает его. После любой из этих команд, вы можете написать 

    #pragma warn  .rvl 

    для восстановления предыдущего статуса предупреждения. Можно также 
отключить это предупреждение в командной строке с помощью опции, подобной 
-wrvl. Другие комбинации предупреждений (их десятки) документируются в 
справочниках Borland. 
    После оптимизации функции, подобной MySquare(), имеет смысл исследовать 
конечный результат. Для его проверки скомпилируйте еще раз код с опцией -S и 
изучите получившийся ассемблерный код. Новая функция MySquare имеет следующий вид: 

    push bp 
    mov	bp, sp 
    mov	bx, word ptr [bp+4] 
    mov	ax, word ptr [bp+4] 
    imul bx 
    pop	bp 
    ret 

    Сравните этот код с предыдущим неоптимизированным примером. Целый параметр 
пересылается непосредственно в ах, и излишняя инструкция jmp отсутствует. 
Компилятор сам сохраняет, инициализирует и восстанавливает bp. Он также 
добавляет инструкцию ret. 
    Как видно из этого небольшого примера, оптимизация функций требует времени 
и прилежания. Не стоит тратить ваше драгоценное время на оптимизацию кода, 
который уже работает с необходимой скоростью. 

    Выполнение команд переходов 

    Инструкции переходов в ассемблере, такие как, jmp или jnl, приводят к 
продолжению выполнения программы, начиная с заданного адреса. В автономных 
программах TASM места переходов идентифицируются метками. В ВASM инструкции 
перехода asm могут передавать управление только меткам С или C++, подобным 
тем, которые используются оператором goto. В действительности инструкции 
перехода в asm не что иное, как оператор goto. 
    Поскольку адресные метки пишутся на С или C++, они должны существовать 
где-то вне операторов asm. Например, предположим, у вас возникла необходимость 
вставить авторскую подпись в поток кода вместо сегмента данных, где ее могут 
быстрее обнаружить и подделать компьютерные пираты. Вы можете закодировать 
команду перехода, минующую текстовое сообщение, следующим образом: 

    asm { 
        jmp L1
       db 13, 10, ' 1992 by Tom Swan', 0 
    } 
    L1: 
    asm { 
      //... Дополнительные инструкции 
   } 

    Инструкция jmp L1 пропускает директиву db (определение байта), вставляющую 
возврат каретки (13), перевод строки (10) и строку авторской подписи в коде 
(обратите внимание на апострофы и явное завершение строки нулем). Адресная 
метка L1 появляется после начального оператора asm, за ней могут следовать 
операторы С и C++ или еще один оператор asm, как в данном случае. 
    Можно также использовать команды перехода по условию, например jnz, jl и 
др. Если адресные метки не достигаются этими "ближними" инструкциями перехода, 
они аварийно завершают выполнение программы. Например, если вы написали 

    asm jne TARGET, 

    BASM ассемблирует эту инструкцию (перейти, если не равно) неизмененной 
только в том случае, если расстояние от места перехода до TARGET находится в 
диапазоне от -128 до +127 байт. Если адресная метка размещается вне этого 
диапазона, ВASM вставляет команду безусловного перехода в точку входа 
аварийного завершения программы. Эта проблема решается путем преобразования 
условного перехода следующим образом: 

    asm je TEMP 
   asm jmp TARGET TEMP: 

    Идея заключается в пропуске безусловного перехода, если выполняется 
обратное условие. Инструкции jmp и je выполняют совместно те же самые 
действия, что и первоначальный переход jne, однако они "достают" гораздо дальше. 
    ВASM автоматически оптимизирует безусловные инструкции перехода. Если вы напишете 

    asm jmp LABEL 


    446     Часть IV. Разработка приложений 

и LABEL ссылается на адрес в пределах от -128 до +127 байт, BASM 
использует более эффективную 2-байтовую инструкцию перехода. Если же адресная 
метка не вписывается в это ограничение, тогда BASM воспользуется менее 
эффективной 3-байтовой инструкцией jmp. 
    Вы можете отменить оптимизацию, предварив метку ключевыми словами NEAR PTR 
или FAR PTR. Например, для задания дальнего перехода к ближней метке вы можете 
написать следующее: 

    asm jmp FAR PTR LABEL 


    Доступ к переменным 

    В операторах asm можно ссылаться по имени на локальные, глобальные и 
регистровые переменные. Можно также непосредственно ссылаться на параметры 
функций. Конечно, может потребоваться использование спецификаторов WORD PTR и 
BYTE PTR для загрузки 16- и 8-битовых значений в регистры. Для целочисленной 
переменной или параметра count инструкция 

    asm mov ax, count 

эквивалентна следующей: 

    asm mov ax, WORD PTR count 

    Для пересылки первого байта count в байтовый регистр следует использовать 
такой спецификатор: 

    asm mov al, BYTE PTR count 

    Можно использовать аналогичный спецификатор DWORD PTR вместе с инструкцией 
LES и косвенными дальними вызовами для загрузки 32-битовых указателей, т.е. 
значений типа "double word" (двойное слово). 
    Можно также пользоваться в операторах asm регистрами si и di без 
конфликтов с регистровыми переменными. Если в операторе asm используются 
регистры si или di, компилятор не запоминает переменные в этих регистрах. 

    Доступ к членам структуры 

    Встраиваемые операторы asm могут ссылаться на члены структур по имени. 
Однако следует быть осторожным и различать, когда инструкциям необходимо 
значение, а когда адрес (относительное смещение члена структуры). 
    Предположим, вы объявили структуру TPoint с двумя целочисленными членами х 
и у. С помощью typedef (необязательно в C++, но полезно в ANSI С) вы можете 
объявить структуру следующим образом: 

    typedef struct point { 
    int x; 
    int у; } TPoint; 

    Затем можно объявить глобальную или локальную переменную с именем cursor 
типа TPoint: 

    TPoint cursor; 

    В операторах С и C++ выражения cursor.x и cursor.y ссылаются на члены 
переменной cursor. В операторах asm можно пользоваться аналогичными 
выражениями. Например, можно запомнить в регистре ах значение члена у 
переменной cursor следующим образом: 

    asm mov ax,  WORD PTR cursor.y // Запомнить значение у в ах 

    Для ссылки на адресное смещение относительно первого байта структуры 
следует воспользоваться клпчевым словом OFFSET: 

    asm mov bx, OFFSET cursor.y  // Запомнить смещение у в Ьх 

    Можно также адресовать структуру регистрами. Например, можно запомнить в 
паре регистров ds:di адрес структуры cursor и запомнить значение у в регистре ах 
следующим образом: 

    asm {
      mov di, OFFSET cursor	// Запомнить местоположение cursor в di 
      mov ax, [di].y                           // Запомнить значение члена у в ах 
    }

    Регистр ds уже ссылается на глобальный сегмент данных программы. В 
инициализации нуждается только di. Компилятор  распознает у как член структуры 
cursor типа TPoint. Конечно, могут возникнуть конфликты, если в еще одвой 
структуре будет объявлен член с именем у. Вы можете решить эту проблему, 
указав явным образом перед членом имя структуры в круглых скобках: 

asm {
    mov di, OFFSET cursor    // Запомнить местоположение cursor в di 
    mov ax, [di].(TPoint)y        // Указать,   какая структура имеется в виду 
}    

    Глава 17. Оптимизация с помощью ассемблера	447 


    Написание внешних модулей на Турбоассемблере

    Встраиваемые операторы BASM особенно подходят для небольших оптимизаций - 
несколько инструкций здесь, несколько там, или же в отдельной функции, которую 
необходюю ускорить. Более продуктивная работа на ассемблере требует 
дополнительных возможностей автономного Турбоассемблера (Turbo Assembler ), 
поставляемого Borland отдельно. 
    ТASM — замечательный продукт. Он может ассемблировать законченные 
приложения, написанные полностью на ассемблере. Он также может похвастаться 
множеством опций и директив: от простых инструкций резервирования памяти до 
сложных макросредств. В TASM имеется даже нечто подобное струкктуре класса, чем 
вы можете воспользоваться для написания объектно-ориентированных модулей 
на ассемблере. 
    Основная ценность TASM для программистов на С и C++ заключается, тем не
менее в создании объектных (с расширением .OBJ) модулей для, их компоновки 
вместе со скомпилированными программами. Вы  можете задать прототип функции 
С или C++, написать операторы, в которых она вызвается, и реализовать ее  
полностью на ассемблере. 
    Как и встраиваемый код BASM, внешние ассемблерные модули при написании
требуют внимательности и усидчивости. Всегда следует сначала удостовериться в том, 
что хлопоты окупятся сторицей, не  стоит переводить либо полностью, либо большую 
часть вашего кода в ассемблер в надежде повысить его производительность.. 
Определите критические участки кода и сконцентрируйтесь на том, чтобы заставить 
их работать как можно быстрее. 

    Использование утилиты H2ASH 

    Наиболее трудоемкий процесс в написании внешних ассемблерных модулей — 
разработка интерфейса с внутренней структурой данных программы. Ассемблер не 
предназначен для шишшивш аяврвяяй над структурами, массивами, строками С, а 
также классами C++, однако вы можете воспользоваться несколькими "трюками" для 
доступа к объектам этих и других типов из операторов ассемблер». 
    Утилита H2ASH.EXE, поставляемая вместе с TASM, может помочь вам сократить
время поиска ответа на вопрос: "Как мне получить доступ к программным данным 
с помощью ассемблерных инструкций и директив?" H2ASH преобразует объявления, 
содержащиеся в заголовочных файлах С и C++ в эквивалентные объявления TASM, 
хранимые в файлах с расширением .ASH (заголовочный файл ассемблера). Вы можете 
воспользоваться этими объявлениями для доступа к вашим программным данным или
же, руководствуясь ими, написать свои собственные директивы. 
    Утилита H2ASH поддерживает большинство конструкций С и классов C++. Она
обрабатыввает файлы #include и может пользоваться преимуществами идеального 
режима — расширенный MASM-подобный синтаксис TASM. К сожалению, H2ASH не 
распознает множественного наследояая, виртуальных базовых классов и 
шаблонов. Она также выдает предупреждения для элементов, подобных макросам, 
которые не транслируются так просто в директивы TASM. Следует рассматривать 
H2ASH как рекомендацию, а  не последнее слово в преобразовании объявлений С и 
C++ в ассемблерный код. Для 32-битового кода, сясяует пользоваться утилитой H2ASH32. 

   ЗАМЕЧАНИЕ

   Прочитайте текстовой файл TSM-UTIL.TXT , содержащийся в каталоге\DOC, для того чтобы 
больше узнать об утилите H2ASH.

    Продемонстрируем работу утилиты H2ASH, обработав с ее помощью несколько
образцов заголовочных файлов. Например, найдите заголовочный файл DB.H 
(листинг 11.15) из главы 11. Перейдите в каталог, содержащий этот файл, и 
введите команду h2ash db.h для создания нового файла DB.ASH с эквивалентными 
директивами TASM. Поскольку в DB.H включается заголовочный файл STDIO Н. 
утялита H2ASH преобразует содержащиеся в нем объявления так же, как в DB.H. 
(Во время выполнения H2ASH может предупреждать вас время от времени о макросах 
и других элементах, которые нельзя преобразовать.) 
    Несмотря на несовершенство результатов, утилита H2ASH мастерски справляется 
с преобразованием большинства объявлений. Например, в DB.H определяются 
следующие константы: 

   #define FALSE 0 
   #define TRUE 1 
   #define NAHELEN 30 
   #define ADDRLEN 30 
   #define CSZLEN 30 
   #define PHONELEN 13
    
    
    448     Часть IV. Разработка приложении 

    Утилита H2ASH переводит их в эквивалентные директивы TASM: 

    FALSE	EQU О 
    TRUE	EQU 1 
    NAMELEN	EOU 30 
    AODRLEN	EOU 30 
    CSZLEN	EQU 30 
    PHONELEN	EQU 13 

    Утилите H2ASH "по зубам" и более заковыристые объявления: 

    typedef struct record { 
      union { 
        long numrecs; 
        long custnum; 
      } info; 
      char name[NAMELEN]; 
      char addr[ADDRLEN]; 
      char csz[CSZLEN]; 
      char phone[PHONELEN]; 
      double balance; 
   } Record; 

       Эта структура легко преобразуется утилитой H2ASH, которая выдаст 
следующие эквивалентные директивы TASM: 

    tag$1UNION 
    numrecs	DD             ? 
    custnum	DD           ? 
    tag$1ENDS 

    record	STRUC 
    info tag$1 <>
    name DB 30 DUP ( ? ) 
    addr DB   30 DUP ( ? )
    csz DB     30 DUP ( ? )
    phoneDB  13 DUP ( ? )
    balance     DD   ?
    record	    ENDS 

    В дополнение к преобразованию существующих имен, таких, как numrecs и 
phone, утилита H2ASH создает все необходимые имена, подобные tag$l. Она также 
создает объявления, необходимые для вызова функций С или C++, для которых в 
программе есть прототипы: 

    GLOBAL С CreateDB	:NEAR 
    GLOBAL С OpenDB	:NEAR 
    GLOBAL С ReadRecord	:NEAR 
    GLOBAL С WriteRecord	:NEAR 


    Написание внешних модулей TASM 

    Следующие программы демонстрируют, как писать внешние ассемблерные модули. 
Для изучения этого раздела вам необходимо приобрести и установить Turbo Assembler. 
Если же у вас нет TASM, пропустите весь материал  до раздела "Использование прерываний". 
    В листинге 17.5 вызывается функция Timer() для доступа к счетчику времени в 
компьютере, который хранится ло адресу 0000:046С и регулярно обновляется процедурой 
BIOS. Однако не спешите компилировать программу. Вы сделаете это позже, после 
создания внешнего модуля Timer(). 

    Листинг 17.5.  TESTTIME.CPP (проверка функции Tlmer())	

    1:   #include <conio.h> 
    2:   #include "timer.h" 
    3:
    4:  main() 
    5:  { 
    6:	while (!kbhit()) { 
    7:	  gotoxy( 1, wherey()); 
    8:	  cprintf("%ld" ,  Timer()); 
    9.  	} 
   10:  	getch(); // Ждать нажатия клавиши 
   11:	return 0; 
   12: }

    В программе не делается ничего особенного, это лишь средство для 
тестирования функции Timer(), которая будет написана на ассемблере. В строке 2 
включается заголовочный файл функции (листинг 17.6). 


    Глава 17. Оптимизация с помощью ассемблера	449     
    15 Освоение  C++4.5 


    Листинг 17.6.  TIMER.Н (заголовочный файл для функции Timer()) 

    1:   // timer.h -- Заголовочный файл для timer.срр 
    2: 
    3:   extern "С"  { 
    4: 
    5:     long Timer(); 
    6: 
    7:    } 

    Прототипы внешних функций в программах на C++ обычно объявляются внутри 
директивы extern "С" { }. Это предотвращает процесс так называемого 
модифицирования имен, согласно которому C++ преобразует имена функций в 
уникальные строки, которые позволяют стандартным компоновщикам обрабатывать 
объектно-ориентированный код. Например, функция с именем TimerO будет иметь 
имя @Timer$gv. Подобные имена затруднительны для чтения, и, вероятно, лучше 
пользоваться модифицированными именами внешних функций в ассемблерных модулях. 
(Компиляторы ANSI С не модифицируют имена функций, так что этот совет касается 
только программ на C++.) 

   СОВЕТ

    Если вам необходимо воспользоваться модифицированными именами (например, 
если вы хотите поддерживать и использовать функцию, написанную и на ассемблере 
и на C++), напишите пустую функцию без операторов и скомпилируйте ее с опцией 
-S для получения ассемблерного файла с модифицированными именами. 
Воспользуйтесь этим файлом как оболочкой, которую следует наполнить 
инструкциями вашей настоящей функции. 

    Далее следует ассемблерный модуль, в котором находятся выполняемые 
инструкции для внешней функции Timer(). В листинге 17.7 приводится реализация 
функции Timer() с помощью синтаксиса идеального режима TASM. 

    Листинг 17.7. TIMER.ASM (реализация функции Timer() во внешнем ассемблерном модуле) 

    1:	IDEAL 
    2:	MODEL small 
    3: 
    4:	CODESEG 
    5:	PUBLIC _Timer 
    6: 
    7:	PROC _Timer 
    8:	xor ax, ax ; Задать ах равным 0000 
    9:	mov es, ax ; Задать es равным 0000 
    10:	mov di, 0046cH ; Задать di равным 046С 
    11:	mov ax, [WORD PTR es:di] ; Взять младший байт timer 
    12:	mov dx, [WORD PTR es:di+2] ; Взять старший байт	timer 
    13:	ret ; Вернуться в место вызова 
    14:	ENDP 
    15: 
    16:	END 

    В строке 2 демонстрируется одна важная деталь — выбор модели памяти. Вы 
должны использовать одну и ту же модель памяти во внешнем модуле и в главной 
программе (см. главу 19). Внутри сегмента кода (который начинается ключевым 
словом CODESEG в строке 4) объявляется имя внешней функции с помощью директивы 
PUBLIC. Предварите глобальное имя символом подчеркивания — таково стандартное 
соглашение глобальных имен в С. 
    Напишите функцию как любую другую отдельную ассемблерную процедуру, 
обычно, между директивами PROC и ENDP (строки 7-14). Вы сами ответственны за 
возврат всех необходимых значений в соответствующих регистрах, предохранение 
от порчи стека, сохранение и восстановление регистров bр и ds и прочую 
низкоуровневую работу. В данном случае функция возвращает длинные целые 
значения в паре регистров ax:dx (строки 11, 12). Не имеет смысла также 
сохранять и восстанавливать bр и стековый указатель sp, поскольку в функции не 
используются параметры, передающиеся через стек, или переменные в стеке. 
    Теперь вы владеете всеми необходимыми компонентами для ассемблирования и 
компиляции примера. С помощью TASM.EXE и автономного компилятора ВСС.ЕХЕ, 
доступных по установленным маршрутам, и находясь в каталоге, содержащем файлы 
TESTTIME.CPP, TIMER.Н и TIMER.ASM, введите из командной строки DOS команду bcc 
testtime timer.asm. Сначала компилятор C++ скомпилирует TESTTIME.CPP в 
TESTTIME.OBJ, затем TASM ассемблирует TIMER.ASM в TIMER.OBJ Наконец, 
компоновщик объединит объектные файлы в исполняемом файле TESTTIMER.EXE. 
Запустите программу и нажмите любую клавишу для остановки таймера. 


    450     Часть IV. Разработка приложений 

    Для ассемблирования модулей порознь воспользуйтесь опцией TASM /ml для 
генерации глобальных имен с различением строчных и прописных букв. Например, 
введите команду tasm /ml timer для ассемблирования TIMER.ASM в TIMER.OBJ, 
скомпилируйте TESTTIME.CPP командой Ьсс -с testtime и скомпонуйте модули, 
введя bcc testtime.obj timer.obj. 
    Хотя это и не отражено в приведенных примерах, ассемблерные модули часто 
нуждаются в экспорте данных так же, как и кода. С помощью директивы DATASEG 
начните сегмент данных и задайте имя для глобальной переменной: 

		 IDEAL 
		MODEL small 
		DATASEG     ;Начать сегмент данных 
		PUBLIC _MyVar 
   _MyVar 		DW	0    ;Переменная размером в слово 
		CODESEG
    ;  В этом месте в модуле располагаются функции 
    END 

    В главном модуле C++ объявите переменную MyVar (без лидирующего символа 
подчеркивания) с помощью директивы extern: 

     extern int MyVar; 

    Теперь вы можете пользоваться переменной MyVar так же, как при ее 
определении в модуле C++. Нет неходимости в использовании опции "С" в 
директиве extern, как это делается с функциями, поскольку имена переменных 
C++ не модифицирует. 
    Для доступа к переменным C++ из внешних ассемблерных модулей следует 
воспользоваться директивой ЕXTRN. Например, если объявлена глобальная 
переменная 

    int Global; 

d ассблерном модуле можно сослаться на переменную Global как на 
внешнюю WORD _Global следующим образом:

    IDEAL 
    MODEL small 
    DATASEG 
    EXTRN _Global:WORD     ; Внешняя переменная размером в слово 
    ; Вставьте в этом месте другие объявления данных 
    CODESEG 
    ;Вставьте в этом месте функции модуля 
    END 

    В листинге 17.8 демонстрируется функция Timer() в альтернативном стиле 
MASM. Ассемблируйте этот модуль с помощью команды tasm /ml timer.rasm, затем 
скомпилируйте и скомпонуйте приложение так же, как и раньше, с помощью команды bcc 
testtime timer.obj. 

    Листинг  17.8.  TIMER.MSM (демонстрация функции Timer() в синтаксисе MASM) 

    1:   .MODEL small 
    3.   .CODE 
    4:    PUBLIC	_Timer 
    5: 
    6:    _Timer	PROC 
    7:   xor ax, ax 
    8:   mov es, ax 
    9:   mov di, 0046CH 
    10: mov ax, WORD PTR es[di] 
    11: mov dx, WORD PTR es[di+2] 
    12. ret 
    13: _Timer	ENDP 
    14: 
    15:  END


    Использование прерываний 

    Прерывания останавливают выполнение программы, затем какое-то время выполняются 
некоторые действия, вновь возобновляется выполнение программы с того же места, 
на котором она была прервана. Компютерные стройства могут вырабатывать сигнал 
прерывания, чтобы уведомить компьютер о таких внешних событиях, как нажатие на 
клавишу или щелчок мыши. Процедура обработки прерывания реагирует на сигнал 
прерывания и возвращает управление прерванной программе, выполняя инструкцию 
iret (interrupt return — возврат из 


    Глава 17 Оптимизация с помощью ассемблера     451 

прерывания). Вы можете писать свои обработчики прерываний на С и C++, 
однако большинство программистов пользуются в таких случаях ассемблером для 
достижения наибольшей производителюостн. 
    Вы также можете вызывать обработчики прерываний с помощью инструкции 
ассемблера int. Например, для использования одной из функций DOS в программе 
вызывается диспетчер DOS: 

    asm int 0x21 // Вызвать диспетчер функций DOS 

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

    Обращение к функциям DOS 

    С помощью встроенного ассемблера ВASM в операторе asm можно легко вызвать 
функцию DOS. Используйте прием, продемонстрированный в листинге 17.9, в 
котором отображается строка "Welcome to BASM!$" двумя способами: с помощью 
функции С и процедуры DOS, выводящей строку. 

    Листинг 17.9.  BASMWELC.CPP (вызов DOS для вывода строки) 

    1:	#include <stdio.h> 
    2: 
    3:	// Строки, завершающиеся символом $ (ASCII$) и нулей (ASCIIZ) 
    4:	char messaged = "Welcome to BASM!$"; 
    5: 
    6:	main() 
    7:	{ 
    8:	  puts(message); // Отобразить строку ASCIZ 
    9:	  asm { 
    10:	     mov ah, 9 // Задать функцию DOS с номером 9 
    11:	     mov dx, OFFSET message // Адресовать строку ASCIIS 
    12:	    int 0x21 // Вызвать DOS через прерывание 0x21 
    13:	  } 
    14:	  return 0; 
    15:	} 

    Все функции DOS имеют идентификационные номера, подобные номеру 9, 
соответствующему функции вывода строки. В этой программе внутри оператора asm 
в строке 10 номер функции DOS записывается в регистр ah, регистру dx 
присваивается смещение первого байта строки, и вызывается диспетчер функций 
DOS с помощью инструкции int 0x21 в строке 12. 

    ЗАМЕЧАНИЕ 

   Для того чтобы узнать что делают функции DOS с другими номерами и что необходимо 
для их вызова,  загляните в справочное пособие по программироваанию в DOS.

    В соответствии с необычными требованиями функции DOS с номером 9 строка 
должна завершаться символом денежной единицы ($), этот пережиток унаследован 
от предка DOS — операционной системы СР/М. Когда вы запустите программу, то 
увидите, что при выполнении строки 8 отобразится вся строка, включая символ $. 
Вы также увидите, как оператор asm тоже выведет строку, но без завершающего 
знака доллара. 

    Вызов функций BIOS 

    Еще один набор процедур обеспечивает ввод-вывод для отдельных компонентов 
компьютера. Набор обработчиков прерываний и другие процедуры хранятся в ПЗУ и 
известны под названием BIOS (Basic Input-Output System — базовая система 
ввода-вывода). Чаще всего эти процедуры вызываются с помощью программных 
инструкций прерывания. 

    ПРЕДУПРЕЖДЕНИЕ 

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


    452     Часть IV. Разработка приложений 

    В листинге 17.10 демонстрируется вызов прерывания BIOS, в данном случае 
для изменения формы курсора. Скомпилируйте и запустите программу для изменения 
вашего курсора из обычного тонкого в большой, который легче рассмотреть на 
заполненном информацией экране. (Для восстановления первоначальной формы 
курсора следует перезагрузиться.) 

    Листинг 17.10.  BI6C.CPP (установка большого курсора) 

    1:	main() 
    2:	{ 
    3:	asm	{ 
    4:	mov	ch,	0	//	Задать	верхнюю		строку	курсора 
    5:	mov	cl,	7	//	Задать	число	строк курсора 
    6:	mov	ah,	1	//	Задать	видеопроцедуру			BIOS	»1 
    7:  	int	0x10		//	Вызвать	BIOS	для	изменения		формы	курсора 
    8.	} 
    9:	return 0; 
    10:	} 

    Единственный оператор asm в строках 3-8 выполняет все действия в этой 
программе: вызывает видеопроцедуру BIOS для изменения формы текстового 
курсора. Сначала в строках 4, 5 инициализируются регистры ch и ci значениями, 
соответствующими числу и начальной позиции горизонтальных линий, образующих 
видимый курсор. (Если вы присмотритесь к текстовому курсору, то сможете 
рассмотреть эти линии на некоторых типах дисплеев.) Поэкспериментируйте с 
этими двумя значениями для создания множества форм курсора. 
    В строке 6 в регистре ah запоминается идентификационный номер 
видеопроцедуры BIOS, отвечающей за вменение формы курсора. После всех этих 
подготовительных действий в строке 7 вызывается обработчик прерывания BIOS с 
помощью выполнения int 0x10 аналогично тому, как в предыдущей программе 
вызывался лкпетчер функций DOS. Сразу после запуска программы должен появиться 
большой курсор. К сожалению, курсор может принять свой прежний вид, поскольку 
другие программы могут изменять, но не восстанавливать форму курсора. 

    Написание процедур обработки прерываний 

    Вдобавок к вызовам функций DOS и BIOS вы можете писать свои собственные 
процедуры обработки прерываний. Для упрощения процесса в Borland C++ 
используется ключевое слово interrupt, преобразующее функцию типа void в 
обработчик прерывания. В программах на С и C++ можно написать функцию — 
обработав прерывания следующим образом: 

    void interrupt AnyName(unsigned bp,  unsigned di,  unsigned si, unsioned ds,  
       unsigned es. unsigned dx, ^signed ex,  unsigned bx, unsigned ax, unsigned 
      ip,  unsigned cs, unsigned flags) 
   {
      // ....   операторы С или C++ 
   }

    Бeззнаковые параметры обеспечивают доступ к регистровым переменным, сохраненным в 
стеке. В случае аппаратных прерываний эти значения останутся нетронутыми, 
поскольку они отображают состояние прерванной  программы. В действительности для большей 
части обработчиков аппаратных прёрываний скорее всего, не придется объявлять ни одного 
параметра. Тем не менее, в программных прерываниях информация обычно передается в 
регистрах, и эту информацию можно получить с помощью вышеперечисленных параметров.
Вы можете включить только те параметры, которые вам необходимы, но вы должны их включать
именно в указанном порядке. Так, если вашей программе необходим регистр ах, в объявлении 
следует указать все параметры с bр по ах. 
    Компилятор добавпяет немного своего собственного кода к interrapt-функциям. В 
скомпилированном виде такие функции начинаются с преамбулы: 

push ах          ; Сохранить регистры 
push bx
push cx
push dx
push es
push ds
push si
push di
push bp
mov bp, DGROUP    ;   Задать bр равным адресу сегмента данных 
mov ds, bp                 ;   Инициализировать DS сегментом данных программы 
mov bp, sp                 ;   Адресовать локальные данные через Ьр 


    Глава 17. Оптимизация с помощью ассемблера	453 

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

    pop	bp                          ;  Восстановить сохраненные регистры 
    pop	di 
    pop	si 
    pop	ds 
    pop	es 
    pop	dx 
    pop	сx 
    pop	bx 
    pop	ax 
    iret	; Вернуться к выполнению прерванной программы 

    После восстановления регистровых значений, сохраненных ранее в стеке, 
функция выполняет инструкцию iret, которая возвращает управление либо 
прерванной программе, либо, в случае программного прерывания, в место 
выполнения инструкции int. При этом также восстанавливаются регистры и флаги, 
запомненные в стеке во время возникновения сигнала или инструкции прерывания. 
    В листинге 17.11 демонстрируется написание и установка функций — 
обработчиков прерываний, написанных с помощью встраиваемых инструкций asm. Вы 
должны скомпилировать и запустить эту программу из подсказки DOS. Программа 
устанавливает свой обработчик вектора прерывания PC 0x1c, который обычно не 
выполняет никаких действий, но тем не менее регулярно вызывается функцией 
BIOS, которую, в свою очередь, вызывает внутренний системный таймер. 
Вызываемый с частотой примерно 17.2 раза в секунду, вектор прерывания 0x1c 
играет роль удобного "крючка" с подвешенными действиями, которые необходимо 
выполнять регулярно, пока программа продолжается, даже не подозревая о 
прерываниях. В MULTIP прерывание запрограммировано отображать время в правом 
верхнем углу текстового дисплея, пока вы вводите тестовую строку. Нажмите 
<Enter> для завершения программы и удаления обработчика прерывания из памяти. 

   ЗАМЕЧАНИЕ

   Если у вас монохромный адаптер дисплея, замените 0xb800 в строке 5 на 0xb000.


    Листинг 17.11.  MULTIP.CPP (демонстрация процедуры обработки прерывания) 

    1:   #include <stdio.h> 
    2:   #include <dos.h> 
    3:   #include <string.h> 
    4: 
    5:  #define DISPSEG 0xb800 // 0хb000 для монохромного дисплея 
    6:	
    7: // Глобальная переменная для первоначального вектора прерывания 0x1с 
    8: void interrupt (far *oldVector){...}; 
    9: 
  10: // Прототипы функций 
  11: void Initlnterrupt(void); 
  12: void DoWhateverYouWant(void); 
  13: void Deinitlnterrupt(void); 
  14: void interrupt ShowTime(...); 
  15: 
  16: main() 
  17: { 
  18:  setcbrk(0); // Предотвращение завершения программы с <Ctrl+C> 
  19:  Initlnterrup();
  20:  DoWhateverYouWant(); 
  21:  Deinitlnterrupt(); 
  22:  return 0; 
  23: } 
  24: 
  25: // Перенаправляет вектор прерывания на нашу процедуру обработки 
  26: void Initlnterrupt(void) 
  27: { 
  28:  oldVector = getvect(0x1c); // Save current vector 
  29:  setvect(0x1c, ShowTime); // Set vector to ShowTime() 
  30: } 
    

    454	Часть IV. Разработка приложений 

    31: 
    32:	// ShowTime() Запускается вместе с примером функции 
    33:	void DoWhateverYouWant(void) 
    34:	{ 
    35:	int i, done = 0; 
    36:	char s[128]; 
    37: 
    38:	while (!done) { 
    39:	puts("\n\nEnter a string (Enter to quit):"); 
    40:	gets(s); 
    41:	done = (strlen(s) == 0); 
    42:	if (!done) { 
    43:	   for (i = 0; i < 40; i++ ) 
    44:	     puts(s); 
    45:	} 
    46:	} 
    47:	} 
    48: 
    49:	// Восстанавливает первоначальный вектор перед завершением программы 
    50:	void Deinitlnterrupt(void) 
    51:	{ 
    52:	  setvect(0x1c, oldVector); 
    53:	} 
    54: 
    55:	// Обработчик прерывания 0х1с. Отображает время на дисплее. 
    56:	void interrupt ShowTime(...) 
    57:	{ 
    58:	asm { 
    59:	xor ax, ax                      // ax <- 0000 
    60:	mov ds, ax                   // ds <- 0000                 
    61:	mov ax, [0x046d]        // Взять значение таймера / 256 
    62:	mov bx, DISPSEG     // bx = адрес отображения 
    63:	mov ds, bx                  // ds = адрес отображения 
    64:	mov word ptr [0х009а], 0x0f07c // Отобразить '|'
    65:	mov bh, 0x70                                // Атрибут = обратный 
    66:	push ax                                         // Запомнить значение таймера 
    67:	xchg ah, al                                    // ah = значение таймера mod 256 
    68:	aam                                              // Коррекция ASCII-умножения 
    69:	or ax, 0x3030                              // Преобразовать в ascii 
    70:	mov bl, ah                                    // Переслать цифру в bl 
    71:	mov [0x0096], bx                        // Отобразить первую цифру часов 
    72:	mov bl, al                                     // Переслать цифру в Ы 
    73:	mov [0x0098], bx                        // Отобразить вторую цифру часов 
    74:	pop ax                                         // Восстановить значение таймера 
    75:	mov ex, 0x0f06                          // Вычислить ах /4.26 
    76:	mul ch                                         // ах <- ах « 15 
    77:	shr ax, cl                                     // ах <- ах / 64 
    78:	ааm                                            // Коррекция ASCII-умножения 
    79:	or ах, 0x3030                             // Преобразовать в ascii 
    80:	mov bl, ah                                  // Переслать цифру в bl
    81:	mov [0х009c], bx                      // Отобразить первую цифру минут 
    82:	mov bl, al                                   // Переслать цифру в bl 
    83:	mov [0х009c], bx                      // Отобразить вторую цифру минут 
    84:	} 
    35:	}

    Перед записью адреса пользовательской процедуры обработки прерывания в 
таблицу векторов прерывания PC следует всегда запоминать старый вектор для его 
восстановления впоследствии. Для этого можно объявить временную переменную, 
подобную следующей: 

    void  interrupt (far *oldVector)(...); 

    В этой строке объявляется переменная oldVector в качестве дальнего 
указателя на interrupt-функцию, имеющую тип void и переменное число аргументов, 
которые представляются в C++ многоточием в скобках, в С — пустой парой скобок. 
Для сохранения вектора прерывания следует присвоить переменной oldVector 
возвращаемое функцией getvect(), как это продемонстрировано в строке 28. Затем 
вызовите функцию setvect (строка 29) для перенаправления вектора прерывания на вашу 
процедуру. Еще один вызов функции setvect() (строка 52) восстанавливает старый вектор 
прерывания перед завершением программы. 
    В строках 33-47 запрашивается строка, после чего она отображается несколько 
раз в цикле for. Этот код нужен в программе для того, чтобы выполнились хоть какие-то 
действия, в то время как обработчик прерывания выыполняется в  фоновом 
режиме, — простая, но эффективная форма параллельного выполнения с помощью 
прерывания таймера. Запустите программу и подождите несколько минут перед вводом 
строки. Несмотря 
    

    Глава 17. Оптимизация с помощью ассемблера	455 

на то что управление находится у функции DoWhateverYouWantO, программный 
обработчик прерывания автоматически обновляет отображаемое время. 
    В функции — обработчике прерывания ShowTime() (строки 56-85) используется 
единственный встраиваемый оператор asm для расшифровки текущего времени, 
перевода двоично-десятичного (BCD) результата в ASCII и записи цифр 
непосредственно в видеобуфер дисплея. Комментарии в листинге поясняют 
назначение каждой инструкции. 
    Трудно представить себе функцию, более системно зависимую, чем ShowTime(), 
которая вряд ли будет работать корректно на всех компьютерах, кроме 
стопроцентно совместимых с IBM PC с обычным или монохромным графическим 
дисплеем. Эта функция определенно не станет работать с нестандартной 
аппаратной частью. 
    Но такова природа ассемблера. Программы, написанные на ассемблере, тесно 
привязаны к своей машине. Однако, несмотря на это ограничение, если 
пользоваться ассемблером разумно, можно оптимизировать программы так, чтобы 
они работали настолько быстро, насколько это возможно, занимая при этом 
минимальное количество пространства. 

    Резюме 

    С помощью ассемблера можно оптимизировать отдельные критические операторы 
так, чтобы они выполнялись максимально быстро, получить доступ к компьютерному 
оборудованию на уровне физических устройств и реализовать процедуры обработки 
прерываний для их более быстрого выполнения. Следует пользоваться опцией -S 
для компиляции программы в текстовый файл с ассемблерным кодом. Можно затем 
изучить этот файл для того, чтобы узнать, как компилятор транслирует 
высокоуровневые операторы в машинный язык. 

    Ассемблерный код используется в программах для повышения 
производительности критических участков кода путем уменьшения их процента в 
общем времени выполнения. Определите такие критические участки в вашей 
программе (возможно, с помощью Turbo Profiler) и оптимизируйте инструкции в 
них с помощью ассемблера. Оптимизация некритического кода в программе обычно 
дает (или вообще не дает) слабый выигрыш в скорости выполнения. 

    Встроенный ассемблер Borland (BASM) ассемблирует встраиваемые операторы 
asm в программах на С и C++. Встраиваемые операторы завершаются точкой с 
запятой или символом новой строки. Несколько встраиваемых операторов следует 
заключать в скобки. 

    В BASM отсутствуют такие сложные средства TASM, как макросы, инструкции, 
специфические для процессоров 80386 и 80486, идеальный режим и расширенный 
ассемблерный синтаксис ТASM. Тем не менее, BASM способен удовлетворить 
большинство запросов, предъявляемых ассемблеру. 

    Встраиваемые операторы BASM распознают комментарии С и C++. В отличие от 
автономных ассемблерных программ, комментарии BASM не начинаются с точки с запятой. 

    Для более изощренного использования ассемблера следует писать внешние 
модули, которые затем можно ассемблировать с помощью ТASM и компоновать 
вместе с скомпилированными программами на С и C++. Этот метод позволяет 
пользоваться преимуществами таких расширенных средств TASM, как идеальный 
режим и макросы. Вы также получаете полное управление над низкоуровневыми 
инструкциями в ассемблируемых функциях. 

    Используйте утилиту H2ASH для упрощения преобразования высокоуровневых 
объявлений, директив #define, структур и прочего в эквивалентные ассемблерные 
объявления. Однако эта утилита — полезное, но несовершенное средство. 

    Прерывания временно приостанавливают выполнение программы для выполнения 
иных задач. Каждое прерывание имеет свой идентификационный номер, который 
также служит индексом в таблице векторов (адресов) обработчиков прерываний в памяти. 

    Обработчики программных прерываний аналогичны процедурам. В программе 
обработчики программных прерываний вызываются с помощью инструкции ассемблера 
int. Например, для вызова диспетчера функций DOS используется инструкция int 
0x21. Вы можете также вызывать процедуры BIOS с помощью программных 
прерываний, но при этом следует быть предельно осторожным; в BIOS существует 
много низкоуровневых процедур, которые при неверном использовании могут 
привести к потере данных. Вы можете запрограммировать свои собственные 
процедуры обработки прерываний на С и C++, однако большинство программистов 
предпочитают пользоваться ассемблером для достижения максимальной 
производительности . 
    

    456 49	Часть IV. Разработка приложений 
