Ниже представлена информация, которая будет полезной при разработке 16-разрядных приложений для работы в DOS.
Borland С++ при компиляции не генерирует на диске никаких промежуточных структур данных (записывая на диск только файлы .OBJ). Вместо этого для хранения промежуточных структур данных между проходами используется оперативная память. Поэтому при недостаточном объеме оперативной памяти вам может выводиться сообщение о нехватке памяти. Чтобы решить эту проблему, уменьшите размер функций или разбейте файл с крупными функциями на несколько частей.
В Borland С++ используется 6 моделей памяти, каждая из которых служит для различных размеров программ и кода. Регистры процессора
Ниже представлены некоторые регистры процессоров 80х86. Эти процессора имеют и другие регистры, но непосредственно к ним обращаться нельзя, поэтому они здесь не показаны.
Аккумулятор (математические операции) | ||
---|---|---|
AX | AH | AL |
Базовый регистр (индексирование) | ||
BX | BH | BL |
Счетчик (индексирование) | ||
CX | CH | CL |
Регистр данных | ||
DX | DH | DL |
CS | Сегментный регистр кода |
DS | Сегментный регистр данных |
SS | Указатель сегмента стека |
ES | Дополнительный регистр сегмента |
SP | Указатель стека |
BP | Указатель базы |
SI | Индекс источника |
DI | Индекс приемника |
Общие регистры чаще всего используются для работы с данными. Каждый из них выполняет некоторые специальные функции, которые доступны только ему, например, некоторые математические операции могут использовать только регистр AX, регистр BX может служить базовым регистром, CX применяется инструкцией LOOP и некоторыми строковыми инструкциями, а DX используется некоторыми математическими операциями неявно. Однако во многих операциях можно использовать все эти регистры и заменять один из них на другой.
Сегментные регистры содержат начальный адрес каждого из 4 сегментов. Как описывается ниже, 16-разрядное значение в сегментном регистре для получения 20-разрядного адреса сегмента сдвигается влево на 4 (умножается на 16).
16-разрядный сегментный 16-разрядное регистр смещение V умножение на 16 /сдвиг влево на 4/ V Значение сегмента, умноженное на 16, равно 20-разрядно- му значению > + < V 20-разрядное значение адреса памяти
Процессоры 80х86 имеют также некоторые специальные регистры:
Функции Borland С++ используют регистр базы (BP) в качестве базового регистра для аргументов и переменных. Параметры имеют положительные смещения от BP, зависящие от модели памяти. При наличии кадра стека BP указывает на сохраненное предыдущее значение BP. Если параметр Standard Stack Frame выключен (Off), то функции без аргументов не используют и не сохраняют BP.
16-разрядный регистр флагов содержит все необходимую информацию о состоянии процессора 80х86 и результатах последних инструкций.
только 80386 286/386 все процессоры 80х86 31 23 15 7 0 V R N IOP O D I T S Z A P C Виртуальный режим 80х86 Возобновление Вложенная задача Уровень защиты ввода-вывода Переполнение Направление Разрешение прерывания Прерывание Знак Признак нуля Вспомогательный перенос Четность Перенос
Например, если вы хотите знать, получен ли при вычитании нулевой результат, непосредственно после этой инструкции вам следует проверить флаг нуля (бит Z в регистре флагов). Если он установлен (то есть имеет ненулевое значение), это будет говорить о том, что результат нулевой. Другие флаги, такие, как флаги переноса и переполнения аналогичным образом сообщают о результатах арифметических и логических операций.
Прочие флаги управляют режимом операций процессора 80х86. Флаг направления управляет направлением, в котором строковые инструкции выполняют перемещение, а флаг прерывания управляет тем, будет ли разрешено внешним аппаратным средствам, таким, например, как клавиатура или модем, временно приостанавливать текущий код для выполнения функций, требующих немедленного обслуживания. Флаг перехвата используется только программным обеспечением, которое служит для отладки другого программного обеспечения (отладчики).
Регистр флагов не считывается и не модифицируется непосредственно. Вместо этого регистр флагов управляется в общем случае с помощью специальных инструкций (таких, как CLD, STI и CMC), а также с помощью арифметических и логических инструкций, модифицирующих отдельные флаги. И наоборот, содержимое отдельных разрядов регистра флагов влияет на выполнение инструкций (например, JZ, RCR и MOVSB). Регистр флагов не используется на самом деле, как ячейка памяти, вместо этого он служит для контроля за состоянием и управления процессором 8086.
Память микропроцессора Intel 80x86 имеет сегментированную архитектуру. Непосредственно можно адресоваться к 64К памяти сегменту. Процессор 80x86 отслеживает 4 различных сегмента: сегмент кода, сегмент данных, сегмент стека и дополнительный сегмент. В сегменте кода находятся машинные инструкции, а в дополнительном сегменте - дополнительные данные. Процессор 80x86 имеет 4 16-разрядных сегмента (по одному на сегмент) - CS, DS, SS и ES, которые указывают на сегмент кода, данных, стека и дополнительный сегмент соответственно. Сегмент может находиться в любом месте памяти, но начинаться должен по адресу, кратному 10. Сегменты могут перекрываться. Например, все четыре сегмента могут начинаться с одного адреса.
Стандартная запись адреса имеет форму "сегмент:смещение", например, 2F84:0546. Начальный адрес сегмента всегда представляет собой 20-битовое число, но так как сегментный регистр содержит только 16 бит, нижние 4 бита полагаются равными 0. Это значит, что сегменты могут начинаться только с тех адресов, у которых последние 4 бита равны 0.
Хотя указатель или функция могут иметь конкретный тип независимо от используемой модели, вы можете выбрать заданный по умолчанию тип указателя, используемый для кода и данных. Существует 4 типа указателей: near (16 бит), far (32 бита), huge (32 бита) и segment (16 бит).
В указателях near (ближние указатели) для вычисления адреса используется один сегментный регистр, например, 16-битовое значение указателя функции складывается со сдвинутым влево содержимым регистра кода CS. С такими указателями легко работать.
Указатели far (дальние указатели) содержат не только смещение в сегменте, но и адрес сегмента (другое 16-битовое значение). Такие указатели позволяют иметь несколько сегментов кода и программы, превышающие по размеру 64К. Здесь нужно учитывать, что в операциях == и != используются 32-битовые значения unsigned long, а не полный адрес памяти. В операциях сравнения <=, >=, < и > используется только смещение.
При прибавлении к указателю значения изменяется только смещение. Если смещение превышает FFFF (максимально возможное значение), то указатель возвращается к началу сегмента. При сравнении указателей лучше использовать ближние указатели или указатели huge.
Указатели huge также занимают 32 бита. Аналогично указателям far, они содержат и адрес сегмента и смещение. Однако, чтобы избежать проблем с указателями, такие указатели нормализуются. Нормализованный указатель - это 32-битовый указатель с максимально возможным значением в сегментном адресе. Так как сегмент может начинаться с каждых 16 байт, это означает, что данное смещение будет иметь значение от 0 до 15. Для нормализации указателя он конвертируется в 20-битовый адрес, а затем используются правые 4 бита смещения и левые 16 бит адреса сегмента. Например, 2F84:0532 преобразуется в абсолютный адрес 2FD72, который нормализуется в 2FD7:0002. Нормализация важна по следующими причинам:
Однако работа с указателями huge связана с дополнительными издержками. Из-за этого арифметические операции с указателями huge выполняются намного медленнее, чем с указателями far.
В 16-разрядных программах Borland С++ вы можете использовать 6 моделей памяти: крохотную, малую, среднюю, компактную, большую и огромную.
Для выбора любой из этих моделей памяти вы должны либо воспользоваться соответствующим параметром меню интегрированной среды, либо ввести параметр при запуске компилятора, работающего в режиме командной строки.
Следующие иллюстрации показывают, как выполняется распределение памяти для всех шести моделей памяти Borland C++.
Сегментные регистры: Размер сегмента: Младший ^ CS,DS,SS адрес _TEXT класс 'CODE' код _DATA класс 'DATA' инициализированные данные _BSS класс 'BSS' DGROUP не инициализирован.данные до 64К динамически распределя- емая область v Свободная область SP(TOS) памяти ^ Старший стек адрес v Начало SP
Сегментные регистры: Размер сегмента: Младший ^ CS адрес _TEXT класс 'CODE' код до 64К DS,SS _DATA класс 'DATA' инициализированные данные _BSS класс 'BSS' DGROUP не инициализирован.данные до 64К динамически распределя- емая об- ласть памя- ти v Свободная область SP(TOS) - памяти ^ стек Начало SP дальняя динамически До конца распределя- памяти емая область v Свободная Старший область адрес v памяти
Сегментные регистры: Размер сегмента: Младший ^ адрес _TEXT класс 'CODE' до 64К sfile код каждый DS,SS sfile _DATA класс 'DATA' Несколько инициализирован. данные sfile _BSS класс 'BSS' CS sfile A не инициализирован.данные до 64К sfile B динамически распределяе- sfile Z мая область памяти v DGROUP Свободная область SP(TOS) памяти ^ стек Начало SP дальняя динамически До конца распределя- памяти емая область v Свободная Старший область адрес v памятиCS указывает одновременно только на один sfile.
Сегментные регистры: Размер сегмента: Младший ^ CS адрес _TEXT класс 'CODE' код до 64К DS _DATA класс 'DATA' инициализированные данные DGROUP до 64К _BSS класс 'BSS' не инициализирован.данные SS Свободная область SP(TOS) памяти ^ стек до 64К Начало SP динамически До конца распределя- памяти емая область v Свободная Старший область адрес v памяти
Несколько sfile sfile A CS sfile B sfile Z Сегментные регистры: Размер сегмента: Младший ^ адрес _TEXT класс 'CODE' до 64К sfile код каждый DS sfile _DATA класс 'DATA' инициализирован. данные DGROUP до 64К _BSS класс 'BSS' не инициализирован.данные SS Свободная область SP(TOS) памяти ^ стек до 64К Начало SP динамически До конца распределя- памяти емая область v Свободная Старший область адрес v памятиCS и DS указывают одновременно только на один sfile.
Несколько sfile sfile A sfile B CS sfile Z Сегментные регистры: Размер сегмента: Младший ^ адрес sfile_TEXT класс 'CODE' до 64К код каждый sfile Несколько sfile sfile_DATA класс 'DATA' до 64К sfile A инициализированные данные каждый DS sfile B sfile sfile Z SS Свободная область SP(TOS) памяти ^ стек До 64К Начало SP динамически До конца распределя- памяти емая область v Свободная Старший область адрес v памяти
В следующей таблице сведены различные модели и их сравнение друг с другом. Модели часто группируются по модели кода или данных на малые (64К) и большие (16М); эти группы соответственно отражены в столбцах и строках таблицы.
Модели tiny, small и compact относятся к малым моделям кода, поскольку по умолчанию указатели кода являются ближними (near). Аналогичным образом, модели compact, large huge относятся к большим моделями данных, поскольку по умолчанию указатели на данные являются дальними (far).
Размер кода Размер данных 64К 16Мб Tiny (данные и код перекрываются; общий размер = 64К) 64K Small (без перекрытия; Medium (данные small, общий размер = 128К) код large) Compact (данные large, Large (данные и код код small) large) 16Мб Huge (то же, что и large, но статические данные > 64K)
При компиляции модуля (некоторый исходный файл с несколькими подпрограммами), результирующий код для этого модуля не может превышать 64К, поскольку весь файл должен компилироваться в один кодовый сегмент. Это верно и в том случае, когда вы используете одну из больших моделей памяти (medium, large или huge). Если ваш модуль слишком велик и не помещается в одном кодовом сегменте (64К), вы должны разбить его на несколько файлов исходного кода, скомпилировать каждый из них по отдельности и затем скомпоновать их в одну программу. Аналогичным образом, хотя модель huge и позволяет иметь размер статических данных больше чем 64К, в каждом отдельном модуле статические данные не должны превышать 64К.
Borland C ++ вводит восемь новых ключевых слов, отсутствующих в языке Си стандарта ANSI (near, far, huge, _cs, _ds, _es, _ss и _seg), которые с некоторыми ограничениями и предупреждениями могут использоваться в качестве модификаторов для указателей (и в некоторых случаях, для функций).
В Borland C++ при помощи ключевых слов near, far или huge вы можете модифицировать объявления функций и указателей. Указатели данных near, far и huge рассматривались в выше. Объекты far объявляются при помощи ключевого слова far. Функции near запускаются при помощи ближних вызовов (near), а выход из них происходит с использованием ближних команд возврата. Аналогичным образом, функции far вызываются дальними вызовами (far) и выполняют дальний (far) возврат. Функции huge похожи на функции far, за исключением того, что функции huge устанавливают регистр DS в новое значение, тогда как функции far не изменяют значения этого регистра.
Существует также четыре специальных ближних (near) указателя данных: __cs, __ds, __es и __ss. Имеются 16-битовые указатели, конкретно связанные с соответствующими сегментными регистрами. Например, если вы объявите указатель следующим образом:
char _ss *p;то p будет содержать 16-битовое смещение в сегмент стека.
Функции и указатели в данной программе по умолчанию бывают ближними или дальними, в зависимости от выбранной модели памяти. Если функция или указатель являются ближними, то они автоматически связываются с регистром CS или DS.
В следующей таблице показано, как это происходит. Отметим, что размер указателя соответствует предельному размеру памяти, равному 64К (ближний, в пределах сегмента) или 1 Мб (дальний, содержит собственный адрес сегмента).
Модель памяти | Указатели функции | Указатели данных |
---|---|---|
Tiny | near, _cs | near, _ds |
Small | near, _cs | near, _ds |
Medium | far | near, _ds |
Compact | near, _cs | far |
Large | far | far |
Huge | far | far |
В объявлениях типа указателя сегмента используется __seg. В результате получаются 16-битовые указатели сегментов. Синтаксис __seg следующий:
тип_данных _seg *идентификаторНапример,
int _seg *name
Любое обращение по ссылке через "идентификатор" предполагает смещение 0. В арифметических операциях с указателями выполняются следующие правила:
Borland С++ позволяет объявлять дальние (far) объекты. Например:
int far x = 5; int far z; extern int far y = 4; static long j;
Компилятор Borland C++ создает для каждого дальнего объекта отдельный сегмент. Параметры компилятора командной строки -zE, -zF и -zH (которые могут также задаваться директивой #pragma option) влияют на имя, класс и группу дальнего сегмента, соответственно. Изменяя эти значения при помощи указания #pragma option, вы тем самым распространяете новые установки на все объявления дальних объектов. Таким образом, для создания в конкретном сегменте дальнего объекта, можно использовать следующую последовательность:
#pragma option -zEmysegment -zHmygroup -zFmyclass int far x; #pragma option -zE* =zH* -zF*
Тем самым x будет помещен в сегмент MYSEGMENT с классом 'MYCLASS' в группе 'MYGROUP', после чего все дальние объекты будут сброшены в значения, используемые по умолчанию. Отметим, что при использовании этих параметров можно поместить несколько дальних объектов в один сегмент:
#pragma option -zEcombined -zFmyclass int far x; double far y; #pragma option -zE* -zF*
И x, и y окажутся в сегменте COMBINED 'MYCLASS', без группы.
В некоторых случаях вам может потребоваться переопределить заданное по умолчание значение типа функции для модели памяти. Например, вы используете модель памяти large, и в программе имеется рекурсивная функция:
double power(double x,int exp) { if (exp <= 0) return(1); else return(x * power(x, exp-1)); }
Каждый раз, когда функция power вызывает сама себя, она должна выполнить дальний вызов, причем используется дополнительное пространства стека и число тактовых циклов. Объявив power как near, можно ускорить выполнение ее благодаря тому, что вызовы этой функции будут ближними:
double __near power(double x,int exp)
Это гарантирует, что функция power может вызываться только из того кодового сегмента, в котором она компилировалась, и что все обращения к ней будут ближними.
Это означает, что при использовании большой модели памяти (medium, large или huge) функцию power можно вызывать только из того модуля, в котором она определена. Прочие модули имеют свои собственные кодовые сегменты и не могут вызывать функции near из других модулей. Более того, ближняя функция до первого к ней обращения должна быть либо определена, либо объявлена, иначе компилятор не знает о необходимости генерировать ближний вызов.
И наоборот, объявление функции как дальней означает генерацию дальнего возврата. В малых моделях кодовой памяти дальняя функция должна быть объявлена или определена до первого к ней обращения, что обеспечит дальний вызов.
Вернемся к примеру функции power. Хорошо также объявить power как static, поскольку предусматривается вызывать ее только из текущего модуля. Если функция будет объявлена как static, то имя ее не будет доступно ни одной функции вне данного модуля.
Только что были рассмотрены случаи, в которых может понадобиться объявить функцию с другой моделью памяти, нежели остальная часть программы. Зачем то же самое может понадобиться для указателей? По тем же причинам, что и для функций: либо для улучшения характеристик быстродействия (объявив __near там, где по умолчанию было бы __far), либо для ссылки за пределы сегмента по умолчанию (объявив __far или __huge там, где по умолчанию бывает __near).
Разумеется, при объявлении функций или указателей с другим типом, нежели используемый по умолчанию, потенциально появляется возможность ошибок. Предположим, имеется следующий пример программы с моделью small:
void myputs(s) char *s; { int i; for (i = 0; s[i] != 0; i++) putc(s[i]); } main() { char near *mystr; mystr = "Hello, world\n"; myputs(mystr); }
Эта программа работает удовлетворительно, хотя объявление mystr как __near избыточно, поскольку все указатели, как кода, так и данных, будут ближними (near) по умолчанию.
Однако, что произойдет, если перекомпилировать эту программу с моделью памяти compact (либо large или huge)? Указатель mystr в функции main останется ближним (16-битовым). Однако, указатель s в функции myputs теперь будет дальним (far), поскольку по умолчанию теперь используется far. Это означает, что попытка создания дальнего указателя приведет к извлечению из стека двух слов, и полученный таким образом адрес, безусловно, не будет являться адресом функции mystr.
Как избежать этой проблемы? Решение состоит в том, чтобы определить myputs в современном стиле Си:
void myputs(char *s) { /* тело myputs */ }
Теперь при компиляции вашей программы Borland C++ знает, что myputs ожидает указатель на char. Поскольку компиляция выполняется с моделью large, то известно, что указатель должен быть __far. Вследствие этого Borland C++ поместит в стек регистр сегмента данных (DS) и 16-битовое значение mystr, образуя тем самым дальний указатель.
Если вы собираетесь явно объявлять указатели как far или near, не забывайте использовать прототипы тех функций, которые могут работать с этими указателями.
Как быть в обратном случае: когда аргументы myputs объявлены как __far, а компиляция выполняется с моделью памяти small? И в этом случае без прототипа функции у вас возникнут проблемы, поскольку функция main будет помещать в стек и смещение, и адрес сегмента, тогда как myputs будет ожидать приема только одного смещения. При наличии определений функций в прототипах main будет помещать в стек только смещение.
Как создать дальний указатель на конкретный адрес памяти (конкретный адрес "сегмент:смещение")? Для этого можно воспользоваться встроенной библиотечной подпрограммой MK_FP, которая в качестве аргумента воспринимает сегмент и смещение, и возвращает дальний указатель. Например:
MK_FP(segment_value, offset_value)
Имея дальний указатель fp, вы можете получить значение сегмента полного адреса с помощью FP_SEG(fp) и значение смещения с помощью FP_OFF(fp). Более полную информацию об этих трех библиотечных функциях Borland C++ можно найти в справочнике по библиотеке.
Borland C++ предлагает для каждой из шести моделей памяти собственную версию библиотеки стандартных подпрограмм. Компилятор Borland C++ при этом проявляет достаточно "интеллекта", чтобы при последующей компоновке брать нужные библиотеки и в нужной последовательности, в зависимости от выбранной вами модели памяти. Однако, при непосредственном использовании компоновщика Borland C++ TLINK (как автономного компоновщика) вы должны явно указывать используемые библиотеки.
Что произойдет, если вы компилируете один модуль с использованием модели памяти small (малая), второй - модели large (большая), и затем хотите скомпоновать их? Что при этом произойдет?
Файлы скомпонуются удовлетворительно, но при этом вы столкнетесь с проблемами. Если функция модуля с моделью small вызывает функцию в модуле с моделью large, она будет использовать при этом ближний вызов, что даст абсолютно неверные результаты. Кроме того, у вас возникнут проблемы с указателями, описанные в разделе "Объявление указателей как near, far или huge", поскольку функция в модуле small ожидает, что принимаемые и передаваемые ей указатели будут __near, тогда как функция в модуле large ожидает работу с указателями __far.
И снова решение заключается в использовании прототипов функций. Предположим, что вы поместили myputs в отдельный модуль и скомпилировали его с моделью памяти large. Затем вы создаете файл заголовка myputs.h (либо с любым другим именем и расширением .h), который содержит следующий прототип функции:
void far myputs(char far *s);
Теперь, если поместить функцию main в отдельный модуль (MYMAIN.C) и выполнить следующие установки:
#include <stdio.h> #include "myputs.h" main() { char near *mystr; mystr = "Hello, world\n"; myputs(mystr); }
то при компиляции данной программы Borland C++ считает прототип функции из файла MYPUTS.H и увидит, что это функция __far, ожидающая указатель __far. В результате этого даже при модели памяти small при компиляции будет сгенерирован правильный вызывающий код.
Как быть, если помимо этого вам требуется компоновка с библиотечными подпрограммами? Лучший подход здесь заключается в том, чтобы выбрать одну из библиотек с моделью large и объявить все как far. Для этого сделайте копии всех файлов заголовка, которые вы обычно включаете (таких, как stdio.h) и переименуйте эти копии (например, fstdio.h).
Затем отредактируйте копии прототипов функций таким образом, чтобы там было явно указано far, например:
int far cdecl printf(char far* format, ...);
Тем самым, не только вызовы подпрограмм будут дальними, но и передаваемые указатели также будут дальними. Модифицируйте вашу программу таким образом, чтобы она включала новый файл заголовка:
#include <fstdio.h> main() { char near *mystr; mystr = "Hello, world\n"; printf(mystr); }
Скомпилируйте вашу программу при помощи компилятора BCC, затем скомпонуйте ее при помощью утилиты TLINK, указав библиотеки с моделью памяти large, например CL.LIB. Смешивание модулей с разными моделями - вещь экстравагантная, но допустимая. Будьте, однако, готовы к тому, что любые неточности здесь приводят к ошибкам, которые очень трудно найти и исправить при отладке.