Бегун.Рекомендую - партнеру
Здесь может быть ваша реклама
|

 Глава2. Основы программирования
 

Глава2. Основы программирования

Подготовка и отладка программы

Процесс подготовки и отладки программы на языке ассемблера включает этапы подготовки исходного текста, трансляции, компоновки и отладки.
Подготовка исходного текста программы выполняется с помощью любого текстового редактора, хотя бы редактора, встроенного в программу Norton Commander, или еще более удобного редактора Norton Editor. При использовании одного из более совершенных текстовых процессоров, вроде Microsoft Word, следует иметь в виду, что эти программы добавляют в выходной файл служебную информацию о формате (размер страниц, тип шрифта и др.), которая будет непонятна транслятору. Однако практически все текстовые редакторы и процессоры позволяют вывести в выходной файл "чистый текст", без каких-либо служебных символов. Именно таким режимом и надлежит воспользоваться в нашем случае.
В принципе для подготовки исходного текста можно воспользоваться любым редактором системы Windows, например, программой WordPad или Блокнотом. Однако в этом случае возникнут неприятности с русским шрифтом. Как известно, корпорация Microsoft приняла для своих русифицированных продуктов собственную кодировку русских символов, расходящуюся со стандартной, используемой в приложениях DOS. Если программу, использующую русский текст в качестве комментариев, или выводящую его на экран, подготовить в одном из редакторов Windows, то при ее просмотре и запуске в среде DOS вместо русского текста вы увидите бессмысленный набор символов. Поэтому программы, предназначенные для выполнения под управлением MS-DOS, лучше и подготавливать в среде DOS. Файл с исходным текстом должен иметь расширение .ASM.
Следующая операция состоит в трансляции исходного текста программы, т.е. в преобразовании строк исходного языка в коды машинных команд. Эта операция выполняется с помощью транслятора с языка ассемблера (т.е. с помощью программы ассемблера). Известные разработчики программного обеспечения - корпорации IBM, Borland, Microsoft и др. предлагают свои варианты трансляторов, несколько различающиеся своими возможностями и системой обозначений. Однако входной язык любого транслятора, включающий в себя мнемонику машинных команд и других операторов и правила написания предложений ассемблера, для всех ассемблеров одинаков, поэтому при подготовке и отладке примеров данной книги можно с равным успехом воспользоваться любой из указанных программ. Мы, как уже отмечалось, использовали программы пакета TASM 5.0 (фирменные названия этих программ - Turbo Assembler,
Turbo Link и Turbo Debugger, а имена соответствующих им файлов - TASM.EXE, TLINK.EXE и TD.EXE).
После трансляции образуются два файла - листинг трансляции и объектный файл с расширением OBJ. Листинг представляет собой текстовый файл, предназначенный для чтения в каком-либо редакторе, и содержит исходный текст оттранслированной программы вместе с машинными кодами команд. В случае обнаружения транслятором каких-либо ошибок, в листинг также включаются сообщения об этих ошибках.
Рассмотрим элементы листинга трансляции примера 1-1 из предыдущей главы. На рис. 2.1 приведен несколько сокращенный текст этого листинга, из которого удалены комментарии к отдельным предложениям.
айту. В этом случае следует использовать перетаскивание в обратном направлении. А именно, разыщите целевой файл, например, при помощи программы Проводник, затем перетащите его значок на карту узла — на изображение родительского документа.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.1. Листинг трансляции программы 1-1.

Рассматривая листинг, можно отметить ряд полезных моментов общего характера. Предложения программы с операторами assume, segment, ends, end, как уже отмечалось ранее, не транслируются в какие-либо машинные коды и не находят отражения в памяти. Они нужны лишь для передачи транслятору служебной информации о способе трансляции команд (assume), границах сегментов (segment и end) и строке, на которой следует завершить обработку исходного текста (end).
Каждому транслируемому предложению программы соответствует определенное смещение, причем задание смещений выполняется в каждом сегменте в отдельности. Первая команда mov AX,data имеет смещение от начала сегмента команд, равное нулю. Она занимает 3 байта, поэтому следующая команда начинается с байта 3 и имеет соответствующее смещение.
Транслятор не смог полностью определить код команды mov AX,data. В этой команде в регистр АХ засылается сегментный адрес сегмента data. Однако этот адрес станет известен лишь в процессе загрузки выполнимого файла программы в память. Поэтому в листинге на месте этого адреса стоят нули, помеченные буквой s, напоминающей о том, что здесь должен быть пока неизвестный сегментный адрес.
Еще одна помеченная команда с кодом ВА 0000 располагается в строке 8 листинга. В этой команде в регистр DX заносится смещение поля с именем msg, расположенное в сегменте данных (ключевое слово offset, указанное перед именем поля, говорит о том, что речь идет не о содержимом ячейки msg, а об ее смещении). Поле msg расположено в самом начале сегмента данных, и его смещение от начала сегмента равно 0, что и указано в коде команды. Почему же эта команда помечена буквой т, являющейся сокращением слова relocatable, переместимый?
Чтобы ответить на этот вопрос, нам придется рассмотреть, как сегменты программы размещаются в памяти. Как уже говорилось, любой сегмент может располагаться в памяти только с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Конкретный адрес программы в памяти зависит от конфигурации компьютера, - какой размер занимает DOS, сколько загружено резидентных программ и драйверов, а также в каком режиме запускается программа - в отладчике или без него. Предположим, что сегментный адрес сегмента команд оказался равным 1306п (рис. 2.2, а). В нашей программе сегмент команд имеет размер 11h байт (что указано в строке 13 листинга), т.е. занимает целый параграф плюс один байт. Сегмент данных имеет размер 14h байт (строка 19 листинга) и тоже требует для своего размещения немного больше одного парафафа. Из-за того, что сегмент данных должен начаться на границе параграфа, ему будет назначен сегментный адрес 1308h и между сегментами образуется пустой промежуток размером 15 байт.
Потеря 15 байт из многомегабайтовой памяти, разумеется, не имеет никакого значения, однако в некоторых случаях, например, при компоновке единой профаммы из большого количества модулей с небольшими по размеру подпрофаммами, суммарная потеря памяти может оказаться значительной.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.2. Расположение сегментов программы в памяти при выравнивании по умолчанию (а) и на байт (б).

Для того, чтобы устранить потери памяти, можно сегмент данных объявить с выравниванием на байт:

data segment byte

Такое объявление даст возможность системе загрузить сегмент данных так, как показано на рис. 2.2, б. Сегмент данных частично перекрывает сегмент команд, начинаясь на границе его последнего параграфа (в нашем случае по адресу 1307h). Для того, чтобы данные не наложились на последние команды сегмента команд, они смещаются вниз так, что начинаются сразу же за сегментом команд. В нашем примере, где сегмент команд "выступает" за сегментный адрес 1307h всего на 1 байт, данные и надо сместить на этот 1 байт. В результате поле msg, с которого начинается сегмент данных, и которое в листинге имело смещение 0, получит смещение 1. Все остальные адреса в сегменте данных также сместятся на один байт вперед. В результате данные будут располагаться в физической памяти вплотную за командами, без всяких промежутков, однако все обращения в сегменте команд к данным должны быть скорректированы на величину перекрытия сегментов, в нашем случае - на 1 байт. Эта коррекция выполняется системой после загрузки программы в память, но еще до ее запуска. Адреса, которые могут потребовать описанной коррекции, и помечаются в листинге трансляции буквой "г". Из сказанного следует очень важный и несколько неожиданный вывод: коды команд программы в памяти могут не совпадать с кодами, показанными в листинге трансляции. Это обстоятельство необходимо учитывать при отладке программ с помощью интерактивного отладчика, который, естественно, показывает в точности то, что находится в памяти, и что не всегда соответствует листингу трансляции.
Вернемся к рассмотрению листинга трансляции. Данные, введенные нами в программу, также оттранслировались: вместо символов текста в загрузочный файл попадут коды ASCII этих символов. Так, буква "П" преобразовалась в код 8Fh, буква "р" в код ЕО и т. д. При выводе этих кодов на экран видеосистема компьютера преобразует их назад в изображения символов, записанных в исходном тексте программы.
Из листинга трансляции легко определить размер отдельных составляющих программы. В нашем случае длина сегмента команд составляет 11h = 17 байт, длина сегмента данных - 14h = 20 байт, а под стек отведено ровно столько, сколько мы запросили в программе - 100h = 256 байт. Размер же всей программы окажется больше суммы длин сегментов, во-первых, из-за пустых промежутков между сегментами (у нас на них уйдет 15 + 12 = 27 байт), и, во-вторых, за счет подсоединения к программе обязательного префикса программы, имеющего всегда размер 256 байт.
Как мы уже отмечали, в результате трансляции программы образуется два файла - с листингом и с объектным модулем программы.
Объектный файл является основным результатом работы транслятора и представляет собой текст программы, преобразованный в машинные коды. Хотя в этом файле уже присутствуют коды команд, он не может быть выполнен. Для того чтобы получить выполнимую программу, объектный файл необходимо скомпоновать.
Компоновка объектного файла выполняется с помощью программы компоновщика (редактора связей). Эта программа получила такое название потому, что ее основное назначение - подсоединение к файлу с основной программой файлов с подпрограммами и настройка связей между ними. Однако компоновать необходимо даже простейшие программы, не содержащие подпрограмм. Дело в том, что у компоновщика имеется и вторая функция - изменение формата объектного файла и преобразование его в выполнимый файл, который может быть загружен в оперативную память и выполнен. Файл с программой компоновщика обычно имеет имя LINK.EXE, хотя это может быть и не так. Например, компоновщик пакета TASM назван TLINK.EXE. В результате компоновки образуется загрузочный, или выполнимый файл с расширением .ЕХЕ.
Отладку и изучение работы готовой программы удобнее всего осуществлять с помощью интерактивного отладчика, который позволяет выполнять отлаживаемую программу по шагам или с точками останова, выводить на экран содержимое регистров и областей памяти, модифицировать (в известных пределах) загруженную в память программу, принудительно изменять содержимое регистров и выполнять другие действия, позволяющие в наглядной и удобной форме контролировать выполнение программы.
Рассмотрим вкратце основные приемы работы с "турбоотладчиком" TD.EXE из пакета TASM. Приступая к работе с отладчиком, следует убедиться, что в рабочем каталоге имеются и загрузочный (Р.ЕХЕ), и исходный (P.ASM) файлы, так как отладчик в своей работе использует оба эти файла. Для запуска отладчика следует ввести команду

td р

На экране появится кадр отладчика, в котором видны два окна - окно Module с исходным текстом отлаживаемой программы и окно Watches для наблюдения за ходом изменения заданных переменных в процессе выполнения программы (рис. 2.3). Окно Watches нам не понадобится, и его можно убрать, щелкнув мышью по маленькому квадратику в левом верхнем углу окна, или введя команду <Alt>+<F3>, предварительно сделав это окно активным. Переключение (по кругу) между окнами осуществляется клавишей <F6>.


Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.З. Начальный кадр отладчика с текстом отлаживаемой программы.

В процессе отладки программы на экран приходится выводить много дополнительных окон; они перекрываются и часто скрывают друг друга. Чтобы увидеть их все одновременно, размер окон приходится уменьшать, а сами окна перемещать по экрану. Режим изменения размеров и положения окна включается командой <Ctrl>+<F5>, после чего клавиши со стрелками перемещают окно по экрану, а те же клавиши при нажатой клавише <Shift> позволяют изменять его размер. Выход из режима настройки окна осуществляется нажатием клавиши <Enter>.
Начальное окно отладчика дает слишком мало информации для отладки программы. В нем можно выполнять программу по частям до местоположения курсора (клавиша <F4>) и команда за командой (клавиша <F8>); можно также с помощью окна Watches наблюдать изменения заданных полей данных. Однако для отладки программы на уровне языка ассемблера необходимо контролировать все регистры процессора, включая регистр флагов, а также, во многих случаях, поля данных вне программы (например, векторы прерываний или системные таблицы). Гораздо более информативным является "окно процессора", которое вызывается с помощью пункта Vicw>CPU верхнего меню или командой <Alt>+<V>+<C> (рис. 2.4).

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.4. Окно процессора с внутренними окнами.

Окно процессора состоит, в свою очередь, из 5 внутренних окон для наблюдения текста программы на языке ассемблера и в машинных кодах, регистров процессора, флагов, стека и содержимого памяти. С помощью этих окон можно полностью контролировать ход выполнения отлаживаемой программы. Для того чтобы можно было работать с конкретным окном, например, прокручивать его содержимое, надо сделать его активным, щелкнув по нему мышью. Перейти из окна в окно можно также с помощью клавиатуры, нажимая клавишу Tab. Посмотрим, какие сведения можно извлечь из содержимого окна процессора.
Содержимое сегментных регистров DS и ES одинаково и составляет HF5h. Эта значит, что программа загружена в память, начиная с физического адреса 11F50, т.е. приблизительно с 70-го килобайта. Чем заняты первые 70 Кбайт памяти? Обычно компьютер конфигурируется так, что в обычной памяти размещается только малая часть DOS (около 16 Кбайт), драйверы обслуживания расширенной памяти и резидентная часть COMMAND.COM. Основная часть DOS, остальные драйверы и необходимые резидентные программы (например, русификатор) переносятся в расширенную память. В этом случае системные области в начале памяти занимают всего 20 - 25 Кбайт. Тем не менее наша программа начинается не с 25-го, а с 70-го килобайта. Произошло это из-за того, что программа запущена под управлением отладчика, который сначала загружается в память сам, и лишь затем загружает отлаживаемую программу. Но отсюда следует, что если бы мы запустили программу без отладчика, она попала бы на другое место в памяти, гораздо ближе к ее началу. В большинстве случаев это обстоятельство не имеет особого значения, так как любая программа должна одинаково успешно выполняться в любом месте памяти, однако необходимо отдавать себе отчет, что отладчик изменяет операционную среду программы (в частности, переносит ее на другое место в памяти). Строго говоря, программа под управлением отладчика выполняется не совсем так, как она выполнялась бы непосредственно в DOS.
Еще один пример "самодеятельности" отладчика можно увидеть в том же окне регистров процессора. Содержимое всех регистров общего назначения (АХ, ВХ, СХ, DX, SI, DI и ВР) равно 0. Отсюда можно сделать вывод, что DOS, загружая программу в память, очищает регистры процессора. Однако на самом деле это совсем не так! Регистры очищает не DOS, а отладчик. При обычном запуске программы исходное содержимое регистров практически непредсказуемо, и ни в коем случае нельзя рассчитывать, что в них будут нули. Иногда можно столкнуться и с более тонким влиянием отладчика на ход выполнения программы, вплоть до того, что некоторые виды программ, например, управляющие подключенной к компьютеру аппаратурой, в отладчике будут выполняться просто неверно.
Итак, после загрузки программы в память содержимое регистров DS и ES оказалось одинаковым. Это вполне естественно, если вспомнить, что перед выполнением оба регистра указывают на префикс программы (см, рис. 1.9). Вслед за префиксом располагается сегмент команд и поскольку префикс всегда занимает точно lOOh байт (т.е. 10h параграфов по 16 байт), то содержимое CS в нашем случае должно быть равно HF5h + 10h = 1205h. Так оно и есть (см. рис. 2.4).
В нашем примере программа должна начать выполняться с метки begin, поскольку именно эту метку мы указали в качестве операнда завершающей директивы end. Эта метка относится к самой первой команде сегмента команд и ее значение (или, что то же самое, смещение первой команды программы) должно быть равно 0. Поэтому исходное значение указателя команд, как это видно из рис. 2.4, тоже равно 0. В дальнейшем, по мере выполнения команд, значение IP будет возрастать. Выполним две первые команды программы, дважды нажав клавишу <F8>. Состояние программы после этой операции показано на рис. 2.5.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.5. Состояние программы после выполнения двух первых команд.

Видно, что указатель команд получил значение 5 и показывает на очередную (еще не выполнявшуюся) команду mov AH,09h, относительный адрес которой равен 5. Сегментный регистр DS получил значение 1207h, что должно соответствовать сегментному адресу сегмента данных. Вспомним, что сегмент команд у нас занимает 11h байт и требует в памяти 2 параграфа. Сегмент команд имеет сегментный адрес 1205h, следовательно, сегментный адрес сегмента данных должен быть равен 1207h, что мы и получили.
Обратим внимание на самую правую колонку в окне процессора, в которой индицируются состояния флагов процессора. Как уже говорилось, состояния флагов заново устанавливаются процессором после выполнения каждой команды, и по ним можно в определенной степени судить о результате команды. С самого начала у нас был установлен только флаг IF (i в окне отладчика), что свидетельствует о включенном механизме аппаратных прерываний; остальные флаги сброшены. После выполнения двух первых команд состояние регистра флагов не изменилось. Произошло это потому, что команда пересылки mov не изменяет состояния флагов. Поскольку в нашей программе нет никаких команд, кроме mov и hit, а команда hit тоже состояния флагов обычно не изменяет, то наблюдать с помощью нашего примера функционирование регистра флагов не удастся.
Рассмотрим теперь стек. Сегмент данных имеет у нас размер 14h байт, и под него в памяти надлежит выделить 2 параграфа. Это объясняет содержимое сегментного регистра стека SS - 1209п. Под стек отведено 256 байт, поэтому исходное положение SP (под дном стека) соответствует смещению l00h.
Наконец, стоит еще обратить внимание на нижнюю половину окна команд, заполненную странными командами add [bx+si],al. Таких команд, да еще в таком количестве, в нашей программе нет, их "придумал" отладчик, пытаясь деассемблировать промежуток между сегментом команд и сегментом данных, заполненный нулями. Код 0000h соответствует команде add [bx+si],al, которую и изобразил отладчик.
Таким образом, рассмотрев информацию, предоставленную отладчиком, мы подтвердили все предыдущие рассуждения о расположении в памяти сегментов программы и об инициализации регистров процессора при загрузке программы в память.
Обратимся теперь к окну дампа. При запуске отладчика в окно дампа выводится содержимое памяти, начиная с адреса DS:0000h, т.е. начало префикса программы (см. рис. 2.4 и 2.5). Для того, чтобы вывести на экран что-либо иное, надо воспользоваться командой <Alt>+<F10>, которая для каждого внутреннего окна процессора открывает дополнительное меню. Вид этого меню зависит от того, какое окне было активным в момент ввода команды. На рис. 2.6 показано дополнительное меню окна дампа.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.6. Дополнительное меню окна дампа памяти.

Чаще всего приходится пользоваться первым пунктом этого меню Goto, с помощью которого можно задать любой адрес (входящий или не входящий в сегменты программы), и получить дамп этого участка.. На рис. 2.7. изображено содержимое окна дампа после ввода начального адреса в виде DS:0 (тот же результат даст начальный адрес DS:msg, а так же и просто msg, так как по умолчанию сегментный адрес берется из DS). Как и следовало ожидать, по этому адресу расположено наше единственное данное - строка текста, выводимая программой на экран. Кстати, в окне дампа видно начало промежутка между сегментами (данных и стека), заполненного нулями.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.7. Дамп сегмента данных. 

Представление данных

В языке ассемблера имеются средства записи целых и вещественных чисел, а также символьных строк и отдельных символов. Целые числа могут быть со знаком и без знака, а также записанными в двоично-десятичном формате. Для целых чисел и символов в составе команд микропроцессора и, соответственно, в языке ассемблера, есть средства обработки - анализа, сравнения, поиска и проч. Для вещественных чисел таких средств в самом микропроцессоре нет, они содержатся в арифметическом сопроцессоре. Поскольку программирование сопроцессора в настоящей книге не рассматривается, то и вещественными числами мы заниматься не будем.
Рассмотрим сначала целые числа без знака и со знаком. Числа без знака получили свое название потому, что среди этих чисел нет отрицательных. Это самый простой вид чисел: они представляют собой весь диапазон двоичных чисел, которые можно записать в байте, слове или двойном слове. Для байта числа без знака могут принимать значения от 00h (0) до FFh (255); для слова - от 0000h (0) до FFFFh (65535); для двойного слова - от 00000000h (0) до FFFFFFFFh (4294967295).
В огромном количестве приложений вычислительной техники для чисел нет понятия знака. Это справедливо, например, для адресов ячеек памяти, кодов ASCII символов, результатов измерений многих физических величин, кодов управления устройствами, подключаемыми к компьютеру. Для таких чисел естественно использовать весь диапазон чисел, записываемых в ячейку того или иного размера. Если, однако, мы хотим работать как с положительными, так и с отрицательными числами, нам придется половину чисел из их полного диапазона считать положительными, а другую половину - отрицательными. В результате диапазон изменения числа уменьшается в два раза. Кроме того, необходимо предусмотреть систему кодирования, чтобы положительные и отрицательные числа не перекрывались.
В вычислительной технике принято записывать отрицательные числа в так называемом дополнительном коде, который образуется из прямого путем замены всех двоичных нулей единицами и наоборот (обратный код) и прибавления к полученному числу единицы. Это справедливо как для байтовых (8-битовых) чисел, так и для чисел размером в слово или в двойное слово (рис. 2.8)

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.8. Образование отрицательных чисел различного размера.

Такой способ образования отрицательных чисел удобен тем, что позволяет выполнять над ними арифметические операции по общим правилам с получением правильного результата. Так, сложение чисел +5 и -5 дает 0; в результате вычитания 3 из 5 получается 2; вычитание -3 из -5 дает -2 и т.д.
Анализируя алгоритм образования отрицательного числа, можно заметить, что для всех отрицательных чисел характерно наличие двоичной единицы в старшем бите. Положительные числа, наоборот, имеют в старшем бите 0. Это справедливо для чисел любого размера. Кроме того, из рис. 2.8 видно, что для преобразования отрицательного 8-битового числа в слово достаточно дополнить его слева восемью двоичными единицами. Легко сообразить, что для преобразования положительного 8-битового числа в слово его надо дополнить восемью двоичными нулями. То же справедливо и для преобразования слова со знаком в двойное слово со знаком, только добавить придется уже не 8, а 16 единиц или нулей. В системе команд МП 86 и, соответственно, в языке ассемблера, для этих операций предусмотрены специальные команды cbw и cwd.
Следует подчеркнуть, что знак числа условен. Одно и то же число, например, изображенное на рис. 2.8 8-битовое число FBh можно в одном контексте рассматривать, как отрицательное (-5), а в другом - как положительное, или, правильнее, число без знака (FBh=251). Знак числа является характеристикой не самого числа, а нашего представления о его смысле.
На рис. 2.9 представлена выборочная таблица 16-битовых чисел с указанием их машинного представления, а также значений без знака и со знаком. Из таблицы видно, что для чисел со знаком размером в слово диапазон положительных значений простирается от 0 до 32767, а диапазон отрицательных значений - от -1 до -32768.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.9. Представление 16-битовых чисел без знака и со знаком.

На рис. 2.10 представлена аналогичная таблица для 8-битовых чисел. Из таблицы видно, что для чисел со знаком размером в байт диапазон положительных значений простирается от 0 до 127, а диапазон отрицательных значений - от -1 до -128.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Рис. 2.10. Представление 8-битовых чисел без знака и со знаком.

Среди команд процессора, выполняющих ту или иную обработку чисел, можно выделить команды, безразличные к знаку числа (например, inc, add, test), команды, предназначенные для обработки чисел без знака (mul, div, ja, jb и др.), а также команды, специально рассчитанные на обработку чисел со знаком (imul, idiv, jg, jl и т.д.). Особенности использования этих команд будут описаны в следующей главе.
Рассмотрим теперь другой вид представления чисел - двоично-десятичный формат (binary-coded decimal , BCD), используемый в ряде прикладных областей. В таком формате выдают данные некоторые измерительные приборы; он же используется КМОП-часами реального времени компьютеров IBM PC для хранения информации о текущем времени. В МП 86 предусмотрен ряд команд для обработки таких чисел.
Двоично-десятичный формат существует в двух разновидностях: упакованный и распакованный. В первом случае в байте записывается двухразрядное десятичное число от 00 до 99. Каждая цифра числа занимает половину байта и хранится в двоичной форме. Из рис. 2.11 можно заметить, что для записи в байт десятичного числа в двоично-десятичном формате достаточно сопроводить записываемое десятичное число символом h.

Иллюстрированный самоучитель по Assembler, программы, электронные книги, раскрутка, оптимизация

Описание данных

Практически любая программа содержит в себе перечень данных, с которыми она работает. Это могут быть символьные строки, предназначенные для вывода на экран; числа, определяющие ход выполнения программы или участвующие в вычислениях; адреса подпрограмм, обработчиков прерываний или просто тех или иных полей программы; специальные коды, например, коды цвета выводимых на экран символов и т.д. Кроме данных, определяемых в тексте программы, в программу часто входят зарезервированные поля, предназначенные для заполнения по ходу выполнения программы, например, результатами вычислений или путем чтения из файла. Все эти данные и зарезервированные поля должны быть определены в составе сегмента данных программы (в принципе они могут быть определены, и часто определяются, не в сегменте данных, а в сегменте команд, но здесь мы не будем касаться этого вопроса).
Для определения данных используются, главным образом, три директивы ассемблера: db (define byte, определить байт) для записи байтов, dw (define word, определить слово) для записи слов и dd (define double, определить двойное слово) для записи двойных слов:

db 255


dw 6.5535


dd 100000000

Кроме перечисленных, имеются и другие директивы, например df (define fanvord, определить поле из 6 байт), dq (define quadword, определить четверное слово) или dt (define tcraword, определить 10-байтовую переменную), но они используются значительно реже.
Для того чтобы к данным можно было обращаться, они должны иметь имена. Имена данных могут включать латинские буквы, цифры (не в качестве первого знака имени) и некоторые специальные знаки, например, знаки подчеркивания (_), доллара ($) и коммерческого at (@). Длину имени некоторые ассемблеры ограничивают (например, ассемблер MASM - 31 символом), другие - нет, но в любом случае слишком длинные имена затрудняют чтение программы. С другой стороны, имена данных следует выбирать таким образом, чтобы они отражали назначение конкретного данного, например counter для счетчика или filename для имени файла:

counter dw 10000


filename db "a:myfile.001'

Значения числовых данных можно записывать в различных системах счисления; чаще других используются десятичная и 16-ричная запись:

size dw 256 ;В ячейку size записывается
                    ;десятичное число 256


setb7 db 80h ;В ячейку setb7 записывается
                    ;16-ричное число 80h

Необходимо отметить неточность приведенных выше комментариев. В памяти компьютера могут храниться только двоичные коды. Если мы говорим, что в какой-то ячейке записано десятичное число 128, мы имеем в виду не физическое содержимое ячейки, а лишь форму представления этого числа в исходном тексте программы. В слове с именем size фактически будет записан двоичный код 0000000100000000, являющийся двоичным эквивалентом десятичного числа 256. Во втором случае в байте с именем setbit? будет записан двоичный эквивалент шестнадцатиричного числа 80h, который составляет 10000000 (т.е. байт с установленным битом 7, откуда и получила имя эта ячейка).
Для резервирования места под массивы используется оператор dup (duplicate, дублировать), который позволяет "размножить" байт, слово или двойное слово заданное число раз:

rawdata dw 300 dup (1)   ;Резервируются 300 слов,
                                        ;заполненных числом 1


string db 80 dup ('^')        ;Резервируются 80 байтов,
                                        ;заполненных знаком '^'

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

mov AX,size

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

data segment


0000h counter dw 10000


0002h pages db "
Страница 1"


000Ch numbers db 0, 1, 2, 3, 4


0011h page_addr dw pages


data ends

Сегмент данных начинается с данного по имени counter, которое описано, как слово (2 байт) и содержит число 10000. Очевидно, что его смещение равно 0. Поскольку это данное занимает 2 байт, следующее за ним данное pages получило смещение 2. Данное pages описывает строку текста длиной 10 символов и занимает в памяти столько же байтов, поэтому следующее данное numbers получило относительный адрес 2 + 10 = 12 = Ch. В поле numbers записаны 5 байтовых чисел, поэтому последнее данное сегмента с именем page_addr размещается по адресу Ch + 5 = 11h.
Ассемблер, начиная трансляцию сегмента (в данном случае сегмента данных) начинает отсчет его относительных адресов. Этот отсчет ведется в специальной переменной транслятора (не программы!), которая называется счетчиком текущего адреса и имеет символическое обозначение знака доллара (S). По мере обработки полей данных, их символические имена сохраняются в создаваемой ассемблером таблице имен вместе с соответствующими им значениями счетчика текущего адреса. Другими словами, введенные нами символические имена получают значения, равные их смещениям. Таким образом, с точки зрения транслятора counter равно 0, pages - 2, numbers - Ch и т.д. Поэтому предложение

page_addr dw pages

трактуется ассемблером, как

page_addr dw 2

и приводит к записи в слово с относительным адресом 11h числа 2 (смещения строки pages).
Приведенные рассуждения приходится использовать при обращении к "внутренностям" объявленных данных. Пусть, например, мы хотим выводить на экран строки "Страница 2", "Страница 3", "Страница 4" и т.д. Можно, конечно, все эти строки описать в сегменте данных по отдельности, но это приведет к напрасному расходу памяти. Экономнее поступить по-другому: выводить на экран одну и ту же строку pages, но модифицировать в ней номер страницы. Модификацию номера можно выполнить с помощью, например, такой команды:

mov pages + 9, ' 2'

Здесь мы "вручную" определили смещение интересующего нас символа в строке, зная, что все данные размещаются ассемблером друг за другом в порядке их объявления в программе. При этом, какое бы значение не получило имя pages, выражение pages + 9 всегда будет соответствовать байту с номером страницы.
Таким же приемом можно воспользоваться при обращении к данному numbers, которое в сущности представляет собой небольшой массив из 5 чисел. Адрес первого числа в этом массиве равен просто numbers, адрес второго числа - numbers + 1, адрес третьего - numbers + 2 и т.д. Следующая команда прочитает последний элемент этого массива в регистр DL:

mov DL,numbers+4

Какой смысл имело объединение ряда чисел в массив numbers? Да никакого, если к этим числам мы все равно обращаемся по отдельности. Удобнее было объявить этот массив таким образом:

nmb0 db 0


nmbl db 1


nmb2 db 2


nmb3 db 3


nmb4 db 4

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

mov SI,4                   ;Индекс элемента в массиве


mov DL,numbers[SI] ;Обращение по адресу


                                  ;numbers + содержимое SI

Иногда желательно обращаться к элементам массива (обычно небольшого размера) то с помощью индексов, то по их именам. Для этого надо к описанию массива, как последовательности отдельных данных, добавить дополнительное символическое описание адреса начала массива с помощью директивы ассемблера label (метка):

numbers       label      byte


nmb0            db          0


nmbl             db          1


nmb2            db          2


nmb3            db          3


nmb4            db          4

Метка numbers должна быть объявлена в данном случае с описателем byte, так как данные, следующие за этой меткой, описаны как байты и мы планируем работать с ними именно как с байтами. Если нам нужно иметь массив слов, то отдельные элементы массива следует объявить с помощью директивы dw, а метке numbers придать описатель word:

numbers     label    word


nmb0          dw        0


nmbl           dw        1


nmb2          dw        2


nmb3       
    dw        3


nmb4          dw        4

В чем состоит различие двух последних описаний данных? Различие есть, и весьма существенное. Хотя в обоих случаях в память записывается натуральный ряд чисел от 0 до 4, однако в первом варианте под каждое число в памяти отводится один байт, а во втором - слово. Если мы в дальнейшем будем изменять значения элементов нашего массива, то в первом варианте каждому числу' можно будет задавать значения от 0 до 255, а во втором - от 0 до 65535.
Выбирая для данных способ их описания, необходимо иметь в виду, что ассемблер выполняет проверку размеров используемых данных и не пропускает команды, в которых делается попытка обратиться к байтам, как к словам, или к словам - как к байтам. Рассмотрим последний вариант описания массива numbers. Хотя под каждый элемент выделено целое слово, однако реальные числа невелики и вполне поместятся в байт. Может возникнуть искушение поработать с ними, как с байтами, перенеся предварительно в байтовые регистры:

mov AL,nmb0 ;Переносим nmb0 в AL


mov DL,nmbl ;Переносим nmb1 в AL


mov CL,nmb2 ;Переносим nmb2 в AL

Так делать нельзя. Транслятор сообщит о грубой ошибке - несоответствии типов, и не будет создавать объектный файл. Однако довольно часто возникает реальная потребность в операциях такого рода. Для таких случаев предусмотрен специальный атрибутивный оператор byte ptr (byte pointer, байтовый указатель), с помощью которого можно на время выполнения одной Команды изменить размер операнда:

mov AL,byte ptr nmb0


mov DL,byte ptr nmbl


mov CL,byte ptr nmb2

Эти команды транслятор рассматривает, как правильные.
Часто возникает необходимость выполнить обратную операцию - к паре байтов обратиться, как к слову. Для этого надо использовать оператор word ptr:

okey db 'OK'



mov AX,word ptr okey

Здесь оба байта из байтовой переменной okey переносятся в регистр АХ. При этом первый по порядку байт, т.е. байт с меньшим адресом, содержащий букву "О" (можно считать, что он является младшим в слове
"OK"), отправится в младшую половину АХ - регистр AL, а второй по порядку байт, с буквой "К", займет регистр АН.
До сих пор речь шла о данных, которые, в сущности, являлись переменными, в том смысле, что под них выделялась память и их можно было модифицировать. Язык ассемблера позволяет также использовать константы, которые являются символическими обозначениями чисел и могут использоваться всюду в тексте программы, как наглядные эквиваленты этих чисел:

maxsize = 0FFFFh


mov CX,maxsize mov CX,0FFFFh

Последние две команды полностью эквивалентны.
При определении констант допустимо выполнение арифметических операций. Пусть нам надо задать позицию символа (или строки символов) на экране. Учитывая, что каждый символ записывается в видеопамяти в двух байтах (в первом - код ASCII символа, а во втором - его атрибут), строка экрана имеет длину 80 символов, а высота экрана составляет 25 строк, то для вывода некоторого символа в середину экрана его смещение в видеопамяти от начала видеостраницы можно определить следующим образом:

position=80*2*12+40*2

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

mes db 'Ждите'


mes_len = $-mes

В этом примере константа mes_len получает значение длины строки mes (в данном случае 5 байт), которая вычисляется как разность значения счетчика текущего адреса после определения строки и ее начального адреса mes. Такой способ удобен тем, что при изменении содержимого строки достаточно перетранслировать программу, и та же константа mes_len автоматически получит новое значение.

   

Структуры и записи

Структуры


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

meddata struc ;Структура с именем meddata


index dd 0 ; Номер карты


sex db 0 ;Пол


birth dw 0 ;Год рождения


datein db ' / / ' ;Дата поступления


dateout db ' / / ' ;Дата выписки


meddata ends ;Конец описания структуры

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

data segment


pat 1 meddata <1234567, '
м',1955, 1З/06/981, '15/06/98'>


pat2 meddata <1982234, '
м',1932, '18/06/98', '25/06/98 '>


pat3 meddata <4389012, '
ж',1966, '01/12/97', '15/12/97'>


pattemp meddata <>


data ends

Имена patl, pat2 и т.д. будут служить именами переменных, каждая из которых содержит полный комплект данных об одном пациенте. Угловые скобки ограничивают конкретные данные, поступающие в каждую структурную переменную. Для переменной с именем pattemp транслятор выделит в памяти 23 байт, поместив в нее в точности то, что было указано в описании структуры (нули и два символьные шаблона для даты):

0,0,0, ' / / ',' / / '

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

mov EAX,patl.index ;ЕАХ=1234567


mov SI,offset patl.datein ;31=
смещениеэлемента patl.datein


mov DL,pat3.sex ;DL='
ж'

Особенности использования в приложениях DOS 32-разрядных регистров (ЕАХ в первой строке приведенного фрагмента) будут описаны в гл. 4.
Адрес конкретной структурной переменной можно поместить в базовый или индексный регистр, и пользоваться им в конструкциях с косвенной адресацией:

mov BX,offset pat3 ;ВХ=смещение pat3


mov EAX,[BX].index ;EAX=438901

 
MKPortal©2003-2008 mkportal.it
MultiBoard ©2007-2009 RusMKPortal