|
|
|
Документация на C--.
|
|
|
|
Содержание.
1 Введение.
1.1 История создания и развития.
1.2 Что такое C--?
1.3 Как установить C--.
2. Управление компиляцией.
2.1 Параметры командной строки компилятора C--.
2.1.1 /ON - Оптимизация числовых выражений.
2.1.2 /DE - Временное расширение разрядности переменной.
2.1.3 /ARGC - Альтернативный обработчик командной строки.
2.1.4 /OST - слияние одинаковых строковых констант.
2.1.5 /D - установка идентификатора в TRUE из командной строки.
2.1.6 /IA - упрощенный ввод ассемблерных инструкций.
2.1.7 /CRI - пропуск повторно включаемого файла.
2.1.8 /IND - импорт имен процедур из DLL.
2.1.9 /WS - задать имя stub файла для программ под windows.
2.1.10 /WBSS - разместить не инициализированные данные в отдельной секции.
2.1.11 /DBG - создание отладочной информации.
2.1.12 /J0 /J1 /J2.
2.1.13 /LST - Создание ассемблерного листинга.
2.1.14 /ENV - Сохранение адреса переменных окружения.
2.1.15 /CPA - Очистка post-области данных.
2.1.16 /W - вывод предупреждений.
2.1.17 /NW - Выборочное отключение типов предупреждений.
2.1.18 /WSI - короткая таблица импорта.
2.2 Директивы транслятора.
2.2.1 ?ifdef/?ifndef
2.2.2 ?initallvar
2.2.3 ?usestartup
2.2.4 ?startusevar
2.2.5 ?atexit
2.2.6 ?startuptomain
2.2.7 ?undef
2.2.8 ?align и ?aligncode
2.2.9 ?pragma
3. Константы.
3.1 Числовые константы.
3.2 Символьные константы.
3.3 Строковые константы.
3.4 Постоянные выражения.
4. Выражения.
4.1 Типы выражений.
4.2 Выражения типа EAX/AX/AL.
4.3 Выражения использующие получатель при вычислении выражения.
4.4 Не - EAX/AX/AL выражения.
4.5 Условные выражения.
4.5.1 Простые условные выражения.
4.5.2 Сложные условные выражения.
4.6 Изменение типа выражения при присваивании.
4.7 Вычисление в регистры EAX/AX/AL со знаком.
5. Идентификаторы.
5.1 Формат идентификатора.
5.2 Зарезервированные идентификаторы.
5.3 Универсальные регистры для 16 и 32-битного режима.
5.4 Предопределенные идентификаторы.
6. Переменные.
6.1 Типы переменных.
6.2 Объявление переменных.
6.3 Глобальные переменные.
6.4 Локальные переменные.
6.5 Динамические переменные и структуры.
6.6 Присваивание одного значения нескольким переменным.
6.7 Переменные типа float.
6.7.1 Формат переменных типа float.
6.7.2 Константы с плавающей точкой.
6.7.3 Диапазон допустимых значений.
6.7.4 Математические операции.
6.7.5 Преобразования типов.
6.7.6 Операции сравнения.
6.7.7 Сравнение переменных типа float с 32-битным регистром.
6.8 Указатели.
7. Адресация.
7.1 Относительная адресация.
7.2 Абсолютная адресация.
8. Работа с блоками данных.
8.1 Структуры.
8.1.1 Что такое структуры.
8.1.2 Синтаксис.
8.1.3 Инициализация структур при объявлении.
8.1.4 Инициализация структуры при выполнении программы.
8.1.5 Операции с элементами структур.
8.1.6 Вложенные структуры.
8.1.7 Отображение тега структуры на блок памяти.
8.1.8 Битовые поля структур.
8.2 Объединения.
8.3 Команды FROM и EXTRACT.
9. Операторы.
9.1 Условные инструкции.
9.2 Циклы do{} while.
9.3 Циклы loop, LOOPNZ, loopnz.
9.4 Цикл while, WHILE.
9.5 Цикл for, FOR.
9.6 Оператор переключатель switch.
9.7 Оператор перехода goto, GOTO.
9.8 Оператор разрыва break, BREAK.
9.9 Оператор продолжения continue, CONTINUE.
9.10 Логическое объединение условий.
9.11 Переход через циклы.
9.12 Инвертирование флага проверки условий.
9.13 Вычисление выражения, а затем проверка условия.
9.14 Проверка битов при операции сравнения.
9.15 Оператор перестановки.
9.16 Оператор отрицания.
9.17 Оператор инверсии.
9.18 Специальные условные выражения.
9.19 Символ $ - вставляет текущий адрес программы.
9.20 Ключевое слово static и оператор ::.
9.21 Оператор sizeof.
9.22 Метки перехода.
10. Ассемблер.
10.1 Поддержка команд ассемблера.
10.2 Ключевое слово asm.
10.3 Префикс dup - повторение инструкций DB/DW/DD.
10.4 Инструкции процессора Pentium III.
11. Процедуры.
11.1 Типы процедур, функций и макрокоманд.
11.2 Стековые процедуры.
11.3 Регистровые процедуры.
11.4 Динамические процедуры.
11.4.1 Установка динамической процедуры в определенное место программы.
11.5 inline-процедуры.
11.5.1 Другое применение inline.
11.6 Процедуры обработки прерываний.
11.7 Замена return на goto.
11.8 Возвращаемые значения.
11.9 Объявление параметров в регистровых процедурах.
11.10 Объявление параметров в стековых процедурах.
11.11 Использование макрокоманд.
11.12 Передача параметров в стековые процедуры через регистры.
11.13 Вызов процедур с адресом в регистре.
11.14 Встоенные в компилятор процедуры.
11.14.1 Процедуры ABORT, ATEXIT и EXIT.
11.14.2 Процедуры inp/inportb, inport, inportd, outp/outportb, outport и
outportd.
11.14.3 Процедуры для работы с вещественными числами.
11.15 Классы.
11.15.1 Объявление процедур в структурах.
11.15.2 Наследование.
11.15.3 Наследование процедур.
12. Типы выходных файлов.
12.1 Выходные файлы типа COM.
12.2 Выходные файлы типа EXE.
12.3 Выходной файл *.EXE с моделью памяти tiny.
12.4 Объектный выходной файл OBJ.
12.5 COM файл symbiosis.
12.5.1 СИМБИОЗ - что это такое?
12.5.2 Как это делать.
12.5.3 Использование.
12.5.4 Злоупотребления.
12.6 SYS - драйверы устройств.
12.7 Компиляция кода расширителей ROM-BIOS.
12.8 32-битные файлы.
12.8.1 32-битный код под DOS.
12.8.2 32-битный код под Windows.
12.8.3 Вызов API процедур по ординалам.
12.8.4 Создание DLL под Windows.
12.8.5 Инициализация DLL при загрузке.
12.8.6 Компиляция ресурсов.
12.9 Выходные файлы для MeOS.
13. Приложения.
13.1 Поиск включаемых файлов.
13.2 Регистры, которые должны быть сохранены.
13.3 C--.ini файл.
13.4 startup.h-- файл.
13.5 mainlib.ldp файл.
13.6 C-- символы.
1. Вступление.
1.1 История создания и развития.
Автором языка SPHINX C-- является Peter Cellik (CANADA). Последняя
авторская версия SPHINX C-- v0.203 от 28.Oct.96. К сожалению автор
отказался от дальнейшего развития языка. С 1998 года, уже почти умерший
проект, подхватил Михаил Шекер (Россия). Изначально компилятор был freeware
(и даже greenware, как его называл Peter Cellik). Таким статус компилятора
остался и поныне.
Первоначально компилятор мог создавать только *.com файлы и был
рассчитан на создание небольших demo-программ и резидентов (TSR). В
дальнейшем возможности компилятора расширялись, так как этого требовало
наше бурное время.
При развитии компилятора, было стремление придерживаться следующих
принципов:
1. Максимально возможная совместимость синтаксиса с последней версией
компилятора написанного Peter Cellik. Это давало возможность с минимальными
затратами (а чаще всего без всяких затрат) адаптировать программы,
написанные для 0.203 версии компилятора, к последней на этот момент версии
компилятора.
2. Сблизить синтаксис компилятора со стандартным языком C. Это могло
значительно облегчить перенос программ написанных на C.
3. Также прилагались усилия, для того, чтобы человек знающий только
ассемблер мог бы с минимальными затратами освоить C--.
Вот эти, зачастую противоречащие друг другу принципы, влияли на выбор
реализации возможностей компилятора. Насколько это удалось - судить Вам.
Если у Вас есть предложения и идеи по улучшению компилятора - пишите.
Мой e-mail sheker@mail.ru . Я с удовольствием выслушаю Ваши предложения, но
не гарантирую, что все они будут реализованы. Если реализовывать все
поступающие предложения, то компилятор превратится в свалку. Но если Ваше
предложение будет ценным (на мой взгляд, так что Вам придется свое
предложение хорошо аргументировать) и его будет возможным реализовать, оно
без сомнения найдет место в компиляторе.
Return to contents.
1.2 Что такое C--?
C-- был разработан, для того чтобы строить маленькие и быстрые
программы. Это наиболее подходит для создания резидентных программ (TSR),
программ, требующих обработку прерываний или программ у которых ограничены
ресурсы.
C-- занимает промежуточное положение между си и ассемблером. В связи с
этим промежуточным положением, Вам, для того чтобы писать программы на C--,
необходимо знать и ассемблер и си. Если Вам надоело возиться с огромными
ассемблерными листингами, а излишняя строгость языка C Вас угнетает, то этот
язык для ВАС.
Сейчас компилятор C-- может создавать 32-битные программы под Windows
(EXE-файлы формата PE) и 32-битные программы под DOS (LE-формат). Имеет
встроенный компилятор ресурсов и дизассемблер для генерации листинга
откомпилированного файла. Поддерживает ассемблерные инструкции процессора
Pentium III и ассемблерные инструкции FPU. Компилятор может генерировать
отладочную информацию совместимую с отладчиками фирмы Borland. Компилятор
может создавать объектные файлы (obj), но только для DOS программ.
C-- разработан только для использования на компьютерах с процессорами
совместимыми с семейством 80x86. Компилятор может работать только с
операционными системами DOS и семейством Windows.
Return to contents.
1.3 Как установить C--.
Компилятору C-- для работы нужны совсем незначительные ресурсы:
процессор 386 или лучше, чуть более 1 Мб дискового пространства и 4Мб
оперативной памяти. Компилятор может быть установлен на компьютеры с
операционной системой Windows 95 или лучше. Компилятор также может работать
в среде чистого DOS. В основном пакете компилятора находится 32-битная DOS
версия компилятора. На сайте http://sheker.chat.ru или
http://c--sphinx.narod.ru можно найти и консольную версию компилятора.
Консольная версия компилятора может работать только в среде Windows, но
она, в отличие от DOS версии, может работать с длинными именами исходных
файлов.
Установить компилятор C-- на Ваш компьютер очень просто. Предположим,
что Вы решили установить C-- на диск C. Создайте на диске C директорию
(папку) с именем C-- или с другим, удобным и понятным для Вас именем
(например, ДОСовской командой: MD C-- или другим доступным Вам способом).
Затем с сайта http://sheker.chat.ru или http://c--sphinx.narod.ru скачайте
файлы full_c--.zip и ful_c--2.zip и разархивируйте их в этой директории.
Затем в файле autoexec.bat можно прописать путь к директории с
компилятором. И все. Компилятор готов к работе. Если Вы добавляли путь к
компилятору в файл autoexec.bat, то Вам придется перегрузить операционную
систему.
Переменная окружения для компилятора C-- задается либо из командной
строки либо из командного файла (лучше всего ее прописать в autoexec.bat).
Эта переменная должна указывать компилятору, где находятся его библиотечные
файлы. Пример:
set C--=c:\c--\lib
Большой необходимости в переменной окружения для сегодняшней версии
компилятора нет. Существует несколько других способов, указать компилятору
место расположения библиотек. Поэтому определять или не определять
переменную окружения дело вашего вкуса и привычек.
Return to contents.
2. Управление компиляцией.
2.1 Параметры командной строки компилятора C--.
Формат командной строки вызова компилятора C--:
C-- [Параметры] [ИМЯ INI ФАЙЛА] [ИМЯ ИСХОДНОГО ФАЙЛА]
Имя исходного файла можно задавать без расширения. Компилятор ищет
файл с расширением c--, cmm, c.
Параметры выделяются предшествующим символом / или -.
Инвертировать функцию опции можно завершающим символом -.
Список поддерживаемых параметров:
/0 использовать только команды 8086/8088 процессора (установлено
по умолчанию при компиляции 16-битных программ).
/1 использовать команды 80186 процессора.
/2 использовать команды и оптимизацию для 80286 процессора.
/3 использовать команды и оптимизацию для 80386 процессора.
(установлено по умолчанию для 32-битных программ).
/4 использовать команды и оптимизацию для 80486 процессора.
/5 использовать команды и оптимизацию для Pentium процессора.
/6 использовать команды и оптимизацию для Pentium MMX процессора.
/7 использовать команды и оптимизацию для Pentium Pro процессора.
/8 использовать команды и оптимизацию для Pentium II процессора.
/9 использовать команды и оптимизацию для Pentium III процессора
(пока не реализовано из-за отсутствии информации).
/A выравнивание данных на четный адрес
по умолчанию разрешено, поддерживает инверсию
/AC выравнивание адреса начала циклов
по умолчанию отключено, поддерживает инверсию
имеет смысл только на процессорах Pentium+
/AL=## установить значение байта заполнения при выравнивании данных
по умолчанию 0.
/AP выравнивание адреса начала процедур.
по умолчанию отключено, поддерживает инверсию
имеет смысл только на процессорах Pentium и лучше
/ARGC вставить блок разбора командной строки
по умолчанию отключено, поддерживает инверсию
/AS выравнивание в структурах.
по умолчанию отключено, поддерживает инверсию
/AT вставить блок поддержки ATEXIT процедуры
по умолчанию отключено, поддерживает инверсию
/C вставить блок игнорирования CTRL-C
по умолчанию отключен, поддерживает инверсию
имеет смысл только под DOS программы
/CRI проверять включаемые файлы на повторную загрузку
по умолчанию включено, поддерживает инверсию
/CPA очистка post-области данных
/D32 создать EXE файл (32 битный код под DOS)
по умолчанию COM
/D=idname определить идентификатор для условной компиляции
по умолчанию нет
/DBG генерировать отладочную информацию
по умолчанию нет
/DE временное расширение разрядности после умножения
по умолчанию отключено, поддерживает инверсию
/DLL создать DLL для Windows32
по умолчанию COM
/ENV сохранение адреса переменных окружения
/EXE создать EXE файл для DOS (модель SMALL)
по умолчанию COM
/HELP /H /? справка, эта информация
/IA имена ассемблерных инструкций являются идентификаторами
по умолчанию отключено, поддерживает инверсию
/IND=name импорт имен из файла name.
/IP=path задать путь поиска включаемых файлов
по умолчанию нет
/IV инициализировать все переменные
по умолчанию отключено, поддерживает инверсию
/J0 не делать начальный jump на main()
по умолчанию отключено, поддерживает инверсию
В COM-файлах не создает jmp на main. В остальных не создается
блок начальной инициализации программы, а управление
передается сразу на main.
/J1 делать короткий jump на main()
по умолчанию нет
имеет смысл только в COM-файлах
/J2 делать jump на main()
по умолчанию да, поддерживает инверсию
имеет смысл только в COM-файлах
/LAI список поддерживаемых ассемблерных инструкций
/LRS загружать числовые константы через стек.
по умолчанию да, поддерживает инверсию
/LST создать ассемблерный листинг
/ME показать мой адрес и имя
/MEOS создать исполняемый файл для MeOS
по умолчанию COM
/MER=## установить максимальное число ошибок
по умолчанию 16
/MIF=file определить имя главного компилируемого файла
/NS запретить подключать stub файлов
по умолчанию нет, поддерживает инверсию
/NW=## выборочное отключение предупреждений
/OBJ создать OBJ файл
только 16 битный код.
по умолчанию COM
/OC оптимизировать по размеру кода
по умолчанию нет, поддерживает инверсию
/ON оптимизация чисел
по умолчанию нет, поддерживает инверсию
/OS оптимизация по скорости выполнения
по умолчанию да, поддерживает инверсию
/OST оптимизация строковых идентификаторов
по умолчанию отключено, поддерживает инверсию
/P вставить блок разборки командной строки
по умолчанию нет, поддерживает инверсию
/R вставить блок уменьшающий размер доступной памяти.
по умолчанию да, поддерживает инверсию
имеет смысл только в DOS-файлах
/S=##### установить размер стека
по умолчанию 2048
/SA=#### начальное смещение адреса запуска программы
имеет смысл только в COM-файлах, по умолчанию 0x100
/SOBJ создать ведомый OBJ файл
по умолчанию COM
/STM перенести блок startup кода в процедуру main
по умолчанию нет, поддерживает инверсию
имеет смысл только в COM-файлах
/SUV=#### начальный адрес не инициализированных переменных, при
использовании ими startup кода.
имеет смысл только в COM-файлах, по умолчанию равен /SA
/SYM надстройка для COM файла
по умолчанию COM
/SYS создать драйвер устройств (SYS)
по умолчанию COM
/TEXE создать EXE файл для DOS (модель TINY)
по умолчанию COM
/UL использовать lea при оптимизации сложения регистров.
по умолчанию да, поддерживает инверсию
/UST использовать startup код для переменных.
имеет смысл только в COM-файлах
по умолчанию нет, поддерживает инверсию
/W разрешить предупреждения
по умолчанию нет, поддерживает инверсию
/W32 создать EXE файл для Windows32 GUI
по умолчанию COM
/W32C создать EXE файл для Windows32 console
по умолчанию COM
/WBSS помещать не инициализированные данные в отдельную секцию.
по умолчанию для /w32 разрешено, для остальных запрещено.
поддерживает инверсию
/WF=file перенаправить вывод предупреждений в файл.
по умолчанию нет
/WFA использовать быстрые вызовы API процедур
по умолчанию да, поддерживает инверсию
только под windows
/WFU создавать таблицу перемещений (для Windows32)
по умолчанию нет, поддерживает инверсию
только под windows
для DLL устанавливается в да
/WIB=##### установить адрес image base
по умолчанию 0x400000
/WMB создавать Windows-файл с единым блоком
по умолчанию да, поддерживает инверсию
только под windows
для DLL устанавливается в нет
/WORDS выдать список зарезервированных идентификаторов
/WS=name указывает имя файла используемого в качестве stub под windows.
/X запретить вставлять в код SPHINXC-- сигнатуру
по умолчанию разрешено, поддерживает инверсию
отключается если есть J0
Примечание: выражение поддерживает инверсию означает, что для данной
опции можно использовать и противоположное значение с помощью символа -
после опции. Пример:
/WFA-
Параметры командной строки можно писать как большими, так и
маленькими буквами.
Return to contents.
2.1.1 /ON - Оптимизация числовых выражений.
При включении в командную строку опции /ON или в файл C--.INI строчки
ON, компилятор будет анализировать операции над числами и где это
можно, сокращать число операций. Например:
Строка до оптимизации | После оптимизации
-----------------------------------------------
AX = var + 7 - 3; | AX = var + 4;
AX = var * 2 * 5; | AX = var * 10;
AX = var * 2 / 4; | AX = var / 2;
AX = var * 10 / 2; | AX = var * 5;
AX = var / 2 / 3; | AX = var / 6;
AX = var / 4 * 8; | AX = var * 2;
AX = var / 16 * 16; | AX = var;
Возможные отрицательные последствия:
Применение этой оптимизации может иметь и негативные последствия.
Например, если Вам нужно выровнять значение переменной на границу
параграфа, Вы напишите строку:
var = var / 16 * 16;
но после оптимизации будет
var = var;
т.е. выравнивание не будет произведено. Этого можно избежать, если
разбить это выражение на два:
var = var / 16;
var = var * 16;
тогда оптимизация не будет произведена. Но для получения более
компактного кода лучше будет записать так:
AX = var;
AX = AX / 16;
AX = AX * 16;
var = AX;
Return to contents.
2.1.2 /DE - Временное расширение разрядности переменной.
Как известно, после умножения может произойти переполнение, т.е
разрядность результата может превысить разрядность исходных операндов и
произойдет искажение результата. Частично решить эту проблему Вам поможет
опция командной строки /DE или строка DE в файле C--.INI. После команды
умножения компилятор будет просматривать остаток строки и если обнаружит,
что расширение разрядности может быть востребовано (востребовать
расширенную разрядность могут операции деления и вычисления остатка), то
будут приняты меры по ее сохранению. Например:
a = b*c+d/e; //здесь будет включена поддержка расширения разрядности
a = b*c+d*e; //здесь поддержки расширения разрядности не будет.
Однако применение этой опции может иметь и негативные последствия.
Покажу это на примере:
пусть имеется выражение
a = b * c / d;
если значения переменных b = 0xC000, c = 0x1000, d=0x10, после запуска
такая программа зависнет с сообщением о том, что произошло переполнение
при делении.
Return to contents.
2.1.3 /ARGC - Альтернативный обработчик командной строки.
Отличие этого обработчика командной строки от parsecommandline
заключается в том, что при вызове PARAMSTR(0); Вы получите адрес строки в
которой указан путь и имя запущенной программы. Следующие вызовы этой
процедуры с увеличивающимся параметром будут возвращать адреса слов
командной строки. А вызов процедуры PARAMCOUNT вернет Вам число слов в
командной строке плюс один.
Альтернативный обработчик командной строки включается директивой
?argc TRUE или из командной строки компилятора ключом /argc или
строчкой argc в файле C--.INI.
Return to contents.
2.1.4 /OST - слияние одинаковых строковых констант.
Если этот режим оптимизации будет активизирован, то компилятор будет
запоминать все строковые константы и при обнаружении одинаковых в код
файла не будет вставлена повторная строковая константа, а будет сделана
ссылка на первую, обнаруженную ранее строковую константу. В оптимизации
участвуют только неименованные строковые константы. Т.е. если массив или
структура будет инициализированы строкой, то такая строка не будет
участвовать в процессе инициализации, так эта строка может быть изменена
в процессе работы программы. Пример:
char var="test"; //эта строка не будет участвовать в процессе
//оптимизации.
void proc(){
WRITESTR("test"); // эта строка будет участвовать в оптимизации.
AX="test"; // переменной AX будет присвоен адрес строки,
// которая была вставлена в код программы в
// предыдущей строке.
}
Обо всех случаях обнаружения повторной строки компилятор будет
выдавать предупреждения.
Включается этот режим оптимизации либо с командной строки /ost, либо
директивой #pragma option ost, либо строкой в файле c--.ini - ost.
Отключить, включенный ранее, этот режим можно директивой #pragma option ost-.
Return to contents.
2.1.5 /D - установка идентификатора в TRUE из командной строки.
Если Вы написали программу, которая может компилироваться по разному,
в зависимости от состояния некоторых идентификаторов (используется режим
условной компиляции), то Вам очень может пригодится эта опция.
Устанавливая с командной строки различные идентификаторы, Вы можете
получать различные варианты программы, не редактируя исходный текст
программы.
Идентификатор вводится с командной строки ключом /d=idname.
Return to contents.
2.1.6 /IA - упрощенный ввод ассемблерных инструкций.
Стало возможным использовать ассемблерные инструкции без префикса $
и вне блока asm. Этот режим включается: с командной строки опцией /ia;
в файле конфигурации строкой ia или директивой #pragma option ia.
Когда этот режим включен, все имена ассемблерных инструкций становятся
зарезервированными словами, т.е. Вы не сможете эти имена использовать в
качестве имен переменных или процедур. Ассемблерные инструкции компилятор
распознает независимо от того, написаны они маленькими или большими
буквами.
Return to contents.
2.1.7 /CRI - пропуск повторно включаемого файла.
Чаще всего, повторно включать файл в компилируемый проект, нет
необходимости, но это иногда происходит из-за того, что некоторые
включаемые файлы сами включают другие файлы. Чтобы этого не происходило
приходится делать проверку на повторную загрузку файла. Теперь эту
функцию берет на себя компилятор и у Вас отпадает необходимость делать
эту проверку.
Но иногда (очень редко) возникает потребность сделать повторное
включение файла. Для этого в компиляторе есть опция командной строки
/cri-, которая запрещает компилятору делать проверку на повторное
включение. Соответственно, для c--.ini файла, это можно сделать строкой
cri- или директивой в компилируемом файле - #pragma option cri-.
Return to contents.
2.1.8 /IND - импорт имен процедур из DLL.
Если Вы хотите в своей программе использовать DLL, для которой нет
заголовочного файла с описанием процедур, то компилятор может
импортировать имена из этой DLL. Для этого Вам надо указать имя этой
библиотеки либо через опцию командной строки /ind=name.dll, либо в
файле INI строкой 'ind=name.dll', либо через директиву '#pragma option
ind=name.dll'.
К недостатком такого способа получения имен можно отнести то, что при
компиляции программы библиотека, из которой импортируются имена,
обязательно должна присутствовать в компьютере. Также, если имена в
библиотеке написаны без суффикса '@number', компилятор не будет
контролировать число параметров передаваемых процедуре. И, к сожалению,
компилятор умеет импортировать имена из библиотек имеющих только формат
PE-файла.
Return to contents.
2.1.9 /WS - задать имя stub файла для программ под windows.
Как известно, в программах под windows есть DOS заглушка, называемая
stub, которой передается управление при запуске такой программы в чистом
DOS-е. Обычно такая заглушка выводит на экран сообщение о том, что эту
программу надо запускать в среде windows.
Вы можете вместо стандартного stub использовать свой. Для этого Вам
необходимо указать имя 16-битного EXE-файла либо через опцию командной
строки /ws=filename, либо строкой в INI-файле ws=filename, либо
директивой #pragma option ws=filename.
Таким образом, у Вас появилась возможность создавать программы,
работающие и под DOS и под windows.
Return to contents.
2.1.10 /WBSS - разместить не инициализированные данные в отдельной секции.
Секция .bss создается автоматически при компиляции программ с ключом
/w32. Если Вы хотите иметь эту секцию и при компиляции программ с
ключами /w32c или /dll Вам необходимо добавить либо в командной
строке опцию /wbss, либо строку wbss в INI-файле, либо директиву
#pragma option wbss.
Использование секции .bss практически не влияет на размер получаемого
файла. Теоретически, для процессоров, у которых есть отдельный кэш для
данных, использование секции .bss, должно повышать скорость работы
программы.
Return to contents.
2.1.11 /DBG - создание отладочной информации.
Если при компиляции программы в командную строку добавить ключ /dbg,
или в файл конфигурации c--.ini добавить строку dbg, то компилятор после
окончания компиляции создаст файл с отладочной информацией. Этот файл
имеет имя главного модуля и имеет расширение *.tds.
Отладочная информация создаваемая компилятором C-- совместима с
отладочной информацией создаваемой компиляторами фирмы Borland. Но, пока,
эта информация реализована еще не в полном объеме. Создаваемой сейчас
отладочной информации достаточно для проведения простейшей отладки
программы.
Для 16-битных программ под DOS для отладки надо использовать Turbo
Debugger из пакета Borland C v4.5 или лучше (файл td.exe).
Для программ под Windows надо использовать 32-битный отладчик из этого
же пакета (файл td32.exe).
Для 32-битных программ, использующих расширитель DOS применять для
отладки Turbo Debugger невозможно. Но, может быть я не знаю, как это
делать. Если Вы знаете, как создавать 32-битные программы с
DOS-расширителем компиляторами фирмы Borland с включением в них отладочной
информации, то расскажите мне. А я попробую применить это для C--.
Return to contents.
2.1.12 /J0 /J1 /J2
Синонимом ключей /J0 /J1 /J2 является директива #jumptomain с
параметрами NONE, SHORT и NEAR соответственно.
Директива #jumptomain выполняет немного различные функции в
зависимости от типа выходного файла.
Компиляция файла типа *.com и *.exe модель памяти tiny:
#jumptomain NONE (-j0) - в этом случае по окончании кода начальной
инициализации программы не генерируется jmp на процедуру main. Эту
директиву следует использовать в случае, если до процедуры main нет других
не динамических процедур и инициализированных переменных.
#jumptomain SHORT (-j1) - в этом случае по окончании кода начальной
инициализации генерируется короткий jmp на процедуру main. Эту директиву
следует использовать, если до процедуры main находится не более 128 байт
кода и данных.
#jumptomain NEAR (-j2) - это состояние устанавливается по умолчанию. При
этом генерируется близкий jmp на процедуру main.
Компиляция файлов *.exe (ключи -exe -d32 -w32 -w32c):
#jumptomain NONE (-j0) - в этом случае код начальной инициализации
программы не генерируется и управление при запуске передается сразу на
процедуру main.
Во всех остальных случаях генерируется код начальной инициализации и
управление на процедуру main передается инструкцией call.
Компиляция файлов *.dll:
#jumptomain NONE (-j0) - в этом случае код начальной инициализации
программы не генерируется и управление при запуске передается сразу на
процедуру main.
Во всех остальных случаях генерируется код заглушки и управление на
процедуру main не передается. Фактически процедура main в этом случае не
нужна.
Процедура main при создании файлов DLL должна выглядеть немного иначе,
чем в других случаях:
dword main ( dword hInstDLL, reason, reserv )
{
...
}
Return to contents.
2.1.13 /LST - Создание ассемблерного листинга.
С помощью дополнительной опции командной строки -lst Вы можете
получить вместе с исполнительным файлом и его ассемблерный листинг.
Листинг будет помещен в файл одноименный с исполнительным файлом и
имеющим расширение *.lst.
Ассемблерный листинг создается независимой от компилятора частью кода
с использованием информации накапливаемой при компиляции программы.
Return to contents.
2.1.14 /ENV - Сохранение адреса переменных окружения.
Если при компиляции программы Вы в командную строку добавите опцию
-ENV или в файл c--.ini строка ENV, то компилятор добавит в вашу
программу переменную environ, в которой при загрузке будет сохранятся
адрес переменных окружения запускаемой программы. Для программ под
Windows это будет полный адрес, а для остальных в этой переменной будет
сохраняться только адрес сегмента.
Return to contents.
2.1.15 /CPA - Очистка post-области данных.
Переменные, которым в теле программы не было присвоено никакое
значение, не включаются в тело скомпилированной программы. Для них
резервируется память за пределами программы. Но эта память может быть
заполнена произвольной информацией.
Если Вам необходимо, чтобы неинициализированные переменные при
загрузке программы всегда содержали одно и тоже значение (ноль) -
включите в командную строку опцию -CPA.
Return to contents.
2.1.16 /W - вывод предупреждений.
По умолчанию компилятор не выводит предупреждения и многие даже не
подозревают о существовании такой полезной опции. В C-- предупреждения
фактически являются подсказками для создания оптимальных программ и
зачастую облегчают отладку программ. В предупреждениях компилятор может
сообщить Вам о том, в каком месте можно использовать короткие формы
операторов IF, WHILE, FOR... О том, какие процедуры, переменные и
структуры определенные в вашей программе не были использованы. О том
какие регистры компилятор использовал без вашего ведома и много другой
полезной информации.
По умолчанию предупреждения выводятся на экран. Но их бывает так
много, что они могут не поместиться на экране. Поэтому в компиляторе есть
опция, по которой все предупреждения выводятся в файл. Имя этого файла
задается в той же опции. Поместив в свой c--.ini файл пару вот этих строк:
w
wf=warning
Вы будете получать в файле warning предупреждения.
Return to contents.
2.1.17 /NW - Выборочное отключение типов предупреждений.
Сейчас компилятор может выдавать 12 типов предупреждений и, иногда их
бывает так много, что становится трудно в них ориентироваться. Теперь
можно выборочно запрещать выдачу предупреждений. Для этого в командной
строке (или в файле C--.INI) можно установить опцию /nw=number, где
number - число от 1 до 12. Этим цифрам соответствуют следующие типы
предупреждений:
1 - Optimize numerical expressions
2 - Compiler used register ..."
3 - Short operator '...' may be used
4 - String '...' repeated
5 - Expansion variable
6 - Signed value returned
7 - '...' defined above, therefore skipped.
8 - Variable/structure/procedure '...' possible not used
9 - Non-initialized variable may have been used
10 - Return flag was destroyed
11 - Code may not be executable
12 - Don't use local/parametric values in inline procedures
Return to contents.
2.1.18 /WSI - короткая таблица импорта.
Таблица импорта обычно состоит в свою очередь из четырех таблиц. Две
таблицы LookUp Table и Import Address Table абсолютно одинаковы.
Опцией командной строки /WSI Вы можете заставить компилятор
генерировать только одну из этих двух одинаковых таблиц (генерируется
только Import Address Table). Тем самым у Вас получится более компактная
таблица импорта, что приведет, в некоторых случаях, к созданию более
компактного выходного файла.
Return to contents.
2.2 Директивы транслятора.
C-- не содержит препроцессор. Тем не менее, есть несколько функций
очень похожих на функции C препроцессора.
Они даются как директивы транслятора. Все директивы транслятора
начинаются с вопросительного знака ? либо с символа #. Вот список имеющихся
директив и их назначение:
? align [val] Выровнять данные программы на четный по
умолчанию или на адрес кратный величине val.
? aligncode [val] Выровнять код программы на четный по
умолчанию или на адрес кратный величине val.
Заполнение производится кодом 0x90.
? aligner (aligner value) определить значение байта вставки.
? alignword (TRUE or FALSE) разрешает или запрещает выравнивание на
четный адрес переменных типа word и int,
значение по умолчанию TRUE.
? argc (TRUE or FALSE) Включить или отключить альтернативный
обработчик командной строки.
? atexit Вставляет в startup код поддержки процедуры
ATEXIT().
? code32 (TRUE/FALSE) разрешает/запрещает генерацию 32-битного
кода.
? codesize оптимизация размера кода (в ущерб скорости).
? compilerversion min-vers указывает, компилятор какой версии необходим
для компиляции данной программы.
? ctrl_c (TRUE or FALSE ) разрешает или запрещает игнорирование
нажатия CTRL-C.
? dataseg (value) указывает компилятору сегментный адрес ОЗУ
для переменных при компиляции ROM-BIOS.
? define (identifier) (token) определяет идентификатор.
? DOSrequired (номер) устанавливает минимальную требуемую версию
DOS: старший байт - номер версии,
младший байт - номер модификации:
0x0101 для версии 1.1 DOS
0x0315 для версии 3.21 DOS
0x0303 для версии 3.3 DOS
0x0600 для версии 6.0 DOS
0x0602 для версии 6.2 DOS и т.д.
? dosstring (TRUE/FALSE) указывает компилятору, что в качестве
терминатора строки надо использовать символ $
? else генерирует альтернативный код если ?ifdef или
?ifndef принимают значение FALSE (пример
использования смотрите в файле FPU.H--)
? endif указывает на конец действия директив ifdef и
ifndef
? fastcallapi (FALSE/TRUE) запретить/разрешить генерацию быстрого вызова
API-процедур (по умолчанию разрешено).
Директива работает при компиляции программ
под Windows.
? fixuptable (TRUE/FALSE) разрешить/запретить создание FixUp таблицы
(по умолчанию запрещено). Директива работает
при компиляции программ под Windows.
? ifdef (identifier) если идентификатор определен, то возвращает
TRUE иначе FALSE
? imagebase value задает адрес Image Base. По умолчанию этот
адрес равен 0x400000. Директива работает при
компиляции программ под Windows.
? ifndef (identifier) если идентификатор определен, то возвращает
FALSE иначе TRUE
? include ("filename") включает другой файл.
? includepath ("path") указание компилятору, в какой директории надо
искать включаемые файлы
? initallvar инициализирует 0 все неинициализированные
переменные.
? jumptomain (NONE, SHORT, NEAR or FALSE)
устанавливает тип перехода к main(),
значение по умолчанию - NEAR.
? maxerrors (number) максимальное количество найденных ошибок,
превысив которое транслятор прекращает
работу, значение по умолчанию - 16.
? movedatarom (TRUE/FALSE) указывает компилятору о необходимости
переноса данных из ПЗУ в ОЗУ.
? parsecommandline (TRUE or FALSE)
включает в программу блок кода для
синтаксического анализа командной строки
значение по умолчанию FALSE.
? pragma может объявлять несколько других директив
? print (number or string) выводит на экран строку или число.
? printhex (number) выводит на экран число в шестнадцатеричном
коде.
? randombyte вставляет в код программы байт случайного
значения.
? resize (TRUE or FALSE) включает функцию изменения после запуска
размера выделенного программе блока памяти
на минимально требуемый объем,
значение по умолчанию TRUE.
? resizemessage (string) сообщение, выводимое на экран перед
аварийным прерыванием выполнения программы,
если изменение размера выделенного программе
блока памяти не выполнено.
? setdinproc по этой директиве компилятор немедленно
вставляет в код компилируемой программы все
вызывавшиеся ранее динамические процедуры.
? sizerom (value) указывает компилятору размер ПЗУ.
? speed оптимизация быстродействия (значение
по умолчанию) в ущерб размеру кода.
? stack (number) определяет размер стека программы в байтах.
? startaddress (number) устанавливает стартовый адрес начала кода,
значение по умолчанию 0x100.
? startuptomain в com-файлах размещает startup-код в
процедуре main().
? startusevar (number) указывает адрес, с которого разрешено
использовать ячейки памяти под
неинициализированные переменные.
? sysattribute (значение) эта директива передает компилятору атрибут
создаваемого драйвера. По умолчанию
устанавливается значение 0x2000.
Действует только с ключом /SYS.
? sysname <текстовая строка> эта директива передает компилятору имя
будущего драйвера. По умолчанию
присваивается имя NO_NAME. Длина имени не
более 8 символов. Действует только с ключом
/SYS.
? syscommand ,, ...; - эта директива
является обязательной при создании
драйверов. По этой директиве компилятору
передается список имен процедур обработки
команд драйвера. Действует только с ключом
/SYS.
? warning (TRUE or FALSE) эта директива разрешает или запрещает выдачу
предупреждений. Директива действует только в
пределах текущего файла и не влияет на
включаемые файлы.
? winmonoblock FALSE запрещает размещение таблиц файла формата PE
в одну секцию.
? undef уничтожает константы объявленные директивой
? define
? use8086 ограничивается при генерации объектного кода
командами 8088/8086 (значение по умолчанию).
? use8088 ограничивается при генерации объектного кода
командами 8088/8086 (значение по умолчанию).
? use80186 допускает при генерации объектного кода
команды и оптимизацию для процессора 80186.
? use80286 допускает при генерации объектного кода
команды и оптимизацию для процессора 80286.
? use80386 допускает при генерации объектного кода
команды и оптимизацию для процессора 80386.
? use80486 допускает при генерации объектного кода
команды и оптимизацию для процессора 80486.
? usePentium допускает при генерации объектного кода
команды и оптимизацию для процессора Pentium.
? useMMX допускает при генерации объектного кода
команды и оптимизацию для процессора Pentium
MMX.
? usestartup разрешает компилятору использовать ячейки
памяти, занимаемые кодом начальной
инициализации программы.
Return to contents.
2.2.1 ?ifdef/?ifndef
Ранее директива ?ifdef срабатывала на наличие константы независимо
от значения ее величины, а директива ?ifndef срабатывала на отсутствие
константы в компилируемом файле. Теперь ?indef срабатывает лишь на
константу отличную от FALSE, а ?ifndef срабатывает как на отсутствие
константы в компилируемом файле, так и на константу имеющую значение
FALSE.
Для директив ?ifdef/?ifndef зарезервированы константы codesize и
speed, которые принимают значение TRUE или FALSE в зависимости от режима
оптимизации. Это будет полезным для создания более гибких библиотек.
Есть возможность проверки типа CPU для которого ведется компиляция.
Допустимые варианты синтаксиса:
?ifdef cpu > 1 //если программа компилируется для CPU выше 80186
?ifndef cpu >= 2 // -------//------------- не больше или равно 80286
?ifdef cpu == 3 // -------//------------- равно 80386
?ifdef cpu != 0 // -------//------------- не равен 8086
?ifdef cpu < 3 // -------//------------- хуже 80386
?ifdef cpu <= 2 // -------//------------- хуже или равен 80286
Эта директива позволит Вам писать одну процедуру для различных типов
CPU.
Return to contents.
2.2.2 ?initallvar
Директивой ?initallvar TRUE включается режим при котором всем
неинициализированным переменным будет присвоено нулевое значение и они
будут располагаться в том месте, где были объявлены. Т.е. практически
исчезнут неинициализированные переменные. Это может быть полезным при
написании драйверов и резидентных программ.
Параметр FALSE этой директивы отключает этот режим.
По умолчанию эта директива установлена в FALSE.
Return to contents.
2.2.3 ?usestartup
Директива ?usestartup разрешает компилятору использовать ячейки кода
начальной инициализации программы (startup) для последующего размещения в
них неинициализированных переменных. Это может быть полезным для получения
более компактного кода, как обычных программ, так и в особенности
резидентных.
Эту директиву применяют только для генерации *.COM файлов.
Return to contents.
2.2.4 ?startusevar
Директивой ?startusevar можно указать начальный адрес с которого
компилятор будет распределять память для неинициализированных переменных.
Например, получив директиву ?startusevar 0x53 компилятор будет
располагать неинициализированные переменные, начиная с адреса 0x53. Это
может быть полезным для получения более компактного кода как для
резидентных, так и для обычных программ.
Эту директиву применяют только для генерации *.COM файлов.
Return to contents.
2.2.5 ?atexit
Директива ?atexit добавляет в startup программы код поддержки
процедуры ATEXIT, резервирует место для хранения 16 адресов процедур и
изменяет код процедур ABORT и EXIT.
Процедура ATEXIT регистрирует процедуру, адрес которой передается ей в
качестве параметра, как процедуру завершения программы. Эта процедура
будет вызвана в момент завершения программы процедурами ABORT или EXIT
или инструкцией RET из main.
Всего можно зарегистрировать до 16 процедур. Процедуры вызываются в
порядке обратном порядку их регистрации.
Return to contents.
2.2.6 ?startuptomain
По этой директиве компилятор в начале файла делает jmp на начало
процедуры main(). Перед началом компиляции этой процедуры компилятор
начнет компиляцию startup кода и лишь затем будет продолжена компиляция
процедуры main(). Тем самым startup код окажется не в начале файла, как
это происходит обычно, а в теле процедуры main(). Это будет полезным при
компиляции резидентных программ (TSR).
Директива ?startuptomain работает только при компиляции com-файлов.
Return to contents.
2.2.7 ?undef
Эта директива уничтожает константы объявленные директивой ?define. Ее
можно применять для изменения в процессе компиляции значения какой-нибудь
константы.
Return to contents.
2.2.8 ?align и ?aligncode
В C-- существует директива ?align, которая делает однократное
выравнивание данных на четный адрес. Но если к этой директиве добавить
число, то выравнивание будет произведено на адрес кратный этому числу.
Например директива ?align 4 дополнит сегмент данных до адреса кратного
4. При выравнивании будут вставляться байты, значения которых определяются
директивой ?aligner, по умолчанию это значение равно нулю. Директива
?align производит выравнивание только в сегменте данных. В тех моделях
памяти, в которых сегмент данных и кода совпадают эту директиву можно
применять и для выравнивания начала процедур.
Директива ?aligncode [value] делает выравнивание в сегменте кода на
адрес кратный значению value, по умолчанию на четный адрес. Значение байта
заполнения в этой директиве является число 0x90 - код инструкции NOP.
Значение байта заполнения для этой директивы изменить нельзя. Т.о. эту
директиву можно применять и внутри исполняемого кода. Например, если Вы
хотите получить быстрый код на 486 процессоре, то рекомендуется делать
выравнивание начала процедур и циклов на адрес кратный 16.
Return to contents.
2.2.9 ?pragma
Директива #pragma это многофункциональнальная директива, которая в
свою очередь имеет свои директивы:
option
Директива option позволяет включить в Ваш код опции командной строки
компилятора. Некоторые опции не могут быть использованы в этой директиве;
другие должны помещаться в самом начале исходного текста. Пример:
#pragma option w32c
Эта директива объявляет компилятору, что надо создать консольный
32-битный файл под windows.
startup
Директивой startup можно указать функцию, которая будет выполнена перед
запуском процедуры main. Эта директива имеет такой формат:
#pragma startup procname
Количество раз, которое можно применять эту директиву в одной
программе не ограничено, но реально можно использовать лишь несколько
тысяч раз.
line
Директива line выводит на экран номер текущей строки и имя файла.
Дополнительно может выводиться содержимое строки находящееся после слова
line. Пример:
#pragma line information
Встретив эту директиву, компилятор выведет на экран номер строки и имя
файла. Также будет выведено сообщение справа от слова line, если оно
есть.
resource
Эта директива может принимать значения start и end. Эти два
значения выделяют начало и конец блока ресурсов, если вы используете его
непосредственно в исходном коде файла, а не в отдельном файле. Пример:
#pragma resource start
MyMenu MENU DISCARDABLE
BEGIN POPUP "Files",HELP
BEGIN
MENUITEM "Open", ID_OPEN
MENUITEM "Save", ID_SAVE
MENUITEM SEPARATOR
MENUITEM "Exit", ID_EXIT
END
MENUITEM "Other", 65535
END
#pragma resource end
Return to contents.
3. Константы.
3.1 Числовые константы.
Представление числовых констант в виде десятичных чисел (чисел с
основанием 10) и шестнадцатеричных чисел (основание счисления 16) полностью
аналогично языку C.
При двоичном представлении чисел (основание 2) число должно начинаться
с символов 0b, за которыми без пробела идет последовательность нулей и
единиц.
При восьмеричном представлении чисел (основание 8) число должно
начинаться с символов 0o, за которыми без пробела идет последовательность
цифр.
Вещественное число отличается от целого по наличию в нем точки.
Начинаться вещественное число должно либо цифрой от 0 до 9, либо знаком
минус. Необязательной частью вещественного числа является показатель
степени. Показатель степени отделяется от числа символом e или E.
Пробелы недопустимы.
Примеры:
0b11111111 // двоичное представление числа 255
0x00F // шестнадцатеричное представление числа 15
0o10 // восьмеричное представление числа 8
1.234567E-20 // вещественное число
C-- вместе с традиционным C-стилем шестнадцатеричных чисел понимает и
числа записанные в стиле ассемблера. Для тех, кто вдруг не знает, сообщаю,
что шестнадцатеричные числа в ассемблере имеют на конце символ h или H.
Если первый символ шестнадцатеричного числа больше 9, то перед ним
обязательно должен быть записан символ 0. Примеры:
1234h
0A000H
К числовым константам можно писать суффиксы L, U и F. Фактически
эти суффиксы в C-- не играют никакой роли, компилятор их просто
проглатывает. Пример:
#define DEF 1L
#define DEF2 2Lu
#define DEF3 3.0F
Эти суффиксы не зависят от регистра, т.е. их можно писать как
маленькими, так и большими буквами.
Return to contents.
3.2 Символьные константы.
Одиночные символьные константы, как и в C, должны заключаться в
одиночные кавычки '.
Также как и в C, для обозначения специальных символов служит обратная
наклонная черта вправо \ с последующим за ней ключевым символом (или
несколькими символами). Поддерживаются следующие специальные символы:
\a /* звуковой сигнал */
\b /* забой */
\f /* перевод страницы */
\l /* перевод строки */
\n /* возврат каретки*/
\r /* возврат каретки*/
\t /* табуляция */
\x?? /* символ ASCII, соответствующий байтовому представлению,
состоящему из двух шестнадцатеричных цифр, расположенных
на месте знаков вопроса */
\??? /* символ ASCII, соответствующий байтовому представлению,
состоящему из трех десятичных цифр, расположенных
на месте знаков вопроса */
Любой другой символ после обратной наклонной черты вправо будет принят
как простой символ.
Символ "Одиночная кавычка" ' может быть введен при помощи конструкции
\'
Символ NULL может быть введен как ''
В C-- поддерживаются и многобуквенные символьные константы. Примеры
многобуквенных символьных констант:
'ab'
'the'
'this is large'
Никакого ограничения на число символов в символьной константе не
накладывается, но различаются только последние 4 символа. Это - максимум,
который может быть сохранен в 32-разрядной переменной. Например, константы
this is large и arge - одинаковы.
C-- обрабатывает все символьные константы как числовые значения ASCII
символов. Для многобуквенных символьных констант первый символ
соответствует старшим разрядам, таким образом, значение для ab будет
закодировано как a*256+b.
Return to contents.
3.3 Строковые константы.
Строковые константы, как и в C, заключаются в двойные кавычки (").
Специальные символы внутри строк обозначаются так же, как и в символьных
константах. Все специальные символы имеют то же значение, что и в
символьных константах за исключением \n, который имеет значение новая
строка и заменяет собой пару символов возврат каретки и перевод
строки.
В настоящее время наибольшая длина строковой константы - 2048 символов,
включая символ-ограничитель 0, таким образом, максимум 2047 значащих
символов.
Return to contents.
3.4 Постоянные выражения.
Постоянное выражение - одиночная числовая константа или несколько
числовых констант, связанных между собой операторами. Числовое значение
выражения вычисляется один раз во время компиляции и далее используется
только его постоянное значение.
Подобно всем выражениям в C--, постоянные выражения всегда вычисляются
слева направо, невзирая на правила арифметики! Это совершенно отлично от
других языков, и при написании выражений надо быть осторожным и помнить,
что 2+3*2=10 а не 8.
Некоторые примеры постоянных выражений:
45 & 1 + 3 // равняется 4
14 - 1 / 2 // равняется 6 (помните целочисленные значения)
1 * 2 * 3 / 2 + 4 // равняется 7
Примеры с применением вещественных чисел:
3.23*1.53+2.0E2 // равняется 204.9419
Return to contents.
4. Выражения.
4.1 Типы выражений.
Имеются три типа выражений в C--, не считая постоянных выражений. Это
выражения типа EAX/AX/AL, выражения типа неEAX/AX/AL и условные выражения.
Все C-- выражения вычисляются слева направо, независимо от старшинства
входящих в выражение математических операций.
Return to contents.
4.2 Выражения типа EAX/AX/AL.
Этот тип выражений применяется в случае, когда его результат может быть
сохранен в переменной в памяти или в регистре EAX или AX или AL.
Если результат может быть сохранен в переменных типа byte или char,
используется нотация AL.
Если результат может быть сохранен в переменных типа word или int,
используется нотация AX.
Если результат может быть сохранен в переменных типа dword, long или
float, используется нотация EAX.
Return to contents.
4.3 Выражения использующие получатель при вычислении выражения.
Если в правой части выражения используется переменная являющаяся
одновременно и приемником, то такие выражения дают различные результаты в
зависимости от того является приемник регистром или переменной памяти. Это
связано с тем, что при вычислении выражения в переменную памяти, вычисление
производится сначала в регистр EAX/AX/AL, и лишь после окончания вычисления
результат будет записан в приемник. Если же приемником является регистр, то
его значение будет меняться после каждой операции вычисления. Пример:
int var;
var = BX = 2;
var = 3 + var; // результатом будет 5
BX = 3 + BX; // результатом будет 6
Return to contents.
4.4 Не - EAX/AX/AL выражения.
Этот тип выражений применяется в случае, когда его результат должен
быть сохранен в любом другом регистре, отличном от аккумулятора EAX, AX
или AL. В процессе вычисления выражения этого типа меняется только
содержимое указанного регистра-получателя, все другие регистры будут
сохранены. Если регистром-получателем служит байтовый регистр, а при
вычислении используются величины размером в слово, одновременно с записью в
младший байт может быть разрушено содержимое старшего байта
регистра-получателя.
Это обстоятельство накладывает некоторые ограничения на операции и
операнды, допустимые в выражениях типа не EAX/AX/AL. Внутри выражений
байтового типа не допускается:
- делать вызовы МАКРОКОМАНД,
- делать вызовы РЕГИСТРОВЫХ процедур
- делать вызовы СТЕКОВЫХ процедур
Ранее в не-EAX/AX/AL выражениях было можно использовать лишь
операции: сложения, вычитания, XOR, OR, AND. Теперь для 16 и 32 битных
регистров почти все ограничения сняты. Но есть еще ограничения на регистры.
Например, если в выражении используется сдвиг на значение переменной, а
приемником являются регистры CX/ECX, то такое выражение компилятор не будет
компилировать:
CX = var * SI * 3 * var >> 3; //вызовет сообщение об ошибке
Примечание: для 8 битных не-AL выражений умножать можно только на
числа: 0, 1, 2, 4, 8, 16, 32, 64 и 128. Все эти ограничения связаны со
стремлением не разрушать другие регистры при использовании не-EAX/AX/AL
выражений.
Return to contents.
4.5 Условные выражения.
Условные выражения - выражения, результатом вычисления которых является
логическое значение да или нет, используемое в операторе if и циклах do {}
while, while, for.
Имеются два типа условных выражений, простые и сложные.
Возможно логическое объединение условий.
Return to contents.
4.5.1 Простые условные выражения.
Простые условные выражения - одиночная лексема или выражение, которое
примет значение да, если расчетное значение отлично от нуля, или значение
нет, если расчетное значение равно нулю.
Return to contents.
4.5.2 Сложные условные выражения.
Сложные условные выражения имеют следующую форму:
(левая_часть оператор_отношения правая_часть)
Где:
левая_часть - любое выражение типа AL/AX/EAX или постоянное выражение.
Тип выражения определяется по типу первой лексемы
(регистра или переменной); значение типа по умолчанию -
word для 16-битных программ и dword для 32-битных. Если
желателен другой тип, перед выражением ставится
соответствующее ключевое слово, определяющее его тип:
byte, char, int, long, dword или float
оператор_отношения - любой из операторов отношения:
==, !=, <>, <, >, <=, или >=.
правая_часть - любой одиночный регистр, одиночная переменная или
постоянное выражение.
Примеры правильных сложных условных выражений:
(X + y > z)
(int CX*DX < = 12*3)
(byte first*second+hold == cnumber)
Примеры недопустимых сложных условных выражений:
(x+y >= x-y) // правая часть не является одиночной лексемой или
постоянным выражением.
(Z = y) // вместо == ошибочно поставлен =
Return to contents.
4.6 Изменение типа выражения при присваивании.
Если после знака равенства написать тип отличный от типа вычисляемой
переменной, то все переменные участвующие в процессе вычисления, будут
преобразовываться к этому новому типу, и лишь конечный результат будет
преобразован к типу вычисляемой переменной. Пример:
int i, a;
long b;
char c;
i = a * b + c ;
Значения переменных a, b, и c в этом примере перед вычислением будут
преобразованы к типу int (типу переменной i). Но если записать это
выражение вот так:
i = long a * b + c ;
то переменные a, b, и c в этом примере перед вычислением будут
преобразованы к типу long, а конечный результат будет преобразован к типу
переменной i - int.
Return to contents.
4.7 Вычисление в регистры EAX/AX/AL со знаком.
По умолчанию все вычисления в регистры производятся как с без знаковыми
величинами.
Например:
int a,b,c;
AX = a * b / c ;
При этом компилятор генерировал без знаковые инструкции div и mul, так как
регистры считаются без знаковыми переменными. Если написать вот так:
AX = int a * b / c ;
то компилятор сгенерирует инструкции idiv и imul.
Обращаю ваше внимание, что для регистра AL можно использовать только
модификатор char, для AX соответственно только int, а для EAX - long. Для
остальных регистров подобное делать нельзя.
Return to contents.
5. Идентификаторы.
5.1 Формат идентификатора.
Идентификаторы в C-- должны начинаться или с символа подчеркивания _
или заглавных или строчных букв. Следующие символы могут быть любой
комбинацией символов подчеркивания, заглавных или строчных букв или чисел
(от 0 до 9). Общая длина идентификатора не может превышать 64 символа.
Символы с кодом больше 0x7A (код символа z) недопустимы.
Примеры допустимых идентификаторов:
_DOG
Loony12
HowdYBoys_AND_Girls
WOW___
X
Примеры недопустимых идентификаторов:
12bogus /* не может начинаться с числа */
WowisthisalongidentifieryupitsureisnotOyoulengthismorethat64chars
/*длина идентификатора превышает 64 */
Y_es sir /* пробелы недопустимы */
The-end /* дефисы недопустимы */
Return to contents.
5.2 Зарезервированные идентификаторы.
Список зарезервированных в C-- идентификаторов, которые не могут
использоваться как общие идентификаторы, поскольку они уже были определены
или зарезервированы для других целей:
BREAK CASE CONTINUE ELSE EXTRACT FALSE FOR
FROM GOTO IF LOOPNZ RETURN SWITCH TRUE
WHILE
CARRYFLAG MINUSFLAG NOTCARRYFLAG NOTOVERFLOW
NOTZEROFLAG OVERFLOW PLUSFLAG ZEROFLAG
__CODEPTR__ __COMPILER__ __DATAPTR__ __DATESTR__ __DATE__ __DAY__
__HOUR__ __LINE__ __MINUTE__ __MONTH__ __POSTPTR__ __SECOND__
__TIME__ __VER1__ __VER2__ __WEEKDAY__ __YEAR__
_export asm break byte case cdecl char continue
default do dword else enum extern far fastcall
float for goto if inline int interrupt long
loop loopnz pascal return short signed sizeof static
stdcall struct switch union unsigned void while word
ESCHAR ESBYTE ESINT ESWORD ESLONG ESDWORD ESFLOAT
CSCHAR CSBYTE CSINT CSWORD CSLONG CSDWORD CSFLOAT
SSCHAR SSBYTE SSINT SSWORD SSLONG SSDWORD SSFLOAT
DSCHAR DSBYTE DSINT DSWORD DSLONG DSDWORD DSFLOAT
FSCHAR FSBYTE FSINT FSWORD FSLONG FSDWORD FSFLOAT
GSCHAR GSBYTE GSINT GSWORD GSLONG GSDWORD GSFLOAT
AX CX DX BX SP BP SI DI
EAX ECX EDX EBX ESP EBP ESI EDI
AL CL DL BL AH CH DH BH
ES CS SS DS FS GS
ST(0) ST(1) ST(2) ST(3) ST(4) ST(5) ST(6) ST(7) ST
st(0) st(1) st(2) st(3) st(4) st(5) st(6) st(7) st
Этот список может быть получен из C-- транслятора в любое время,
запуском его с опцией /WORDS из командной строки.
Если Вы пользуетесь при компиляции опцией командной строки /ia, которая
позволяет использовать ассемблерные инструкции не заключая их в блоки asm и
без префикса $, то все имена ассемблерных инструкций становятся
зарезервированными словами. Причем имена ассемблерных инструкций компилятор
различает независимо от того, написаны они маленькими или большими буквами.
Список имен поддерживаемых компилятором ассемблерных инструкции можно
получить запустив компилятор с опцией /LAI.
Кроме этого в ассемблерных инструкциях становятся зарезервированными
следующие идентификаторы:
ax cx dx bx sp bp si di
eax ecx edx ebx esp ebp esi edi
al cl dl bl ah ch dh bh
es cs ss ds fs gs
DR0 DR1 DR2 DR3 DR4 DR5 DR6 DR7
CR0 CR1 CR2 CR3 CR4 CR5 CR6 CR7
TR0 TR1 TR2 TR3 TR4 TR5 TR6 TR7
MM0 MM1 MM2 MM3 MM4 MM5 MM6 MM7
XMM0 XMM1 XMM2 XMM3 XMM4 XMM5 XMM6 XMM7
dr0 dr1 dr2 dr3 dr4 dr5 dr6 dr7
cr0 cr1 cr2 cr3 cr4 cr5 cr6 cr7
tr0 tr1 tr2 tr3 tr4 tr5 tr6 tr7
mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7
xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7
Return to contents.
5.3 Универсальные регистры для 16 и 32-битного режима.
При создании библиотечных процедур очень часто приходится писать
варианты процедуры для работы в 16-битном и 32-битном режимах, которые
отличаются друг от друга лишь использованием в них либо 16-битных либо
32-битных регистров соответственно. Но можно писать лишь одну процедуру,
используя в ней новый синтаксис регистров. Если компилятор встретит вот
такой синтаксис:
(E)AX=0;
то компилятор будет использовать при компиляции 16-битного кода регистр
AX, а при компиляции 32-битного кода регистр EAX.
Использование автоматических регистров позволит упростить библиотечные
файлы и сделать их более понятными.
Return to contents.
5.4 Предопределенные идентификаторы.
Идентификаторы, определяемые компилятором в зависимости от режима
компиляции:
__TLS__ идет компиляция под windows (w32, w32c, dll).
__DLL__ идет компиляция dll.
__CONSOLE__ идет компиляция консольного приложения windows
__WIN32__ идет компиляция GUI-шного приложения
__FLAT__ компилируется 32-битный код.
__MSDOS__ компилируется 16-битный код.
__TINY__ используется модель памяти tiny в 16-битном режиме
__SMALL__ используется модель памяти small в 16-битном режиме
__DOS32__ компилируется 32-битный код под DOS (d32)
__COM__ компилируется com-файл
__SYS__ компилируется sys-файл
__ROM__ компилируется rom-файл
__OBJ__ компилируется obj-файл
__TEXE__ компилируется exe-файл модели tiny
__EXE__ компилируется exe-файл модели small
__MEOS__ компилируется исполняемый файл для MenuetOS
codesize компиляция ведется с оптимизацией на размер кода
speed компиляция ведется с оптимизацией на быстродействие кода
cpu определяет тип процессора для которого ведется компиляция:
0 - 8086
1 - 80186
2 - 80286
3 - 80386
4 - 80486
5 - Pentium
6 - Pentium MMX
7 - Pentium II
Эти идентификаторы могут быть проверены директивами #ifdef или #ifndef.
Идентификатор cpu может быть использован лишь с операторами проверки
условий:
#ifdef cpu > 3 //если тип процессора больше 80386
Return to contents.
6. Переменные.
6.1 Типы переменных.
В C-- имеется семь типов переменных (именованных областей памяти), это:
byte, word, dword, char, int, long, float.
Следующая таблица показывает размер и диапазон представляемых величин
каждого из типов переменной:
NAME | SIZE | VALUE RANGE | VALUE RANGE
тип |размер | диапазон представления | диапазон представления
|в байт.| в десятичной системе | в шестнадцатеричной системе
---------------------------------------------------------------------------
byte | 1 | 0 to 255 | 0x00 to 0xFF
word | 2 | 0 to 65535 | 0x0000 to 0xFFFF
dword | 4 | 0 to 4294967295 | 0x00000000 to 0xFFFFFFFF
char | 1 | -128 to 127 | 0x80 to 0x7F
int | 2 | -32768 to 32767 | 0x8000 to 0x7FFF
long | 4 | -2147483648 to 2147483647 | 0x80000000 to 0x7FFFFFFF
float | 4 | -3,37E38 to +3,37E38 | 0xFF7FFFFF to 0x7FFFFFFF
Примечание: для работы с типами float, dword и long используются
32-разрядные целочисленные команды, следовательно, для их выполнения нужно
иметь процессор не хуже 80386, что сейчас не является большой проблемой.
Для совместимости со стандартом, принятом в языке C, введены
новые зарезервированные слова: short, signed, unsigned. Для типа int
в 32-битном режиме изменена разрядность. Вот таблица всех вариантов новых
типов данных:
---------------------------------------------------------
| полный тип |допустимые сокращения|старые аналоги|
---------------------------------------------------------
|signed char |char | char |
|signed int |signed, int | int/long |
|signed short int |short, signed short | int |
|signed long int |long, signed long | long |
|unsigned char |--- | byte |
|unsigned int |unsigned | word/dword |
|unsigned short int|unsigned short | word |
|unsigned long int |unsigned long | dword |
---------------------------------------------------------
Старые типы byte, word и dword поддерживаются по прежнему и имеют
функционально прежнее значение. Изменения коснулись лишь типа int. Он в
16-битном режиме, также как и тип unsigned int, имеет 16-битный размер, а
в 32-битном режиме эти оба типа имеют размер в 32-бита. На первый взгляд
такие свойства типа int вносят некоторую путаницу, но это дает большой
выигрыш при использовании этого типа в библиотечных файлах, которые могут
быть использованы при компиляции 16-битных и 32-битных программ.
Return to contents.
6.2 Объявление переменных.
Синтаксис для объявления переменных следующий:
variable-type identifier;
где variable-type - char, byte, int, word, long, dword или float.
Одновременно могут быть объявлены несколько идентификаторов одного типа:
variable-type identifier1, identifier2, ... , identifierN;
Одномерные массивы могут быть объявлены следующим образом:
variable-type identifier[elements];
где elements - постоянное выражение для количества переменных этого типа,
объединенных в массив.
Инициализированные массивы можно объявлять без указания числа
элементов. При этом будет создан массив по фактическому числу элементов.
variable-type identifier[] = { const1, const2 };
Переменные при объявлении могут быть проинициализированы следующим
образом:
variable-type identifier = value;
Некоторые примеры глобальных объявлений:
byte i,j; /* объявляет две переменные типа byte с именами i и j */
word see[10] /* объявляет массив с именем see, состоящий из 10
элементов типа word */
int h,x[27] /* объявляет, переменную типа int с именем h,
и массив с именем x, состоящий из 27 элементов типа int */
long size=0; /* объявлена переменная типа long с именем size и ей присвоено
значение 0. */
Return to contents.
6.3 Глобальные переменные.
Глобальные переменные - это переменные, область действия которых
распространяется на всю программу. В C-- использовать глобальные переменные
можно в процедурах, расположенных ниже места ее объявления. Т.е. если Вы
пишите процедуру, в которой используете переменную var, а саму переменную
объявляете ниже текста процедуры, то компилятор выдаст ошибку. Это связано
с тем, что компилятор может знать тип переменной только после их
объявления. Но для таких переменных можно использовать взятие их адреса,
так как адрес переменной не зависит от его типа. Пример:
void Proc(){
gvar = 0; /* компилятор выдаст сообщение об ошибке, т.к. он еще не знает
типа переменной gvar */
AX = #gvar; /* несмотря на то, что компилятор не знает и адреса этой
переменной такое выражение будет откомпилировано */
}
int gvar;
Но все же ситуация не безнадежна и нам удастся добиться того, чего мы
задумали. В этом нам поможет альтернативный синтаксис обращения к
переменным:
void Proc(){
DSINT[#gvar] = 0; /* компилятор успешно откомпилирует это выражение т.к.
ему теперь известен тип переменной gvar */
}
int gvar;
Память под глобальные переменные выделяется в сегменте данных. Если
переменная при объявлении инициализируется (т.е. ей присвоено какое-то
значение), то переменная будет включена в код компилируемого файла. Если
переменная не инициализируется, то место для переменной будет
зарезервировано сразу же за последним байтом скомпилированной программы.
Return to contents.
6.4 Локальные переменные.
Локальные переменные - это переменные область действия которых
распространяется лишь в пределах одной процедуры. Объявлять локальные
переменные, в отличии от современных версий C, можно между именем процедуры
и первой открывающейся фигурной скобкой. Пример:
void PROC ()
int i; //объявлена локальная переменная типа int с именем i
{
for ( i=0; i<10; i++ ) WRITE(1);
}
Память под локальные переменные отводится в сегменте стека.
К локальным переменным можно отнести и параметры стековых процедур. Под
них также отводится память в стеке.
Можно инициализировать локальные переменные при их объявлении. Но есть
некоторые ограничения. Нельзя инициализировать массивы и многомерные
структуры. Инициализировать можно одним значением, т.е нельзя при
инициализации локальных переменных пользоваться перечислением заключенным в
фигурные скобки и операторами FROM и EXTRACT.
Имена локальных переменных могут совпадать с именами глобальных
переменных или процедур, но тогда Вы не сможете обратиться к глобальной
переменной или вызвать одноименную процедуру.
Локальные переменные можно объявлять и в начале блока процедуры. Но
только до начала тела процедуры. Пример:
void proc(){
int locproc; // объявление локальной процедуры
locproc=0; // а теперь пошло тело процедуры
int locproc; // а на это объявление переменной компилятор выдаст сообщение
// об ошибке, т.к. уже началось тело процедуры
}
Return to contents.
6.5 Динамические переменные и структуры.
Наряду с уже известными Вам динамическими процедурами в C-- есть
возможность использовать динамически и переменные и структуры. Динамические
переменные и структуры обозначаются также как и динамические процедуры -
символом двоеточия перед началом их объявления. И также как и динамическая
процедура, динамическая переменная или структура будет вставлена в код,
лишь в том случае, если она будет использована в программе.
Динамические переменные и структуры найдут применение в библиотеках.
Использовать их непосредственно в программах нет смысла.
У динамических переменных, структур также как и у процедур, есть один
недостаток - Вы не сможете знать, в каком месте откомпилированного кода они
будут расположены, и в каком порядке. Но необходимость это знать бывает
очень редко.
Динамические инициализированные переменные и структуры в файле будут
расположены в его самом конце, после динамических процедур. Эту их
особенность можно использовать, если Вам будет необходимо, чтобы данные не
были разбросаны среди кода, а были сгруппированы в одном месте.
Return to contents.
6.6 Присваивание одного значения нескольким переменным.
Если Вам необходимо присвоить нескольким переменным одинаковые значения:
var1=0;
var2=0;
var3=0;
то теперь это можно записать более коротко:
var1=var2=var3=0;
При использовании такой записи генерируется более компактный и более
быстрый код.
Return to contents.
6.7 Переменные типа float.
6.7.1 Формат переменных типа float.
Для представления значений с плавающей точкой в язык C-- введен тип
float. Этому типу соответствует действительное число одинарной точности
FPU.
Формат представления данных с плавающей точкой включает три поля:
знака, мантиссы и порядка. Знак определяется старшим значащим разрядом.
Поле мантиссы содержит значащие биты числа, а поле порядка содержит
степень 2 и определяет масштабирующий множитель для мантиссы.
31 30.....23 22........0
| | | | |
| | | -------------- - поле мантиссы
| ------------------------ - поле порядка
--------------------------- - бит знака
Return to contents.
6.7.2 Константы с плавающей точкой.
Компилятор отличает вещественное число от целого по наличию в нем
точки. Начинаться вещественное число должно либо цифрой от 0 до 9, либо
знаком минус. Необязательной частью вещественного числа является
показатель степени. Показатель степени отделяется от числа символом e или
E. Пробелы недопустимы. Вот примеры допустимого синтаксиса:
0.98
-15.75
3.14e2
1.234567E-20
Return to contents.
6.7.3 Диапазон допустимых значений.
Вещественное число типа float может находиться в диапазоне от 3.37E38
до -3.37E38. Минимально близкое к нулю значение равняется 1.17E-38 и
-1.17E-38. Записывать вещественное число одинарной точности более чем 8
цифрами не имеет смысла. Показатель степени может принимать значения от
+38 до -38.
Return to contents.
6.7.4 Математические операции.
Компилятор поддерживает 4 основных действия над переменными типа
float: сложение, вычитание, умножение и деление. Поддерживается также
инкремент (var++ - увеличение на 1), декремент (var-- - уменьшение на 1),
смена знака (-var) и обмен значениями (var1 >< var2). Остальные
математические операции будут реализованы либо уже реализованы во внешних
библиотеках. При вычислении значения переменной float можно использовать
и переменные других типов, они будут автоматически преобразованы в тип
float.
ВНИМАНИЕ! Составные математические операции выполняются в том
порядке, в котором они записаны, невзирая на правила арифметики.
Return to contents.
6.7.5 Преобразования типов.
При математических операциях конечным итогом которых является
переменная типа float, все операнды других типов перед вычислением будут
преобразованы в тип float. При присваивании переменной типа float значения
переменной другого типа оно также будет преобразовано в тип float.
Если при целочисленных вычислениях одним из операндов будет переменная
типа float, то из него будет выделена целая часть, которая и примет
участие в вычислениях. При присваивании целочисленной переменной значения
переменной типа float, из нее также будет выделена целая часть, которая и
будет присвоена целочисленной переменной.
Return to contents.
6.7.6 Операции сравнения.
Если при операции сравнения левым операндом является переменная или
выражение типа float, а правым является целочисленное значение, то
целочисленное значение будет преобразовано в вещественный тип. Если же
левым операндом является целочисленное выражение или переменная, а правым
операндом значение типа float, то из правого операнда будет выделена целая
часть, которая и примет участие в сравнении.
Return to contents.
6.7.7 Сравнение переменных типа float с 32-битным регистром.
В регистрах могут содержаться знаковые, без знаковые и вещественные
данные. По умолчанию считается, что в регистре находится без знаковое целое
число. При сравнении переменных типа float с 32-битным регистром можно
указывать тип данных содержащихся в регистре. Для этой цели можно
использовать модификаторы: signed, unsigned, float. Примеры:
float f=1.0;
void PROC()
{
IF( f < signed ECX) //в регистре ECX находится знаковое число
IF( unsigned EBX > f) //в регистре EBX находится без знаковое число
IF( f == float EAX ) //в EAX находится число формата float
}
ВНИМАНИЕ! При операции сравнения с участием переменой типа float,
содержимое регистра AX будет разрушено.
Return to contents.
6.8 Указатели.
В C-- сейчас указатели реализованы не в полном объеме. Поэтому многие
вещи, которые возможны в обычных языках C, здесь будут недоступны.
Пример применения указателей в C--:
char *string[4]={"string1", "string2", "string3", 0}; //массив указателей
char *str="string4";
main()
int i;
char *tstr;
{
FOR(i=0; string[i]!=0; i++){
WRITESTR(string[i]);
WRITELN();
}
FOR(tstr=str;byte *tstr!=0; tstr++){
WRITE(byte *tstr);
}
}
Указатели можно использовать при передаче параметров процедурам, а в
самих процедурах в качестве как локальных, так и параметрических
переменных. Указатели можно также использовать в структурах. Можно
использовать указатели на указатели. Введена поддержка указателей на
процедуры:
void (*proc)(); //объявление указателя на процедуру
По умолчанию указатели на процедуру являются указателями на процедуру в
стиле pascal, независимо от регистра, в котором написано имя процедуры и
режима компиляции. Если Вам необходимо, чтобы был использован другой тип
вызова, то его необходимо указать при объявлении указателя на процедуру.
При инициализации указателей компилятор не контролирует то, чем
инициализируется указатель. Т.е. Вы можете указателю на char присвоить
указатель на int или указателю на процедуру присвоить адрес переменной.
Это может вызвать ошибку в работе программы.
Return to contents.
7. Адресация.
7.1 Относительная адресация.
Изначально индексный доступ к элементам в массивах любого типа в
компиляторе осуществлялся побайтно, независимо от объявленного типа данных.
Индексы ограничены форматом поля RM процессора 8086, таким образом,
доступны только следующие форматы индексов (где индекс - значение
16-разрядной константы или постоянного выражения):
variable[index]
variable[index+BX+SI]
variable[index+BX+DI]
variable[index+BP+SI]
variable[index+BP+DI]
variable[index+SI]
variable[index+DI]
variable[index+BP]
variable[index+BX]
Начиная с версии 0.210, появилась возможность использовать в качестве
индекса переменных типа char byte int word long dword. При этом
доступ к элементам массива осуществляется в зависимости от объявленного типа
массива.
Также начиная с версии 0.210 появилась возможность использовать в
качестве индексных и базовых регистров при относительной адресации любые
32-битные регистры.
Если Вы для адресации к элементам массива будете использовать регистры и
числовые константы, из которых можно получить поле RM для инструкций 8086
процессора или комбинацию полей RM BASE и SIB для 80386 процессора, то
компилятор будет использовать эти регистры для генерации инструкции с этими
полями. В результате Вы получите относительную побайтную адресацию к
элементам массива.
Если же из этих регистров невозможно получить поля RM, BASE, SIB,
или для адресации будет использована переменная, то компилятор сначала
вычислит это выражение в регистр (E)SI или другой, подходящий регистр, а
затем умножит содержимое этого регистра на разрядность Вашего массива. Таким
образом, в этом случае вы будете иметь поэлементную адресацию в массиве.
Пример:
AX = var [ 5 ];
AX = var [ BX + 5 ];
AX = var [ BX + CX ];
AX = var [ i ];
Компилятор сгенерирует следующий код:
test.c-- 7: AX=var[5];
0100 A12501 mov ax,[125h]
test.c-- 8: AX=var[BX+5];
0103 8B872501 mov ax,[bx+125h]
test.c-- 9: AX=var[BX+CX];
0107 89DE mov si,bx
0109 01CE add si,cx
010B 01F6 add si,si
010D 8B842001 mov ax,[si+120h]
test.c-- 10: AX=var[i];
0111 8B362201 mov si,[122h]
0115 01F6 add si,si
0117 8B842001 mov ax,[si+120h]
Как Вы видите, первые два выражения были преобразованы в одну
ассемблерную инструкцию, и получилась побайтная адресация. В двух следующих
выражениях получить одну ассемблерную инструкцию не удалось и компилятор
применил для этих выражений поэлементную адресацию.
Такой двойственный подход реализован с целью сохранения совместимости
новых возможностей с предыдущими.
Несмотря на кажущуюся для неискушенного пользователя путаницу, этот
механизм легко понять и запомнить по следующему простому правилу: если Вы
используете в качестве индекса только цифровое значение или регистр BX, SI,
DI, BP или любой 32-битный регистр, то компилятор сгенерирует код с
побайтной адресацией. Если же в качестве индекса будет использована
переменная, то компилятор сгенерирует код с поэлементной адресацией. Если
же Вы хорошо знакомы с ассемблером, то Вам не составит большого труда
понять в каких случаях Вы получите побайтную, а в каких поэлементную
адресацию.
Иногда требуется иметь побайтный доступ к элементам массива используя в
качестве индекса переменную. Например
AX=var[i];
Для этого выражения будет сгенерирована поэлементная адресация, а нам
нужна побайтовая. Для этого можно написать так:
SI=i;
AX=var[SI];
Но можно это записать короче:
AX=DSWORD[#var+i];
В обоих этих случаях Вы получите побайтную адресацию к элементам массива
var. В первом варианте Вы сможете контролировать какой регистр будет
использован в качестве индекса, а во втором варианте компилятор будет сам
выбирать регистр для использования в качестве индекса.
Важно всегда помнить о двойственном подходе компилятора к вычислению
адреса в массиве. Еще раз кратко: если Вы в массиве адресуетесь используя
числовую константу или регистры BX,DI,SI,BP компилятор использует эти
значения без изменения. Во всех других случаях будет коррекция значения в
зависимости от типа массива.
Return to contents.
7.2 Абсолютная адресация.
Абсолютная адресация также возможна. Действуют те же самые ограничения
на индексы, что и при относительной адресации.
Вычисленный индекс будет абсолютен в сегменте, регистр которого указан.
Можно указывать любой из регистров DS, CS, SS и ES. На процессорах 80386 и
более новых можно указывать также регистры FS и GS.
Синтаксис - точно такой же, как и в относительной адресации, за
исключением того, что указывается не переменная, а сегмент и тип данных.
Могут применяться следующие указатели:
// адресация в сегменте данных
DSBYTE [смещение] // адресует байт в сегменте DS
DSWORD [смещение] // адресует слово в сегменте DS
DSCHAR [смещение] // адресует char в сегменте DS
DSINT [смещение] // адресует int в сегменте DS
DSDWORD [смещение] // адресует dword в сегменте DS
DSLONG [смещение] // адресует long в сегменте DS
DSFLOAT [смещение] // адресует float в сегменте DS
// адресация в сегменте кода
CSBYTE [смещение] // адресует байт в сегменте CS
CSWORD [смещение] // адресует слово в сегменте CS
CSCHAR [смещение] // адресует char в сегменте CS
CSINT [смещение] // адресует int в сегменте CS
CSDWORD [смещение] // адресует dword в сегменте CS
CSLONG [смещение] // адресует long в сегменте CS
CSFLOAT [смещение] // адресует float в сегменте CS
// адресация в сегменте стека
SSBYTE [смещение] // адресует байт в сегменте SS
SSWORD [смещение] // адресует слово в сегменте SS
SSCHAR [смещение] // адресует char в сегменте SS
SSINT [смещение] // адресует int в сегменте SS
SSDWORD [смещение] // адресует dword в сегменте SS
SSLONG [смещение] // адресует long в сегменте SS
SSFLOAT [смещение] // адресует float в сегменте SS
// адресация в дополнительном сегменте данных
ESBYTE [смещение] // адресует байт в сегменте ES
ESWORD [смещение] // адресует слово в сегменте ES
ESCHAR [смещение] // адресует char в сегменте ES
ESINT [смещение] // адресует int в сегменте ES
ESDWORD [смещение] // адресует dword в сегменте ES
ESLONG [смещение] // адресует long в сегменте ES
ESFLOAT [смещение] // адресует float в сегменте ES
// адресация в дополнительном сегменте 2 (80386) +
FSBYTE [смещение] // адресует байт в сегменте FS
FSWORD [смещение] // адресует слово в сегменте FS
FSCHAR [смещение] // адресует char в сегменте FS
FSINT [смещение] // адресует int в сегменте FS
FSDWORD [смещение] // адресует dword в сегменте FS
FSLONG [смещение] // адресует long в сегменте FS
FSFLOAT [смещение] // адресует float в сегменте FS
// адресация в дополнительном сегменте 3 (80386) +
GSBYTE [смещение] // адресуют байт в сегменте GS
GSWORD [смещение] // адресуют слово в сегменте GS
GSCHAR [смещение] // адресуют char в сегменте GS
GSINT [смещение] // адресуют int в сегменте GS
GSDWORD [смещение] // адресуют dword в сегменте GS
GSLONG [смещение] // адресуют long в сегменте GS
GSFLOAT [смещение] // адресует float в сегменте GS
Примеры:
Загрузить в AL байт из ячейки с шестнадцатеричным адресом 0000:0417
ES = 0x0000;
AL = ESBYTE [0x417];
Переместить слово из ячейки с шестнадцатеричным адресом 2233:4455
в ячейку с шестнадцатеричным адресом A000:0002
$PUSH DS
DS = 0x2233;
ES = 0xA000;
ESWORD [0x0002] = DSWORD [0x4455];
$POP DS
Сохранить вычисленное значение выражения X + 2, имеющее
тип int в ячейке с шестнадцатеричным адресом FFFF:1234
ES = 0xFFFF;
ESINT [0x1234] = X + 2;
Сохранить BX в сегменте стека по смещению 42:
SSWORD [42] = BX;
Return to contents.
8. Работа с блоками данных.
8.1 Структуры.
8.1.1 Что такое структуры.
Структура позволяет объединить в одном объекте совокупность значений,
которые могут иметь различные типы.
Return to contents.
8.1.2 Синтаксис.
struct [<тег>] { <список-объявлений-элементов> }
<описатель>[,<описатель>...];
struct <тег> <описатель> [,<описатель>];
Объявление структуры начинается с ключевого слова struct и имеет две
формы записи.
В первой форме типы и имена элементов структуры специфицируются в
списке-объявлений-элементов. Необязательный в данном случае тег - это
идентификатор, который именует структурный тип, определенный данным
списком объявлений элементов. описатель специфицирует либо переменную
структурного типа, либо массив структур данного типа.
Вторая синтаксическая форма объявления использует тег структуры для
ссылки на структурный тип, определенный где-то в другом месте программы.
Список объявлений элементов представляет собой последовательность из
одной или более объявлений переменных. Каждая переменная, объявленная в
этом списке, называется элементом структуры.
Элементы структуры запоминаются в памяти последовательно в том
порядке, в котором они объявляются. Выравнивание элементов внутри
структуры по умолчанию не производится. Но существует опция, включение
которой в командную строку позволяет иметь выравнивание и внутри
структуры. Сама структура выравнивается на четный адрес если включено
выравнивание.
Примеры объявлений структур:
struct test
{
int a;
char b[8];
long c;
} rr, ff[4];
В этом примере объявлены структура с именем rr и массив из 4 структур
с именем ff. Всему набору переменных присвоено название (тег) test. Этот
тег можно использовать для объявления других структур. Например:
struct test dd;
Здесь объявлена структура с именем dd, имеющая набор элементов
описанных в теге test.
При объявлении структур с ранее объявленным тегом ключевое слово
struct можно не писать. Т.е можно написать вот так:
test dd;
Return to contents.
8.1.3 Инициализация структур при объявлении.
После объявления структуры ее элементы могут принимать произвольные
значения. Что бы этого не было надо структуры проинициализировать.
Инициализировать структуры при их объявлении можно только глобальные. C--
поддерживает несколько способов инициализации структур при их объявлении:
1. Одним значением:
struct test dd=2;
В этом примере всем элементам структуры dd присваивается значение 2.
2. Массивом значений:
struct test dd={1,2,,6};
В этом примере первому элементу структуры dd присваивается значение 1,
второму - 2, четвертому - 6. Пропущенным и не доинициализированным
значениям будет присвоено 0 значение.
3. Командой FROM:
struct test dd=FROM "file.dat";
В этом примере на место где расположена структура dd при компиляции будет
загружено содержимое файла . Если размер файла больше чем размер
структуры, то лишние байты будут загружены в код программы, но они не
будут востребованы. Если размер файла меньше чем размер структуры, то
недостающие байты структуры будут заполнены нулями.
4. Командой EXTRACT:
struct test dd=EXTRACT "file.dat", 24, 10;
В этом примере на место где расположена структура dd при компиляции будет
загружен фрагмент из файла file.dat длиной 10 байт со смещения 24.
Недостающие байты будут заполнены нулями.
Return to contents.
8.1.4 Инициализация структуры при выполнении программы.
При выполнении программы, кроме присвоения каждому элементу структуры
значения, можно проинициализировать всю структуру присвоением ей числа или
переменной. Примеры:
void proc()
struct test aa[5],rr;
int i;
{
aa[0]=0x12345678;
aa[i]=int 0x12345678;
aa=long 0x12345678;
rr=i;
В первом примере память, занимаемая первой структурой массива из 5
структур, будет заполнена байтом 0x78 (по умолчанию).
Во втором примере память, занимаемая (i+1)-вой структурой массива из 5
структур, будет заполнена словом 0x5678.
В третьем примере память, занимаемая всем массивом из 5 структур, будет
заполнена длинным словом 0x12345678.
В четвертом примере память, занимаемая структурой rr, будет заполнена
содержимым переменной i.
Можно также копировать содержимое одной структуры в другую. Например:
rr=aa[2];
Будет скопировано содержимое третьей структуры массива структур aa в
структуру rr.
Return to contents.
8.1.5 Операции с элементами структур.
С элементами структур можно выполнять все те операции, которые
доступны для переменных соответствующего типа. Например: Объявлена
структура:
struct test
{
int a;
char b[8];
long c;
} rr[3];
Пример допустимого синтаксиса:
rr.a = rr.b[i] * rr[1].c + i ;
Примечание:
При операциях с элементами массива структур и с индексированными
элементами, в которых в качестве индекса или номера структуры используется
переменная, компилятор может использовать регистры SI и DI, а в некоторых
ситуациях (например: rr[i].b[j] >< rr[i+1].b[j+2] ) будет задействован и
регистр DX.
Для отдельных элементов структуры, можно получать их адрес, размер
и смещение в теге структуры. Вот пример:
struct AA //объявление тега структуры
{
word a[3]; // первый элемент структуры
char b; // второй элемент структуры
long c; // третий элемент структуры
};
struct BB //тег второй структуры
{
word aa; // первый элемент
AA bb; // второй элемент - вложенная структура
}ss; // объявляем структуру с тегом BB
void proc()
{
AX=#ss.bb.b; // получить адрес элемента b структуры bb в структуре ss
AX=#BB.bb.b; // получить смещение этого же элемента в теге BB
AX=sizeof(ss.bb); // получить размер элемента bb в структуре ss
AX=sizeof(BB.bb); // получить размер элемента bb в теге BB
}
Return to contents.
8.1.6 Вложенные структуры.
При объявлении тегов структур можно использовать теги других,
объявленных ранее структур. Пример вложенных структур:
struct RGB
{
byte Red;
byte Green;
byte Blue;
byte Reserved;
};
struct BMPINFO
{
struct BMPHEADER header; //описание этой структуры пропущено
struct RGB color[256];
}info;
Предположим Вам нужно получить содержимое переменной Red десятого
элемента color. Это можно будет записать так:
AL=info.color[10].Red;
Но существует одно ограничение использования вложенных структур в C--.
Это невозможность использования переменной в качестве индекса более одного
раза при обращении к многоэкземплярным структурам. Поясним это на примере:
struct ABC
{
int a;
int b;
int c;
};
struct
{
struct ABC first[4]; //4 экземпляра структуры ABC
int d;
}second[4];
int i,j;
void proc()
{
AX=second[i].first[j].a; //такая запись вызовет сообщение об ошибка, так
//как переменная использовалась в двух местах
AX=second[2].first[j].a; //а этот синтаксис допустим.
AX=second[i].first[3].a;
}
Return to contents.
8.1.7 Отображение тега структуры на блок памяти.
Отображение тега структуры на блок памяти является альтернативой
указателям на структуры.
Альтернативный способ использования указателей на структуры позволит
Вам самим выбрать регистр, в котором будет хранится адрес структуры и
самим следить за его сохранностью и по мере необходимости восстанавливать
его содержимое.
Объяснить, как использовать отображение тега структуры на память,
наверное, будет проще на примере:
struct AA //объявление тега структуры
{
word a[3]; // первый элемент структуры
char b; // второй элемент структуры
long c; // третий элемент структуры
};
byte buf[256]; //блок памяти, на который будет отображен тег структуры
void proc1()
{
...
proc2 ( #buf ); // вызов процедуры с передачей ей в качестве параметра
// адреса блока памяти
...
}
long proc2 (unsigned int pointer_to_mem)
{
int i;
BX=pointer_to_mem; // в BX загрузим адрес блока памяти
FOR(i=0; i<3; i++){ // в массив элемента a записать -1
BX.AA.a[i]=-1;
}
BX.AA.b=0;
ES:BX.AA.c=EAX;
return BX.AA.c; // вернуть содержимое элемента c
}
В 16-битном режиме для хранения адреса структуры можно использовать
регистры: BX,DI,SI,BP. Но лучше для этого использовать регистр BX.
Регистры DI и SI может использовать компилятор при вычислении адреса
многоэлементных объектов. Регистр BP компилятор использует для работы с
локальными и параметрическими переменными. В 32-битном режиме можно
использовать любой кроме ESP и EBP регистр, а регистры EDI и ESI надо
использовать осторожно.
Return to contents.
8.1.8 Битовые поля структур.
Битовые поля структур используются для экономии памяти, поскольку
позволяют плотно упаковать значения, и для организации удобного доступа к
регистрам внешних устройств, в которых различные биты могут иметь
самостоятельное функциональное назначение.
Объявление битового поля имеет следующий синтаксис:
<тип> [<идентификатор>]:<константа>;
или на примере:
int var:5; //объявление битового поля размером 5 бит с именем var
Битовое поле состоит из некоторого числа битов, которое задается
числовым выражением константа. Его значение должно быть целым
положительным числом и его значение не должно превышать числа разрядов,
соответствующие типу определяемого битового поля. В C-- битовые поля
могут содержать только без знаковые значения. Нельзя использовать массивы
битовых полей, указатели на битовые поля.
идентификатор именует битовое поле. Его наличие необязательно.
Неименованное битовое поле означает пропуск соответствующего числа битов
перед размещением следующего элемента структуры. Неименованное битовое
поле, для которого указан нулевой размер, имеет специальное назначение:
оно гарантирует, что память для следующего битового поля будет начинаться
на границе того типа, который задан для неименованного битового поля.
Т.е. будет произведено выравнивание битового поля на 8/16/32 бита.
В C-- все битовые поля упаковываются одно за другим независимо от
границ типа идентификаторов. Если последующее поле не является битовым
полем, то оставшиеся до границы байта биты не будут использованы.
Максимальный размер битового поля равен 32 бита для типа dword/long, 16
бит для типа word/int и 8 бит для типа byte/char. Битовые поля можно
объединять, т.е. использовать их в операторе union. sizeof
примененный к битовому полю вернет размер этого поля в битах. При
использовании битового поля, его содержимое будет расширятся в регистр
как без знаковое целое число.
Return to contents.
8.2 Объединения.
Объединения позволяют в разные моменты времени хранить в одном объекте
значения различного типа.
Память, которая выделяется под объединение, определяется размером
наиболее длинного из элементов объединения. Все элементы объединения
размещаются в одной и той же области памяти с одного и того же адреса.
Значение текущего элемента объединения теряется, когда другому элементу
объединения присваивается значение.
В C-- реализованы так называемые анонимные объединения. Т.е.
объединениям не присваивается имя, а обращение к элементам объединения
происходит как к обычной переменной. Пример:
union
{
dword regEAX;
word regAX;
byte regAL;
}; // объявили, что 3 переменные расположены по одному и тому же
// физическому адресу
void test()
{
regEAX = 0x2C;
BL = regAL; //в регистре BL окажется значение 0x2C
}
Объединять можно переменные различных типов, массивы, строковые
переменные и структуры. Объединения могут быть глобальными и локальными, а
также располагаться внутри структур (пока в объединениях внутри структур
нельзя использовать структуры). Глобальные объединения могут быть
инициализированными и неинициализированными. Чтобы получить
инициализированное объединение достаточно проинициализировать лишь первый
элемент объединения. Если же первый элемент объединения не инициализирован,
а следующие элементы инициализированы, то это вызовет сообщение компилятора
об ошибке.
Return to contents.
8.3 Команды 'FROM' и 'EXTRACT'.
В C-- есть очень оригинальные команды, которых нет в других языках. Это
FROM и EXTRACT.
Команда FROM имеет синтаксис:
<тип_переменной> <имя_переменной> = FROM <имя_файла>;
Встретив эту команду при компиляции, компилятор загрузит в выходной
файл содержимое файла имя_файла, а имя_переменной будет идентификатором
начала загруженного кода. Вот пример использования этой команды из файла
tinydraw.c--:
byte palette[PALSIZE] = FROM "TINYDRAW.PAL"; // buffer for palette
Команда EXTRACT имеет синтаксис:
<тип_переменной> <имя_переменной> = EXTRACT <имя_файла>, <начало>, <длина>;
Встретив эту команду при компиляции, компилятор загрузит в выходной
файл из файла имя_файла число байт равное длина со смещения начало, а
имя_переменной будет идентификатором начала загруженного кода. Вот пример
использования этой команды:
byte LIT128 = EXTRACT "8X16.FNT", 16*128, 16;
byte LIT130 = EXTRACT "8X16.FNT", 16*130, 16;
Return to contents.
9. Операторы.
9.1 Условные инструкции.
Условные инструкции, при помощи которых осуществляется ветвление, такие
же как в C.
C-- имеет две инструкции ветвления. if и IF.
if делает близкий условный переход, а IF делает короткий
(8-разрядный) условный переход. IF выполняется быстрее и может экономить
до 3 байт в размере кода, но может осуществлять переходы только в пределах
127 байтов кода.
Условные инструкции, как и в C, могут сопровождаться, как одиночной
командой, так и блоком из нескольких команд, заключенных в фигурные скобки
{ и }. Условные инструкции имеют те же ограничения, что и условные
выражения.
Если за инструкцией IF следует больше чем 127 байтов кода, транслятор
выдаст следующее сообщение об ошибке:
IF jump distance too far, use if.
Это можно просто исправить, заменив в этом месте инструкцию IF на if.
Команды else и ELSE используются точно так же, как в языке C.
Отличие их в том, что ELSE имеет ограничение адреса перехода 127 байт,
такое же как IF. else генерирует код на 1 байт длиннее, чем ELSE.
Команды IF и else, а также if и ELSE могут свободно смешиваться
как в следующем примере:
if( x == 2 )
WRITESTR("Two");
ELSE{ WRITESTR("not two.");
printmorestuff();
}
Если за инструкцией ELSE следует больше чем 127 байтов кода,
транслятор выдаст следующее сообщение об ошибке:
ELSE jump distance too far, use else.
Это можно просто исправить, заменив в этом месте инструкцию ELSE на
else.
Return to contents.
9.2 Циклы do{} while.
В таком цикле блок кода, составляющий тело цикла, будет повторяться,
пока условное выражение имеет значение истинно.
Истинность условного выражения проверяется после выполнения тела цикла,
поэтому блок кода будет выполнен, по крайней мере, один раз.
Пример do {} while цикла, в котором тело будет исполнено пять раз:
count = 0;
do {
count++;
WRITEWORD(count);
WRITELN();
} while (count < 5);
Условное выражение в do {} while инструкции должно соответствовать тем же
правилам, что и в инструкциях IF и if.
Return to contents.
9.3 Циклы loop, LOOPNZ, loopnz.
Циклы loop повторяют блок кода, пока определенная переменная или
регистр, выполняющие роль счетчика цикла, содержат значение, отличное от
нуля. В конце выполнения блока кода, составляющего тело цикла, указанная
переменная или регистр - уменьшается на 1, а затем проверяется на равенство
нулю. Если переменная (или регистр) не равна нулю, тело цикла будет
выполнено снова, и процесс повторится.
Пример использования цикла loop в котором в качестве счетчика цикла
использована переменная:
count = 5;
loop( count )
{WRITEWORD(count);
WRITELN();
}
Наибольший эффект дает использование регистра CX для циклов с небольшим
телом, поскольку в этом случае компилятором генерируется цикл с применением
машинной команды LOOP.
Если перед стартом счетчик циклов содержит нулевое значение, команды
тела цикла будут выполнены максимальное число раз для диапазона переменной
(256 раз для 8-битного счетчика (переменной типа byte или char), 65536 для
16-битного счетчика (переменной типа word или int), и 4294967296 для
32-битного счетчика (переменной типа dword или long).
В следующем примере цикл будет выполнен 256 раз:
BH = 0;
loop (BH)
{
}
Если в команде не указано никакого счетчика цикла, цикл будет
продолжаться бесконечно.
Следующий пример будет непрерывно выводить символ звездочки (*) на
экран:
loop()
WRITE('*');
Программист, если хочет, может использовать или изменять значение
переменной счетчика цикла внутри цикла.
Например, следующий цикл выполнится только 3 раза:
CX = 1000;
loop( CX )
{
IF( CX > 3 )
CX = 3;
}
Цикл можно также прервать оператором разрыва BREAK или break. Вот
тот же пример с использованием BREAK:
CX = 1000;
loop( CX )
{
IF( CX > 3 )
BREAK;
}
Циклы LOOPNZ/loopnz отличаются от цикла loop, тем, что перед входом
в цикл проверяется равенство нулю аргумента цикла. Если аргумент равен
нулю, то тело цикла ни разу не выполнится (в цикле loop в этом случае
тело цикла выполнится максимальное число раз). Цикл LOOPNZ получается
максимально эффективным при оптимизации на размер кода, если в качестве
параметра-счетчика используется регистр CX/ECX. При этом компилятор
использует ассемблерные инструкции JCXZ/JECXZ и LOOP.
Return to contents.
9.4 Цикл while, WHILE.
Синтаксис:
while(<выражение>)
<оператор>
Цикл выполняется до тех пор, пока значение выражения не станет
ложным. Вначале вычисляется выражение. Если выражение изначально ложно,
то тело оператора while вообще не выполняется и управление сразу
передается на следующий оператор программы.
Цикл WHILE аналогичен циклу while, но при этом генерируется код на
3 байта короче. Размер сгенерированного кода в цикле WHILE должен быть
меньше 127 байт.
Примеры:
while ( i < 20 ){
WRITEWORD(i);
i++;
}
WHILE (i < 20 ) @WRITEWORD(i); //цикл либо будет бесконечным либо не
//выполнится ни разу
Return to contents.
9.5 Цикл for, FOR.
Синтаксис:
for ([<начальное выражение>]; [<условие>]; [<приращение>])
<оператор>
Цикл for выполняется до тех пор, пока значение условия не станет
ложным. Если условие изначально ложно, то тело оператора for вообще не
выполняется и управление сразу передается на следующий оператор программы.
Начальное выражение и приращение обычно используются для инициализации
и модификации параметров цикла.
Первым шагом при выполнении for является вычисление начального
выражения, если оно имеется. Затем вычисляется условие и производится
его оценка следующим образом:
1) Если условие истинно, то выполняется тело оператора. Затем
вычисляется приращение (если оно есть), и процесс повторяется.
2) Если условие опущено, то его значение принимается за истину. В
этом случае цикл for представляет бесконечный цикл, который может
завершиться только при выполнении в его теле операторов break, goto,
return.
3) Если условие ложно, то выполнение цикла for заканчивается и
управление передается следующему оператору.
Цикл FOR аналогичен циклу for, но при этом генерируется код на 3
байта короче. Размер сгенерированного кода в цикле FOR должен быть меньше
127 байт.
Примеры:
for(i=0;i<5;i++){
WRITESTR("СТРОКА ");
WRITEWORD(i);
WRITELN();
}
Число начальных выражений и число приращений не ограничено. Каждый
оператор в начальных выражениях и приращениях должен разделяться
запятой. Пример:
for ( a=1, b=2 ; a<5 ; a++, b+=a ) {...
Также есть возможность логического объединения условий. Объединять
можно до 32 условий. Каждое объединяемое условие должно быть заключено в
скобки. Пример:
for ( a=0 ; (a>=0) && (a<10) ; a++ ){...
Return to contents.
9.6 Оператор переключатель switch.
Синтаксис:
switch(<выражение>){
case <константа>:
<оператор>
...
case <константа>:
<оператор>
...
...
default:
<оператор>
}
Оператор переключатель switch предназначен для выбора одного из
нескольких альтернативных путей выполнения программы. Выполнение начинается
с вычисления значения выражения. После этого управление передается одному
из операторов тела переключателя. В теле переключателя содержатся
конструкции: case константа:, которые синтаксически представляют собой
метки операторов. Оператор, получающий управление, - это тот оператор,
значение константы которого совпадают со значением выражения
переключателя. Значение константы должно быть уникальным.
Выполнение тела оператора-переключателя switch начинается с выбранного
таким образом оператора, и продолжается до конца тела или до тех пор, пока
какой-либо оператор не передаст управление за пределы тела.
Оператор, следующий за ключевым словом default, выполняется, если ни
одна из констант не равна значению выражения. Если default опущено, то
ни один оператор в теле переключателя не выполняется, и управление
передается на оператор, следующий за switch.
Для выхода из тела переключателя обычно используется оператор разрыва
break (BREAK).
Пример:
switch (i){
case 'A':
WRITE(i);
i++;
BREAK;
case 32:
WRITE('_');
i++;
BREAK;
default:
WRITE('i');
}
Оператор switch сейчас в компиляторе может реализовываться трем
способами: двухтабличным, табличным и методом последовательных проверок.
Табличный метод является самым быстрым, а при большом числе операторов
case и при незначительной разнице между максимальным и минимальным
значениями case он еще может быть и более компактным. Но у него есть и
недостатки: в 16-битном режиме компилятор всегда использует регистр BX, а в
32-битном режиме, если операндом switch является регистр, то его значение
будет разрушено.
В методе последовательных проверок блок сравнений находится в начале
тела оператора switch, это позволяет избавиться от 1-2 лишних jmp. Но
компилятор не может определить, какой тип перехода использовать при
проверке значений case. Это будет Вашей заботой. Если размер кода от
начала тела оператора switch до места расположения оператора case
меньше 128 байт, можно использовать короткий переход. В этом случае Вы
можете указать оператор CASE, что приведет к генерации более компактного
кода. Компилятор в предупреждениях будет Вам подсказывать о возможности
использования операторов CASE. Использование оператора CASE в случаях,
когда размер блока кода более 128 байт приведет к выдаче компилятором
сообщения об ошибке.
При двухтабличном методе создаются две таблицы - таблица адресов входа в
тело оператора switch/SWITCH и таблица значений case. Генерируется
процедура сравнения входного значения со значениями во второй таблице. Если
есть совпадение, то делается переход по адресу из второй таблицы. Этот
метод является самым медленным, но при большом числе значений case (более
15) он становится самым компактным.
При оптимизации кода на размер, компилятор предварительно вычисляет
размер кода, который может быть получен всеми методами и реализует самый
компактный. При оптимизации на скорость преимущество отдается табличному
методу, если размер таблицы получается не слишком большим.
Для оператора switch введена также и короткая его форма - SWITCH.
Ее можно применять в случае, если размер блока кода между началом тела
оператора и оператором default (если он отсутствует, то концом тела
оператора switch) меньше 128 байт. О возможности использования короткой
формы компилятор будет сообщать в предупреждениях.
Для оператора case/CASE, который может использоваться только в теле
блока оператора switch/SWITCH, можно указывать диапазон значений. Сначала
надо указывать меньшее значение, затем после многоточия большее. Пример:
switch(AX){
case 1...5:
WRITESTR("Range AX from 1 to 5");
BREAK;
};
Раньше Вам бы пришлось писать более громоздкую конструкцию:
switch(AX){
case 1:
case 2:
case 3:
case 4:
case 5:
WRITESTR("Range AX from 1 to 5");
BREAK;
};
Кроме того, что новый формат записи более компактен и более читабелен,
но еще при этом компилятор создает более компактный и быстрый код.
Return to contents.
9.7 Оператор перехода goto, GOTO.
Синтаксис:
goto <метка>;
.
.
.
<метка>:
Оператор перехода goto передает управление на оператор помеченный
меткой. Аналогом в ассемблере оператору goto является команда jmp near.
Аналогом в ассемблере оператору GOTO является команда jmp short.
Return to contents.
9.8 Оператор разрыва break, BREAK.
Оператор разрыва break прерывает выполнение операторов do-while,
for, switch, while, loop, loopnz, LOOPNZ. Он может содержаться
только в теле этих операторов. Управление передается оператору, следующему
за прерванным циклом.
Оператор BREAK аналогичен break, но при этом генерируется код на 1
байт короче. Размер сгенерированного кода от места где применяется BREAK
до конца цикла должен быть меньше 127 байт.
Примеры:
FOR (i=0; ; i++){
FOR(j=0; j < WIDTH; j++){
IF(i==5)BREAK;
}
IF(i==10)BREAK;
}
Return to contents.
9.9 Оператор продолжения continue, CONTINUE.
Оператор продолжения continue передает управление на следующую
итерацию в циклах do-while, for, while, loop, loopnz. В циклах
do-while, while, loop следующая итерация начинается с вычисления
условного выражения. Для цикла for следующая итерация начинается с
вычисления выражения приращения, а затем происходит вычисление условного
выражения.
Оператор CONTINUE аналогичен continue, но при этом генерируется код на
1 байт короче. Размер сгенерированного кода от места где применяется
CONTINUE до начала итерации должен быть меньше 127 байт.
Return to contents.
9.10 Логическое объединение условий.
Существует возможность логического объединения сравнений в условиях
IF и if, циклах do{}while, while{}, WHILE{}, for{} и FOR{}.
Каждое сравнение при их логическом объединении должно быть заключено в
скобки. Объединять можно не более 32 сравнений.
В отличие от C в C-- анализ логических объединений происходит слева
направо и все лишние скобки будут восприняты компилятором как ошибочные.
Это несколько снижает гибкость и возможности применения этих объединений,
но такова традиция и философия, заложенная в C--.
Пример:
if ( (a>3) && (b>4) || (c<8) ){
Т.е. если произвести расшифровку этого условия, то получится следующее:
условие выполнится если a>3 и b>4 или a>3 и c<8.
Return to contents.
9.11 Переход через циклы.
Для операторов BREAK, break, CONTINUE, continue введена
поддержка числового параметра, определяющего, сколько циклов надо
пропустить, прежде чем будет выполнен этот оператор. Например, мы имеем три
вложенных цикла:
do{
loop(CX){
for(BX=0;BX<10;BX++){
break; //стандартный оператор
break 0; //break с параметром - пропустить 0 циклов
break 1; //break с параметром - пропустить 1 цикл
break 2; //break с параметром - пропустить 2 цикла
}
LABL0:
}
LABL1:
}while (DX!=0);
LABL2:
В третьем цикле находится группа различных вариантов оператора break.
Первым стоит стандартный оператор break, при выполнении которого
управление будет передаваться за пределы третьего цикла - на метку LABL0.
Вторым идет оператор break 0, при выполнении которого будет пропущено 0
циклов и управление будет передано опять же на метку LABL0. Таким
образом, запись break и break 0 являются синонимами. Третьим идет
оператор break 1, при выполнении которого будет пропущен один цикл и
управление будет передано за пределы второго цикла на метку LABL1. Ну и
наконец, последним идет оператор break 2, при выполнении которого
компилятор пропустит два цикла и передаст управление за пределы третьего,
на метку LABL2. Метки в этом примере проставлены для удобства объяснения.
Ну и я надеюсь, Вам понятно, что значение параметра не может превышать
числа циклов находящихся перед текущим. Так для одиночного цикла этот
параметр может принимать максимальное и единственное значение - 0.
Return to contents.
9.12 Инвертирование флага проверки условий.
Инвертирование флага проверки условий в операциях сравнения if/IF
for/FOR while/WHILE происходит с помощью символа ! - not.
Выражени
IF ( NOTCARRYFLAG )... и IF ( ! CARRYFLAG )...
IF ( proc() == 0 )... и IF ( ! proc() ) ...
являются синонимами.
Return to contents.
9.13 Вычисление выражения, а затем проверка условия.
В операциях сравнения в левом операнде теперь допустимо использовать
вычисления выражения с присваиванием и операции инкремента, декремента.
Например:
IF (i=a+2 != 0 )...
IF ( i++ )...
IF ( a-- )...
IF ( i+=4 == 0 )...
Во всех этих примерах сначала произойдет вычисление выражения в левой
части операции сравнения, а потом будет произведено сравнение результата с
правой частью выражения сравнения.
Return to contents.
9.14 Проверка битов при операции сравнения.
Если в левой части выражения сравнения написано: BX & 5, то при
вычислении выражения содержимое регистра BX будет изменено инструкцией
and. Но иногда возникает необходимость в проверке битов без изменения
содержимого регистра BX. Для этих целей надо использовать инструкцию
test. Как же указать компилятору, в каких ситуациях использовать
инструкцию and, а в каких test? В стандартных языках C для этого
используется механизм приоритетов - если выражение заключено в скобки, то
производится его вычисление, если нет, то производится проверка. Но C-- не
поддерживает приоритетов. Для разрешения этой проблемы в C-- решено
использовать непосредственно саму инструкцию test. Вот допустимые
варианты синтаксиса:
IF ( $test AX,5 )
IF ( ! $test AX,5)
IF ( asm test AX,5)
IF ( ! asm { test AX,5 } )
Return to contents.
9.15 Оператор перестановки.
В C-- есть оператор, который не встречается в других языках, это
оператор перестановки. Оператор перестановки меняет местами содержимое двух
переменных. Символьное обозначение этого оператора ><. Переменные с обеих
сторон оператора перестановки должны иметь одинаковый размер, 8 бит и 8
бит, 16 бит и 16 бит, или 32 бита и 32 бита.
Вот некоторые примеры:
AX >< BX; // сохраняет значение BX в AX и значение AX в BX
CH >< BL; // меняет местами содержимое регистров CH и BL
dog >< cat; /* меняет местами значения переменной dog и переменной cat*/
counter >< CX; // меняет местами значения переменной counter
// и содержимое регистра CX
Если перестановка осуществляется между двумя 8-разрядными переменными в
памяти, будет разрушено содержимое регистра AL. Если перестановка - между
двумя 16-разрядными переменными в памяти, будет разрушено содержимое
регистра AX. Если перестановка - между двумя 32-разрядными переменными в
памяти, будет разрушено содержимое EAX. В любом другом случае, например,
между переменной в памяти и регистром, значения всех регистров будут
сохранены.
Return to contents.
9.16 Оператор отрицания.
C-- поддерживает быстрый синтаксис смены знака переменной - оператор
отрицания. Поставив - (знак минус) перед идентификатором переменной памяти
или регистра и ; (точку с запятой) после идентификатора, вы смените знак
переменной памяти или регистра.
Вот некоторые примеры:
-AX; // результат тот же, что и при 'AX = -AX;' ,но быстрее.
-tree; // то же самое, что 'tree = -tree;' ,но быстрее.
-BH; // меняет знак BH.
Return to contents.
9.17 Оператор инверсии.
C-- поддерживает быстрый синтаксис выполнения логической инверсии
значения переменной - оператор инверсии. Поставив ! (восклицательный знак)
перед идентификатором переменной памяти или регистром и ; (точку с
запятой) после идентификатора, вы выполните логическую (выполнится
ассемблерная команда NOT) инверсию текущего значения переменной. Вот
некоторые примеры:
!AX; // то же самое, что ' AX ^ = 0xFFFF; ' но быстрее.
!node; // заменяет значение 'node' его логической инверсией.
!CL; // то же самое, что ' CL ^ = 0xFF ' но быстрее.
Return to contents.
9.18 Специальные условные выражения.
C-- поддерживает восемь специальных условных выражений:
CARRYFLAG
NOTCARRYFLAG
OVERFLOW
NOTOVERFLOW
ZEROFLAG
NOTZEROFLAG
MINUSFLAG
PLUSFLAG
Они могут использоваться вместо любых нормальных условных выражений.
Если Вы желаете, например, выполнить блок кода только если установлен флаг
переноса, Вам следует использовать следующую последовательность команд:
IF( CARRYFLAG )
{
// здесь вы чего-то делаете
}
Если Вы желаете непрерывно выполнять блок кода до тех пор, пока не
установится флаг переполнения, Вам следует использовать нечто подобное
следующему куску кода:
do {
// здесь вы опять чего-то делаете
} while( NOTOVERFLOW );
Return to contents.
9.19 Символ $ - вставляет текущий адрес программы.
Символ $, кроме того, что является признаком последующей ассемблерной
инструкции, в языке C--, как и в языке Assembler может указывать текущий
адрес (смещение) компилируемой программы. Но в C-- он имел ограниченные
возможности. Он мог быть использован лишь как аргумент в операторах
GOTO/goto и ассемблерных инструкциях DW/DD/JMP.
Этот символ может находиться в любом месте вычисляемого числового
выражения и может быть применен в любом месте совместно с другими числовыми
выражениями.
Примеры применения:
DW #main-$ //записать расстояние от процедуры main до текущего места
GOTO $+2; //перейти по адресу на 2 больше, чем текущий адрес
Return to contents.
9.20 Ключевое слово static и оператор ::.
Если перед объявлением глобальной переменной, структуры или процедуры
указать слово static, то эти переменная, структура или процедура будут
доступны только в том файле, в котором они были объявлены. Т.е. если Вы
включите этот файл в другой директивой include, то переменные объявленные
во включаемом файле со словом static не будут доступны в основном файле,
и Вы можете в основном файле объявить другие переменные с такими же
именами.
Если Вы примените слово static при объявлении локальной переменной в
процедуре, то память для этой переменной будет выделена не в стеке, а в
области данных процедуры. Но эта переменная будет доступна только внутри
процедуры, в которой она была объявлена. Применение static к локальным
переменным дает возможность сохранять значение переменной для следующего
входа в процедуру.
Слово static можно применять к любому глобальному объекту
(переменной, структуре, процедуре). Для локального использования это слово
можно применять только к переменным.
Если в Вашей программе есть глобальная и локальная переменная с
одинаковыми именами, то в процедуре, в которой объявлена эта локальная
переменная, Вы не имели доступа к одноименной глобальной переменной.
Применив перед именем переменной оператор ::, Вы получите доступ к
глобальной переменной. Пример:
int var; //объявляем глобальную переменную
void proc()
int var; //объявляем локальную переменную с именем уже существующей
//глобальной переменной
{
(E)AX=var; //имеем доступ только к локальной переменной
(E)AX=::var; //а так можно получить доступ к глобальной переменной
}
Return to contents.
9.21 Оператор sizeof.
Операция sizeof определяет размер памяти, который соответствует объекту
или типу. Операция sizeof имеет следующий вид:
sizeof (<имя типа>)
Результатом операции sizeof является размер памяти в байтах,
соответствующий заданному объекту или типу.
В C-- оператор sizeof можно применять к переменным, регистрам, типам
переменных, структурам, процедурам, текстовым строкам и файлам.
Если операция sizeof применяется к типу структуры, то результатом
является размер тега данной структуры.
Если операция sizeof применяется к текстовой строке, то результатом
операции является размер строки плюс завершающий нуль. Например:
sizeof ("Test")
результатом этой операции будет число 5. Если Вы напишите такую
конструкцию:
char a="Test";
sizeof(a)
то результатом будет 5 - размер памяти, отведенный для переменной a.
При использовании оператора sizeof с именем структуры вставляет
фактический размер памяти, занимаемый структурой. Это особенно важно, если
Вы объявили массив структур.
Оператор sizeof можно применять и к имени определенной ранее
процедуры. Результатом будет размер этой процедуры. Но для динамических
процедур всегда будет ноль.
Операцию sizeof можно применять и к файлам. Это бывает очень полезным
при использовании оператора FROM, но может применяться и в других случаях.
Пример применения оператора sizeof к файлам:
sizeof ( file "filename.dat" )
Результатом этой операции будет размер файла "filename.dat".
Return to contents.
9.22 Метки перехода.
Метки перехода применяются для указания начальных точек участков кода,
используемых командами перехода встроенного ассемблера и операторами
goto/GOTO.
Имеются два типа меток перехода: глобальные и локальные. Глобальные
метки, как следует из названия, это метки, которые видимы из любого места в
программе. Локальные метки видны только в пределах своего процедурного
блока, и не определены за его пределами.
Метки определяются идентификатором, оканчивающимися двоеточием. Если
идентификатор содержит хотя бы один символ строчных букв (букв нижнего
регистра, маленьких букв), это глобальная метка перехода, в противном
случае, это локальная метка перехода.
Глобальные метки перехода не должны использоваться внутри динамических
процедур; там можно использовать только локальные метки. Это важно помнить,
поскольку, из-за применения такого средства как макрокоманды, динамическая
процедура может присутствовать в нескольких местах кода, что будет
означать, что метке соответствует больше чем один адрес.
Метки вне процедур фактически располагаются в области данных программы.
Если данные и код находятся в одном сегменте (а именно так организованна
программа, написанная на C--), то метки вне процедур становятся простым и
эффективным методом для получения расстояний между частями программы. В
качестве имен для меток вне процедур могут быть использованы уникальные
идентификаторы, в которых можно использовать большие, маленькие и смесь
больших и маленьких букв.
Return to contents.
10. Ассемблер.
10.1 Поддержка команд ассемблера.
Встроенный в C-- ассемблер поддерживает все инструкции 8088/8086,
80286, 80386, 80486, Pentium, Pentium II и Pentium III процессоров.
Все инструкции встроенного ассемблера должны начинаться с символа
доллара $. Поддерживается также ключевое слово asm, которое являясь
синонимом к символу доллара, еще и поддерживает объединение ассемблерных
инструкций в блоки.
Return to contents.
10.2 Ключевое слово asm.
Ключевое слово asm является синонимом к $ - префикс ассемблерной
команды. После слова asm можно писать блок ассемблерных команд. Пример:
asm {
.
.
push AX
labl:
push BX
mov AX,0x1234
jmp short labl
.
.
.
}
Метки внутри блока ассемблерных команд допустимы.
Return to contents.
10.3 Префикс dup - повторение инструкций DB/DW/DD.
Для ассемблерных инструкции DB, DW, DD введена возможность использовать
префикс повторений dup. Применение этого префикса имеет следующий
синтаксис:
$DW NUMREP dup VALTOREP
NUMREP - число повторов инструкции DW.
VALTOREP - величина, которая будет повторена NUMREP раз.
В отличие от аналога этого префикса из ассемблера повторяемую величину
заключать в скобки нельзя.
Return to contents.
10.4 Инструкции процессора Pentium III.
В компилятор добавлена поддержка 19 новых инструкций MMX расширения
MASKMOVQ mmx,mmx
MOVNTQ m64,mmx
PAVGB mmx,mmx/m64
PAVGW mmx,mmx/m64
PEXTRW r32,mmx,i8
PINSRW mmx,r32/m16,i8
PMAXUB mmx,mmx/m64
PMAXSW mmx,mmx/m64
PMINUB mmx,mmx/m64
PMINSW mmx,mmx/m64
PMOVMSKB r32,mmx
PMULHUW mmx,mmx/m64
PREFETCHT0 mem
PREFETCHT1 mem
PREFETCHT2 mem
PREFETCHNTA mem
SFENCE
PSADBW mmx,mmx/m64
PSHUFW mmx,mmx/m64,i8
и 46 инструкций SSE расширения.
ADDPS xmm,m128/xmm
ADDSS xmm,xmm/m32
ANDNPS xmm,xmm/m128
ANDPS xmm,xmm/m128
COMISS xmm,xmm/m32
DIVPS xmm,m128/xmm
DIVSS xmm,xmm/m32
MAXPS xmm,m128/xmm
MAXSS xmm,xmm/m32
MINPS xmm,m128/xmm
MINSS xmm,xmm/m32
MULPS xmm,m128/xmm
MULSS xmm,xmm/m32
ORPS xmm,xmm/m128
RCPPS xmm,xmm/m128
RCPSS xmm,xmm/m32
RSQRTPS xmm,xmm/m128
RSQRTSS xmm,xmm/m32
SQRTPS xmm,m128/xmm
SQRTSS xmm,xmm/m32
SUBPS xmm,m128/xmm
SUBSS xmm,xmm/m32
UCOMISS xmm,xmm/m32
UNPCKHPS xmm,xmm/m128
UNPCKLPS xmm,xmm/m128
XORPS xmm,xmm/m128
CMPPS xmm,xmm/m128,i8
CMPSS xmm,xmm/m32,i8
SHUFPS xmm,xmm/m128,i8
CVTPI2PS xmm,m64/mmx
CVTSI2SS xmm,m32/r32
CVTPS2PI mmx,m128/xmm
CVTTPS2PI mmx,xmm/m128
CVTSS2SI r32,xmm/m128
CVTTSS2SI r32,xmm/m128
LDMXCSR m32
STMXCSR m32
MOVHLPS xmm,xmm
MOVLHPS xmm,xmm
MOVMSKPS r32,xmm
MOVNTPS m128,xmm
MOVAPS m128/xmm,xmm/m128
MOVSS xmm/m32,xmm/m32
MOVUPS xmm/m128,m128/xmm
MOVHPS xmm/m64,m64/xmm
MOVLPS xmm/m64,m64/xmm
Многие из этих инструкций могут использовать в качестве операнда
64-битные и 128-битные ячейки памяти. Компилятор C-- сейчас может работать
только с 32-битными переменными. Поэтому для инструкций использующих в
качестве операнда ячейки памяти размером больше 32-бит можно использовать
переменные любых типов. Компилятор не будет выдавать на это сообщений об
ошибке, будет использован адрес этой переменной, а сама инструкция будет
использовать нужное ей число битов памяти, начиная с адреса указанной
переменной. Например:
Для инструкции movaps один из операндов может быть 128-битной
ячейкой памяти. Для этой инструкции допустимы следующий синтаксис:
byte var8_128[16];
word var16_128[8];
dword var32_128[4];
void proc()
{
asm{
movaps var8_128,xmm0 //в массив из 16 байт будет записано содержимое XMM0
movaps xmm1,var16_128 //в XMM1 будет записано содержимое 8 слов
movaps var32_128,xmm1 //в массив из 4 двойных слов будет записано XMM1
}
}
Return to contents.
11. Процедуры.
11.1 Типы процедур, функций и макрокоманд.
Сейчас C-- поддерживает 4 типа вызова процедур: cdecl, pascal, stdcall
и fastcall. Вот краткие характеристики этих типов вызовов процедур:
cdecl Этот тип вызова процедур является по умолчанию для языка С. Он
характеризуется тем, что параметры процедуры передаются в порядке обратном
их записи. Очистка стека от параметров производится после завершения работы
процедуры. Этот способ вызова процедур очень удобен для процедур с
переменным числом параметров.
pascal Этот тип вызова предполагает, что параметры передаются в том
порядке, в котором они записаны в программе. Освобождение стека от
параметров производит сама вызываемая процедура. Этот тип вызова является
более компактным, чем cdecl.
stdcall Этот тип вызова является гибридом первых двух. Параметры
передаются процедуре в порядке обратном, тому в котором они записаны в
программе. Освобождение стека от параметров производится в самой вызываемой
процедуре.
fastcall Этот тип вызова процедур предполагает что передача параметров
процедуре производится через регистры, тем самым отпадает необходимость
освобождения стека от параметров. Для этого типа вызова процедуры
существуют ограничения по числу передаваемых параметров. Для C это три
параметра, а для C-- шесть. В C-- параметры передаются по умолчанию в
следующем порядке: 1-й - AX/EAX, 2-й - BX/EBX, 3 - CX/ECX, 4 - DX/EDX, 5 -
DI/EDI, 6 - SI/ESI. Параметры типов char или byte могут передаваться в
количестве не более 4 или только в первых 4 регистрах: 1 - AL, 2 - BL, 3 -
CL, 4 - DL. Этот порядок регистров может быть изменен, если явно указать
его либо при объявлении процедуры, либо при ее определении. Процедуры типа
fastcall иногда еще называют регистровыми.
В C-- по умолчанию, если имя процедуры написано большими буквами, то
считается, что эта процедура имеет тип вызова fastcall. Если же в имени
процедуры есть хотя бы одна маленькая буква, то по умолчанию считается, что
эта процедура имеет тип вызова pascal, за исключением программ
компилируемых с ключом /w32 /w32c или /DLL. В них по умолчанию применяется
тип вызова процедур stdcall. Если же Вы хотите изменить тип вызова процедур
из по умолчанию на любой другой, то эту процедуру надо обязательно объявить
с указанием типа желаемого вызова.
Объявление процедур введено для того, чтобы сообщать компилятору о
типе возврата из процедур, способе передачи параметров процедуре и их числе.
Return to contents.
11.2 Стековые процедуры.
Стековые процедуры по умолчанию объявляются при помощи идентификатора,
который содержит, по крайней мере, один символ строчных букв (букв нижнего
регистра, маленьких букв). Таким образом, стековые процедуры легко отличимы
от регистровых процедур, поскольку для имен регистровых процедур символы
строчных букв запрещены.
Параметры для стековых процедур, если они есть, могут иметь любой тип
byte, char, word, int, dword, long или float.
Параметры передаются в соответствии с правилами, принятыми для данного
типа процедур. Если процедура не имеет объявления, то компилятор не следит
за числом и типом передаваемых параметров. В этом случае у Вас появляется
свобода в их использовании, но Вы должны осознавать и последстви
неправильного их использования.
В списке параметров для каждого параметра указывается его тип.
Параметры одного типа, идущие подряд, разделяются запятыми. Формальные
параметры разного типа в объявлении функции разделяются символом ;.
В следующем примере стековая процедура возвращает сумму всех своих
параметров (имеющих различные типы) как величину типа word:
word add_them_all (int a,b,c; byte d,e; word x,y)
{
return( a+b+c+d+e+x+y );
}
Ранее C-- делал вызовы стековых процедур лишь в стиле pascal.
Преимуществом этого способа вызова процедур является компактность и более
простой механизм генерации кода. К недостаткам, а соответственно и
преимуществам С-стиля, можно отнести жесткую привязанность паскалевских
процедур к числу и типу передаваемых параметров (попробуйте при вызове
процедуры в стиле pascal опустить один параметр и получите 100% зависание).
Напомню некоторые технические детали обоих типов вызовов процедур.
Кадр стека C-- для близких процедур стека в стиле pascal:
АДРЕС
...
BP + FFFE предпоследний байта локальных переменных
BP + FFFF последний байт локальных переменных
BP + 0000 Сохраненный BP
BP + 0002 RET адрес
BP + 0004 последнее слово передаваемых процедуре параметров (если они
есть)
BP + 0006 предпоследнее слово передаваемых процедуре параметров
...
BP + nnnn первое слово передаваемых процедуре параметров
Освобождение стека от переданных процедуре параметров происходит прямо
в самой процедуре командой RET nnnn - где nnnn является размером переданных
в стек параметров.
Кадр стека C-- для близких процедур стека в стиле си:
АДРЕС
...
BP + FFFE предпоследний байта локальных переменных
BP + FFFF последний байт локальных переменных
BP + 0000 Сохраненный BP
BP + 0002 RET адрес
BP + 0004 первое слово передаваемых процедуре параметров (если они
есть)
BP + 0006 второе слово передаваемых процедуре параметров
...
BP + nnnn последнее слово передаваемых процедуре параметров
Процедуры в стиле С заканчиваются командой RET. Освобождение стека от
параметров происходит в том месте откуда была вызвана процедура. Обычно это
делается командой ADD SP,nnnn. Т.е. компилятор может точно знать сколько и
каких параметров Вы передаете в данном случае процедуре и соответственно
освобождает стек после завершения процедуры. Это очень удобно для процедур,
которые могут обрабатывать переменное число параметров (например, процедуры
типа printf).
Объявление процедуры имеет следующий вид:
rettype modif procname();
Первым идет необязательный тип возврата из процедур. По умолчанию он
для 16-битных программ равен word, а для 32-битных dword. Затем должен идти
также необязательный модификатор. По умолчанию все стековые процедуры в C--
(за исключением режима компиляции программ под Windows, где по умолчанию
действует стиль вызова процедур stdcall) имеют стиль pascal. Далее идет им
процедуры со скобками, которые являются признаком того что Вы объявляете
процедуру, а не переменную. Завершает объявление символ точка с запятой.
При объявлении процедур в C-- прописывать параметры процедуры
необязательно (тогда компилятор не будет контролировать число и тип
передаваемых параметров), но если Вы их вставите, то включится механизм
контроля за числом и типом параметров.
Return to contents.
11.3 Регистровые процедуры.
Регистровые процедуры определяются, по умолчанию, при помощи
идентификатора, который не содержит символов строчных букв. Или же явным
указанием что это регистровая процедура с помощью ключевого слова fastcall.
Как уже было сказано, параметры (если они есть) для регистровой
процедуры передаются через регистры. Регистровые процедуры могут иметь не
более 6 параметров. Если параметры имеют тип int или word, регистры по
умолчанию используются в следующем порядке: AX, BX, CX, DX, DI, и SI.
Первые четыре параметра могут также иметь тип char или byte, в этом случае
задействуются регистры AL, BL, CL и DL соответственно. Любой из шести
параметров может иметь тип long, dword или float, тогда для него
используется регистр EAX, EBX, ECX, EDX, EDI, или ESI.
В следующем примере регистровая процедура с именем TOGETHER возвращает
значение типа word как результат умножения первого параметра, имеющего тип
word, на второй параметр того же типа:
word TOGETHER() /* AX = первый параметр, BX = второй параметр */
{
return (AX * BX);
}
В следующем примере регистровая процедура с именем SHOW_NUM, которая не
возвращает никакого значения, зато выводит на экран первый параметр
(имеющий тип int), затем разделительный знак в виде двоеточия :, а затем
второй параметр (имеющий тип byte) :
void SHOW_NUM () /* AX = первое число, BL = второе число */
{
$ PUSH BX
WRITEINT (int AX);
WRITE (':');
$ POP BX
WRITEWORD (BL);
}
Но если в процедуре сделать объявление порядка и типов используемых
регистров, то возможно произвольное использование регистров. Более подробно
об этом можно почитать в разделе об объявлениях параметров в регистровых
процедурах.
Для того, чтобы использовать регистровую процедуру как макрокоманду,
она должна быть объявлена как динамическая процедура. Динамические
процедуры описаны в следующем подразделе.
Return to contents.
11.4 Динамические процедуры.
Динамические процедуры - процедуры, которые определены, но вставляются
в код программы, только если есть вызов. Динамические процедуры могут
использоваться как макрокоманды.
Определение динамической процедуры начинается с символа двоеточия ':'.
Пример динамической процедуры стека:
: void setvideomode (byte mode)
{
AL = mode;
AH = 0;
$ INT 0x10
}
Пример динамической регистровой процедуры:
: int ABS () /* AX = число, абсолютное значение которого ищется*/
{
IF (int AX < 0)
-AX;
}
Return to contents.
11.4.1 Установка динамической процедуры в определенное место программы.
Динамические процедуры, если они не используются как макросы и если
они были востребованы в программе, вставляются в код программы в самом
конце компиляции. В каком точно месте Вашей программы они окажутся узнать
невозможно. Если же Вам необходимо, чтобы какая-то динамическая процедура
находилась в конкретном месте программы, то это можно сделать таким
образом:
:void proc ( int par1, par2)
{
...
}
Мы имеем динамическую процедуру, код которой был бы расположен ранее
кода обычной процедуры нашей программы. Для этого перед определением этой
процедуры надо написать такую строку:
@ void proc ();
В итоге динамическая процедура будет вставлена в код программы не в
конце ее, как обычно, а в месте, где будет расположена эта строка. Если
динамическая процедура имеет параметры, то прописывать эти параметры
необязательно.
В компиляторе есть еще более мощное средство, позволяющее все
динамические объекты ( процедуры, переменные, структуры ) расположить в
указанном месте, а не в конце программы, как обычно. Это директива
#setdinproc. Встретив эту директиву, компилятор немедленно расположит все
известные ему на этот момент динамические объекты в месте объявления этой
директивы. Последующие динамические объекты будут располагаться как
обычно, в конце программы, если конечно, не будет повторно применена
директива #setdinproc.
Это может быть применено и быть полезным при создании резидентных
программ (TSR) и драйверов устройств.
Return to contents.
11.5 inline-процедуры.
inline-процедурами могут быть динамические процедуры, которые можно
использовать как макросы. Но в отличие от макросов, inline-процедуры, при
включенной оптимизации на скорость, автоматически вставляются в код, а при
оптимизации кода на размер, делается вызов их, как динамических процедур.
Но иногда бывает нужно при включенной оптимизации на размер кода, чтобы
процедуры вставлялись в код, а не делался их вызов. Для этих целей введена
директива #inline TRUE. Этой же директивой ( #inline FALSE ), можно при
оптимизации на скорость делать вызовы процедур, вместо их вставки.
Важно помнить, что статус директивы #inline автоматически меняется при
смене режима оптимизации. При установке оптимизации на скорость статус
директивы #inline устанавливается в TRUE, а при смене режима оптимизации по
размеру кода, устанавливается в FALSE. Поэтому применяйте директиву #inline
лишь после смены режима оптимизации.
Директивы меняющие режим оптимизации #codesize, #speed и директива
#inline, объявленные внутри процедуры распространяются только на оставшуюся
часть процедуры, т.е. они становятся локальными. Для того чтобы изменения
были глобальными эти директивы надо объявлять вне тела процедуры.
Для того чтобы определить inline-процедуру, надо в первой строке с
именем процедуры вместо символа динамической процедуры (:) написать
ключевое слово inline. Пример определения inline-процедуры:
inline int fastcall abs(AX)
{
IF ( int AX < 0 ) -AX ;
}
Return to contents.
11.5.1 Другое применение inline.
Ключевое слово inline имеет в процедурах и другое применение. Если
это слово расположено перед началом блока процедуры, то для такой
процедуры не создается кадр стека и не генерируется завершающий процедуру
ret. Пример:
void PROC ()
inline
{
...
}
Такие процедуры не должны содержать локальных переменных. Если
процедура является регистровой (тип fastcall), то с передачей ей
параметров нет проблем. Если же процедура является стековой, то передать
в такую процедуру параметры Вы можете, но воспользоваться этими
параметрами используя их имена, Вы уже не сможете. Это происходит потому,
что в этих процедурах кадр стека не формируется. Пример:
void proc (int par1, par2)
inline
{
AX=par1; /* компилятор обратится с параметру 'par1' через регистр BP.
Но так как кадр стека не был создан, при выполнении этого
кода программа будет работать не правильно. */
...
}
Встретив такое определение процедуры, компилятор выдаст предупреждение
о том, что в таких процедурах использовать локальные и параметрические
переменные нельзя.
Return to contents.
11.6 Процедуры обработки прерываний.
Процедуры обработки прерываний определяются следующим способом:
interrupt procedure_name ()
{
// put code here (здесь должен быть код обработки)
}
Процедуры обработки прерываний не сохраняют никаких регистров
автоматически, и никакие регистры сами по себе не загружаются перед
передачей управления обработчику прерывания, следовательно, на Вашей
совести сохранение значений регистров в стеке и последующий их возврат, а
также загрузка регистра DS нужным значением.
Вот пример обработчика прерывания, который сохраняет значения всех
регистров и загружает регистр DS:
interrupt safe_handle ()
{
$ PUSH DS
$ PUSH ES
$ PUSHA // для выполнения этой команды нужен процессор не хуже 80286
DS = CS; // здесь DS загружается для работы с моделью памяти типа tiny
/* do your thing here (здесь вы делаете свою обработку)*/
$ POPA // для выполнения этой команды нужен процессор не хуже 80286
$ POP ES
$ POP DS
}
При завершении процедуры прерывания будет автоматически сгенерирована
инструкция выхода из обработчика прерывания - IRET.
Return to contents.
11.7 Замена return на goto.
В некоторых ситуациях, при компиляции программы, оператор return
будет заменяться на goto. Это происходит при разрешенной оптимизации по
размеру кода для операторов return, которые расположены внутри процедуры
и, естественно, если размер кода для выполнения return больше, чем размер
кода для реализации goto. Для динамических процедур, которые используются
как макросы, такая замена будет производится всегда. Оператор goto будет
выполнен на конец процедуры, там, где будет располагаться единственный
выход из процедуры. В динамических процедурах, используемых в качестве
макросов, return в конце процедуры будет пропущен компилятором.
Таким образом, снято последнее ограничение на использование
динамических процедур в качестве макросов. Любая динамическая процедура
может быть использована как макрос.
Для оператора goto существует его более короткий аналог - GOTO.
Для получения более компактного кода для оператора return введен также
более короткий оператор RETURN. Его можно использовать, если от места
его применения до конца процедуры находится не более 128 байт. Если Вы
будете использовать RETURN на большем расстоянии до конца процедуры, то
компилятор выдаст сообщение об ошибке. При использовании return на
расстоянии меньше 128 байт до конца кода, компилятор выдаст вам
предупреждение о возможном использовании RETURN.
Return to contents.
11.8 Возвращаемые значения.
Возвращаемые из функций значения располагаются в регистрах. В таблице
показано, какой регистр используется для каждого из возвращаемых типов:
--------------------------------------------
| возвращаемый тип | используемый регистр |
--------------------------------------------
| byte | AL |
| word | AX |
| dword | EAX |
| char | AL |
| int | AX |
| long | EAX |
| float | EAX |
--------------------------------------------
Самый простой способ вернуть значение из функции состоит в том, чтобы
использовать команду return(), но вместо этого можно напрямую загрузить
возвращаемое значение в соответствующий регистр. Например, следующие две
функции возвращают одно и то же значение:
byte proc_one ()
{
return (42);
}
byte proc_two ()
{
AL = 42;
}
Многие DOS функции 0x21 прерывания в качестве индикатора успешного
выполнения используют установку или сброс carry флага. Использовать флаги
процессора при возврате из процедур можно и в других случаях, когда надо
иметь статус успешного или не успешного выполнения процедуры. Это позволит
более полно использовать возможности процессора и соответственно уменьшит
размер кода и повысит быстродействие программы.
Наряду с флагами, при возврате из процедур, по прежнему остается
возврат различных типов и через регистр AL/AX/EAX. Если для процедуры
объявлено, что она имеет тип возврата int и CARRYFLAG, то при использовании
такой процедуры в операциях сравнения IF, WHILE... будет делаться проверка
carry флага, а не сравнение регистра AX. Пример использования возврата
флагов из процедур:
int CARRYFLAG FOPEN(); // объявление процедуры
void proc()
{
IF ( FOPEN(name,0) ) Error ( "Not open file" );
}
Варианты допустимого синтаксиса для использования возврата флага:
IF ( ! FOPEN() )...
IF ( @ FOPEN() )...
IF ( ! @ FOPEN() )...
IF ( handl = FOPEN() )...
IF ( handl = @ FOPEN() )...
IF ( ! handl = FOPEN() )...
IF ( ! handl = @ FOPEN() )...
А вот варианты, в которых, несмотря на то, что для процедуры объявлен
возврат флага, будет производиться сравнение регистра AX:
IF ( FOPEN() == 5 )... // производится сравнение
IF ( FOPEN() + 2 )... // результат процедуры подвергается дальнейшему
// вычислению, в результате которого флаги будут
// изменены.
Return to contents.
11.9 Объявление параметров в регистровых процедурах.
Ранее каждому параметру регистровой процедуры соответствовал строго
определенный регистр. Например, для переменных типа int или word первый
параметр передавался через регистр AX, 2-й - BX, 3-й - CX, 4-й - DX, 5-й -
DI, 6-й - SI. Поэтому, если Вам было необходимо передать только один
параметр через регистр SI, то приходилось перед ним писать пять запятых.
Вот как, например, выглядит вызов процедуры STRCPY:
void main ()
{
STRCPY ( , , , , #dest, #sourc ) ;
}
Теперь регистры могут располагаться при передаче параметров
произвольным образом. Надо только объявить компилятору о том, какой регистр
закреплен за каким параметром данной процедуры. После такого объявления
компилятор будет сам следить за тем, через какой регистр передавать
параметр процедуре, его размерностью и числом передаваемых параметров. Вот
как будет выглядеть объявление и использование процедуры STRCPY:
void STRCPY ( DI, SI ) ; //это объявление процедуры
void main ()
{
STRCPY ( #dest, #sourc ) ; //а это вызов процедуры
}
Можно не делать объявления процедуры, а указать расположение регистров
в заголовке процедуры. Но тогда такая процедура должна вызываться только
после ее определения. Вот пример процедуры выводящей на экран несколько
одинаковых символов:
void PUTNCHAR(AL,CX,BL,BH)
/* 1 параметр в AL - код символа, который будет выведен
2 параметр в CX - число выводимых символов
3 параметр в BL - цветовой атрибут
4 параметр в BH - номер видеостраницы
*/
{
AH=9;
$INT 0x10
}
При объявлении регистровой процедуры можно также указывать какой тип
переменной ожидает процедура (знаковый/без знаковый или вещественный). По
умолчанию считается без знаковый тип. Однако знаковый тип указывать есть
смысл только если параметр передается через регистр AL/AX/EAX. Через другие
регистры переменная всегда передается как без знаковая. Пример объявления
регистровой процедуры с указанием типов:
int fastcall Exampl( word CX, int AX, DX, float ESI ) ;
| | | | | | |
| | | | | | |---- 4-й парам. имеет тип float и
| | | | | | перед. через регистр ESI.
| | | | | |-------- 3-й парам. имеет по умолч.
| | | | | тип word и перед. через DX.
| | | | |------------ 2-й парам. имеет тип int и
| | | | передается через регистр AX.
| | | |---------------------- 1-й парам. имеет тип word и
| | | передается через регистр CX.
| | |------------------------------- Имя объявляемой процедуры.
| |---------------------------------------- Модификатор, указывающий, что
| эта проц. явл. регистровой.
|--------------------------------------------- Процедура возвращает перемен.
типа int.
Если Вы сделали объявление регистров процедуры, то компилятор будет
строго следить за количеством указанных параметров при вызове этой
процедуры и выдавать сообщения об ошибке, если их будет меньше или больше.
С одной стороны это хорошо - есть контроль за тем, что Вы ничего не забыли
или не добавили лишнего при вызове процедуры. С другой стороны иногда
бывают необязательные параметры, а их теперь придется прописывать. Но если
Вы при вызове процедуры не укажете ни одного параметра, то компилятор не
будет Вам выдавать сообщение об ошибке. Это дает Вам возможность
проинициализировать регистры, через которые Вы передаете параметры, вне
вызова процедуры. Но если Вы укажете, хоть один параметр, то Вам придется
указывать и остальные, иначе компилятор будет считать, что Вы их случайно
пропустили и выдаст сообщение об ошибке.
Если Вы не объявили регистры ни при объявлении регистровой процедуры,
ни в заголовке самой процедуры, то компилятор будет считать, что параметры
в эту процедуру передаются старым способом. Таким образом, достигается
полная совместимость с предыдущими версиями компилятора.
Return to contents.
11.10 Объявление параметров в стековых процедурах.
Как известно, ранее в C-- контроль за числом и типом передаваемых
процедуре параметров возлагался на программиста. Поэтому возникла непростая
задача, совместить одновременно отсутствие контроля за параметрами (для
совместимости с предыдущими версиями) и ее наличие. В результате
компромиссов появился вариант немного отличающийся от традиционно принятого
в языках C.
Главное отличие - это то, что параметры, определяемые при определении
процедуры, не будут восприниматься компилятором для контроля за ними. Во
всех языках C допускается совмещение прототипа процедуры и ее объявления.
В C-- для того, чтобы включился контроль за параметрами стековой процедуры,
надо эту процедуру обязательно объявить. Но не всякое объявление процедуры
будет сигналом компилятору о включении контроля за параметрами этой
процедуры. Если при объявлении в круглых скобках ничего не будет, то
компилятор не будет отслеживать параметры, передаваемые этой процедуре. В
C++ такое объявление означает, что процедуре не передаются никакие
параметры. В C-- для этого надо при объявлении процедуры в круглых скобках
обязательно написать void. Например:
int proc ( void ) ;
Встретив такое объявление процедуры, компилятор будет следить за тем,
чтобы этой процедуре не были переданы параметры.
При объявлении процедуры имена параметров можно опускать. Как известно,
в C-- параметры процедуры одного типа записываются через запятую. Для смены
типа используют точку с запятой. При объявлении смену типа можно
производить и после запятой:
void ptoc ( int a, b, c; word d );
void proc ( int, int, int, word );
void proc ( int, int, int; word );
Все эти примеры объявлений являются идентичными и допустимыми.
Для контроля за процедурами с переменным числом параметров был введен
новый для C-- элемент синтаксиса - многоточие или его еще называют эллипс.
Вот как будет выглядеть объявление процедуры printf:
void cdecl printf ( word, ... );
Return to contents.
11.11 Использование макрокоманд.
Теперь любая динамическая процедура может быть использована как макрос.
Если перед вызовом динамической процедуры поставить символ @, то код этой
процедуры будет вставлен, а не вызван инструкцией CALL.
При использовании стековых динамических процедур в качестве макросов
очистка стека от переданных параметров производится ассемблерной
инструкцией ADD SP,SIZE_PARAMETRS сразу после окончания кода вставленного
макроса. Поэтому, если эта процедура использовала флаги в качестве
возврата, то они будут разрушены.
Return to contents.
11.12 Передача параметров в стековые процедуры через регистры.
При передаче параметров через регистры, чаще всего получается более
компактный и быстрый код. Но содержимое регистров может быть легко
разрушено. Если в Вашей процедуре, какой-то из параметров используется
однократно для того, чтобы в начале процедуры инициализировать какой-то
регистр, то Вы можете передать это значение в процедуру сразу через
регистр, минуя стадию засовывания и извлечения содержимого в стек. Пример:
int proc (int param1, param2, param3)
{
(E)BX = param3;
(E)BX.TEG_STRUCT.var = proc2 (param1,papra2);
proc3 (param1,param2);
}
В этом примере параметр param3 используется лишь для того, чтобы
инициализировать регистр (E)BX, поэтому его можно сразу передать через
регистр:
int proc (int param1, param2, (E)BX)
{
(E)BX.TEG_STRUCT.var = proc2 (param1,papra2);
proc3 (param1,param2);
}
Как Вы видите, процедура немного упростилась.
В принципе, порядок расположения стековых и регистровых параметров не
принципиален. Но надо помнить, что содержимое регистров может быть легко
разрушено, и поэтому лучше всего регистровые параметры инициализировать
лишь после того, как были засунуты в стек все стековые параметры. Для
процедур типа pascal регистровые параметры лучше располагать после
стековых параметров. Для процедур типа cdecl и stdcall сначала лучше
располагать регистровые параметры.
Return to contents.
11.13 Вызов процедур с адресом в регистре.
В C-- допустимо делать вызов процедуры, адрес которой находится в
регистре. Параметры для такого вызова передаются только через стек. Тип
вызова процедуры для программ под Windows stdcall, для остальных pascal.
Адрес процедуры для 32-битных программ должен находится в 32-битном
регистре, а для 16-битных программ в 16-битном регистре. Считается, что
такой вызов имеет возврат типа unsigned int. Пример:
BX = # proc;
BX (a);
IF ( BX(b) == 0 ) AX=2;
Вы получите следующий код:
test.c-- 8: BX=#proc;
0104 BB1A01 mov bx,11Ah
test.c-- 9: BX(a);
0107 FF76FC push word ptr [bp-4]
010A FFD3 call near bx
test.c-- 10: IF (BX(b) == 0)AX=2;
010C FF76FE push word ptr [bp-2]
010F FFD3 call near bx
0111 85C0 test ax,ax
0113 7503 jne 118h
0115 B80200 mov ax,2
Return to contents.
11.14 Встроенные в компилятор процедуры.
Для некоторых процедур Вы не найдете их исходные тексты в библиотеках
компилятора. Код этих процедур генерирует компилятор. Вот список этих
процедур:
ABORT Прекращение выполнения программы
atan Вычислить арктангенс числа
atan2 Вычислить арктангенс числа
ATEXIT Зарегистрировать функцию выполняющуюся при выходе.
cos Возвращает косинус угла
EXIT Закончить программу с кодом ошибки
exp Возвращает экспоненту числа
inp/inportb Считать один байт из порта
inport Считать слово из порта
inportd Считать двойное слово из порта
fabs Возвращает абсолютное значение числа
log Вычисляет натуральный логарифм числа
log10 Вычисляет десятичный логарифм числа
outp/outportb Записать один байт в порт
outport Записать слово в порт
outportd Записать двойное слово в порт
sin Возвращает синус угла
sqrt Извлечь квадратный корень через FPU.
tan Возвращает тангенс угла
Размещение этих процедур непосредственно в компиляторе, связано с тем,
что в настоящий момент компилятор может таким образом генерировать более
эффективный код, чем если бы эти процедуры располагались в библиотеках.
В будущем, по мере развития компилятора, эти процедуры постепенно будут
выносится из компилятора в библиотеки.
Но ничто не мешает Вам уже сейчас написать свои одноименные
библиотечные процедуры. Встретив определение такой процедуры, компилятор не
будет выдавать никаких сообщение, он просто будет применять Ваш вариант
процедуры.
Return to contents.
11.14.1 Процедуры ABORT, ATEXIT и EXIT.
Процедуры ABORT и EXIT связаны с работой директивы #atexit и
процедурой ATEXIT. Наиболее оптимальную их реализацию и взаимную
интеграцию может сделать только компилятор. Именно поэтому эти процедуры
поддерживаются компилятором.
Процедура ATEXIT - регистровая процедура, которая регистрирует
функцию, адрес которой передается ей в качестве параметра, т.е. через
регистр (E)AX, как функцию завершения программы. При успешной регистрации
ATEXIT возвращает 0. Всего можно зарегистрировать до 16 функций.
Завершающие функции не должны иметь параметров и возврата. Эти
функции будут выполняться в порядке обратном очередности регистрации в
случае, если Вы будете завершать работу программы через вызовы процедур
ABORT или EXIT или закончится работа процедуры main. Если Вы
завершите работу программы вызовом процедуры ExitProcess под Windows или
вызовом AH=0x4C; $int 0x21 под DOS, выход из программы произойдет без
запуска зарегистрированных функций.
Процедура ABORT и EXIT, если не включена директива #atexit делают
вызов процедуры ExitProcess под Windows и вызов AH=0x4C; $int 0x21 под
DOS. Процедуре ABORT не передаются никакие параметры, и она завершает
работу программы с кодом возврата 0. Процедуре EXIT передается в
качестве параметра код возврата, с которым она и завершает работу
программы.
Return to contents.
11.14.2 Процедуры inp/inportb, inport, inportd, outp/outportb, outport и
outportd
Эти процедуры всегда вставляются в код как макросы, т.е. для этих
процедур никогда не генерируется вызов процедуры. В зависимости от
значения порта, с которым работают эти процедуры, генерируется разный
код. Все это позволяет получать более компактный код.
Процедуры чтения из порта имеют такой прототип:
byte inp ( word port );
word inport ( word port );
dword inportd ( word port );
Процедуры записи в порт имеют такой прототип:
void outp ( byte val; word port );
void outport ( word val; word port );
void outportd ( dword val; word port );
Имена процедур inp и inportb, также как и имена outp и outportb
являются синонимами.
Return to contents.
11.14.3 Процедуры для работы с вещественными числами.
Эти процедуры реализуются компилятором и всегда вставляются в код как
макросы, т.е. для них никогда не генерируется вызов процедуры. Кроме
этого, если параметром одной процедуры является вызов другой, то
результат работы второй процедуры остается в стеке FPU, а первая
процедура использует этот результат непосредственно из стека. Таким
образом получаются более компактный код. Вот вымышленный пример:
test.c-- 7: f = sin( sqrt(1) );
0100 D9061C01 fld [11Ch]
0104 D9FA fsqrt
0106 D9FE fsin
0108 D91E2001 fstp [120h]
010C 9B fwait
Эти процедуры имеют следующий прототип:
float atan ( float val );
float atan ( float val, val2 );
float cos ( float val );
float exp ( float val );
float fabs ( float val );
float log ( float val );
float log10 ( float val );
float sin ( float val );
float sqrt ( float val );
float tan ( float val );
Return to contents.
11.15 Классы.
11.15.1 Объявление процедур в структурах.
С введение поддержки объявления процедур в структурах, структура
становится подобной классу в C++. Т.е. такая процедура становится методом
класса. Пример:
struct Point // объявление класса
{
int x; // элементы данных
int y; // класса типа Point
void SetX(int); // объявление методов
void SetY(int); // класса Point
};
void Point::SetX(int _x) //определение процедуры класса Point
{
IF((_x>=0)&&(_x<=MAX_X)) x=_x;
// переменные x, y являются членами этого класса и поэтому доступ к ним из
// процедур этого же класса осуществляется напрямую.
}
void main()
Point p; //определяем структуру в стеке
{
p.y = p.x = 0;
p.SetX(1);
}
При вызове процедуры являющейся методом класса ей неявным образом
передается адрес этого класса (структуры). В самой процедуре этот адрес
доступен через имя параметрической переменной this. Эту переменную
автоматически генерирует компилятор. Если в объявление процедуры в
структуре указать ключевое слово static, то такой процедуре адрес
класса не передается и переменная this не генерируется.
Процедура объявленная в структуре может быть динамической. Для этого,
при ее определении, в самом ее начале, надо написать символ двоеточия :
(также как и для обычных динамических процедур). Но такая динамическая
процедура не может быть использована как макрос.
Return to contents.
11.15.2 Наследование.
В C-- поддерживаются простые и множественные наследования. Объявление
структуры с наследованием имеет следующий синтаксис:
struct Derived : Base1, Base2, ... Basen
{
int x0;
};
Число базовых структур в производном не ограничено. При множественном
наследовании структура может наследовать два и более экземпляра базовой
структуры. При этом возникает неоднозначность. Пример:
struct A
{
int x,y;
. . .
};
struct B : A //структура B наследует A
{
. . .
};
struct C : A //структура C наследует A
{
. . .
};
struct D : B, C //структура D наследует B и C
{
. . .
};
void main()
D d; //выделяем для структуры D память в стеке и присваиваем ей имя d
{
d.x0=0;
В этом примере структура D наследует два экземпляра структуры A и
в ней находятся два элемента с именем x0. Компиляторы C++ при записи
типа d.x0=0 выдают сообщение об ошибке. C-- эту запись обрабатывает,
присваивание производится по умолчанию в элемент из последней базовой
структуры, имеющей элемент x0. Для того чтобы получить доступ ко
второму элементу x0 (физически этот элемент находится в структуре
первым), необходимо применить операцию разрешения видимости:
d.B::x0=0;
Из всего этого следует, что записи:
d.x0=0;
и
d.C::x0=0;
являются равнозначными.
Return to contents.
11.15.3 Наследование процедур.
Если в базовом классе есть процедура, а в производном классе Вы эту
процедуру переопределили, то эта процедура будет переопределена и в
базовом классе. Таким образом процедура определенная в базовом классе
будет потеряна. Пример:
struct Point // базовый класс
{
int x; // элементы данных
int y; // класса типа Point
void SetX(int); // объявление методов
void SetY(int); // класса Point
};
void Point::SetX(int _x) // определение процедуры класса Point
{
IF((_x>=0)&&(_x<=MAX_X)) x=_x;
}
struct Point2 : Point // производный класс
{
int x2;
}
struct Point3 : Point // еще один производный класс
{
int z;
}
void Point3::SetX(int _x) // в этом производном классе переопределяем
{ // процедуру SetX
IF((_x>=80)&&(_x<=MAX_X)) x=_x;
}
Процедура SetX, определенная в базовом классе Point, теперь будет
недоступна. Вместо кода определенного в этом классе, будет вызываться код
процедуры, определенный в наследуемом классе Point3. При вызове процедуры
SetX из другого производного класса Point2 будет также вызываться код
процедуры, определенный в производном классе Point3. Переопределяя
процедуру таким образом, Вы замените код этой процедуры в базовом классе и
во всех его наследуемых классах.
Если Вам необходимо, чтобы код новой процедуры был доступен
одновременно с кодом старой процедуры, то в производном классе Вам
необходимо сделать еще одно объявление этой процедуры. Пример:
struct Point // базовый класс
{
int x; // элементы данных
int y; // класса типа Point
void SetX(int); // объявление методов
void SetY(int); // класса Point
};
void Point::SetX(int _x) // определение процедуры класса Point
{
IF((_x>=0)&&(_x<=MAX_X)) x=_x;
}
struct Point2 : Point // производный класс
{
int x2;
}
struct Point3 : Point // еще один производный класс
{
int z;
void SetX(int); // в наследуемом классе делаем еще одно объявление
// процедуры SetX
}
void Point3::SetX(int _x) // в этом производном классе переопределяем
{ // процедуру SetX
IF((_x>=80)&&(_x<=MAX_X)) x=_x;
EDI=this;
EDI.Point.SetX(_x); // делаем вызов одноименной процедуры из
// базового класса
}
Теперь из производного класса Point3 Вам доступны две различные
процедуры с одним именем SetX. А из базового класса Point и из другого
производного класса Point2 будет по прежнему доступен только базовый
вариант процедуры SetX.
Return to contents.
12. Типы выходных файлов.
12.1 Выходные файлы типа COM.
Этот тип выходного файла получается автоматически по умолчанию.
Изначально C-- мог делать только файлы формата типа COM. В настоящее
время появилась возможность получать файла типа EXE с моделями памяти tiny
и small для 16-битного кода, а также 32-битные для DOS и Windows. Также
есть возможность получения выходного файла в формате OBJ, что позволяет
связывать программы на C-- с программами на других языках.
Return to contents.
12.2 Выходные файлы типа EXE.
Этот формат файла можно получить, если компилировать с ключом командной
строки /exe или /e.
Возможно также поддержка EXE-формата через выходной файл формата OBJ,
который можно затем обработать линковщиком, не входящим в пакет C--.
Return to contents.
12.3 Выходной файл *.EXE с моделью памяти tiny.
Фактически код файла *.exe модели tiny ничем не отличается от кода
*.com. В сущности, это тот же com-файл, к которому добавлен 32-байтный
заголовок exe-файла. Единственное отличие возникает, когда Вы компилируете
файл с директивой ?resize TRUE. В com-файле, по этой директиве, в код
программы добавляется соответствующий код, изменяющий размер доступной
памяти. В exe-файле для этих целей будет скорректирован заголовок
exe-файла.
Чтобы получить exe-файл с моделью памяти tiny, надо запустить
компилятор с ключом в командной строке /TEXE.
Return to contents.
12.4 Объектный выходной файл OBJ.
В настоящее время C-- может только создавать OBJ-файлы, но не может их
компоновать.
Ранее C-- создавал obj-файлы, которые могли быть подключены к проектам
созданным на других языках, т.е. ведомые (slave) модули. Причем из C--
модулей для основного проекта были доступны только процедуры и эти
процедуры не должны были использовать глобальные переменные.
Теперь же C-- может создавать основной модуль (master), который может
быть слинкован в самостоятельный файл.
Для obj-файлов появилась возможность использовать внешние (extern)
процедуры, переменные или структуры. Для этого достаточно их объявить как
extern. Причем ключевое слово extern должно быть всегда первым. Пример
объявления внешних объектов:
extern void cdecl _printf(); // объявление внешней процедуры _printf имеющей
// тип cdecl и тип возврата void
extern int buts,cubs; // объявление двух внешних переменных типа int
extern struct IPXL ipxl; // объявление внешней структуры ipxl имеющей тег
// IPXL, причем тег этой структуры должен быть
// описан ранее.
Появление возможности объявлять внешние объекты позволяет подключать к
obj-модулю на C-- модули написанные на других языках или подключать к
программе на C-- процедуры из библиотек на других языках. При объявлении
внешних объектов очень важно правильно указать тип процедуры и ее имя. Если
Вы будете использовать внешние процедуры, написанные на C то чаще всего,
Вам нужно будет указывать модификатор cdecl, а к имени процедуры или
переменной добавлять префикс _.
Из основного (master) obj-файла написанного на C-- для других
obj-модулей доступны все процедуры, глобальные переменные и глобальные
структуры.
Чтобы получить ведомый obj-модуль при компиляции надо использовать ключ
/sobj.
C-- может создавать obj-файлы с моделью памяти tiny и small. По
умолчанию создаются модули с моделью tiny. Чтобы получить obj-файл с
моделью памяти small надо запустить компилятор с ключами /obj и /exe.
Для создания obj-файлов для 32-битного DOS в командной строке Вам
необходимо указать ключи /d32 и /obj. Использовать полученный obj-файл мне
удалось лишь с помощью wlink и расширителя zrdx.exe.
Создание obj-файлов под windows не предусмотрено.
Return to contents.
12.5 COM файл symbiosis.
12.5.1 СИМБИОЗ - что это такое?
Транслятор C-- имеет ключ, позволяющий добавлять компилируемую
программу к концу уже имеющегося COM файла. Это называют COM-файл
Symbiosis. Когда такая программа запускается, управление сначала получает
добавленный код C--, и только после выполнения его процедуры main()
управление получит первоначальный код COM-файла.
Если добавленный вами код завершается EXIT() или ABORT(), программа
прекратится, и первоначальный код COM-файла не будет выполнен. Это
позволяет программе, добавленной к COM файлу, определять, будет ли
управление передано на первоначальный код.
Return to contents.
12.5.2 Как это делать.
Чтобы сделать это, Вы должны использовать ключ /SYM в командной
строке компилятора, в которой указывается полное имя COM-файла, к
которому что-то добавляется. При этом оригинал COM-файла не меняется, а
новый файл содержит его в себе. Например, чтобы откомпилировать программу
HELLO.C-- к концу копии C:\command.сом используют следующую команду:
C-- /SYM C:\COMMAND.COM HELLO.C--
Будет создан выходной файл HELLO.COM .
Return to contents.
12.5.3 Использование.
Вы можете, вероятно, придумать большое количество путей использования
этой функции, типа:
- Добавление защиты с использованием пароля к некоторым
специальным COM файлам.
- Уменьшение памяти, доступной COM файлу при запуске.
- Инициализация режима видео для COM файла.
Return to contents.
12.5.4 Злоупотребления.
Любой злоумышленник может придумать и вредные применения для этой
функции. Наиболее очевидное из них - создание троянских коней. Я хотел бы
указать, что это неконструктивное использование C--, и любое
разрушительное использование симбиозов COM-файлов запрещено.
Return to contents.
12.6 SYS - драйверы устройств.
Компилятор значительно облегчит Ваш труд при написании драйверов.
Компилятор сам создаст заголовок драйвера и процедуры СТРАТЕГИЯ и
ПРЕРЫВАНИЕ. Вам остается лишь написать код обработки команд.
Что бы откомпилировать файл драйвера устройства, надо добавить в
командную строку ключ /SYS. Кроме того, появились новые директивы
компилятору, которые действуют только с этим ключом. Вот они:
?sysattribute значение - эта директива передает компилятору
атрибут создаваемого драйвера. По умолчанию устанавливается значение
0x2000.
?sysname <текстовая строка> - эта директива передает компилятору
имя будущего драйвера. По умолчанию присваивается имя "NO_NAME". Длина
имени не более 8 символов.
?syscommand command_0,command_1, ... command_n; - эта директива
является обязательной. По этой директиве компилятору передается список имен
процедур обработки команд драйвера. Имена разделены запятыми. Список должен
заканчиваться символом точка-с-запятой. Можно передать не более 25 команд.
Если какая-то команда не имеет кода поддержки, то в список надо записать
слово NONE.
По умолчанию компилятор для драйвера не создает стек. Драйвер может
пользоваться системным стеком. Но, говорят, что он имеет маленькую глубину.
Если Ваши процедуры активно используют стек, и Вы не надеетесь на системный,
то директивой ?stack <величина> можно заставить драйвер пользоваться своим
стеком.
Вашим процедурам обработки команд при передаче управления в регистрах
ES:BX будет передан адрес заголовка запроса. Регистр DS равен CS. При
возврате управления ваши процедуры должны сохранить регистр DS. В регистре
AX должен находиться код возврата. Остальные регистры могут быть
использованы произвольным образом.
Процедуру обработки команды инициализации желательно располагать
последней (чтобы иметь возможность отдать адресное пространство занимаемое
этой процедурой операционной системе). Перед этой процедурой, если Вы в
других процедурах обработки команд используете динамические процедуры,
обязательно должна быть директива ?setdinproc. Глобальные переменные должны
быть обязательно проинициализированы.
Return to contents.
12.7 Компиляция кода расширителей ROM-BIOS.
Расширители ROM-BIOS (BIOS видеоконтроллеров, сетевых карт...) имеют
определенную структуру и требования. C-- теперь может облегчить Вам процесс
создания кода ROM-BIOS. Если запустить компилятор на компиляцию с ключом
командной строки /ROM, то компилятор создаст сигнатуру (заголовок)
ROM-BIOS, заполнит оставшееся свободное место до указанного размера ПЗУ
кодом заполнения, подсчитает и скорректирует контрольную сумму ПЗУ.
Для этого режима компиляции есть несколько специфических директив:
1. ?sizerom value - эта директива сообщает компилятору размер ПЗУ в
байтах. Если эта директива не указана, то компилятор сам выберет
минимальный подходящий размер ПЗУ из ряда: 1024, 2048, 4096, 8192, 16384,
32762 или 65536. Свободное от кода и данных место будут заполнены до конца
размера ПЗУ байтом заполнения определяемого директивой ?aligner. По
умолчанию он равен нулю, для РПЗУ типа 27ххх этот байт имеет смысл сделать
равным 0xFF. Последний байт ПЗУ будет скорректирован компилятором таким
образом, чтобы контрольная сумма равнялась нулю.
2. ?movedatarom TRUE/FALSE - эта директива сообщает компилятору есть ли
необходимость копировать данные из ПЗУ в ОЗУ. По умолчанию она установлена
в FALSE. Если эту директиву определить TRUE, то компилятор вставит в
область инициализации код перемещающий данные из ПЗУ в ОЗУ. При этом
регистр DS будет установлен на сегмент ОЗУ. Стек также будет переустановлен
на этот сегмент. Таким образом, процедура main получит управление с
регистрами AX = ES = DS = SS = сегменту ОЗУ с перенесенными в него данными.
Если эту директиву установить в FALSE, регистр DS все равно будет
переустановлен на адрес сегмента ОЗУ, так как Ваш код будет использовать
этот сегмент для неинициализированных глобальных переменных.
Инициализированные переменные останутся в ПЗУ и все обращения к ним будут
производиться через регистр CS. Так же останется не тронутым (таким, каким
его установил главный BIOS) и стек.
3. ?dataseg value - этой директивой компилятору сообщается сегментный
адрес ОЗУ, который может быть использован вашим кодом. По умолчанию он
равен 0x70. Этот адрес вы можете узнать в любой момент, считав его из вашего
кода по смещению 4. Например: DS = CSWORD[4];
Некоторые замечания:
1. Не забывайте, что в момент инициализации ROM-BIOS, DOS еще не
загружен, и соответственно все процедуры использующие вызовы DOS работать
не будут.
2. Нельзя завершать работу программы процедурами ABORT() или EXIT() и им
подобным. Работа расширителя ROM-BIOS должна завершаться только выходом из
процедуры main().
3. Если директива ?movedatarom установлена в FALSE, то будьте внимательны
при работе с инициализированными переменными. Они в этом режиме доступны
только для чтения, и адресуются через регистр CS.
Return to contents.
12.8 32-битные файлы.
12.8.1 32-битный код под DOS.
Для того чтобы откомпилировать 32-битную программу под DOS надо
запустить компилятор с ключом командной строки /d32. Но работа 32-битной
программы под DOS-ом невозможна без расширителя DOS. Для C-- можно
использовать DOS4GW или zrdx.exe или любой другой расширитель DOS. Чтобы
компилятор знал, где искать stub файл и его имя, надо в файл c--.ini
прописать строку stub=path_name_to_stub_file. Пример:
stub=c:\c--\zrdx.exe
Если не добавлять в c--.ini эту строку, то компилятор сгенерирует
32-битный exe-файл, но без расширителя DOS. Если в командной строке
вместе с ключом /d32 указать и ключ /ns, то строка с переменной stub из
файла c--.ini будет аннулирована, и вы получите файл без расширителя DOS.
Для 32-битного DOS-файла можно использовать директивы компилятора
?parsecommandline TRUE/FALSE или его расширенный вариант ?argc
TRUE/FALSE. Реализована и поддержка директивы ?atexit TRUE/FALSE.
Сейчас для 32-битных DOS-файлов используется LE-формат. Так как LE
формат является стандартным, то теперь можно использовать почти любой
stub, понимающий этот формат. Файлы LE формата можно сжимать программами
типа UPX.EXE и ей подобными.
Если Вы используете stub, который затем загружает DOS4GW.EXE, то
начало Вашей программы должно иметь специальную сигнатуру. Компилятор
автоматически сформирует ее, если Вы в командной строке или в c--.ini
файле укажете ключ /DOS4GW. Такой ключ Вам необходимо будет применять,
если Вы будете использовать в качестве stub 4gs.exe.
Существует также поддержка блока кода использующего для перехода и
работы в 32-битном режиме возможности DPMI сервиса. Исходный текст этого
блока находится в файле startup.h-- и компилируется, если в командной
строке указана опция /stub=dpmi или в файле c--.ini написать строку
stub=dpmi. Недостатком этого способа перехода и работы в 32-битном
режиме являются необходимость обязательного функционирования на
запускаемом компьютере DPMI сервиса. Так как, программа загружается как
обычная DOS программа, и лишь в процессе работы переходит в 32-битный
режим работы, размер программы ограничен размером свободной DOS памяти.
Ну а преимуществом его является компактный размер исполняемого файла.
Return to contents.
12.8.2 32-битный код под Windows.
Для того чтобы откомпилировать программу, написанную под Windows надо
запустить компилятор с ключом командной строки /w32.
Если Вы в своей программе используете вызовы API-процедур, то эти
процедуры надо предварительно обязательно объявить. Объявление процедур
имеет следующую форму:
extern WINAPI "DLL_name"
{
returncode procname1();
returncode procname2();
procname3();
}
где:
DLL_name - имя и расширение dll-библиотеки, в которой находятся эти
процедуры.
returncode - тип возврата из api-процедур. По умолчанию он равен dword.
Программы, написанные под Windows, имеют одну немаловажную
особенность - все параметры в стековые процедуры передаются в обратном
порядке (так называемый C-стиль), но очистка стека от параметров
происходит в самих процедурах. Получается своеобразный гибрид C и pascal
стилей - stdcall.
С помощю ключа /W32C компилятор создает консольный файл под Windows.
Если при компиляции указывали опцию командной строки /j0 или
директиву #jumptomain NONE, то Ваша программа будет компилироваться без
использования кода начальной инициализации, описание которого находится в
файле startup.h--.
Код начальной инициализации для программ под Windows имеет следующий
вид:
hThisInst=GetModuleHandleA(0);
#ifdef __CONSOLE__
hStdOut=GetStdHandle(-11);
#endif
lpszArgs=GetCommandLineA();
#ifdef __environ;
environ=GetEnvironmentStringsA();
#endif
main();
ExitProcess(EAX);
Таким образом, в глобальных переменных hThisInst будет находится
handl запущенного файла, а в lpszArgs адрес командной строки Вашего
файла. Если Вы в командной строке указали опции /p или /argc или в
начале вашего файла есть директивы #parsecommandline TRUE или argc TRUE,
то компилятор создаст дополнительный код сделающий разборку этой
командной строки на части. Если Вы компилируете консольную программу, то
в вашей программе будет еще одна глобальная переменная - hStdOut. В этой
переменной хранится handl стандартного вывода (экрана). Если Вы при
компиляции программы указали опцию /env, то в глобальной переменной
environ хранится адрес переменной окружения программы.
После завершения работы процедуры main выполнятся процедура
ExitProcess, которой в качестве параметра передается регистр EAX. Т.о.
Вам для завершения работы программы будет достаточно сделать выход из
процедуры main, предварительно загрузив в регистр EAX нужный Вам код
возврата.
Некоторые компиляторы создают DLL, в которых имена экспортируемых
процедур имеют такой формат:
ProcName@8
В этом имени после символа @ указывается размер стека с
параметрами, передаваемых процедуре.
Объявлять такие процедуры нужно так:
extern WINAPI "name.dll"
{
ProcName@8 ;
}
т.е. без круглых скобок. В программе, при обращении к такой процедуре, ее
имя надо писать без суффикса @8, т.е. вот так - ProcName(param1,param2);
Return to contents.
12.8.3 Вызов API процедур по ординалам.
В динамически подключаемых библиотеках (DLL) каждой процедуре, кроме
ее имени, соответствует уникальное число, которое называется ординалом. И
поэтому, кроме общепринятого вызова API-процедуры по имени, можно делать
вызов и по ординалу. Теоретически, при использовании вызова по ординалу,
загрузка файла должна происходить быстрее. Так как в выходной файл не
будут включены списки имен процедур, вызов которых производится по
ординалам, то выходной файл может получиться немного меньшим по размеру.
Чтобы компилятор создал файл, использующий вызов API-процедур по
ординалам, надо сделать две вещи:
1. Разрешить компилятору это делать. Для этого надо в опциях командной
строки (или в файле C--.INI) указать ключ WO.
2. Сообщить компилятору - какой номер ординала соответствует какому
имени процедуры. Процедуры, для которых не был указан ординал, будет
создан вызов по имени. Установить соответствие имен процедур ординалу
можно двумя способами:
a). Автоматически, с помощью опции командной строки IND=name.dll,
по которой компилятор просканирует эту библиотеку и импортирует из
нее все имена и ординалы процедур. (Импорт возможет только из
библиотек имеющих формат PE).
b). В ручную указать в объявлении API-процедур и ее ординал. Делается
это так: после имени процедуры ставится точка, а за ней указывается
номер ординала. Вот пример объявления API-процедуры с указанием ее
ординала:
extern WINAPI "user32.dll"
{
............
long MessageBoxA.429();
............
}
В библиотеках (DLL), иногда существуют процедуры, для которых не
указано их имя, но указан номер ординала. Вызов таких процедур по имени
не возможен, но можно это сделать по ординалу (если, конечно Вы знаете,
для чего эта процедура и что она делает). Для этого в объявлении
API-процедуры Вам надо придумать для этой процедуры уникальное имя и
указать реальный ординал. Затем в программе Вы будете обращаться к этой
процедуре по вымышленному имени. Но если Вы случайно откомпилируете такой
файл без ключа WO, то при запуске этой программы Вы получите сообщение,
о том, что данного имени в библиотеке нет.
К сожалению, нет никаких гарантий того, что номер ординала для данной
процедуры не изменится при смене версии динамической библиотеки. Поэтому
использовать ординалы надо осторожно.
Return to contents.
12.8.4 Создание DLL под Windows.
Динамически подключаемые библиотеки позволят получать более
компактные программы и ускорить процесс компиляции. К минусам
использования DLL можно отнести необходимость наличия самих файлов DLL на
запускаемом компьютере и немного увеличивается время запуска программы.
Для того чтобы процедура стала доступной для других программ надо в
исходном тексте перед именем процедуры прописать ключевое слово - _export.
Пример:
void _export testproc()
{
....
}
Для того чтобы создать DLL, нужно написать файл, в котором будут
процедуры с ключевыми словами _export. Вспомогательные процедуры, которые
могут понадобиться для работы основных экспортируемых процедур, объявлять
как _export необязательно. Затем этот файл нужно откомпилировать с ключом
/dll. В результате Вы получите готовую динамически подключаемую
библиотеку.
Return to contents.
12.8.5 Инициализация DLL при загрузке.
Иногда, для работы процедур из динамических библиотек (DLL), бывает
необходимым инициализировать некоторые переменные значениями, зависящими
от текущего состояния операционной системы, например, получить дескриптор
этой библиотеки.
Директивой #jumptomain NONE (-j0) управление при запуске передается
сразу на процедуру main.
Во всех остальных случаях генерируется код заглушки и управление на
процедуру main не передается. Фактически процедура main в этом случае не
нужна.
Процедура main при создании файлов DLL должна выглядеть немного иначе,
чем в других случаях:
dword main ( dword hInstDLL, reason, reserv )
{
...
}
Return to contents.
12.8.6 Компиляция ресурсов.
Встроенный в C-- компилятор ресурсов по своим возможностям уступает
специализированным компиляторам ресурсов, но этих возможностей, как мне
кажется, будет достаточно для большинства Ваших задач.
Будет проще перечислить то, что встроенный в C-- компилятор ресурсов
не умеет делать. Не обрабатываются операторы ресурсов: VERSION,
VERSIONINFO и определяемые пользователем ресурсы. При необходимости,
данные, вводимые с помощью этих операторов, можно ввести с помощью
оператора RCDATA. У многих операторов ресурсов есть необязательные
параметры loading и 'memory'. Поддержка этих параметров не
реализована. Встретив эти параметры, компилятор их просто пропустит.
Заставить компилятор C-- обрабатывать ресурсы можно двумя способами:
1. Включить в свой проект директивой #include файл с расширением
.rc. Файлы с таким расширением компилятор считает файлом с ресурсами.
Файл ресурсов необходимо включать в Ваш проект лишь после включения
заголовочных файлов Windows.
2. Ресурсы можно располагать в теле исходного текста программы в
произвольном месте. Текст ресурсов должен начинаться с директивы #pragma
resource start, а заканчиваться директивой #pragma resoutce end.
Ресурсы могут быть разделенными на части и эти части можно располагать в
любом удобном для Вас месте (глупо располагать ресурсы в блоке
комментариев и потом удивляться, почему они не были откомпилированы).
Компилятор соберет эти части и откомпилирует.
Имена операторов можно писать как большими, так и маленькими буквами,
но имена идентификаторов чувствительны к регистру. В тексте ресурсов
можно использовать директивы и комментарии.
Ничто не мешает Вам использовать компиляторы ресурсов от других
языков. Главное, чтобы синтаксис файла ресурсов соответствовал выбранному
компилятору.
Return to contents.
12.9 Выходные файлы для MeOS.
Исполняемые файлы для операционной системы MenuetOS поддерживаются
компилятором совсем недавно. Для того, чтобы откомпилировать файл для
MenuetOS, нужно в опциях компилятору указать /meos. Вы получите файл без
расширения, который потом можно будет выполнить в среде операционной
системы MenuetOS.
Если при компиляции файла Вы не указывали опцию /j0 или не
использовали директиву #jumptomain NONE, то компилятор будет использовать
файл начальной инициализации startup.h--, в котором для операционной
системы MenuetOS создан блок инициализации и завершения программы.
Завершать выполнение таких программ можно просто выйдя из процедуры main.
Return to contents.
13. Приложения.
13.1 Поиск включаемых файлов.
Поиск включаемого в вашу программу файла, имя которого объявляется
директивой include и заключено в двойные кавычки "", производится
компилятором по такой схеме:
сначала делается попытка открыть файл в текущей директории. Если файла там
нет, то далее делается попытка открыть файл в директории указанной
директивой #includepath. Если директива не была задана или файла в этой
директории не оказалось, то делается попытка открыть файл в директории
указанной в командной строке командой /ip=path. Если эта команда не была
задана или файла в указанной директории не оказалось, то делается попытка
открыть файл в директории указанной в файле C--.INI командой ip=. Если эта
команда не была задана или файла в указанной директории не оказалось, то
делается попытка открыть файл в директории, на которую указывает переменная
окружения C--. Если переменная окружения не была задана или файла в этой
директории не оказалось, то делается последняя попытка открыть файл в
директории, откуда был запущен компилятор.
Если имя включаемого файла заключено в угловые скобки < >, то поиск
этого файла производится в противоположном направлении, за исключением
того, что поиск в текущей директории не производится.
Для консольной версии компилятора имена главного модуля и включаемых
файлов могут иметь длину более 8 символов.
Return to contents.
13.2 Регистры, которые должны быть сохранены.
Регистры, которые должны сохраняться - BP, DI, SI, DS, SS, SP, CS и IP.
BP используется как указатель на локальные и параметрические
переменные в стеке, что и требует его сохранения.
DI и SI сохранять не обязательно, если программист осознает
последствия. DI и SI часто используются для индексации массивов, как
например в формуле:
dog = firehydrant(1,red) + legs[DI];
Если DI не сохранялся в процедуре firehydrant, значение, присвоенное
переменной dog, скорее всего, будет неправильным, поскольку индекс для
массива legs был изменен. В сущности, для точного согласования все
процедуры должны иметь специальное указание в комментарии на то, что в них
не сохраняется содержимое регистров DI и/или SI.
DS указывает на сегмент данных, и все операции с глобальными
переменными пользуются этим значением.
SS хранит сегмент стека и должен сохраняться. SP указывает на текущую
позицию в стеке и тоже должен сохраняться.
CS хранит сегмент кода программы. Все команды выбираются с
использованием CS и IP, следовательно их значения должны сохраняться. IP,
как известно, указатель адреса команды, и CS и IP непосредственно не могут
изменяться в процессорах 8086, 8088, 80286, 80386, 80486,...
Return to contents.
13.3 C--.ini файл.
C--.ini файл предназначен для предустановки по умолчанию параметров
компилятора.
Сейчас компилятор поддерживает огромное число параметров командной
строки. Правильное их использование позволит Вам получать более компактный
код и может значительно облегчить Вам отладку программы. Но так как этих
параметров очень много набирать их каждый раз в командной строке бывает
утомительно и не исключена возможность пропустить какой-нибудь параметр.
Чтобы избавить Вас от всех этих напастей и был введен c--.ini файл.
Параметры командной строки прописываются в этом файле построчно.
Синтаксис тот же, что и в командной строке, но без ведущего обратного слэша
или минуса. Если файл расположен в директории, на которую указывает
переменная окружения set c--= или если эта переменная не определена,
то в той же директории где и файл c--.exe, то эти параметры
распространяются на все компилируемые программы. Если же файл c--.ini
расположен в текущей директории, то параметры считываются только из этого
файла и действуют только для текущего проекта.
Допустимо использование комментариев. Признаком начала комментария
является символ ;. Все последующие символы после ; и до конца строки
считаются комментарием.
Пример C--.ini файла:
r-
X
3 ;это комментарий
os
ini-файл может иметь любое имя (но расширение должно быть обязательно
ini). Имя этого файла с расширением должно быть передано компилятору в
командной строке. Файл c--.ini загружается и обрабатывается автоматически
до загрузки файла указанного в командной строке.
Таким образом, файл *.ini можно использовать подобно make-файлу - в нем
Вы можете указать и имя главного компилируемого модуля, и все необходимые
для его компиляции настройки.
Как альтернативу c--.ini файлу, параметры командной строки можно
прописывать непосредственно в начале главного файла компилируемого проекта,
используя директиву pragma option. С одной стороны это обеспечит Вашему
проекту независимость от настроек компилятора, если Ваш проект будет
компилироваться на другом компьютере. Но с другой стороны некоторые
настройки являются индивидуальными для данного компьютера (это расположение
библиотек, имена и расположение stub-файлов). Какой вариант использовать
решать Вам, но как говорят, и я с этим согласен, лучше пользоваться золотой
серединой - Часть параметров прописать в c--.ini файле, а другую
непосредственно в компилируемом файле.
Return to contents.
13.4 startup.h-- файл.
В этом файле находятся исходные тексты, которые компилируются
компилятором в код начальной инициализации файла, для всех поддерживаемых
компилятором типов выходных файлов. Этот файл должен находится либо в
директории вместе с компилятором, либо в директории с библиотечными файлами.
Этот файл включается компилятором в проект автоматически, а включение его
директивой include может привести к нежелательным результатам.
В блоке начальной инициализации программы может производится (если Вы
это укажете с помощью опций командной строки или используя директивы),
разбор командной строки на параметры, сохранение переменой окружения,
поддержка работы процедуры ATEXIT, изменение размера доступной памяти для
*.com файлов и многие другие подготовительные операции. Если Вы
откомпилируете свой файл не используя никаких опций командной строки и у
Вас будет отсутствовать c--.ini файл, а в самом компилируемом файле у Вас
будут отсутствовать директивы, то при компиляции *.com файла в него будет
включен блок изменяющий размер доступной памяти и сигнатура SPHINXC--.
Если Вы компилируете файл типа *.exe (кроме файла модели tiny для DOS)
и используете директиву jumptomain NONE или ключ командной строки /j0,
то для этого проекта файл startup.h-- компилятором не используется. Не
используется этот файл также при компиляции *.com файлов если, кроме /j0,
в этом проекте не используется разбор командной строки (/p /argc), не
применяется процедура ATEXIT (/at), не используется адрес переменной
окружения (/env), не используется очистка области post-адресов (/cpa), не
используется уменьшение доступной программе памяти (/r) и не используется
заглушка нажатий CTRL-C (/c).
Кроме блока начальной инициализации программы в файле startup.h--
находятся динамические процедуры:
void CLEARPOSTAREA( (E)AX ); - очистка post-области данных.
unsigned int PARAMSTR( ECX ); - получить адрес элемента командной строки
unsigned int PARAMCOUNT(); - получить число элементов в командной строке
При разборе командной строки на составляющие ее элементы для 32-битных
программ реализована поддержка длинных имен. Для 16-битных программ
поддержка разбора командной строки с учетом длинных имен подключается, если
Вы в начале свой программы укажете директиву:
#define _USELONGNAME TRUE
либо в c--.ini файле или в командной строке компилятора укажете опцию
d=_USELONGNAME.
Return to contents.
13.5 mainlib.ldp файл.
В этом файле находится большое число процедур из основной библиотеки
компилятора в уже откомпилированном виде. Все процедуры откомпилированы в
4-х различных режимах оптимизации. В этот файл также вынесены многие
процедуры, которые ранее были внутри компилятора. Использование ранее
откомпилированных процедур повышает скорость компиляции.
Эти процедуры откомпилированы только для 16-битного режима работы
программы. Если Вы будете использовать эти процедуры в 32-битной программе,
то компилятор на это не выдаст никаких сообщений и включит эту процедуру в
Ваш код. Но при запуске такой программы она неизбежно потерпит крах.
Использовать эту библиотеку очень просто. Все что нужно, это
расположить эту библиотеку в одной с компилятором директории. Тогда
компилятор, если встретит в вашей программе вызов процедуры, которая не
была определена ни во включаемых в программу библиотечных файлах, ни в
вашей программе, будет искать эту процедуру в файле mainlib.ldp. Если эта
процедура будет найдена в этом файле, то ее код будет перенесен в Ваш файл,
иначе будет выдано сообщение о неизвестной процедуре. Таким образом, чтобы
процедура была вставлена в вашу программу из библиотеки mainlib.ldp Вам
нужно в свою программу не включать библиотечный файл, содержащий процедуру с
таким же именем.
Список процедур находящихся в этой библиотеке можно получить с помощью
специальной программы cmmlib.exe. Эту программу можно найти в архиве
cmmlib.rar. Извлеките программу cmmlib.exe из этого архива и расположите ее
в одной с компилятором директории. Затем запустите эту программу с ключом
/L и Вы получите список процедур находящихся в этой библиотеке.
Return to contents.
13.6 C-- символы.
SYMBOL|FUNCTION |EXAMPLE
--------------------------------------------------------------------
/* |начинают блок комментария |/* комментарий */
*/ |завершают блок комментария|/* комментарий */
| |
// |комментарий до конца линии|// комментарий
| |
= |присвоение |AX = 12;
+ |сложение |AX = BX + 12;
- |вычитание |house = dog - church;
* |умножение или указатель |x = y * z; AL = * var;
/ |деление |x1 = dog / legs;
& |поразрядное логическое И |polution = stupid & pointless;
| |поразрядное логическое ИЛИ|yes = i | mabe;
^ |поразрядн. исключающее ИЛИ|snap = got ^ power;
<< |битовый сдвиг влево |x = y << z;
>> |битовый сдвиг вправо |x = y >> z;
| |
+= |сложение |fox += 12; // fox = fox +12;
-= |вычитание |cow -= BX; // cow = cow - BX;
*= |умножение |a *= b; // a = a * b;
/= |деление |a /= b; // a = a / b;
&= |поразрядное логическое И |p &= q; // p = p & q;
|= |поразрядное логическое ИЛИ|p |= z; // p = p | z;
^= |поразрядн. исключающее ИЛИ|u ^= s; // u = u ^ s;
<<= |битовый сдвиг влево |x <<= z; // x = x << z
>>= |битовый сдвиг вправо |x >>= z; // x = x >> z
| |
>< |обмен значениями |x >< y; /* меняет местами значения x и y */
| |
== |проверка на равенство |IF(AX == 12)
> |проверка на больше чем |IF(junk > BOGUS)
< |проверка на меньше чем |if( x < y )
>= |проверка больше или равно |if(AX >= 12)
<= |проверка меньше или равно |IF(BL >= CH)
!= |проверка на неравенство |IF(girl != boy)
<> |проверка на отличие |if (cat<>dog) /* та же функция что != */
| |
@ |вставка кода |@ COLDBOOT(); /* вставляет COLDBOOT код */
: |динамическая процедура |: functionname () //объявляет functionname
$ |ассемблерная команда |$ PUSH AX /* заносит AX в стек */
# |получение адреса(смещения)|loc = #cow; /* loc = address of cow */
|или директива | #resize FALSE
! |оператор NOT или смена |!x_var; if(!proc())
|флага операции сравнения. |
... |любое число параметров в | void proc(...);
:: |разрешение видимости | ::var=0;
Return to contents.
Сайт создан в системе uCoz
|