Как вывести сообщение assembler

Статья 1. Простейшая программа на языке ассемблера

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

Пример 1.1. Простейшая программа

Следует заметить, что при вводе исходного текста программы с клавиатуры можно использовать как прописные, так и строчные буквы: транслятор воспринимает, например, строки text segment и TEXT SEGMENT одинаково. Однако, с помощью ключа /ML можно заставить транслятор различать прописные и строчные буквы в именах. Тогда строки text segment и TEXT segment уже не будут эквивалентны. фактически они будут описывать два разных сегмента. Неэквивалентность прописных и строчных букв касается только имен; строки

во всех случаях воспринимаются одинаково.

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

Наша программа содержит 13 строк — предложений языка ассемблера. Первое предложение с помощью оператора segment открывает сегмент команд программы. Сегменту дается произвольное имя text . Описатель ‘code’ (так называемый класс сегмента) говорит о том, что это сегмент команд (слово code в переводе может означать и коды, и команды программы). В конце предложения после точки с запятой располагается комментарий. Таким образом, предложение языка ассемблера может состоять из четырех полей: имени, оператора, операндов и комментария, располагаемых в перечисленном порядке.

Читайте также:  Чем отмыть следы фукорцина с кожи

Любая программа должна обязательно состоять из сегментов — без сегментов программ не бывает. Обычно в программе задаются три сегмента: команд, данных и стека, но мы в нашей простой программе пока ограничились одним сегментом команд.

В предложении 2 мы с помощью оператора assume сообщаем ассемблеру (программе-транслятору), что сегментные регистры CS и DS будут указывать на один и тот же сегмент text . Сегментные регистры (а всего их в процессоре четыре) играют очень важную роль. Когда программа загружается в память и становится известно, по каким адресам памяти она располагается, в сегментные регистры заносятся начальные адреса закрепленных за ними сегментов. В дальнейшем любые обращения к ячейкам программы осуществляются путем указания сегмента, в котором находится интересующая нас ячейка, а также номера того байта внутри сегмента, к которому мы хотим обратиться. Этот номер носит название относительного адреса, или смещения. Поскольку в единственном сегменте нашей программы будут размещаться и команды, и данные, мы указываем ассемблеру оператором assume (assume — предположим), что и сегментный регистр команд CS, и сегментный регистр данных DS будут указывать на сегмент text . При этом в регистр CS адрес начала сегмента будет загружен автоматически, а регистр DS нам придется инициализировать вручную.

Строго говоря, в приведенной программе, где нет прямых обращений к ячейкам сегмента данных, не было необходимости сопоставлять в операторе assume сегмент text с сегментным регистром DS (сопоставление сегмента команд с сегментным регистром команд CS обязательно во всех случаях). Учитывая, однако, что практически в любой разумной программе обращения к полям данных имеются, мы с самого начала написали оператор assume в том виде, в каком он используется в реальных программах.

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

Предложение 3, начинающееся с метки begin , является первой выполнимой строкой программы. Для того, чтобы процессор знал, с какой строки начать выполнять программу после ее загрузки в память, начальная метка программы указывается в качестве операнда самого последнего оператора программы end (см. предложение 13). Можно подумать, что указание точки входа в программу излишне: ведь как будто и так ясно, что программу надо начать выполнять с начала, а закончить, дойдя до конца. Однако в действительности для программ, написанных на языке ассемблера, это совсем не так! Текст программы может начинаться с описания подпрограмм или полей данных. В этом случае предложение программы, с которого нужно начать ее выполнение, может располагаться где-то в середине текста программы. И завершается выполнение программы совсем не обязательно в ее последних строках, а там, где стоят предложения вызова специальной программы операционной системы, предназначенной именно для завершения текущей программы и передачи управления системе (см. предложения 8. 10). Однако начиная от точки входа, программа выполняется строка за строкой точно в том порядке, в каком эти строки написаны программистом.

В предложениях 3 и 4 выполняется инициализация сегментного регистра DS. Сначала значение имени text (т.е. адрес сегмента text ) загружается командой mov (от move, переместить) в регистр общего назначения процессора AX, а затем из регистра AX переносится в регистр DS. Такая двухступенчатая операция нужна потому, что процессор в силу некоторых особенностей своей архитектуры не может выполнить команду непосредственной загрузки адреса в сегментный регистр. Приходится пользоваться регистром AX в качестве «перевалочного пункта». Кстати, обратите внимание на то, что операнды в командах языка ассемблера записываются в несколько неестественном для европейца порядке — действие команды осуществляется справа налево.

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

Для того, чтобы обратиться к DOS, надо загрузить в регистр общего назначения AH номер требуемой функции, в другие регистры — исходные данные для выполнения этой функции, после чего выполнить команду int 21h (int — от interrupt, прерывание), которая передаст управление DOS. Вывод на экран строки текста можно осуществить функцией 09h, которая требует, чтобы в регистре DX содержался адрес выводимой строки. В предложении 6 адрес строки message загружается в регистр DX, а в предложении 7 осуществляется вызов DOS.

После того, как DOS выполнит затребованные действия, в данном случае выведет на экран текст «Наука умеет много гитик» (помните одноименный карточный фокус?), выполнение программы продолжится. Вообще-то нам вроде ничего больше делать не нужно. Однако на самом деле это не так. После окончания работы программы DOS должна выполнить некоторые служебные действия. Надо освободить занимаемую нашей программой память, чтобы туда можно было загрузить следующую программу. Надо вызвать системную программу, которая выведет на экран запрос DOS и будет ждать следующей команды оператора. Все эти действия выполняет функция DOS с номером 4Ch. Эта функция предполагает, что в регистре AL находится код завершения нашей программы, который она передаст DOS. При желании код завершения только что закончившейся программы можно «выловить» в DOS и проанализировать, но сейчас мы этим заниматься не будем. Если программа завершилась успешно, код завершения должен быть равен 0, поэтому в предложении 9 мы загружаем 0 в регистр AL и вызываем DOS уже знакомой нам командой int 21h .

После последнего выполнимого предложения программы можно описывать используемые в ней данные. У нас в качестве данных выступает строка текста. Текстовые строки вводятся в программу с помощью директивы ассемблера db (от define byte, определить байт), и заключаются в апострофы. Для того, чтобы в программе можно было обращаться к данным, поля данных, как правило, предваряются именами. В нашем случае таким именем является вполне произвольное обозначение message , с которого начинается предложение 11.

Выше, в предложении 6, мы через регистр DX передали DOS адрес начала выводимой на экран строки текста. Но как DOS определит, где эта строка закончилась? Хотя нам конец строки в программе отчетливо виден, однако в машинных кодах, из которых состоит выполнимая программа, он никак не отмечен, и DOS, выведя на экран слово «гитик», продолжит вывод байтов памяти, расположенных за нашей фразой. Поэтому DOS следует передать информацию о том, где кончается строка текста. Некоторые функции DOS требуют указания в одном из регистров длины выводимой строки, однако функция 09h работает иначе. Она выводит текст до символа $, которым мы и завершили нашу фразу.

Директива ends (end segment, конец сегмента) в предложении 12 указывает ассемблеру, что сегмент text закончился.

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

Источник

19. Простые процедуры в ассемблер

Статья основана на материале xrnd с сайта asmworld (из учебного курса по программированию на ассемблер 16-битного процессора 8086 под DOS).

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

Команды CALL и RET

Для работы с процедурами предназначены команды CALL и RET . С помощью команды CALL выполняется вызов процедуры. Эта команда работает почти также, как команда безусловного перехода (JMP), но с одним отличием — одновременно в стек сохраняется текущее значение регистра IP. Это позволяет потом вернуться к тому месту в коде, откуда была вызвана процедура. В качестве операнда указывается адрес перехода, который может быть непосредственным значением (меткой), 16-разрядным регистром (кроме сегментных) или ячейкой памяти, содержащей адрес.

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

Ближние и дальние вызовы процедур

Существует 2 типа вызовов процедур. Ближним называется вызов процедуры, которая находится в текущем сегменте кода. Дальний вызов — это вызов процедуры в другом сегменте. Соответственно существуют 2 вида команды RET — для ближнего и дальнего возврата. Компилятор FASM автоматически определяет нужный тип машинной команды, поэтому в большинстве случаев не нужно об этом беспокоиться.

В учебном курсе мы будем использовать только ближние вызовы процедур.

Передача параметров

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

Возвращаемое значение

Кроме передачи параметров часто нужно получить какое-то значение из процедуры. Например, если процедура что-то вычисляет, хотелось бы получить результат вычисления. А если процедура что-то делает, то полезно узнать, завершилось действие успешно или возникла ошибка. Существуют разные способы возврата значения из процедуры, но самый часто используемый — это поместить значение в один из регистров. Обычно для этой цели используют регистры AL и AX. Хотя вы можете делать так, как вам больше нравится.

Сохранение регистров

Хорошим приёмом является сохранение регистров, которые процедура изменяет в ходе своего выполнения. Это позволяет вызывать процедуру из любой части кода и не беспокоиться, что значения в регистрах будут испорчены. Обычно регистры сохраняются в стеке с помощью команды PUSH, а перед возвратом из процедуры восстанавливаются командой POP. Естественно, восстанавливать их надо в обратном порядке. Примерно вот так:

Пример

Для примера напишем процедуру для вывода собщения в рамке и протестируем её работу, выведя несколько сообщений. В качестве параметра ей будет передаватся адрес строки в регистре BX. Строка должна заканчиваться символом ‘$’ . Для упрощения процедуры можно разбить задачу на подзадачи и написать соответствующие процедуры. Прежде всего нужно вычислить длину строки, чтобы знать ширину рамки. Процедура get_length вычисляет длину строки (адрес передаётся также в BX) и возвращает её в регистре AX.

Для рисования горизонтальной линии из символов предназначена процедура draw_line. В DL передаётся код символа, а в CX — количество символов, которое необходимо вывести на экран. Эта процедура не возвращает никакого значения. Для вывода 2-х символов конца строки написана процедура print_endline. Она вызывается без параметров и тоже не возвращает никакого значения. Коды символов для рисования рамок можно узнать с помощью таблицы символов кодировки 866 или можно воспользоваться стандартной программой Windows «Таблица символов», выбрав шрифт Terminal.

Результат работы программы выглядит вот так:

Отладчик Turbo Debugger

Небольшое замечание по поводу использования отладчика. В Turbo Debugger нажимайте F7 («Trace into»), чтобы перейти к коду вызываемой процедуры. При нажатии F8(«Step over») процедура будет выполнена сразу целиком.

Упражнение

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

Источник

Оцените статью