Cамоучитель по Assembler

         


Синтаксис ассемблера

Предложения, составляющие программу, могут представлять собой синтаксическую конструкцию, соответствующую команде, макрокоманде, директиве или комментарию. Для того чтобы транслятор ассемблера мог распознать их, они должны формироваться по определенным синтаксическим правилам. Для этого лучше всего использовать формальное описание синтаксиса языка наподобие правил грамматики. Наиболее распространенные способы подобного описания языка программирования — синтаксические диаграммы и расширенные формы Бэкуса—Наура. Для практического использования более удобны синтаксические диаграммы. К примеру, синтаксис предложений ассемблера можно описать с помощью синтаксических диаграмм, показанных на следующих рисунках.

Рис. 1. Формат предложения ассемблера

Рис. 2. Формат директив

Рис. 3. Формат команд и макрокоманд

На этих рисунках: имя метки — идентификатор, значением которого является адрес первого байта того предложения исходного текста программы, которое он обозначает; имя — идентификатор, отличающий данную директиву от других одноименных директив. В результате обработки ассемблером определенной директивы этому имени могут быть присвоены определенные характеристики; код операции (КОП) и директива — это мнемонические обозначения соответствующей машинной команды, макрокоманды или директивы транслятора; операнды — части команды, макрокоманды или директивы ассемблера, обозначающие объекты, над которыми производятся действия. Операнды ассемблера описываются выражениями с числовыми и текстовыми константами, метками и идентификаторами переменных с использованием знаков операций и некоторых зарезервированных слов. Как использовать синтаксические диаграммы?
Очень просто: для этого нужно всего лишь найти и затем пройти путь от входа диаграммы (слева) к ее выходу (направо). Если такой путь существует, то предложение или конструкция синтаксически правильны. Если такого пути нет, значит эту конструкцию компилятор не примет. При работе с синтаксическими диаграммами обращайте внимание на направление обхода, указываемое стрелками, так как среди путей могут быть и такие, по которым можно идти справа налево. По сути, синтаксические диаграммы отражают логику работы транслятора при разборе входных предложений программы.

Допустимыми символами при написании текста программ являются: все латинские буквы: A—Z, a—z. При этом заглавные и строчные буквы считаются эквивалентными; цифры от 0 до 9; знаки ?, @, $, _, &; разделители , . [ ] ( ) < > { } + / * % ! ' " ? \ = # ^. Предложения ассемблера формируются из лексем, представляющих собой синтаксически неразделимые последовательности допустимых символов языка, имеющие смысл для транслятора.

Лексемами являются: идентификаторы — последовательности допустимых символов, использующиеся для обозначения таких объектов программы, как коды операций, имена переменных и названия меток. Правило записи идентификаторов заключается в следующем: идентификатор может состоять из одного или нескольких символов. В качестве символов можно использовать буквы латинского алфавита, цифры и некоторые специальные знаки — _, ?, $, @. Идентификатор не может начинаться символом цифры. Длина идентификатора может быть до 255 символов, хотя транслятор воспринимает лишь первые 32, а остальные игнорирует. Регулировать длину возможных идентификаторов можно с использованием опции командной строки mv. Кроме этого существует возможность указать транслятору на то, чтобы он различал прописные и строчные буквы либо игнорировал их различие (что и делается по умолчанию). Для этого применяются опции командной строки /mu, /ml, /mx; цепочки символов — последовательности символов, заключенные в одинарные или двойные кавычки; целые числа в одной из следующих систем счисления: двоичной, десятичной, шестнадцатеричной. Отождествление чисел при записи их в программах на ассемблере производится по определенным правилам:

  • Десятичные числа не требуют для своего отождествления указания каких-либо дополнительных символов, например 25 или 139.
  • Для отождествления в исходном тексте программы двоичных чисел необходимо после записи нулей и единиц, входящих в их состав, поставить латинское “b”, например 10010101b.
  • Шестнадцатеричные числа имеют больше условностей при своей записи:
  • Во-первых, они состоят из цифр 0...9, строчных и прописных букв латинского алфавита a, b, c, d, e, f или A, B, C, D, E, F.
  • Во-вторых, у транслятора могут возникнуть трудности с распознаванием шестнадцатеричных чисел из-за того, что они могут состоять как из одних цифр 0...9 (например 190845), так и начинаться с буквы латинского алфавита (например ef15). Для того чтобы "объяснить" транслятору, что данная лексема не является десятичным числом или идентификатором, программист должен специальным образом выделять шестнадцатеричное число. Для этого на конце последовательности шестнадцатеричных цифр, составляющих шестнадцатеричное число, записывают латинскую букву “h”. Это обязательное условие. Если шестнадцатеричное число начинается с буквы, то перед ним записывается ведущий ноль: 0ef15h. Таким образом, мы разобрались с тем, как конструируются предложения программы ассемблера. Но это лишь самый поверхностный взгляд.

    Практически каждое предложение содержит описание объекта, над которым или при помощи которого выполняется некоторое действие. Эти объекты называются операндами.
    Их можно определить так:
    операнды — это объекты (некоторые значения, регистры или ячейки памяти), на которые действуют инструкции или директивы, либо это объекты, которые определяют или уточняют действие инструкций или директив.

    Операнды могут комбинироваться с арифметическими, логическими, побитовыми и атрибутивными операторами для расчета некоторого значения или определения ячейки памяти, на которую будет воздействовать данная команда или директива.

    Возможно провести следующую классификацию операндов: постоянные, или непосредственные, операнды адресные операнды перемещаемые операнды счетчик адреса регистровый операнд базовый и индексный операнды структурные операнды Записи Рассмотрим подробнее характеристику операндов из приведенной классификации: Постоянные или непосредственные операнды — число, строка, имя или выражение, имеющие некоторое фиксированное значение. Имя не должно быть перемещаемым, то есть зависеть от адреса загрузки программы в память. К примеру, оно может быть определено операторами equ или =.

     
    num     equ     5
    imd = num-2
            mov     al,num  ;эквивалентно mov al,5 
    ;5 здесь непосредственный операнд
            add     [si],imd        ; imd=3 - непосредственный операнд
            mov     al,5            ;5 - непосредственный операнд
            
    В данном фрагменте определяются две константы, которые затем используются в качестве непосредственных операндов в командах пересылки mov и сложения add.  Адресные операнды — задают физическое расположение операнда в памяти с помощью указания двух составляющих адреса: сегмента и смещения (рис. 4).

    Рис. 4. Синтаксис описания адресных операндов

    К примеру:

     
            mov     ax,0000h
            mov     ds,ax
            mov     ax,ds:0000h     ;записать слово в ax из области памяти по
                                            ;физическому адресу 0000:0000
            
    Здесь третья команда mov имеет адресный операнд.  Перемещаемые операнды — любые символьные имена, представляющие некоторые адреса памяти. Эти адреса могут обозначать местоположение в памяти некоторых инструкции (если операнд — метка) или данных (если операнд — имя области памяти в сегменте данных).
    Перемещаемые операнды отличаются от адресных тем, что они не привязаны к конкретному адресу физической памяти. Сегментная составляющая адреса перемещаемого операнда неизвестна и будет определена после загрузки программы в память для выполнения.

    К примеру:

      
    data    segment
    mas_w   dw      25 dup (0)
    …
    code    segment
    …
            lea     si,mas_w        ;mas_w - перемещаемый операнд
     
    В этом фрагменте mas_w — символьное имя, значением которого является начальный адрес области памяти размером 25 слов. Полный физический адрес этой области памяти будет известен только после загрузки программы в память для выполнения.  Счетчик адреса — специфический вид операнда. Он обозначается знаком $.
    Специфика этого операнда в том, что когда транслятор ассемблера встречает в исходной программе этот символ, то он подставляет вместо него текущее значение счетчика адреса. Значение счетчика адреса, или, как его иногда называют, счетчика размещения, представляет собой смещение текущей машинной команды относительно начала сегмента кода.
    В формате листинга счетчику адреса соответствует вторая или третья колонка (в зависимости от того, присутствует или нет в листинге колонка с уровнем вложенности). Если взять в качестве пример любой листинг, то видно, что при обработке транслятором очередной команды ассемблера счетчик адреса увеличивается на длину сформированной машинной команды. Важно правильно понимать этот момент.
    К примеру, обработка директив ассемблера не влечет за собой изменения счетчика. Директивы, в отличие от команд ассемблера, — это лишь указания транслятору на выполнение определенных действий по формированию машинного представления программы, и для них транслятором не генерируется никаких конструкций в памяти. В качестве примера использования в команде значения счетчика адреса можно привести следующий:
      
     jmp $+3 ;безусловный переход на команду mov
     cld ;длина команды cld составляет 1 байт
     mov al,1
      
    При использовании подобного выражения для перехода не забывайте о длине самой команды, в которой это выражение используется, так как значение счетчика адреса соответствует смещению в сегменте команд данной, а не следующей за ней команды. В нашем примере команда jmp занимает 2 байта. Но будьте осторожны, длина команды зависит от того, какие в ней используются операнды. Команда с регистровыми операндами будет короче команды, один из операндов которой расположен в памяти. В большинстве случаев эту информацию можно получить, зная формат машинной команды и анализируя колонку листинга с объектным кодом команды.  Регистровый операнд — это просто имя регистра. В программе на ассемблере можно использовать имена всех регистров общего назначения и большинства системных регистров.
       
     mov al,4 ;константу 4 заносим в регистр al
     mov dl,pass+4 ;байт по адресу pass+4 в регистр
    dl
     add al,dl ;команда с регистровыми
    операндами
    Базовый и индексный операнды. Этот тип операндов используется для реализации косвенной базовой, косвенной индексной адресации или их комбинаций и расширений Структурные операнды используются для доступа к конкретному элементу сложного типа данных, называемого структурой Записи (аналогично структурному типу) используются для доступа к битовому полю некоторой записи. Операнды являются элементарными компонентами, из которых формируется часть машинной команды, обозначающая объекты, над которыми выполняется операция.
    В более общем случае операнды могут входить как составные части в более сложные образования, называемые выражениями.
    Выражения представляют собой комбинации операндов и операторов, рассматриваемые как единое целое.

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

    Возможные типы операндов мы уже рассмотрели. Перечислим теперь возможные типы операторов ассемблера и синтаксические правила формирования выражений ассемблера. Арифметические операторы Операторы сдвига Операторы сравнения Логические операторы Индексный оператор Оператор переопределения типа Оператор переопределения сегмента Оператор именования типа структуры Оператор получения сегментной составляющей адреса выражения Оператор получения смещения выражения  

    В табл. 2 приведены поддерживаемые языком ассемблера операторы и перечислены их приоритеты. Дадим краткую характеристику операторов: Арифметические операторы. К ним относятся: унарные “+” и “–”; бинарные “+” и “–”; умножения “*”; целочисленного деления “/”; получения остатка от деления “mod”. Эти операторы расположены на уровнях приоритета 6, 7, 8 в табл. 2. Например,

        
    tab_size equ 50 ;размер массива в байтах
    size_el equ 2 ;размер элементов
    …
    ;вычисляется число элементов массива и заносится в
    регистр cx
     mov cx,tab_size / size_el ;оператор “/”

    Рис. 5. Синтаксис арифметических операций Операторы сдвига выполняют сдвиг выражения на указанное количество разрядов (рис. 6). Например,

        
    mask_b equ 10111011
    …
    mov al,mask_b shr 3 ;al=00010111

    Рис. 6. Синтаксис операторов сдвига Операторы сравнения (возвращают значение “истина” или “ложь”) предназначены для формирования логических выражений (см. рис. 7 и табл. 1). Логическое значение “истина” соответствует цифровой единице, а “ложь” — нулю. Например,

         
    tab_size equ 30 ;размер таблицы
    …
     mov al,tab_size ge 50 ;загрузка размера
    таблицы в al
     cmp al,0 ;если tab_size < 50, то
     je m1 ;переход на m1
    …
    m1: …
    В этом примере если значение tab_size больше или равно 50, то результат в al равен 0ffh, а если tab_size меньше 50, то al равно 00h. Команда cmp сравнивает значение al с нулем и устанавливает соответствующие флаги в flags/eflags. Команда je на основе анализа этих флагов передает или не передает управление на метку m1.

    Рис. 7. Синтаксис операторов сравнения

    Таблица 1. Операторы сравнения
    Оператор Значение
    eq ИСТИНА, если выражение_1 равно выражение_2
    ne ИСТИНА, если выражение_1 не равно выражение_2
    lt ИСТИНА, если выражение_1 меньше выражение_2>ИСТИНА, если выражение_1 не равно выражение_2
    le ИСТИНА, если выражение_1 меньше или равно выражение_2
    gt ИСТИНА, если выражение_1 больше выражение_2
    ge ИСТИНА, если выражение_1 больше или равно выражение_2
    Логические операторы выполняют над выражениями побитовые операции (рис. 8). Выражения должны быть абсолютными, то есть такими, численное значение которых может быть вычислено транслятором. Например:
          
    flags   equ     10010011
             mov    al,flags xor 01h        ;al=10010010;пересылка в al поля flags с
                                            ;инвертированным правым битом

    Рис. 8. Синтаксис логических операторов Индексный оператор [ ]. Не удивляйтесь, но скобки тоже являются оператором, и транслятор их наличие воспринимает как указание сложить значение выражение_1 за этими скобками с выражение_2, заключенным в скобки (рис. 9). Например,

            mov     ax,mas[si]      ;пересылка слова по адресу mas+(si) в регистр ax

    Рис. 9. Синтаксис индексного оператора

    Заметим, что в литературе по ассемблеру принято следующее обозначение: когда в тексте речь идет о содержимом регистра, то его название берут в круглые скобки. Мы также будем придерживаться этого обозначения.
    К примеру, в нашем случае запись в комментариях последнего фрагмента программы mas + (si) означает вычисление следующего выражения: значение смещения символического имени mas плюс содержимое регистра si.

    Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением (рис. 10).
    Тип может принимать одно из следующих значений: byte, word, dword, qword, tbyte, near, far. Например,

    d_wrd   dd      0
    ...
            mov     al,byte ptr d_wrd+1 ;пересылка второго байта из двойного слова
    Поясним этот фрагмент программы. Переменная d_wrd имеет тип двойного слова. Что делать, если возникнет необходимость обращения не ко всей переменной, а только к одному из входящих в нее байтов (например, ко второму)? Если попытаться сделать это командой
    mov al,d_wrd+1, то транслятор выдаст сообщение о несовпадении типов операндов. Оператор ptr позволяет непосредственно в команде переопределить тип и выполнить команду.

    Рис. 10. Синтаксис оператора переопределения типа Оператор переопределения сегмента : (двоеточие) заставляет вычислять физический адрес относительно конкретно задаваемой сегментной составляющей: “имя сегментного регистра”, “имя сегмента” из соответствующей директивы SEGMENT или “имя группы” (рис. 11). Этот момент очень важен, поэтому поясню его подробнее. При обсуждении сегментации мы говорили о том, что микропроцессор на аппаратном уровне поддерживает три типа сегментов — кода, стека и данных. В чем заключается такая аппаратная поддержка? К примеру, для выборки на выполнение очередной команды микропроцессор должен обязательно посмотреть содержимое сегментного регистра cs и только его. А в этом регистре, как мы знаем, содержится (пока еще не сдвинутый) физический адрес начала сегмента команд. Для получения адреса конкретной команды микропроцессору остается умножить содержимое cs на 16 (что означает сдвиг на четыре разряда) и сложить полученное 20-битное значение с 16-битным содержимым регистра ip. Примерно то же самое происходит и тогда, когда микропроцессор обрабатывает операнды в машинной команде. Если он видит, что операнд — это адрес (эффективный адрес, который является только частью физического адреса), то он знает, в каком сегменте его искать — по умолчанию это сегмент, адрес начала которого записан в сегментном регистре ds.
    А что же с сегментом стека? Посмотрите раздел "Программная модель микропроцессора", там, где мы описывали назначение регистров общего назначения.
    В контексте нашего рассмотрения нас интересуют регистры sp и bp. Если микропроцессор видит в качестве операнда (или его части, если операнд — выражение) один из этих регистров, то по умолчанию он формирует физический адрес операнда используя в качестве его сегментной составляющей содержимое регистра ss. Что подразумевает термин “по умолчанию”? Вспомните “рефлексы”, о которых мы говорили на уроке 1. Это набор микропрограмм в блоке микропрограммного управления, каждая из которых выполняет одну из команд в системе машинных команд микропроцессора. Каждая микропрограмма работает по своему алгоритму. Изменить его, конечно же, нельзя, но можно чуть-чуть подкорректировать. Делается это с помощью необязательного поля префикса машинной команды (см. формат машинной команды). Если мы согласны с тем, как работает команда, то это поле отсутствует. Если же мы хотим внести поправку (если, конечно, она допустима для конкретной команды) в алгоритм работы команды, то необходимо сформировать соответствующий префикс.
    Префикс представляет собой однобайтовую величину, численное значение которой определяет ее назначение. Микропроцессор распознает по указанному значению, что этот байт является префиксом, и дальнейшая работа микропрограммы выполняется с учетом поступившего указания на корректировку ее работы. Сейчас нас интересует один из них - префикс замены (переопределения) сегмента. Его назначение состоит в том, чтобы указать микропроцессору (а по сути, микропрограмме) на то, что мы не хотим использовать сегмент по умолчанию. Возможности для подобного переопределения, конечно, ограничены. Сегмент команд переопределить нельзя, адрес очередной исполняемой команды однозначно определяется парой cs:ip. А вот сегменты стека и данных — можно. Для этого и предназначен оператор “:”. Транслятор ассемблера, обрабатывая этот оператор, формирует соответствующий однобайтовый префикс замены сегмента. Например,

    .code
    ...
            jmp     met1    ;обход обязателен, иначе поле ind
    будет трактоваться ;как очередная команда
    ind     db      5       ;описание поля данных в сегменте команд
    met1:
    ...
    mov al,cs:ind ;переопределение сегмента позволяет работать с
                            ;данными, определенными внутри сегмента кода

    Рис. 11. Синтаксис оператора переопределения сегмента Оператор именования типа структуры . (точка) также заставляет транслятор производить определенные вычисления, если он встречается в выражении.  Оператор получения сегментной составляющей адреса выражения seg возвращает физический адрес сегмента для выражения (рис. 12), в качестве которого могут выступать метка, переменная, имя сегмента, имя группы или некоторое символическое имя.

    Рис. 12. Синтаксис оператора получения сегментной составляющей Оператор получения смещения выражения offset позволяет получить значение смещения выражения (рис. 13) в байтах относительно начала того сегмента, в котором выражение определено.

    Рис. 13. Синтаксис оператора получения смещения

    Например,

     
    .data
    pole    dw      5
    ...
    .code
    ...
            mov     ax,seg  pole
            mov     es,ax
            mov     dx,offset       pole    ;теперь в паре es:dx полный адрес pole
     

    Как и в языках высокого уровня, выполнение операторов ассемблера при вычислении выражений осуществляется в соответствии с их приоритетами (см. табл. 2). Операции с одинаковыми приоритетами выполняются последовательно слева направо. Изменение порядка выполнения возможно путем расстановки круглых скобок, которые имеют наивысший приоритет.

    Таблица 2. Операторы и их приоритет
    Оператор Приоритет
    length, size, width, mask, (, ), [, ], <, > 1
    . 2
    : 3
    ptr, offset, seg, type, this 4
    high, low 5
    +, - (унарные) 6
    *, /, mod, shl, shr 7
    +, -, (бинарные) 8
    eq, ne, lt, le, gt, ge 9
    not 10
    and 11
    or, xor 12
    short, type 13

    Директивы сегментации

    В ходе предыдущего обсуждения мы выяснили все основные правила записи команд и операндов в программе на ассемблере. Открытым остался вопрос о том, как правильно оформить последовательность команд, чтобы транслятор мог их обработать, а микропроцессор — выполнить.

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

    Синтаксическое описание сегмента на ассемблере представляет собой конструкцию, изображенную на рис. 14:

    Рис. 14. Синтаксис описания сегмента

    Важно отметить, что функциональное назначение сегмента несколько шире, чем простое разбиение программы на блоки кода, данных и стека. Сегментация является частью более общего механизма, связанного с концепцией модульного программирования. Она предполагает унификацию оформления объектных модулей, создаваемых компилятором, в том числе с разных языков программирования. Это позволяет объединять программы, написанные на разных языках. Именно для реализации различных вариантов такого объединения и предназначены операнды в директиве SEGMENT.
    Рассмотрим их подробнее. Атрибут выравнивания сегмента (тип выравнивания) сообщает компоновщику о том, что нужно обеспечить размещение начала сегмента на заданной границе. Это важно, поскольку при правильном выравнивании доступ к данным в процессорах i80х86 выполняется быстрее. Допустимые значения этого атрибута следующие: BYTE — выравнивание не выполняется. Сегмент может начинаться с любого адреса памяти; WORD — сегмент начинается по адресу, кратному двум, то есть последний (младший) значащий бит физического адреса равен 0 (выравнивание на границу слова); DWORD — сегмент начинается по адресу, кратному четырем, то есть два последних (младших) значащих бита равны 0 (выравнивание на границу двойного слова); PARA — сегмент начинается по адресу, кратному 16, то есть последняя шестнадцатеричная цифра адреса должна быть 0h (выравнивание на границу параграфа); PAGE — сегмент начинается по адресу, кратному 256, то есть две последние шестнадцатеричные цифры должны быть 00h (выравнивание на границу 256-байтной страницы); MEMPAGE — сегмент начинается по адресу, кратному 4 Кбайт, то есть три последние шестнадцатеричные цифры должны быть 000h (адрес следующей 4-Кбайтной страницы памяти).

    По умолчанию тип выравнивания имеет значение PARA.   Атрибут комбинирования сегментов (комбинаторный тип) сообщает компоновщику, как нужно комбинировать сегменты различных модулей, имеющие одно и то же имя. Значениями атрибута комбинирования сегмента могут быть: PRIVATE — сегмент не будет объединяться с другими сегментами с тем же именем вне данного модуля; PUBLIC — заставляет компоновщик соединить все сегменты с одинаковыми именами. Новый объединенный сегмент будет целым и непрерывным. Все адреса (смещения) объектов, а это могут быть, в зависимости от типа сегмента, команды и данные, будут вычисляться относительно начала этого нового сегмента; COMMON — располагает все сегменты с одним и тем же именем по одному адресу. Все сегменты с данным именем будут перекрываться и совместно использовать память. Размер полученного в результате сегмента будет равен размеру самого большого сегмента; AT xxxx — располагает сегмент по абсолютному адресу параграфа (параграф — объем памяти, кратный 16; поэтому последняя шестнадцатеричная цифра адреса параграфа равна 0). Абсолютный адрес параграфа задается выражением xxx. Компоновщик располагает сегмент по заданному адресу памяти (это можно использовать, например, для доступа к видеопамяти или области ПЗУ), учитывая атрибут комбинирования. Физически это означает, что сегмент при загрузке в память будет расположен, начиная с этого абсолютного адреса параграфа, но для доступа к нему в соответствующий сегментный регистр должно быть загружено заданное в атрибуте значение. Все метки и адреса в определенном таким образом сегменте отсчитываются относительно заданного абсолютного адреса; STACK — определение сегмента стека. Заставляет компоновщик соединить все одноименные сегменты и вычислять адреса в этих сегментах относительно регистра ss. Комбинированный тип STACK (стек) аналогичен комбинированному типу PUBLIC, за исключением того, что регистр ss является стандартным сегментным регистром для сегментов стека. Регистр sp устанавливается на конец объединенного сегмента стека. Если не указано ни одного сегмента стека, компоновщик выдаст предупреждение, что стековый сегмент не найден. Если сегмент стека создан, а комбинированный тип STACK не используется, программист должен явно загрузить в регистр ss адрес сегмента (подобно тому, как это делается для регистра ds).

    По умолчанию атрибут комбинирования принимает значение PRIVATE.   Атрибут класса сегмента (тип класса) — это заключенная в кавычки строка, помогающая компоновщику определить соответствующий порядок следования сегментов при собирании программы из сегментов нескольких модулей. Компоновщик объединяет вместе в памяти все сегменты с одним и тем же именем класса (имя класса, в общем случае, может быть любым, но лучше, если оно будет отражать функциональное назначение сегмента). Типичным примером использования имени класса является объединение в группу всех сегментов кода программы (обычно для этого используется класс “code”). С помощью механизма типизации класса можно группировать также сегменты инициализированных и неинициализированных данных; Атрибут размера сегмента. Для процессоров i80386 и выше сегменты могут быть 16 или 32-разрядными. Это влияет, прежде всего, на размер сегмента и порядок формирования физического адреса внутри него. Атрибут может принимать следующие значения: USE16 — это означает, что сегмент допускает 16-разрядную адресацию. При формировании физического адреса может использоваться только 16-разрядное смещение. Соответственно, такой сегмент может содержать до 64 Кбайт кода или данных; USE32 — сегмент будет 32-разрядным. При формирования физического адреса может использоваться 32-разрядное смещение. Поэтому такой сегмент может содержать до 4 Гбайт кода или данных. Все сегменты сами по себе равноправны, так как директивы SEGMENT и ENDS не содержат информации о функциональном назначении сегментов. Для того чтобы использовать их как сегменты кода, данных или стека, необходимо предварительно сообщить транслятору об этом, для чего используют специальную директиву ASSUME, имеющую формат, показанный на рис. 15. Эта директива сообщает транслятору о том, какой сегмент к какому сегментному регистру привязан. В свою очередь, это позволит транслятору корректно связывать символические имена, определенные в сегментах. Привязка сегментов к сегментным регистрам осуществляется с помощью операндов этой директивы, в которых имя_сегмента должно быть именем сегмента, определенным в исходном тексте программы директивой SEGMENT или ключевым словом nothing. Если в качестве операнда используется только ключевое слово nothing, то предшествующие назначения сегментных регистров аннулируются, причем сразу для всех шести сегментных регистров. Но ключевое слово nothing можно использовать вместо аргумента имя сегмента; в этом случае будет выборочно разрываться связь между сегментом с именем имя сегмента и соответствующим сегментным регистром (см. рис. 15).

    Рис. 15. Директива ASSUME 

    На уроке 3 мы рассматривали пример программы с директивами сегментации. Эти директивы изначально использовались для оформления программы в трансляторах MASM и TASM. Поэтому их называют стандартными директивами сегментации.

    Для простых программ, содержащих по одному сегменту для кода, данных и стека, хотелось бы упростить ее описание. Для этого в трансляторы MASM и TASM ввели возможность использования упрощенных директив сегментации. Но здесь возникла проблема, связанная с тем, что необходимо было как-то компенсировать невозможность напрямую управлять размещением и комбинированием сегментов. Для этого совместно с упрощенными директивами сегментации стали использовать директиву указания модели памяти MODEL, которая частично стала управлять размещением сегментов и выполнять функции директивы ASSUME (поэтому при использовании упрощенных директив сегментации директиву ASSUME можно не использовать). Эта директива связывает сегменты, которые в случае использования упрощенных директив сегментации имеют предопределенные имена, с сегментными регистрами (хотя явно инициализировать ds все равно придется).

    В листинге 1 приведен пример программы с использованием упрощенных директив сегментации:

     
            Листинг 1. Использование упрощенных директив сегментации
    ;---------Prg_3_1.asm-------------------------------
    masm                    ;режим работы TASM: ideal или masm
    model   small           ;модель памяти
    .data                   ;сегмент данных
    message db      'Введите две шестнадцатеричные цифры,$'
    .stack                  ;сегмент стека
            db      256     dup ('?')       ;сегмент стека
    .code                   ;сегмент кода
    main    proc            ;начало процедуры main
            mov     ax,@data        ;заносим адрес сегмента данных в регистр ax
            mov     ds,ax   ;ax в ds
    ;далее текст программы (см. сегмента кода в листинге 3.1 книги)
            mov     ax,4c00h        ;пересылка 4c00h в регистр ax
            int     21h             ;вызов прерывания с номером 21h
    main    endp            ;конец процедуры main
    end     main            ;конец программы с точкой входа main
    Синтаксис директивы MODEL показан на рис. 16.

    Рис. 16. Синтаксис директивы MODEL

    Обязательным параметром директивы MODEL является модель памяти. Этот параметр определяет модель сегментации памяти для программного модуля. Предполагается, что программный модуль может иметь только определенные типы сегментов, которые определяются упомянутыми нами ранее упрощенными директивами описания сегментов. Эти директивы приведены в табл. 3.

    Таблица 3. Упрощенные директивы определения сегмента
    Формат директивы 
    (режим MASM)
    Формат директивы 
    (режим IDEAL)
    Назначение
    .CODE [имя] CODESEG[имя] Начало или продолжение сегмента кода
    .DATA DATASEG Начало или продолжение сегмента инициализированных данных. Также используется для определения данных типа near
    .CONST CONST Начало или продолжение сегмента постоянных данных (констант) модуля
    .DATA? UDATASEG Начало или продолжение сегмента неинициализированных данных. Также используется для определения данных типа near
    .STACK [размер] STACK [размер] Начало или продолжение сегмента стека модуля. Параметр [размер] задает размер стека
    .FARDATA [имя] FARDATA [имя] Начало или продолжение сегмента инициализированных данных типа far
    .FARDATA? [имя] UFARDATA [имя] Начало или продолжение сегмента неинициализированных данных типа far
    Наличие в некоторых директивах параметра [имя] говорит о том, что возможно определение нескольких сегментов этого типа. С другой стороны, наличие нескольких видов сегментов данных обусловлено требованием обеспечить совместимость с некоторыми компиляторами языков высокого уровня, которые создают разные сегменты данных для инициализированных и неинициализированных данных, а также констант.

    При использовании директивы MODEL транслятор делает доступными несколько идентификаторов, к которым можно обращаться во время работы программы, с тем, чтобы получить информацию о тех или иных характеристиках данной модели памяти (см. табл. 5). Перечислим эти идентификаторы и их значения (табл. 4).

    Таблица 4. Идентификаторы, создаваемые директивой MODEL
    Имя идентификатора Значение переменной
    @code Физический адрес сегмента кода
    @data Физический адрес сегмента данных типа near
    @fardata Физический адрес сегмента данных типа far
    @fardata? Физический адрес сегмента неинициализированных данных типа far
    @curseg Физический адрес сегмента неинициализированных данных типа far
    @stack Физический адрес сегмента стека
    Если вы посмотрите на текст листинга 1, то увидите пример использования одного из этих идентификаторов. Это @data; с его помощью мы получили значение физического адреса сегмента данных нашей программы.

    Теперь можно закончить обсуждение директивы MODEL. Операнды директивы MODEL используют для задания модели памяти, которая определяет набор сегментов программы, размеры сегментов данных и кода, способ связывания сегментов и сегментных регистров. В табл. 5 приведены некоторые значения параметра модель памяти директивы MODEL. 

    Таблица 5. Модели памяти
    Модель Тип кода Тип данных Назначение модели
    TINY near near Код и данные объединены в одну группу с именем DGROUP. 
    Используется для создания программ формата .com.
    SMALL near near Код занимает один сегмент, данные объединены в одну группу с именем DGROUP. 
    Эту модель обычно используют для большинства программ на ассемблере
    MEDIUM far near Код занимает несколько сегментов, по одному на каждый объединяемый программный модуль. 
    Все ссылки на передачу управления — типа far. 
    Данные объединены в одной группе; все ссылки на них — типа near
    COMPACT near far Код в одном сегменте; 
    ссылка на данные — типа far
    LARGE far far Код в нескольких сегментах, по одному на каждый объединяемый программный модуль
    Параметр модификатор директивы MODEL позволяет уточнить некоторые особенности использования выбранной модели памяти (табл. 6).
    Таблица 6. Модификаторы модели памяти
    Значение модификатора Назначение
    use16 Сегменты выбранной модели используются как 16-битные (если соответствующей директивой указан процессор i80386 или i80486)
    use32 Сегменты выбранной модели используются как 32-битные (если соответствующей директивой указан процессор i80386 или i80486)
    dos Программа будет работать в MS-DOS
    Необязательные параметры язык и модификатор языка определяют некоторые особенности вызова процедур. Необходимость в использовании этих параметров появляется при написании и связывании программ на различных языках программирования.

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

    Упрощенные директивы целесообразно использовать для простых программ и программ, предназначенных для связывания с программными модулями, написанными на языках высокого уровня. Это позволяет компоновщику эффективно связывать модули разных языков за счет стандартизации связей и управления.