Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых

Операционная система MS-DOS

© Александр Фролов, Григорий Фролов
Том 1, книга 3, М.: Диалог-МИФИ, 1992.

[Назад] [Содеожание]

7.3. Сегментная адресация памяти

Особенностью архитектуры процессоров INTEL 8086, 80286, 80386, 80486 является использование механизма сегментации адресного пространства. Сегментация вызывает трудности у тех программистов, которые раньше работали на ЭВМ типов PDP, СМ ЭВМ, ЕС ЭВМ. В этих машинах программа имеет дело с логическими адресами, которые тем или иным способом отображаются на физические адреса. Программа может не знать подробностей отображения логических адресов на физические, она работает только с логическим адресом.

Прообраз процессора 8086 - оригинальный микропроцессор INTEL 8080 - имел линейное адресное пространство размером 64 килобайта. В этом микропроцессоре логический и физический адреса совпадали - все 16 адресных линий (адресных шин) использовались непосредственно для адресации памяти, а программы оперировали абсолютными шестнадцатиразрядными адресами.

Однако быстро растущие потребности программ в оперативной памяти привели к необходимости расширения адресного пространства. Следующий микропроцессор 8086 имел уже 20 адресных линий, что позволило непосредственно адресовать до мегабайта оперативной памяти. Архитектурное решение этого микропроцессора позволило легко адаптировать накопленное в большом количестве программное обеспечение для микропроцессора 8080.

Микропроцессор 8086 является шестнадцатиразрядным, поэтому использование двадцатиразрядного адреса в 16-разрядных командах неэффективно. Вместо указания в командах полного 20-разрядного адреса используется двухкомпонентная адресация, причем каждая компонента использует только 16 разрядов.

Эти компоненты называются сегментной компонентой адреса и компонентой смещения. Логический 20-разрядный адрес получается сложением двух компонент, причем сегментный адрес перед сложением умножается на 16 (сдвигается влево на 4 разряда). Сложение и сдвиг выполняется аппаратно, поэтому на формирование 20-разрядного адреса дополнительно время не затрачивается.

На рисунке показано, как в процессоре 8086 происходит формирование 20-разрядного адреса из адреса сегмента и смещения:

19                      4 3        0
+----------------------------------+
¦ Сегментный адрес       ¦ 0 0 0 0 ¦
+----------------------------------+ 

  +
19      16 15                      0
+----------------------------------+
¦ 0 0 0 0 ¦               Смещение ¦
+----------------------------------+ 

=
19                                0
+----------------------------------+
¦ Полный 20-разрядный адрес        ¦
+----------------------------------+ 


Адрес сегмента сдвигается влево на 4 бита с заполнением младших битов нулями, смещение расширяется до 20 битов и складывается со сдвинутым адресом сегмента. Например, если адрес сегмента равен 1234h, а смещение равно 1116h, то полный 20-разрядный адрес будет 12340h + 01116h = 13456h.

Таким образом, оперируя 16-разрядными адресами сегмента и смещением, процессор может адресовать мегабайт памяти. Для хранения сегментных адресов и смещений процессор имеет специальные регистры.

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

Сегменты могут перекрываться или не перекрываться.

Для хранения сегментных адресов процессор имеет 4 сегментных регистра: CS, DS, ES, SS. Эти регистры содержат соответственно адреса сегментов кода, данных, дополнительных данных и стека.

При адресации выполняющегося кода вместе с регистром CS используется регистр смещения IP. Пара регистров CS:IP всегда указывает на текущую выполняющуюся команду.

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

Мы приведем несколько примеров программ, составленных на языке ассемблера. Эти программы используют различное количество сегментов и могут служить шаблоном для составления ваших собственных программ.

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

Текст программы:

; В этом месте расположен 
сегмент кода. Он содержит
; выполняющуюся программу.

code    segment

; Директива assume сообщает ассемблеру, как будут
; использоваться сегментные регистры. Эта директива
; не выполняет загрузку сегментных регистров, она
; нужна ассемблеру только для правильного вычисления
; смещений.

        assume cs:code, ds:code

; Эта строка нужна для создания com-программы.

        org 100h

; При запуске программы управление будет передано
; на оператор с меткой start.
; Первое, что должна сделать программа - правильно
; загрузить сегментные регистры.
; Регистр CS загружается операционной системой
; при запуске программы, поэтому его загружать не надо.
; Регистры DS и ES должны указывать на начало
; сегмента кода, так как программа состоит из одного
; сегмента.

start: 
        mov     ax, cs
        mov     ds, ax

; Выводим сообщение msg из сегмента данных

        mov     ah, 9h
        mov     dx, OFFSET msg
        int     21h

; Завершаем работу программы

        mov     ax, 4C00h
        int     21h

; Строка, которую программа выведет на экран.

msg     db      "Hello, world.", 13, 10, "$"

code    ends

        end     start


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

; Создаем сегмент стека. 
Размер стека - 256 байт,
; стек выравнен на границу параграфа (para).

stack   segment para stack

; Резервируем 256 байт для стека.

        db      100h dup (?)

stack   ends

; Создаем сегмент данных. Этот сегмент выравнен на
; границу двухбайтового слова (word).

data    segment word

; Строка, которую программа выведет на экран.

msg     db      "Hello, world.", 13, 10, "$"

data    ends

; В этом месте расположен сегмент кода. Он содержит
; выполняющуюся программу.

code    segment

; Директива assume сообщает ассемблеру, как будут
; использоваться сегментные регистры. Эта директива
; не выполняет загрузку сегментных регистров, она
; нужна ассемблеру только для правильного вычисления
; смещений.

        assume  cs:code, ds:data, ss:stack

; При запуске программы управление будет передано
; на оператор с меткой start.
; Первое, что должна сделать программа - правильно
; загрузить сегментные регистры.
;
; Следующие два оператора инициализируют сегментный
; регистр данных DS.

start:  
        mov     ax, data
        mov     ds, ax

; Инициализируем сегментный регистр стека и
; указатель стека (регистры SS и SP).
; Эта операция должна выполняться в состоянии
; процессора с запрещенными прерываниями, иначе
; если регистр SS будет содержать уже новое значение,
; а SP - старое и если в этот момент произойдет
; прерывание, адрес возврата и значение регистра флагов
; будут записаны в не предназначенную для этого область.

        cli
        mov     ss, ax
        mov     sp, OFFSET stack
        sti

; Выводим сообщение msg из сегмента данных

        mov     ah, 9h
        mov     dx, OFFSET msg
        int     21h

; Завершаем работу программы

        mov     ax, 4C00h
        int     21h

code    ends

        end     start


Макроассемблер MASM версии 5.0 и более поздних версий, а также Quick Assembler содержит директивы, упрощающие описание сегментов. Это такие директивы, как .CODE, .DATA, .MODEL и другие. Вы найдете подробное описание этих директив в соответствующей документации по ассемблеру. Остановимся подробнее на директиве .MODEL.

Эта директива задает так называемую модель памяти, используемую программой. Что это такое?

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

  • Tiny;
  • Small;
  • Medium;
  • Compact;
  • Large;
  • Huge.

Модель памяти Tiny используется небольшими программами, состоящими из одного сегмента и имеющими формат COM. Использование этой модели памяти - единственный способ получения загрузочного модуля в формате COM.

В модели Small один сегмент используется для кода, один для хранения данных и размещения стека программы. Общий размер программы в этом случае ограничен величиной 128 килобайтов. Большинство небольших программ используют именно эту модель памяти.

Если ваша программа оперирует небольшим объемом данных, но размер кода превышает 64 килобайта, вам подходит модель Medium. В этой модели используется несколько сегментов для хранения кода и только один - для данных.

Модель Compact, в отличие от Medium, использует один сегмент для кода и несколько - для данных. Эта модель больше всего подходит для небольших программ, обрабатывающих большие массивы данных.

Модель памяти Large предоставляет возможность использовать несколько сегментов для кода и несколько сегментов для данных. Эта модель обычно используется большими программами, которые обрабатывают большие объемы данных.

И, наконец, модель памяти Huge. Эта модель аналогична Large, но для программ, составленных на языке Си, она позволяет использовать массивы данных, имеющие размер более одного сегмента.

Приведем два примера использования моделей памяти в программах, составленных на языке ассемблера. Эти примеры аналогичны тем, которые мы только что рассмотрели. Первая программа использует модель памяти Tiny:

; Определяем используемую модель памяти

        .model  tiny

; Определяем сегмент данных.

        .data
msg     db      "Hello, world.", 13, 10, "$"

; Определяем сегмент кода.

        .code

; Макрокоманда startup выполняет все необходимые
; инициализирующие действия, которые зависят от
; модели памяти.

        .startup

; Выводим сообщение на экран

        mov     ah, 9h
        mov     dx, OFFSET msg
        int     21h

; Завершаем выполнение программы

        .exit

        END

Вторая программа использует модель памяти Small, в ней мы дополнительно определили свой стек:

; Определяем используемую модель памяти

        .model  small

; Определяем сегмент данных.

        .data
msg     db      "Hello, world.", 13, 10, "$"

; Определяем свой стек, его размер - 256 байтов

        .stack  100h

; Определяем сегмент кода.

        .code

        .startup

; Выводим сообщение на экран

        mov     ah, 9h
        mov     dx, OFFSET msg
        int     21h

; Завершаем выполнение программы

        .exit

        END


Для программ, составленных на языке Си, модель памяти указывается при трансляции. Если используется пакетный транслятор, модель указывается при помощи опций в командной строке. Если вы работаете в интегрированной среде, такой как Quick C, модель задается при помощи соответствующего меню, а сама программа не содержит каких-либо директив, определяющих используемую модель памяти.

Как правильно выбрать модель памяти?

Если ваша программа небольшая по размеру, то вам подойдут модели TINY или SMALL. При использовании остальных моделей памяти возможно увеличение размера загрузочного модуля и времени выполнения программы из-за того, что в операциях с данными и при вызове подпрограмм используются полные адреса, состоящие из сегмента и смещения. Это означает, в частности, что если при трансляции программы была использована модель LARGE, то при обращении к каждой переменной и при вызове каждой подпрограммы (функции) будет использоваться полный адрес.

Для сокращения накладных расходов отдельные переменные и функции можно разместить в отдельном сегменте. Для этого их надо описать специальным образом - используя ключевое слово near (для С 6.0 и QC 2.5 можно использовать _near).

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

В противоположность к только что описанному ключевое слово far (_far для С 6.0 и QC 2.5) говорит о том, что данные или функция могут располагаться в любом месте памяти, не обязательно в текущем сегменте, и для адресации необходимо использовать полный 32-битовый адрес.

Ключевое слово huge (_huge) необходимо использовать при описании массвов, которые по своим размерам могут превышать 64К. Для адресации при этом будет использоваться полный 32-битовый адрес. Для функций это ключевое слово не применяется.

Приведем несколько примеров описания данных и функций с использованием ключевых слов near, far, huge.

// Используемая модель памяти - SMALL

char dim1[250];
char _far dim2[45000];
char _huge dim3[80000];
char _far *far_ptr;
char _far * _far * far_ptr1;

int _far function1(void);
// Используемая модель памяти - LARGE

char _near dim4[2000];

char _far * _near function2(void);


Исчерпывающие сведения об использовании моделей памяти можно почерпнуть из документации на используемый транслятор.

[Назад] [Содеожание]