Сегменты
Сегменты
Ранее уже рассматривался оператор SEGMENT. Теперь есть возможность
рассмотреть его более подробно и исследовать дополнительные
возможности, которые он предоставляет.
До сих пор в большинстве примеров программ присутствовал только
один оператор SEGMENT. Так как программный код должен находиться в
некотором сегменте, то нужно присвоить ему имя. Учитывая, что
ассемблер должен суметь определить адрес сегмента, единственный
оператор ASSUME в прграмме идентифицирует только один сегмент
программы. В подобных случаях возможности сегментации программ
микропроцессора 8088 используются не полностью, но часто это и не
нужно. Если программа и ее данные помещаются в пределах одной и той
же адресуемой области памяти объемом 64 кбайт, то нет необходимости
использовать возможности процессора в сегментации памяти.
Существуют ситуации, когда в программе нужно использовать более
одного оператора SEGMENT. Одно из таких применений рассматривалоясв
гл.5 в нескольких примерах, использующих DOS. В этих примерах в
программе определялся сегмент STACK. Имя, выбранное для сегмента,
несущественно, но его тип, указанный в операторе SEGMENT, должен
быть STACK, так как файлу типа .EXE для выполнения программы
необходимо отвести стековую область. Если в программе не задать
сегмент STACK, то загрузчик DOS сохранит организацию стека в
некотором месте памяти, которое может оказаться неприемлемым. В
этом случае программа может работать недостаточно хорошо.
Другое назначение оператора SEGMENT - расположением данных в
определенном месте памяти. Как известно, при использовании DOS
лучше всего, если программа имеет перемещаемый программный сегмент.
В этом случае нас не заботит, куда DOS загружает программу. Но в
некоторых случаях фактическое расположение команд или данных
оказывается существенным. В этих случаях для задания местоположения
данных можно воспользоваться директивой AT оператора SEGMENT.
Чтобы понять значение указателя AT, рассмотрим пример. В этом
примере программа использует как Отправную точку систему BIOS, хра-
нящаяся в ПЗУ персональной ЭВМ. Хотя язык ассемблера является очень
эффективным средством программирования, с другой стороны это
довольно трудный инструмент, особенно для больших программ. Поэтому
выбор языка ассемблера обусловливается свойствами, которые делают
его выгодным для решения определенной задачи. В случае IBM PC язык
ассемблера - лучший язык для программирования функций, выполняемых
ROM BIOS. Эти функции можно охарактеризовать как управление устрой-
ствами ввода-вывода, где обычно требуется оперировать с отдельными
битами. Программирование подобных задач сводится к возможности ма-
нипулировать содержимым точно заданных ячеек памяти и портов ввода-
вывода. Язык ассемблера также используется в тех случаях, когда
необходима минимизация размера программы или максимальное быстро-
действие программы. Всем эти требования предъявляет и система ROM
BIOS.
В рассматриваемом примере используется часть BIOS. В одной из
последующих глав будет рассмотрено, как заменять части системы
BIOS. Однако в данном случае нас интересует доступ к наборам
данных, которые использует ROM BIOS. Если вы посмотрите
ассемблерный листинг для ROM BIOS (он приводится в приложении A
технического руководства по IBM PC), то увидите, что сегмент DATA
располагается в сегменте 40H или по абсолютному адресу 400H.
Приведенная на Фиг. 6.12 программа обращается в область данных ПЗУ
системы BIOS c определенной целью. В сегменте DATA имеется
переменная KB_FLAG, которая указывает текущее состояние
переключателя регистров. Одна из жалоб, часто высказываемых по
поводу клавиатуры IBM, состоит в том, что неизвестно, работаете ли
вы в верхнем регистре (CAPS LOCK) или в нижнем. Программа на Фиг.
6.12 считывает значение бита, соответствующего CAPS LOCK, и
изображает его в верхнем правом углу цветного графического дисплея.
Хотя в данной программе это не реализовано, мы будем предполагать,
что при реальном использовании этого фрагмента программы, верхний
правый угол экрана зарезервируется для описанного индикатора.
Сегмент DATA на Фиг. 6.12 показывает, как программист может
передать в программу информацию, расположенную по абсолютным адре-
сам. Оператор DATA SEGMENT использует директиву AT для того, чтобы
обеспечить безусловную привязку данного сегмента к параграфу 40H.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:25
Фиг. 6.12 Использование сегментов Page 1-1
PAGE ,132
TITLE Фиг. 6.12 Использование сегментов
0000 DATA SEGMENT AT 40H
0017 ORG 17H
0017 ?? KB_FLAG DB ?
= 0040 CAPS_STATE EQU 40H
0018 DATA ENDS
0000 VIDEO SEGMENT AT 0B800H
009E ORG 158
009E ?? INDICATOR DB ?
009F VIDEO ENDS
0000 CODE SEGMENT
ASSUME CS:CODE
0000 CAPS PROC FAR
0000 1E START: PUSH DS ; Адрес возврата
0001 B8 0000 MOV AX,0
0004 50 PUSH AX
0005 B8 ---- R MOV AX,DATA ; Адрес сегмента DATA
0008 8E D8 MOV DS,AX
ASSUME DS:DATA
000A B8 ---- R MOV AX,VIDEO ; Адрес сегмента VIDEO
000D 8E C0 MOV ES,AX
Фиг. 6.12 Расположение сегмента (начало)
ASSUME ES:VIDEO
000F DISPLAY_CAPS:
000F B0 18 MOV AL,18H ; Символ "стрелка вверх" имеет код 18H
0011 F6 06 0017 R 40 TEST KB_FLAG,CAPS_STATE ; Определение состояния клавиши CAPS
0016 75 02 JNZ CAPS_LOCK
0018 B0 19 MOV AL,19H ; Символ "стрелка вниз" имеет код 19H
001A CAPS_LOCK:
001A 26: A2 009E R MOV INDICATOR,AL ; Вывод в верхний левый угол экрана
001E B4 06 MOV AH,6 ; Функция ДОС ввода с клавиатуры
; и вывода на дисплей
0020 B2 FF MOV DL,0FFH ; Направление - ввод с клавиатуры
0022 CD 21 INT 21H
0024 3C 00 CMP AL,0 ; Проверка на наличие символа
0026 74 E7 JZ DISPLAY_CAPS ; Нет символа
0028 3C 25 CMP AL,'%' ; Проверка на символ конца
002A 74 08 JE RETURN
002C B4 02 MOV AH,2 ; Функция вывода на дисплей
002E 8A D0 MOV DL,AL ; Выводимый символ
0030 CD 21 INT 21H
0032 EB DB JMP DISPLAY_CAPS ; Повторение
0034 RETURN:
0034 CB RET ; Возврат в ДОС
0035 CAPS ENDP
0035 CODE ENDS
END START
Фиг. 6.12 Расположение сегмента (продолжение)
Просматривая листинг ROM BIOS, мы находим переменную KB_FLAG со
смещением 17H в сегменте DATA. Оператор ORG 17H данной программы
задает смещение этой переменной в оттранслированной программе.
Наконец, смысл оператора EQU, определяющего константу CAPS_STATE
следует непосредственно из листинга BIOS ПЗУ. Заданный этой
константой бит указывает текущее состояние переключателя CAPS LOCK.
В приведенной на Фиг. 6.12 программе имеется еще один оператор
SEGMENT. Он определяет сегмент VIDEO с адресом 0B800H. Это
сегментный адрес буфера для адаптера цветного- графического
дисплея. Этот адрес нужен для вывода состояния индикатора на экран
дисплея. Если мы хотим поместить символ в правый верхний угол
экрана, при условии, что строка на экране содержит 80 символов, то
смещение соответствующей ячейки должно быть равно 158 в десятичном
представлении. Программируемые характеристики оборудвания ПК
описываются в гл.8, а пока вы можете принять сказанное на веру.
Первая часть программы устанавливает необходимую адресацию
сегментов. Регистр DS указывает на сегмент DATA, а регистр ES - на
сегмент VIDEO. Хотя в программе эти сегменты объявлены директивой
AT абсолютными, ассемблер все же обозначает их значком "R", как
перемещаемые. Программа LINK, тем не менее, подставляет в
соответствующие поля данных правильные значения.
Программа тестирует переменную KB_FLAG, а ассемблер в
результате генерирует правильное смещение, равное 17H. В данном
примере символ стрелка вниз используется для обозначения обычного
режима, а стрелка вверх обозначает режим CAPS LOCK. Введенные с
клавиатуры символы считываются программой с помощью функции DOS,
выводящей эим символя на дисплей. В данном примере для выхода из
программы был произвольно выбран символ %. Если пользователь вводит
любой другой символ, то программа выводит его на дисплей и
возвращается к ожиданию ввода следующих.
Если ввести и запустить данную программу, то вы увидите в
верхнем правом углу цветного графического дисплея направленную вниз
или вверх стрелку. Если для цветного дисплея установлен режим 40
символов в строке, при выполнении данной программы
стрелка-индикатор будет выводиться во второй сверху строке. Если
нужно использовать эту программу с адаптером монохромного дисплея,
то измените адрес сегмента VIDEO на адрес 0B000H, соответственно
местоположению буфера монохромного дисплея.
При выполнении данной программы с адаптером цветного
графического дисплея в режиме 80 символов в строке вы увидите на
экране сильную помеху, "снег". Эта интерференция на экране
происходит из-за прямой передачи данных из программы в буфер
дисплея. В случае монохромного адаптера или цветного-графического
дисплея в режиме 40 символов в строке этой помехи не будет. О
причинах этого эффекта и о том, как его избежать, мы узнаем при
рассмотрении аппаратного обеспечения IBM PC.
Существуют и другие применения нескольких операторов SEGMENT в
одной программе. Если программе требуется область данных объмом
более 64 кбайт, то она должна организовать доступ к этим данным.
Как правило, вы воспользуетесь для обращения к этой области данных
некоторой схемой управления памятью. В такой ситуации вам будет
доступна вся эта область данных (за исключением некоторых
фиксированных участков) косвенную адресацию.
В качестве примера рассмотрим, как интерпретатор команд DOS
загружает программы. DOS загружает транзитную программу на границу
параграфа сразу за резидентной частью DOS. Размер этой резидентной
части может варьироваться в зависимости от числа дисководов в
системе. Кроме того, этот размер может существенно возрастать при
использовании в DOS прерывания INT 27H, которое заканчивает
выполнение программы, но оставляет ее резидентной в памяти. При
этом программный загрузчик DOS должен адресоваться к сегментному
префиксу PSP той программы, которую он загружает. Проще всего
задать эту структуру данных с помощью отдельного оператора SEGMENT.
На Фиг. 6.13 показано объявление сегмента, которое можно
использовать в двух различных местах. Если бы можно было посмотреть
текст исходной программы для загрузчика DOS, то мы бы обнаружили
там подобное объявление. В случае программы, использующей структуру
.EXE, такая сегментация могла бы обеспечить доступ к переменным в
сегментном префиксе PSP. В приведенном на Фиг. 5.6 примере
программы с применением функций DOS, использовалась структура файла
типа .COM. Это позволяло нам обращаться к различным ячейкам
сегмента PSP через смещение относительно блока PSP. Задача весьма
облегчалась тем, что DOS загружала программу в тот сегмент, который
содержал PSP.
В случае .EXE-файла блок PSP находится не в том же сегменте,
что и команды программы. Так как при передаче управления программе
типа .EXE DOS устанавливает регистры DS и ES на сегмент PSP, то
имеет смысл обращаться с PSP как с отдельным сегментом. Приведенный
на Фиг. 6.13 фрагмент программы из сегмента CODE, показывает, как
можно обращаться к данным в блоке PSP.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:31
Фиг. 6.13 Структура Программного Префикса Page 1-1
PAGE ,132
TITLE Фиг. 6.13 Структура Программного Префикса
0000 PROGRAM_SEGMENT_PREFIX SEGMENT
0000 0002[ INT_20 DB 2 DUP (?)
??
]
0002 ???? MEMORY_SIZE DW ?
0004 0005[ LONG_CALL DB 5 DUP (?)
??
]
0009 ???????? TERMINATE_ADDR DD ?
000D ???????? CTRL_BREAK DD ?
005C ORG 05CH
005C 0010[ FCB1 DB 16 DUP (?)
??
]
006C ORG 06CH
006C 0010[ FCB2 DB 16 DUP (?)
??
]
0080 ORG 080H
0080 0080[ DTA DB 128 DUP (?)
??
]
0100 PROGRAM_SEGMENT_PREFIX ENDS
0000 CODE SEGMENT
ASSUME CS:CODE,DS:PROGRAM_SEGMENT_PREFIX
0000 A1 0002 R MOV AX,MEMORY_SIZE
0003 CODE ENDS
END
Фиг. 6.13 Префикс программного сегмента
Обратите внимание, что сегмент PSP на Фиг. 6.13 на самом деле
не содержит никаких значений для переменных. Например, мы знаем,
что в первых двух байтах PSP содержится код прерывания INT 20H.
Однако мы решили показать, что в этом месте находится поле длиной 2
байта без каких-либо указаний о содержащихся там значениях. Мы
должны делать именно так, чтобы в результате нашего описания
сегмента ни редактор связей, ни загрузчик не попытались записать в
память каких-либо данных. Фактически, мы используем этот сегмент
как средство для объявления данных. Оператор SEGMENT объявляет
структуру данных, которую мы называем префиксом программного
сегмента. Ее местоположение в памяти не фиксировано, а определяется
одним из сегментных регистров. В нашем примере на Фиг. 6.13 это
местоположение определяется регистром DS.
Точно такой же способ можно использовать для обозначения любой
структуры данных, которая может быть расположена в произвольном
месте памяти микропроцессора 8088. Эта структура данных может быть,
например, болком управления для операционной системы, либо строкой
текста для текстового редактора, или даже блоком параметров
конкретной подпрограммы. Каждый объект структуры данных
располагается в своем отдельном сегменте. Таким образом, при
обращении программы к каждому элементу структуры данных сегментный
регистр указывает на начало (или на близкую к началу точку) этого
элемента. Программа не обращается к двум различным элементам с
одним и тем же значением сегментного регистра. Для каждого элемента
всегда устанавливается свой адрес сегмента.
Здесь следует немного остановиться на том, какие вообще есть
методы распределения памяти микропроцессора 8088. IBM PC с
микропроцессором 8088 может адресовать до 1Мбайт оперативной
памяти, но один сегмент может охватывать не более 64 кбайт. Даже с
четырьмя сегментными регистрами программа не имеет возможности
охватить всю память, не используя некоторых способов сегментации.
Если все данные помещаются в 64К, то нет нужды волноваться:
просто поместите все данные в один сегмент. Если же мы полагаем,
что программе требуется область данных, превышающая 64К, то нам
придется решать задачу распределения памяти. При этом возможны две
стратегии. В обоих случаях мы будем предполагать, что вся
совокупность данных может быть разбита на меньшие блоки (такие как
отдельные переменные, строки текста, управляющие блоки или
массивы), объемом не более 64К каждый.
Первый метод распределения памяти применяется в ситуации, когда
вашей главной заботой является экономия памяти. При этом методе вы
располагаете объекты данных в первых же свободных участках памяти.
Программа, управляющая доступом к областям данных, должна при этом
для каждой переменной использовать четырехбайтовый указатель. Из
них два байта используется для смещения и еще два байта для значе-
ния сегмента. Когда программе нужно полусить доступ к данным, она
извлекает адрес из области хранения адресов с помощью команд LDS
или LES. Если вам требуется еще большая экономия памяти, то вы
фактически можете хранить указатель в трехбайтовом поле. Два байта
содержат адрес сегмента данных, а оставшийся байт содержит смещение
данного объекта внутри сегмента. Начальное смещение всегда будет
иметь значение от 0 до 15, так как значение сегмента всегда кратно
16.
Хотя описанный метод наиболее эффективен в отношении объема
памяти, занимаемой данными, у него имеются пара недостатков.
Максимальная длина объекта данных немного меньше, чем 64 кбайт. В
рамках данной стратегии наихудшим окажется случай, когда абсолютный
адрес объекта данных кончается на 0FH. Так как максимальное
значение смещения в любом сегменте равно 0FFFFH, то максимальная
длина переменной будет 64К - 15, или 65521 байт. Второй недостаток
этого метода связан с затратами памяти для хранения указателей к
объектам данных. При большом числе объектов для хранения наряду с
ними всех четырехбайтовых (или трехбайтовых) указателей потребуется
много памяти.
Примером использования описанного метода распределения памяти
может служить блок управления файлом FCB. В последнем примере
работающей с DOS программы мы располагали блок FCB в произвольном
месте программы. Какого-либо выравнивания местоположения этой
структуры данных не производилось. Затем при обращении к DOS для
выполнения файловой операции программе понадобился четырехбайтовый
указатель. Идентификация блока FCB для DOS осуществлялось парой
регистров DS:DX.
При втором методе распределиня памяти все объекты данных
располагаются на границах параграфов. Это сразу же упрощает
указатель, определяющий объект данных. Этот указатель состоит
только из двух байтов, которые определяют местонахождение сегмента
с этими данными. Так как распределение памяти всегда начинается с
границы параграфа, то начальное смещение данных будет всегда равно
нулю. Однако при таком методе, расходуется дополнительная память.
Каждый раз, когда вы располагаете в памяти новый объект, возможна
потеря до 15 байт памяти. Это происходит, если последний байт
предыдущего объекта попадает точно на границу параграфа. Так как
граница следующего параграфа будет через 15 байт, то эти 15 байт в
промежутке теряются. Кроме того, при такой стратегии минимальная
длина объекта равна 16 байт. Даже если данные будут занимать меньше
места, оставшиеся байты все равно не могут быть использованы.
Как было отмечено, второй метод распределения памяти
используется загрузчиком DOS при запуске программ. DOS загружает
программу на ближайшую границу параграфа. Так как DOS исходит из
того, что в памяти располагается мало больших по размерам объектов,
то при данном методе издержки памяти будут невелики. Однако, если
ваша прикладная программа использует много небольших объектов, то
выравнивание по параграфам может оказаться слишком дорогим.
Второй метод распределения памяти, использующий выравнивание по
параграфам, позволяет определять области данных с помощью структуры
SEGMENT. Если же хотите использовать первый метод распределения
памяти, то вам потребуется другой способ определения структур
данных. Такой способ объявления данных как раз рассматривается в
следующем разделе.