Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Textnew2

.pdf
Скачиваний:
13
Добавлен:
06.02.2018
Размер:
1.48 Mб
Скачать

мые названия сегментов кодов команд и данных есть, соответственно, .text и .data, причем начальная точка обозначения формальным образом входит в состав имени.

Другой неожиданной для начинающей особенностью ассемблерных программ является необходимость явного обозначения машинной инструкции, с которой должна начать выполняться программа. Это отвечает естественно заложенной в архитектуру компьютеров возможность начать выполнение любой программы с любого места в области памяти, занятой кодами машинных команд. Любая отметка места в памяти, относящаяся как к машинным командам, так и к данным представляет собой на языке ассемблера так называемую метку. Метка на языке ассемблера есть просто имя, соотносимое началу строки ассемблерной программ и размещаемое в начале этой строки. Традиционная методика использования меток требует, чтобы обозначение метки в том месте, где она именует строку программы, завершалась служебным символом двоеточия. В ассемблере nasm это требование является необязательным, но мы будем им пользоваться для отметок в тексте собственно машинных команд. В ассемблерах типа Intel требуется метки, обозначающие места в тексте машинных команд, обязательно завершать двоеточием, а метки, обозначаемые данные, как правила, используются без завершающих двоеточий.

Место, с которого начинает выполняться программа, называется точкой входа. Метка точки входа в программу на языке NASM должна обязательно быть объявлена видимой извне объектного модуля, который возникает из исходного текста при традиционной методике создания программы. Такое объявление осуществляет директива GLOBAL. Назначение и возможности этой директивы гораздо шире, чем указание с ее помощью точки входа, но более подробное обсуждение данного вопроса будет проведено позднее. Использование директивы GLOBAL заключается в записи после ключевого слова этой директивы того имени, которое предполагается задать как глобальное. (В общем случае в одной этой директиве может быть через запятые указано много имен, которые предполагается использовать как глобальные.)

Программа - компоновщик в Linux по сложившейся здесь традиции требует имени _start в качестве обозначения начала программы (так как точка входа всей программы всегда одна, то такой выбор не несет никакого мыслимого ограничения). Указанное имя начинается со служебного символа подчеркивания, которое считается также входящим в состав этого имени. В общем случае имена в NASM могут состоять из алфавитно-цифровых символов, начинающихся с буквы, причем символ подчеркивания считается буквой. Кроме того, служебные имена могут начинаться с одной или двух точек. Использование служебных имен будет описываться особо по ходу изложения.

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

11

писании программы, но, в связи с крайне скромным числом сегментов в современных программах, теперь такое решение кажется скорее красивым, чем целесообразным. В указанных ассемблерах типа Intel именование как директивы SEGMENT, так и директивы ENDS производится записью имени перед служебным словом директивы. (Следует, правда, отметить, что так называемый Turbo ассемблер фирмы Borland/Inprise имеет режим, в котором имя сегмента следует за служебным словом, а не наоборот.) Кроме рассмотренной особенности описания сегментов ассемблеры типа Intel используют для указания завершения специальную директиву END, по назначению и форме очень напоминающую одноименную директиву в языке Pascal. В указанных же ассемблерах директива END используется также для указания метки точки входа.

Таким образом сегментная структура программы для NASM имеет вид

GLOBAL _start

SEGMENT .text _start:

. . . ; задание машинных команд на данном ассемблере SEGMENT .data

. . . ; описание данных на ассемблере NASM

причем сегменты .text и .data могут быть переставлены местами. Аналогичная же программа на ассемблере MASM или TASM будет иметь общий вид

имя_сегмента_кода SEGMENT имя_точки_входа:

. . . ; задание машинных команд на данном ассемблере и служебных конструкций

имя_сегмента_кода ENDS имя_сегмента_данных SEGMENT

. . . ; описание данных на ассемблере MASM или TASM

имя_сегмента_данных ENDS END имя_точки_входа

где также сегменты данных и команд могут быль переставлены местами. Забегая вперед, отметим, что в современных ОС Windows (32-битных полноценных ОС, в отличие от уже почти забытой промежуточной 16-битной Windows и MS-DOS) имена сегментов кода и данных также должны задаваться по некоторым фиксированным соглашениям, так что практически то же могут считаться постоянными.

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

12

.bss или BSS. Следует учитывать, что транслятор NASM по разному "относится" к содержимому указанных модификаций сегментов данных. Если программист делает попытку помещения неинициализированных им данных в сегмент с именем

.data или DATA, то выдается предупреждение, а область самих таких данных заполняются нулевыми байтами. В свою очередь, попытка объявить данные (т.е. описать области размещения данных без задания их значений) в сегментах .bss или BSS приводит к сообщаемой ошибке.

2. ПРОСТЕЙШИЕ СРЕДСТВА АССЕМБЛЕРА

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

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

В рассматриваемых ассемблерах для описания именованного места в памяти, аналогичного переменной языка высокого уровня, служат директивы, задаваемые ключевыми словами DB, DW, DD и DQ. Ключевые слова допускается записывать как строчными, так и прописными буквами. Наименование директив происходит от первых букв фраз Define Byte, Define Word, Define Double word, Define Quadra word. Обычное использование этих директив имеет вид

Имя_области_данных Da описание_содержимого_данных

где символом a условно обозначен один из символов B, W, D и Q. Простейшее использование этих директив описания данных содержит описание лишь одного элемента указанного в ключевом слове размера, так что в этом случае директива DB выделяет для данных 1 байт, директива DW - выделяет 2 байта, директива DD - 4 байта, а директива DQ - 8 байтов.

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

perv dw 4, 7, 11, 29

задает для дальнейшего использование именованную область данных с именем perv, элементы этого описания состоят из слов (пар байтов), причем в первом из них с самого начала выполнения программы записано число 4, во втором - число 7, в третьем число 11, а в четвертом - число 29.

Заметим, что ассемблер NASM различает строчные и прописные буквы в именах, вводимых программистом подобно языку Си (и в отличие от языка Паскаль, где такого различия не делается). Обозначения же директив можно в нем задавать

13

как строчными, так и прописными буквами. Такое решение очень тесно сближает этот ассемблер с системным языком Си. Ассемблеры же MASM и TASM в режиме по умолчанию позволяют не различать строчные и прописные буквы, хотя такая особенность потребовалась для внешних имен при программировании для MS Windows. Поэтому она задается изменением режима работы компилятора этих ассемблеров путем использования при их вызове опции /mx или /ml. Первая из опций задает режим различения строчных и прописных букв для внешних имен, а вторая - для всех имен, использованных в обрабатываемой программе.

При задании содержимого именованных областей памяти предоставляется возможность широкого использования различных систем счисления в записи числовых констант. Десятичные константы записываются обычным путем как последовательности десятичных цифр, которым может предшествовать знак минуса. Шестнадцатеричные константы записываются последовательностями шестнадцатеричных цифр, причем в NASM для явного указания, что это шестнадцатеричная форма записи либо перед нею ставится идентифицирующий префикс, либо за нею ставится идентифицирующий суффикс. В качестве таких префиксов можно использовать пару символов 0x (как в языке Си) или служебный символ $, а в качестве указанного суффикса - H (латинский символ - строчный или прописной). При использовании двух последних вариантов в качестве первой цифры шестнадцатеричной константы следует использовать дополнительную цифру 0, когда старшая значащая цифра этой константы отлична от десятичной цифры (при начале записи с буквенного символа транслятор может не разобраться, что записано числовое значение, а не недоопределенное имя). (Ассемблеры MASM и TASM допускают только суффиксную форму записи числовых констант, отличных от десятичных.) Директива DQ в NASM предполагает использование только с инициализацией числовыми значениями с плавающий точкой.

Для записи двоичной константы используется последовательность двоичных цифр, завершающаяся служебным символом B (латинской буквой - строчной или прописной), а для записи восьмеричных констант последовательность восьмеречных цифр должна завершаться служебным символом Q (латинской буквой - строчной или прописной). В качестве значений можно использовать обозначения отдельных символов, для чего последние заключаются в апострофы (коды которых в само такое значение не попадают) и даже последовательности символов, заключенные в апострофы.

Далее приведены примеры определения именованных данных в NASM

db 0x55

; байт содержит шестнадцатеричное число 0x55

db 0x55,0x56,0x57

; три байта, занятые константами

db 'a',0x55 ; первый байт заполняет символом a, второй числовой константой db 'hello',13,10,'$' ; восемь байтов, в первых пяти из которых текст 'hellow' dw 0x1234 ; в младшем байте константа 0x34, в старшем константа 0x12

dw 'a'

; пара байтов, в младшем код буквы a, в старшем 0

dw 'ab'

; символы, размещенные в паре байтов

dw 'abc'

; два слова (!!), в младшем коды 0x41 0x42,

14

; в старшем код\ы 0x43 0x00

dd 0x12345678 ; последовательные байты со значениями 0x78 0x56 0x34 0x12

Предоставляемые ассемблером NASM возможности настолько широки, что мы предпочтем использовать при задании констант только тот вариант, который полностью совместим с другими только что упомянутыми ассемблерами. Именно, в дальнейшем текущем изложении будет использоваться исключительно запись с идентифицирующими суффиксами H, Q и B.

Кроме описания инициализированных данных (задаваемых директивами DB, DW, DD, DQ) совершенно необходимо иметь аналогичные средства для описания неинициализированных данных (аналогов неинициализированных переменных из языков высокого уровня). В NASM для этих целей служат директивы, задаваемые ключевыми словами RESB, RESW, RESD и RESQ (начальная часть их наименований происходит от английского слова RESERVE). Использование этих директив имеет общий вид

имя_резервируемой_области RESa число_байтов_в_области

где символ a условно обозначает один из символов B, W, D или Q. Далее приведены примеры таких неинициализированных описаний

buffer resb 64

;

резервируется 64 байта

 

wordvar resw 1

; резервируется одно слово

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

Для описания области данных с повторяемыми значениями (заполненными нулями, пробелами и т.п.) удобно иметь соответствующие языковые конструкции краткого описания таких значений. В NASM для этих целей предназначена конструкция TIMES. Как правило, она применяется в виде

имя_области_данных TIMES число описание_директивы

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

15

buffer times 64 db 0

которая задает повторение описания байта с содержимым 0 шестьдесят четыре раза. Тем самым описывается буфер размеров в 64 байта, первоначально заполняемый нулями.

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

buffera db 64 dup(0)

Вкачестве средства резервирования в MASM и TASM (вместо RESx ) используется специальный метасимвол ? (вопросительный знак), который предназначается для задания вместо конкретного инициализирующего значения. Так в указанных ассемблерах резервировании памяти для областей buffer и wordvar соответствующие описания задаются как

buffer DB 64 DUP(?) ; резервируется 64 байта wordvar DW ? ; резервируется одно слово

Для введения далее используемых именованных числовых констант во всех упомянутых ассемблерах служит директива на основе служебного слова EQU. Она используется в виде

имя_константы EQU выражение

где часть выражение в простейших случаях может задаваться просто обычной

константой. Например, описание N EQU 128

вводит для дальнейшего использования символическую константу N, которую, в частности, можно использовать в следующих директивах

buffer times N db 0 abc dw N

Последние директивы описывают (а основе предварительного задания N) именованную область из 128 нулевых байтов и обозначенную именем abc область данных размером в одно слово, проинициализированную числом 128.

2.2. Обращения к функциям ОС посредством прерываний

Сегмент машинных команд в программе на ассемблере состоит из строк, задающих отдельные команды. Наиболее важными по частоте применения являются команды перемещения данных, называемые также командами пересылки (данных). В рассматриваемых ассемблерах мнемокодом таких команд служит слово MOV (сокращение от английского слова move). Команды эти имеют два операнда, один задает исходные данные или место их хранения, другой операнд задает место хранения результата. Общую форму записи команд пересылки можно представить в виде

MOV куда, откуда_или_что

16

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

MOV cl, bh MOV edx, esi

переписывает данные из регистра BH в регистр CL, а вторая из приведенных команд пересылает значение из регистра ESI в регистр EDX. В рассматриваемых ассемблерах принято систематическое правило: местонахождение результата (если он только указывается явно в записи команды) всегда задается в самом левом операнде команды. (Для других ассемблеров может применяться другое правило, в частности ассемблер AT&T требует, чтобы место размещения результата команды задавалось в правом ее операнде.)

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

MOV EAX, 1237 MOV DL, 'W'

при выполнении процессором помещают, соответственно, в регистр EAX десятичное число 1237 и в регистр DL код символа W.

Если метка metka (имя области данных) обозначает начало области данных, то команда пересылки

MOV edx, metka

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

Заметим, что только что рассмотренное решение относится только к ассемблеру NASM. В ассемблерах же MASM и TASM для аналогичной цели указания адрес области данных в команде используется запись вида OFFSET имя_области_данных, так что приведенная выше для NASM команда запишется для них в виде

MOV edx, OFFSET metka

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

17

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

Встандартных библиотеках языка Си для любой операционной системы имеется функция вывода нижнего уровня, которая имеет прототип

unsigned int write(int handle, char *buffer, unsigned int size)

икоторая выводит символы текста из буфера buffer, заданного вторым его аргументом в файл, заданный хэндлом handle, причем выводит size байтов этого текста, если только это возможно.

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

Вбольшинстве хорошо спроектированных ОС, для доступа к стандартным файлам служат стандартные значения хэндлов, равные 0, 1 и 2. Именно, через хэндл, равный нулю, в дальнейшем обеспечивается доступ к стандартному вводу (по умолчанию, если при запуске программы не сделано соответствующего переназначения, стандартный ввод идет с клавиатуры). Через хэндл, равный единицы, обеспечивается доступ к стандартному выводу (т.е. по умолчанию - вывод на экран). Хэндл со значение два, предназначен для оперативного вывода сообщений об ошибках, и также по умолчанию его использование приводит к выводу на экран. Обозначение стандартных ввода и выводов хэндлами со значениями 0, 1, 2 присуща операционным системам Unix, OS/2 и даже старенькой однозадачной MS-DOS, но в ОС типа Windows используется более сложный вариант, который будет рассматриваться значительно позже.

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

Дальше мы упираемся в особенности обращения к программным функциям операционных систем.

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

INT номер_прерывания

где номер_прерывания есть небольшое целое неотрицательное число, не превосхо-

дящее 255. Вместо вызова подпрограммы по имени этой подпрограммы, команда прерывания позволяет вызвать подпрограмму по условному небольшому номеру. Изнанка такого обращения состоит в том, что практически только операционная система может предварительно соотнести такому номеру начало подпрограммы. Команды программных прерываний используют для более "низменных" целей очень мощный и сложный механизм прерываний процессора. Это аппаратно-про-

18

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

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

Для начального изучения возможностей и использования ассемблера сейчас важно лишь то, что к внутренним действиям ряда операционных систем можно обратится командами INT номер. При этом нужно знать, который номер прерывания задать и как передать аргументы подпрограмме ОС, которая будет выполнять наш запрос. Сразу же обратим внимание, что доступ к внутренним функциям ОС через программные прерывания возможет в ОС Linux и MS-DOS, но скрыт в качестве промежуточного механизма для таких ОС как Windows и OS/2. Программный интерфейс последних использует доступ к функциям ОС через именованные функции, доступные в свою очередь с помощью специальных библиотек подпрограмм, постоянно находящихся в оперативной памяти и одновременно обслуживающих множество запросов. (Они называются библиотеками динамической компоновки.) Последний вариант доступа будет как более сложный рассмотрен значительно позже - после изучения средств ассемблера, позволяющих описать такие обращения.

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

INT 80H

а какая именно функция нужна задается числом в регистре eax (это число должно быть занесено в регистр eax перед выполнением команды INT 80H).

Все аргументы, заданные прототипом системной функции на языке Си, должны быть предварительно помещены в регистры ebx, ecx, edx, esi и edi. Причем первый аргумент прототипа функции должен быть помещен в регистр ebx, второй аргумент ее в регистр ecx, третий - в регистр edx, четвертый в регистр esi и пятый - в регистр edi. Практически для пользователя такой системы нужно знать только номер, соответствующий базовой системной функции. Эти номера содержаться в файле, unistd.h находящиеся в большинстве модификаций Linux в каталоге

/usr/include/asm.

Для функции write текущие версии Linux дают число 4, для функции read - число 3, для функции exit - число 1. Этих функций, как ни странно, оказывается достаточно для всех рассматриваемых далее программ, которые предназначены для ознакомления с программированием на ассемблере.

19

Изученного нами материала уже достаточно для написания простейшей программы, результаты работы которой можно наблюдать. Эта программа, приведенная своим исходным текстом на рис. 2.2.1, предназначена для вывода на экран текста Privet!, за которым тут же выводится отдельный символ доллара (символ $).

GLOBAL _start

SEGMENT .text _start:

;--- write(1, txt, 7) == <4>(ebx, ecx, edx) mov eax,4 ; N function=write

mov ebx,1 ; N handle=1 (stdout) mov ecx, txt ; address of txt

mov edx,7 ; number of byte int 80h

;--- write(1, symbl, 1) == <4>(ebx, ecx, edx) mov eax,4 ; N function=write

mov ebx,1 ; N handle=1 (stdout) mov ecx, symbl ; address of symb mov edx,1 ; number of byte

int 80h

mov eax,1 ; N function=exit int 80h

SEGMENT .data txt db 'Privet!' symbl db '$'

Рис. 2.2.1. Простейшая программа вывода текста на ассемблере NASM

В этой программе описано два сегмента со стандартными именами .text и .data, причем в сегменте данных описаны две именованных области данных: первая из них имеет имя txt и содержит текст, заданный в директиве DB, а вторая состоит всего из одного байта с именем symbl внутри программы и содержащий символ $.

Сегмент машинных команд по существу содержит записанные на ассемблере последовательные вызовы системных функций write(1, txt, 7), write(1, symbl, 1) и exit(0). Каждый такой вызов состоит из занесения в регистры аргументов и обращения к команде INT 80H. Занесение аргументов производится командами пересылки MOV. Первый аргумент этих функций, задаваемый во всех трех данных вызовах константой, заносится в регистр ebx. Второй аргумент для функций write в нашем примере задает адрес области текста, который должен быть выведен, поэтому для его занесения использованы команды вида

MOV ecx, имя_области_текста

20

Соседние файлы в предмете Операционные системы