Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Уроки ассемблеру. Быстро и просто. Урок 10


Сегодня мы поговорим о сегментах. Возьмём для примера программу вывода строки из урока 2:
;Всё, что следует за значком ";" - это комментарий.

.286 ;Разрешает ассемблирование непривилегированных инструкций
;процессора 80286 (реальный режим) и инструкций арифметического
;сопроцессора 80287.

CSEG segment ;Даём имя сегменту, а точнее определяем абсолютный
;сегмент в памяти программ по определённому адресу.
;Имя нашего сегмента будет CSEG.

assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG ;Задаём сегментные регистры, которые будем использовать для
;вычисления действующего адреса для всех меток и переменных, опре-
;делённых для сегмента или группы сегментов с указанным именем.
;У нас их четыре, - CS, DS, ES, SS и они будут указывать на наш
;единственный сегмент (мы его назвали CSEG).

org 100h ;Устанавливаем счётчик инструкций в текущем сегменте в соот-
;ветствии с адресом, задаваемым "выражением".
;Сейчас этот счётчик равен 100h - используется для всех программ
;типа .com

begin: ;Метка начала программы.

;Выводим фразу, используя функцию 9 прерывания 21h.
mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию).
mov dx,offset helloworld ;Указываем, что за фразу мы будем выводить.
int 21h ;Выводим фразу.

int 20h ;Выходим в DOS.

helloworld db 'Hello, world!$' ;Определяем переменную helloworld, доступную побайтно, с фразой
;"Hello, world!". В одинарных кавычках, после знака "!" ставим
;знак "$".

CSEG ends ;Указываем на завершение сегмента CSEG.
end begin ;Конец программы.


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

Давайте попробуем вывести на экран DOS две фразы вида "Hello, world!" и "Hello, world! (2)". Как это сделать привычным нам способом вы, думаю, знаете:

;Всё, что следует за значком ";" - это комментарий.

.286 ;Разрешает ассемблирование непривилегированных инструкций
;процессора 80286 (реальный режим) и инструкций арифметического
;сопроцессора 80287.

CSEG segment ;Даём имя сегменту, а точнее определяем абсолютный
;сегмент в памяти программ по определённому адресу.
;Имя нашего сегмента будет CSEG.

assume cs:CSEG, ds:CSEG, es:CSEG, ss:CSEG ;Задаём сегментные регистры, которые будем использовать для
;вычисления действующего адреса для всех меток и переменных, опре-
;делённых для сегмента или группы сегментов с указанным именем.
;У нас их четыре, - CS, DS, ES, SS и они будут указывать на наш
;единственный сегмент (мы его назвали CSEG).

org 100h ;Устанавливаем счётчик инструкций в текущем сегменте в соот-
;ветствии с адресом, задаваемым "выражением".
;Сейчас этот счётчик равен 100h - используется для всех программ
;типа .com

begin: ;Метка начала программы.

;Выводим фразу, используя функцию 9 прерывания 21h.
mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию).
mov dx,offset helloworld1 ;Указываем, что за фразу мы будем выводить.
int 21h ;Выводим фразу.

;Выводим фразу, используя функцию 9 прерывания 21h.
mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию).
mov dx,offset helloworld2 ;Указываем, что за фразу мы будем выводить.
int 21h ;Выводим фразу.

int 20h ;Выходим в DOS.

helloworld1 db 'Hello, world!$' ;Определяем переменную helloworld1, доступную побайтно, с фразой
;"Hello, world!". В одинарных кавычках, после знака "!" ставим
;знак "$".
helloworld2 db 'Hello, world! (2)$' ;Определяем переменную helloworld2, доступную побайтно, с фразой
;"Hello, world!". В одинарных кавычках, после знака "!" ставим
;знак "$".

CSEG ends ;Указываем на завершение сегмента CSEG.
end begin ;Конец программы.


А теперь поместим данные об этих фразах в два разных сегмента. Внимание! До нынешнего момента мы создавали программы .com, работающие только с одним сегментом! Пора перейти к созданию *.exe файлов. Создаём нижеследующий файл test.asm в кодировке 866 и вводим нашу команду без атрибутов: ml test.asm:

;Всё, что следует за значком ";" - это комментарий.

.286 ;Разрешает ассемблирование непривилегированных инструкций
;процессора 80286 (реальный режим) и инструкций арифметического
;сопроцессора 80287.

MESSAGE1 segment USE16 ;Начало сегмента MESSAGE1
;Сообщаем ассемблеру, что внутрисегментная адресация является
;16-ти разрядной - use16

helloworld1 db 'Hello, world!$' ;Определяем переменную helloworld1, доступную побайтно, с фразой
;"Hello, world!". В одинарных кавычках, после знака "!" ставим
;знак "$".
MESSAGE1 ends ;Указываем на завершение сегмента MESSAGE1.

MESSAGE2 segment USE16 ;Начало сегмента MESSAGE2
;Сообщаем ассемблеру, что внутрисегментная адресация является
;16-ти разрядной - use16

helloworld2 db 'Hello, world! (2)$' ;Определяем переменную helloworld2, доступную побайтно, с фразой
;"Hello, world!". В одинарных кавычках, после знака "!" ставим
;знак "$".
MESSAGE2 ends ;Указываем на завершение сегмента MESSAGE2.

CSEG segment USE16 ;Начало сегмента CSEG - в котором идёт наш код программы.
;Сообщаем ассемблеру, что внутрисегментная адресация является
;16-ти разрядной - use16

assume cs:CSEG, ds:MESSAGE1 ;Для сегментирования адресов из сегмента CSEG выбирается регистр cs,
;а для сегментирования адресов из сегмента MESSAGE1 - ds.
;org 100h ;Внимание! Директива org 100h не используется!!!
begin: ;Начало программы - точка входа.

mov ax,MESSAGE1 ;Заносим адрес первого сегмента в ds черех ax.
mov ds,ax ;

mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию).
mov dx,offset helloworld1 ;Указываем, что за фразу мы будем выводить.
int 21h ;Выводим фразу.

mov ax,MESSAGE2 ;Заносим адрес второго сегмента в ds черех ax.
mov ds,ax ;

mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию).
mov dx,offset helloworld2 ;Указываем, что за фразу мы будем выводить.
int 21h ;Выводим фразу.

;Выходим в DOS
mov ah,4Ch ;Используем вместо int 20h
int 21h

CSEG ends ;Указываем на завершение сегмента CSEG.
end begin ;Конец программы (точки входа).

У нас должен быть создан файл test.exe, правда, с сообщением об ошибке вроде: "LINK : warning L4021: no stack segment". Сейчас это информационное сообщение (для небольших программ можно проигнорировать). Стек имеет смысл задавать, если это действительно надо, например, для больших и многосегментных программ. Но чтобы убрать сообщение об ошибке, нам достаточно после директивы .286 дописать следующую часть кода:

SST segment stack "stack"
dw 10 dup(?)
SST ENDS

Что действительно достойно внимания в этой программе.

MESSAGE1 segment USE16 - Без "USE16" программа-ассемблер будет использовать USE32 (т.е. 32-х разрядные данные) и выдавать ошибку на строках вида mov dx,offset Message;

assume cs:CSEG, ds:MESSAGE1 - Указываем masm, что в значение сегмента CSEG установлен сегментный регистр cs, а в значение сегмента MESSAGE1 установлен сегментный регистр ds;

В сегменте MESSAGE1 хранится наша первая строка, мы и направляем его в ds;

;org 100h - Все COM программы начинаются с адреса 100h. Директива "org 100h" нужна для резервирования места для PSP (префикс программного сегмента). Когда COM-программа начинает работать, все сегментные регистры содержат адрес PSP - 256-байтового (т.е. 100h в шестнадцатеричном исчислении) блока, который резервируется операционной системой DOS непосредственно перед COM или EXE программой в памяти. Так как адресация начинается со шестнадцатеричного смещения 100 от начала PSP, то в программе после оператора SEGMENT кодируется директива ORG 100h. EXE файлы же используют дополнительно регистры (и стек), и необходимость непосредственного указания в адресации со шестнадцатеричного смещения 100 отпадает (но PSP остаётся!).

mov ax,MESSAGE1
mov ds,ax
- Заносим данные о MESSAGE1 в ds, но не напрямую, а через регистр ax (напрямую занести их нельзя). Направляем регистр ds на наш первый сегмент. Чуть дальше мы определим в dx смещение для фразы "Hello, world!". Теперь нам должны быть понятен текст вроде: "Данные о фразе 'Hello, world!' хранятся в DS:DX". Далее мы сменим сегмент и определим в dx новое смещение;

mov ah,4Ch - Раньше для выхода в DOS мы использовали прерывание 20h. Для *.exe-программы мы будем использовать функцию 4Ch прерывания 21h.

Усложним немного пример, но для начала расскажем о моделях памяти и директиве .model. Данная директива указывается в начале кода программы.

Модели памяти
Модель Тип кода Тип данных Назначение модели
TINY near near Код, данные и стек объединены в одну группу с именем DGROUP и размером до 64Кб. Используется для создания программ формата .com. Некоторые языки эту модель не поддерживают.
СS=DS=SS=DGROUP
SMALL near near Код занимает один сегмент, данные и стек объединены в одну группу с именем DGROUP (хотя для описания могут использоваться разные сегменты). Эту модель обычно используют для большинства программ на ассемблере.
CS=_text
DS=SS=DGROUP
MEDIUM far near Код занимает несколько сегментов, по одному на каждый объединяемый программный модуль. Все ссылки на передачу управления — типа far (вызов подпрограмм). Данные и стек объединены в одной группе DGROUP; все ссылки на них — типа near (для доступа к данным используется только смещение).
CS=<модуль>_text
DS=SS=DGROUP
COMPACT near far Код находится в одном сегменте, данные и стек в группе DGROUP и могут занимать несколько сегментов, так что для обращения к данным требуется указывать сегмент и смещение (ссылка на данные — типа far).
CS=_text
DS=SS=DGROUP
LARGE far far Код может занимать несколько сегментов, по одному на каждый объединяемый программный модуль. Стек и данные находятся в группе DGROUP. Для ссылки на данные используются дальние указатели - far.
CS=<модуль>_text
DS=SS=DGROUP
HUGE far far Тоже что и модель LARGE, что касается TurboAssebmler.
FLAT far far Тоже, что и TINY, но используются 32-битная адресация, так что максимальный размер сегмента, содержащего и данные, и код, и стек - 4Гб.

Автор таблицы: asmforfun

А теперь давайте усложним предыдущий пример. Все данные у нас будут находиться в одном сегменте, а исполняемый код — сначала в одном, потом в другом. То есть в определённый момент мы "прыгнем" из сегмента в сегмент. Посмотрим, как это будет выглядеть:

;Всё, что следует за значком ";" - это комментарий.

.286 ;Разрешает ассемблирование непривилегированных инструкций
;процессора 80286 (реальный режим) и инструкций арифметического
;сопроцессора 80287.

.model small ;Указываем модель.
;Код занимает один сегмент, данные объединены в одну группу
;с именем DGROUP.
;Эту модель обычно используют для большинства программ на ассемблере

SST segment stack "stack" ;Начало сегмента SST, определяющего стек.
dw 10 dup(?) ;Резервируем область памяти.
SST ENDS ;Указываем на завершение сегмента SST.

DATAS segment USE16 ;Начало сегмента DATAS
;Сообщаем ассемблеру, что внутрисегментная адресация является
;16-ти разрядной - use16

helloworld1 db 'Hello, world!$' ;Определяем переменную helloworld1, доступную побайтно, с фразой
;"Hello, world!". В одинарных кавычках, после знака "!" ставим
;знак "$".

helloworld2 db 'Hello, world! (2)$' ;Определяем переменную helloworld2, доступную побайтно, с фразой
;"Hello, world!". В одинарных кавычках, после знака "!" ставим
;знак "$".
DATAS ends ;Указываем на завершение сегмента DATAS2.

DATA2 segment USE16
newmetka:
mov ax,DATAS
mov ds,ax

mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию).
mov dx,offset helloworld2 ;Указываем, что за фразу мы будем выводить.
int 21h ;Выводим фразу.

;Выходим в DOS
mov ah,4Ch ;Используем вместо int 20h
int 21h

DATA2 ends

CSEG segment USE16 ;Начало сегмента CSEG - в котором идёт наш код программы.
;Сообщаем ассемблеру, что внутрисегментная адресация является
;16-ти разрядной - use16

assume cs:CSEG, ds:DATAS, es:DATA2, ss:SST ;

begin: ;Начало программы - точка входа.

mov ax,DATAS
mov ds,ax

mov ah,9 ;Загружаем в регистр ah число 9 (указываем функцию).
mov dx,offset helloworld1 ;Указываем, что за фразу мы будем выводить.
int 21h ;Выводим фразу.

mov ax,DATA2
mov ds,ax


JMP FAR PTR newmetka

CSEG ends ;Указываем на завершение сегмента CSEG.
end begin ;Конец программы (точки входа).

Здесь мы видим новую инструкцию — JMP FAR PTR newmetka
Она означает, что мы "прыгаем" (вспоминаем команду jmp) в сегмент, указанный в ds, на метку newmetka.

Команда jmp имеет пять разновидностей:
— переход прямой короткий (в пределах -128... + 127 байтов);
— переход прямой ближний (в пределах текущего программного сегмента) ;
— переход прямой дальний (в другой программный сегмент);
— переход косвенный ближний;
— переход косвенный дальний.
Все разновидности переходов имеют одну и ту же мнемонику jmp, хотя и различающиеся коды операций. Во многих случаях транслятор может определить вид перехода по контексту, в тех же случаях, когда это невозможно, следует использовать атрибутные операторы:

short - прямой короткий переход;
near ptr - прямой ближний переход;
far ptr - прямой дальний переход;
word ptr - косвенный ближний переход;
dword ptr - косвенный дальний переход.

На сегодня это, пожалуй, всё.

В избранное