Учебный курс. Часть 31. Сегментная адресация

Автор: xrnd | Рубрика: Учебный курс | 14-04-2011 | Распечатать запись Распечатать запись

До этой части учебного курса мы создавали только COM-программы, в которых один и тот же сегмент использовался для кода, данных и стека. Однако, для дальнейшего изложения необходимо подробнее разобраться с сегментной адресацией.

Формирование адреса в реальном режиме

Основная идея сегментной адресации в том, что адрес состоит из двух частей — сегментной и смещения. Обычно их записывают через двоеточие (например 0100:0500). Линейный адрес любой ячейки памяти получается в результате сложения смещения и сегментной части, сдвинутой на 4 бита влево.

Начало сегмента всегда выровнено на границу параграфа (адрес кратен 16 байтам). Максимальный размер сегмента равен 216 = 64 КБайта. А всего можно адресовать 220 = 1 МБайт памяти. Конечно, сейчас такой объем памяти кажется смешным, но раньше это было очень много 🙂

Одна из особенностей сегментной адресации — неоднозначность представления адреса. Допустим, требуется обратиться к ячейке памяти по адресу 00400. Этот адрес может быть представлен как 0000:0400, 0040:0000, 0020:0200 и так далее.

Загруженная в память программа может одновременно работать с четырьмя сегментами. Сегменты могут перекрываться или даже совпадать, как это было в случае с COM-программой.

Для всех команд подразумевается сегмент «по умолчанию». Например, команды PUSH и POP работают с сегментом стека. Если операнд такой команды находится в памяти, то он берётся из сегмента данных. Команды JMP и LOOP вычисляют адрес перехода в сегменте кода.

Создание DOS EXE

Возможности сегментной адресации полностью реализуются в исполняемом файле DOS EXE. Не путайте этот формат с исполняемым файлом Windows (PE EXE)! Расширение такое же, но файл имеет совершенно другую структуру.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
format MZ                       ;Исполняемый файл DOS EXE (MZ EXE)
entry code_seg:start            ;Точка входа
stack 200h                      ;Размер стека
 
;----------------------------------------------------------------------
segment data_seg                ;Cегмент данных
hello db 'Hello, asmworld!$'    ;Строка
 
;----------------------------------------------------------------------
segment code_seg                ;Сегмент кода
start:                          ;Отсюда начинается выполнение программы
    mov ax,data_seg             ;\
    mov ds,ax                   ;/ Инициализация регистра DS
 
    mov ah,09h                  ;\
    mov dx,hello                ; > Вывод строки
    int 21h                     ;/
 
    mov ax,4C00h                ;\
    int 21h                     ;/ Завершение программы

В первой строке после директивы format нужно поставить MZ, чтобы FASM сгенерировал нужный нам файл.

Во второй строке указывается точка входа, то есть метка, с которой начинается выполнение программы. Имя метки дополняется названием сегмента, в котором она находится.

После директивы stack можно указать требуемый размер сегмента стека в байтах (по умолчанию используется 4096). Дальше файл состоит из сегментов, которые объявляются с помощью директивы segment. После директивы записывается название сегмента.

В моём примере файл состоит из двух сегментов. В первом находятся данные (а точнее строка), во втором — код. Выполнение программы начинается с метки start в сегменте кода. После запуска необходимо инициализировать регистр ds, чтобы выбрать нужный сегмент данных. Для этого используются 2 команды, так как невозможно напрямую записать значение в сегментный регистр (нет команды MOV ds,значение).

Работу программы с сегментами можно увидеть в отладчике. Обратите внимание, что cs, ds, es и ss имеют разные значения:

Префиксы переопределения сегментов

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

Особенность синтаксиса FASM в том, что префикс пишется внутри квадратных скобок (так как по смыслу является частью адреса):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
format MZ                       ;Исполняемый файл DOS EXE (MZ EXE)
entry code_seg:start            ;Точка входа
stack 200h                      ;Размер стека
 
;----------------------------------------------------------------------
segment data_seg                ;Cегмент данных
hello db 'Hello, asmworld!$'    ;Строка
 
;----------------------------------------------------------------------
segment code_seg                ;Сегмент кода
start:                          ;Отсюда начинается выполнение программы
    mov ax,data_seg             ;\
    mov ds,ax                   ;/ Инициализация регистра DS
 
    mov ah,09h                  ;\
    mov dx,hello                ; > Вывод строки
    int 21h                     ;/
 
    mov ax,eseg                 ;\
    mov es,ax                   ;/ Инициализация регистра ES
    mov al,[cs:start]           ;Чтение байта, с которого начинается код
    cmp al,0xB8
    jnz exit
    mov word[es:0000h],1234h    ;Запись значения в сегмент eseg
 
exit:
    mov ax,4C00h                ;\
    int 21h                     ;/ Завершение программы
 
;----------------------------------------------------------------------
segment eseg
    rw 1                        ;Зарезервировать 1 слово

Дальние переходы, вызовы процедур и возвраты

Дальними (far) называются переходы в другой сегмент кода. При их выполнении меняется содержимое регистра cs. Они могут только безусловными. Ближние (near) переходы осуществляются в пределах одного сегмента. Аналогично есть дальние и ближние вызовы процедур, а также дальние и ближние возвраты.

Команда дальнего вызова процедуры сохраняет в стек не только ip, но и cs, чтобы можно было вернуться в текущий сегмент кода. Команда RET является синонимом ближнего возврата RETN. Дальний возврат осуществляется командой RETF. Она восстанавливает из стека регистры ip и cs.

Для наглядности пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
format MZ                       ;Исполняемый файл DOS EXE (MZ EXE)
entry seg1:start                ;Точка входа
 
;-------------------------------------------------------------------------
segment seg1                    ;Сегмент первый
hello db 'Hello, asmworld!$'    ;Строка
 
start:                          ;Отсюда начинается выполнение программы
    push cs                     ;\
    pop ds                      ;/ Инициализация регистра DS
    jmp seg2:do_it
 
exit:
    mov ah,08h
    int 21h
    mov ax,4C00h                ;\
    int 21h                     ;/ Завершение программы
 
;-------------------------------------------------------------------------
segment seg2                    ;Сегмент второй
do_it:
    mov dx,hello                ;DX = СМЕЩЕНИЕ строки в seg1
    call seg3:print_str         ;Дальний вызов (cs,ip в стек)
    jmp seg1:exit               ;Дальний переход
 
;-------------------------------------------------------------------------
segment seg3
; Дальняя процедура для вывода строки (ds:dx = адрес строки)
print_str:
    mov ah,09h
    int 21h
    retf                        ;Дальний возврат (восстанавливает ip,cs)

Программа состоит из трёх сегментов. Сначала выполняется переход во второй, затем вызов процедуры в третьем сегменте. Кстати, сегмент может содержать код и данные вместе — я поместил строку в начало первого сегмента.

Упражнение

Напишите программу, которая сравнивает две переменные и выполняет переход в другой сегмент в зависимости от результата сравнения. Если меньше, переход в сегмент 1. Если больше — в сегмент 2. Иначе в сегмент 3.

Комментарии: