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

Начало сегмента всегда выровнено на границу параграфа (адрес кратен 16 байтам). Максимальный размер сегмента равен 216 = 64 КБайта. А всего можно адресовать 220 = 1 МБайт памяти. Конечно, сейчас такой объем памяти кажется смешным, но раньше это было очень много 🙂
Одна из особенностей сегментной адресации — неоднозначность представления адреса. Допустим, требуется обратиться к ячейке памяти по адресу 00400. Этот адрес может быть представлен как 0000:0400, 0040:0000, 0020:0200 и так далее.
Загруженная в память программа может одновременно работать с четырьмя сегментами. Сегменты могут перекрываться или даже совпадать, как это было в случае с COM-программой.

Для всех команд подразумевается сегмент «по умолчанию». Например, команды PUSH [1] и POP [2] работают с сегментом стека. Если операнд такой команды находится в памяти, то он берётся из сегмента данных. Команды JMP [3] и LOOP [4] вычисляют адрес перехода в сегменте кода.
Создание DOS EXE
Возможности сегментной адресации полностью реализуются в исполняемом файле DOS EXE. Не путайте этот формат с исполняемым файлом Windows (PE EXE)! Расширение такое же, но файл имеет совершенно другую структуру.
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 [5] ds,значение).
Работу программы с сегментами можно увидеть в отладчике. Обратите внимание, что cs, ds, es и ss имеют разные значения:

Префиксы переопределения сегментов
Иногда нужно изменить используемый сегмент данных только для одной команды. Например, хочется прочитать байт из текущего сегмента кода или записать в сегмент стека. Для этого предназначены префиксы переопределения сегмента. Названия префиксов совпадают с названиями сегментных регистров.
Особенность синтаксиса FASM в том, что префикс пишется внутри квадратных скобок (так как по смыслу является частью адреса):
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 [6] является синонимом ближнего возврата RETN [6]. Дальний возврат осуществляется командой RETF [6]. Она восстанавливает из стека регистры ip и cs.
Для наглядности пример:
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.