6.7. Сегменты

Ранее уже рассматривался оператор 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:00:28
Фиг. 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
                                  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:00:28
Фиг. 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. Если же хотите использовать первый метод распределения памяти, то вам потребуется другой способ определения структур данных. Такой способ объявления данных как раз рассматривается в следующем разделе.