Иллюстрированный самоучитель по Assembler

       

Сегментная структура программ


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

Каким образом понятие сегментов памяти отражается на структуре программы? Следует заметить, что структура программы определяется, с одной стороны, архитектурой процессора (если обращение к памяти возможно только с помощью сегментов, то и программа, видимо, должна состоять из сегментов), а с другой - особенностями той операционной системы, под управлением которой эта программа будет выполняться. Наконец, на структуру программы влияют также и правила работы выбранного транслятора - разные трансляторы предъявляют несколько различающиеся требования к исходному тексту программы. При подготовке этой книги для трансляции и отладки примеров программ использовался пакет TASM 5.0 корпорации Borland International; он удобен, в частности, наличием наглядного многооконного отладчика. Вопрос этот, однако, не принципиален, и читатель может для отладки примеров, приведенных в книге, воспользоваться любым ассемблером, ознакомившись предварительно с его описанием.

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

Пример 1-1. Простая программа с тремя сегментами

;Укажем соответствие сегментных регистров сегментам

assume CS:code,DS:data

;Опишем сегмент команд


code segment ;Откроем сегмент команд

begin: mov AX,data ;Настроим DS

mov DS,AX ;на сегмент данных;

Выведем на экран строку текста





mov АН,09h ;Функция DOS вывода на экран

mov DX,offset msg ;Адрес выводимой строки

int 21h ;Вызов DOS



;Завершим программу

mov AX,4C00h ;Функция DOS завершения программы

int 21h ;Вызов DOS

code ends ;Закроем сегмент команд



;Опишем сегмент данных

data segment ;Откроем сегмент данных



msg db "Программа работает!$' ;Выводимая строка



data ends ;Закроем сегмент данных



;Опишем сегмент стека



stk segment stack ;Откроем сегмент стека

db 256 dup (?) ;Отводим под стек 256 байт



stk ends ;Закроем сегмент стека

end begin ;Конец текста с точкой входа

Следует заметить, что при вводе исходного текста программы с клавиатуры можно использовать как прописные, так и строчные буквы; транслятор воспринимает, например, строки MOV AX,DATA и mov ax.data одинаково. Однако с помощью соответствующих ключей можно заставить транслятор различать прописные и строчные буквы в отдельных элементах предложений. В настоящей книге в текстах программ и при описании операторов языка в основном используются строчные буквы, за исключением обозначений регистров, которые для наглядности выделены прописными буквами.

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

В программе 1-1 описаны три сегмента: сегмент команд с именем code, сегмент данных с именем data и сегмент стека с именем stk. Описание каждого сегмента начинается с ключевого слова segment, предваряемого некоторым именем, и заканчивается ключевым словом end, перед которым указывается то же имя, чтобы транслятор знал, какой именно сегмент мы хотим закончить. Имена сегментов выбираются вполне произвольно. Текст программы заканчивается директивой ассемблера end, завершающей трансляцию. В качества операнда этой директивы указывается точка входа в программу; в нашем случае это метка begin.



Порядок описания сегментов в программе, как правило, не имеет значения. Часто программу начинают с сегмента данных, это несколько облегчает чтение программы, и в некоторых случаях устраняет возможные неоднозначности в интерпретации команд, ссылающиеся на данные, которые еще не описаны. Мы в начале программы расположили сегмент команд, за ним - сегмент данных и в конце - сегмент стека; такой порядок предоставляет некоторые удобства при отладке программы. Важно только понимать, что в оперативную память компьютера сегменты попадут в том же порядке, в каком они описаны в программе (если специальными средствами ассемблера не задать иной порядок загрузки сегментов в память).

Сегменты вводятся в программу с помощью директив ассемблера segment и ends. Что такое директива ассемблера? В тексте программы встречаются ключевые слова двух типов: команды процессора (mov, int) и директивы транслятора (в данном случае термины "транслятор" и "ассемблер" являются синонимами, обозначая программу, преобразующую исходный текст, написанный на языке ассемблера, в коды, которые будут при выполнении программы восприниматься процессором). К директивам ассемблера относятся обозначения начала и конца сегментов segment и ends; ключевые слова, описывающие тип используемых данных (db, dup); специальные описатели сегментов вроде stack и т. д. Директивы служат для передачи транслятору служебной информации, которой он пользуется в процессе трансляции программы. Однако в состав выполнимой программы, состоящей из машинных кодов, эти строки не попадут, так как процессору, выполняющему программу, они не нужны. Другими словами, операторы типа segment и ends не транслируются в машинные коды, а используются лишь самим ассемблером на этапе трансляции программы. С этим вопросом мы еще столкнемся при рассмотрении листингов программ.

Еще одна директива ассемблера используется в первом предложении программы:

assume CS:code,DS:data

Здесь устанавливается соответствие сегмента code сегментному регистру CS и сегмента data сегментному регистру DS. Первое объявление говорит о том, что сегмент code является сегментом команд, и встречающиеся в этом сегменте метки принадлежат именно этому сегменту, что помогает ассемблеру правильно транслировать команды переходов. В нашей программе меток нет, и эту часть предложения можно было бы опустить, однако в более сложных программах она необходима (при использовании транслятора MASM эта часть объявления необходима в любой, даже самой простой программе).



Второе объявление помогает транслятору правильно обрабатывать предложения, в которых производится обращение к полям данных сегмента data. Выше уже отмечалось, что для обращения к памяти процессору необходимо иметь две составляющие адреса: сегментный адрес и смещение. Сегментный адрес всегда находится в сегментном регистре. Однако в процессоре два сегментных регистра данных, DS и ES, и для обращения к памяти можно использовать любой из них. Разумеется, процессор при выполнении команды должен знать, из какого именно регистра он должен извлечь сегментный адрес, поэтому команды обращения к памяти через регистры DS или ES кодируются по-разному. Объявляя соответствие сегмента data регистру DS, мы предлагаем транслятору использовать вариант кодирования через регистр DS.

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

mov AX,data ;Настроим DS

mov DS,AX ;на сегмент данных

с которых начинается наша программа. Сначала значение имени data (т.е. адрес сегмента data) загружается командой mov в регистр общего назначения процессора АХ, а затем из регистра АХ переносится в регистр DS. Такая двухступенчатая операция нужна потому, что процессор в силу некоторых особенностей своей архитектуры не может выполнить команду непосредственной загрузки адреса в сегментный регистр. Приходится пользоваться регистром АХ в качестве "перевалочного пункта".

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



Назначением программы 1- 1 предполагается вывод на экран текстовой строки "Программа работает!", описанной в сегменте данных. Следующие предложения программы как раз и выполняют эту операцию. Делается это не непосредственно, а путем обращения к служебным программам операционной системы MS-DOS, которую мы для краткости будем в дальнейшем называть просто DOS. Дело в том, что в составе команд процессора и, соответственно, операторов языка ассемблера нет команд вывода данных на экран (как и команд ввода с клавиатуры, записи в файл на диске и т.д.). Вывод даже одного символа на экран в действительности представляет собой довольно сложную операцию, для выполнения которой требуется длинная последовательность команд процессора. Конечно, эту последовательность команд можно было бы включить в нашу программу, однако гораздо проще обратиться за помощью к операционной системе. В состав DOS входит большое количество программ, осуществляющих стандартные и часто требуемые функции - вывод на экран и ввод с клавиатуры, запись в файл и чтение из файла, чтение или установка текущего времени, выделение или освобождение памяти и многие другие.

Для того, чтобы обратиться к DOS, надо загрузить в регистр общего назначения АН номер требуемой функции, в другие регистры - исходные данные для выполнения этой функции, после чего выполнить команду hit 21h (int - от interrupt, прерывание), которая передаст управление DOS. Вывод на экран строки текста можно осуществить функцией 09h, которая требует, чтобы в регистрах DS:DX содержался полный адрес выводимой строки. Регистр DS мы уже инициализировали, осталось поместить в регистр DX относительный адрес строки, который ассоциируется с именем поля данных msg. Длину выводимой строки указывать нет необходимости, так как функция 09h DOS выводит на экран строку от указанного адреса до символа доллара, который мы предусмотрительно включили в выводимую строку. Заполнив все требуемые для конкретной функции регистры, можно выполнить команду int 21h, которая осуществит вызов DOS.



Как завершить выполняемую программу? В действительности завершение программы - это довольно сложная последовательность операций, в которую входит, в частности, освобождение памяти, занятой завершившейся программой, а также вызов той системной программы (конкретно - командного процессора COMMAND.COM), которая выведет на экран запрос DOS, и будет ожидать ввода следующих команд оператора. Все эти действия выполняет функция DOS с номером 4Ch. Эта функция предполагает, что в регистре AL находится код завершения нашей программы, который она передаст DOS. Если программа завершилась успешно, код завершения должен быть равен 0, поэтому мы в одном предложении mov AX,4C00h загружаем в АН 4Ch, а в AL - 0, и вызываем D'OS уже знакомой нам командой int 21h.

Для того, чтобы выполнить пробный прогон приведенной программы, ее необходимо сначала оттранслировать и скомпоновать. Пусть исходный текст программы хранится в файле с именем P.ASM. Трансляция осуществляется вызовом ассемблера TASM.EXE с помощью следующей .команды DOS;

tasm /z/zi/n p/p,p

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

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

Ключ /n подавляет вывод в листинг перечня символических обозначений в программе, от чего несколько уменьшается информативность

листинга, но сокращается его размер.

Стоящие далее параметры определяют имена файлов: исходного (P.ASM), объектного (P.OBJ) и листинга (P.LST). При желании можно в строке вызова транслятора указать полные имена файлов с их расширениями, однако необходимости в этом нет, так как по умолчанию транслятор использует именно указанные выше расширения.

Строка вызова компоновщика имеет следующий вид:

tlink /x/v p,p

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

Ключ /v передает в загрузочный файл информацию, используемую отладчиком. Стоящие далее параметры обозначают имена модулей: объектного (Р.ОЫ) и загрузочного (Р.ЕХЕ).



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

tasm /z/zi/n p,p,p

tlink /х/v р,р

Запуск подготовленной программы Р.ЕХЕ осуществляется командой .р.ехе или просто

При загрузке программы сегменты размещаются в памяти, как показано на рис. 1.9.



Рис. 1.9. Образ программы в памяти.

Образ программы в памяти начинается с сегмента префикса программы (Program Segment Prefics, PSP), образуемого и заполняемого системой. PSP всегда имеет размер 256 байт; он содержит таблицы и поля данных, используемые системой в процессе выполнения программы. Вслед за PSP располагаются сегменты программы в том порядке, как они объявлены в программе. Сегментные регистры автоматически инициализируются следующим образом: ES и DS указывают на начало PSP (что дает возможность, сохранив их содержимое, обращаться затем в программе к PSP), CS - на начало сегмента команд, a SS - на начало сегмента стека. В указатель команд IP загружается относительный адрес точки входа в программу (из операнда директивы end), а в указатель стека SP - величина, равная объявленному размеру стека, в результате чего указатель стека указывает на конец стека (точнее, на первое слово за его пределами).

Таким образом, после загрузки программы в память адресуемыми оказываются все сегменты, кроме сегмента данных. Инициализация регистра DS в первых строках программы позволяет сделать адресуемым и этот сегмент.

Рисунок 1.9 еще раз подчеркивает важнейшую особенность архитектуры процессоров Intel: адрес любой ячейки памяти состоит из двух слов, одно из которых определяет расположение в памяти соответствующего сегмента, а другое - смещение в пределах этого сегмента. Смысл сегментной части адреса, хранящейся всегда в одном из сегментных регистров, в реальном и защищенном режиме различен; в МП 86 сегментная часть адреса, после умножения ее на 16, определяет физический адрес начала сегмента в памяти.



Отсюда следует, что сегмент всегда начинается с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Сегментный адрес можно рассматривать, как номер параграфа, с которого начинается данный сегмент. Размер сегмента определяется объемом содержащихся в нем данных, но никогда не может превышать величину 64 Кбайт, что определяется максимально возможной величиной смещения.

Сегментный адрес сегмента команд хранится в регистре CS, а смещение к адресуемому байту - в указателе команд IP. Как уже отмечалось, после загрузки программы в IP заносится смещение первой команды программы; процессор, считав ее из памяти, увеличивает содержимое IP точно на длину этой команды (команды процессоров Intel могут иметь длину от 1 до 6 байт), в результате чего IP указывает на вторую команду программы. Выполнив первую команду, процессор считывает из памяти вторую, опять увеличивая значение IP. В результате в IP всегда находится смещение очередной команды, т. е. команды, следующей за выполняемой. Описанный алгоритм нарушается только при выполнении команд переходов, вызовов подпрограмм и обслуживания прерываний.

Сегментный адрес сегмента данных обычно хранится в регистре DS, a смещение может находится в одном из регистров общего назначения, например, в ВХ или SI. Однако в МП 86 два сегментных регистра данных - DS и ES. Дополнительный сегментный регистр ES часто используется для обращения к полям данных, не входящим в программу, например к видеобуферу или системным ячейкам. Однако при необходимости его можно настроить и на один из сегментов программы. В частности, если программа работает с большим объемом данных, для них можно предусмотреть два сегмента и обращаться к одному из них через регистр DS, а к другому - через ES.


Содержание раздела