Данному образовательному сайту пришлось несколько раз менять свое имя. С 2011 года доступ к нему обеспечивается по URL
http://educomp.runnet.ru

emc.km.ru (2001-2007) ==> educomp.org.ru (2007-2011) ==> educomp.runnet.ru (2011-...)
Более подробно об истории сайта можно прочитать здесь.


Учебные модели компьютера



Модели (software):

"Е14" (parallel !!!)

Модели (hardware):






Награды сайта
Награды сайта

Об образовательных возможностях Debug

(переход к другим статьям из этой серии)

2. Что такое ассемблер?

Если кто-то считает программирование на ассемблере «прямым» общением с компьютером, то он ошиба-
ется. Ассемблер – это язык высокого уровня (пускай и самый низкоуровневый среди высокоуровневых языков).
Крис Касперски

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

Бытует мнение, что любая деятельность с машинными кодами есть работа на ассемблере. На самом деле это не так, и ассемблер есть некоторый более высокий уровень общения с процессором. Рассмотрим данную проблему подробнее.

2.1. Двоичный код – основа функционирования компьютера

В любом уважающем себя учебнике информатики сказано, что вся информация хранится и обрабатывается в компьютере в двоичной системе. Сама программа обработки также представляет собой не что иное, как двоичный код. В настоящее время, когда пользователь отделен от компьютерного «железа» несколькими слоями программного обеспечения, данный факт не очевиден: вы вводите в электронную таблицу десятичное число, и в соседней ячейке ответ выводится также в десятичном виде. Так что тезис о том, что внутри компьютера вычисления были произведены в двоичном коде, во многом приходится принимать на веру [1].

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

Итак, компьютер способен непосредственно обрабатывать только двоичную информацию. Принять бинарную информацию он может из устройств внешней памяти, а также от других компьютеров через посредство сети. Хотя устройства внешней памяти весьма разнообразны, их объединяет именно то, что они хранят любые данные в двоичном виде, т.е. в виде, пригодном для наиболее быстрого и оперативного использования. Что же касается человека, то он, разумеется, нуждается в специальных устройствах, которые преобразуют его информацию во внутреннюю компьютерную форму и обратно. Для ввода бинарных машинных кодов человек легко может использовать клавиатуру. Важно понимать, что прием данных с клавиатуры (даже если предположить, что некий оригинал вводит чисто двоичный код!) всегда происходит по определенной программе, которая преобразует поток набираемых символов в двоичные числа и отправляет последние в необходимое место ОЗУ. Замена громоздких двоичных чисел более компактными восьмеричными или даже шестнадцатеричными числами [2] дела не меняет, лишь несколько усложняя программу кодирования.

Мы видим, что простейшим способом организовать ввод цифровых кодов в память является несложное преобразование последовательности символов, соответствующих допустимым значениям цифр, в двоичный код. Именно такая программа, называемая монитором, имелась в ПЗУ отечественных «ДВК-образных» машин (семейство ДВК, БК, УКНЦ). Монитор позволял вводить коды в виде восьмеричных цифр и записывать их в набранный таким же образом адрес ОЗУ. Еще он умел запустить машинную программу и перехватить ее завершение, а также выполнял некоторые другие действия. Если внимательно подумать, то по своей сути такой монитор являлся непосредственным предшественником отладчиков типа Debug. Главной особенностью последнего по сравнению с монитором является возможность символьного представления машинных инструкций. Таков первый шаг от машинных кодов к ассемблеру.

2.2. Ассемблер: символьная запись команд

Цифровой ввод, легко распознаваемый машиной, довольно неудобен для человека. В целом большинству людей проще запомнить некоторое слово (даже на неродном языке, например, ADD или LOOP) чем комбинации цифр, им соответствующие. Аналогичным образом часто отказываются от обращения ко внутренним регистрам микропроцессора по номерам, заменяя их буквенными обозначениями (EAX, AX, AH, AL, BX, IP и т.д.) Подобная замена особенно уместна, если регистры микропроцессора не являются универсальными и имеют выделенное назначение (например, A – аккумулятор). Заметим, что неуниверсальность рабочих регистров и их жестким образом фиксированное использование, что свойственно семейству процессоров Intel, вовсе не единственный возможный вариант. Скажем, в семействе PDP логическая структура процессоров была более стройной: все регистры общего назначения РОН могли использоваться в машинных инструкциях на равных основаниях; в такой ситуации давать регистрам имена было менее удобно, чем просто пронумеровать.

Описанные символьные обозначения стали называться мнемониками (мнемонический значит облегчающий запоминание). Вот несколько мнемоник для записи машинных инструкций процессора Intel.

Таблица 1
hex-код мнемоника комментарий
B81200 MOV AX,12 занести число 12 в регистр AX
01D8 ADD AX,BX сложить регистры AX и BX
8B1E1001 MOV BX,[110] прочитать в BX ячейку ОЗУ с адресом 110

Примечание. В таблице жирным шрифтом выделен код, описывающий операцию и способы обращения к данным; обычным шрифтом набраны непосредственные данные и адреса ячеек ОЗУ. Обратите внимание, что использованные в командах двухбайтовые числа хранятся «задом наперед»: число 0110 лежит в памяти не как 01 10, а, наоборот, 10 01. Подобный способ хранения, принятый в IBM PC, носит название обратный порядок хранения байтов.

Думается, читатели согласятся, что мнемонический способ записи команд легче понимается и запоминается. Именно поэтому при работе с Debug обычно пользуются именно им. Мнемоническое представление команд является неотъемлемой частью ассемблера, но отнюдь не главной его частью. Наибольшее достоинство языка ассемблер состоит в возможности заменять символическими именами не только операции и регистры, но и конкретные адреса памяти. В последнем случае программист получает возможность освободиться от «привязки» к конкретным адресам памяти, что, в свою очередь, позволяет элементарно, как в обычном текстовом редакторе, удалять или дополнять команды программы. Такая замечательная возможность заслуживает более подробного обсуждения.

2.3. Ассемблер: идентификаторы и директивы

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

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

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

2.3.1. Задача, подводящая к идентификаторам

Пусть мы хотим решить на компьютере простейшую задачу, которая состоит вычислении по формуле r = x + y. Напишем и реализуем программу решения, а затем посмотрим, что потребуется исправлять, если мы захотим добавить к нашей формуле еще одну операцию, например: r = x + y – z.

Договоримся, что все переменные являются двухбайтовыми целыми. Примем, что область хранения переменных будет находиться «в самом начале» – начиная с адреса 102. Поскольку по принятым в MS-DOS соглашениям программа стартует, начиная с адреса 100, придется предусмотреть «обход» области данных.

Таблица 2
адреса содержимое комментарии
100,101 jmp 108 на начало программы (обход данных)
102,103 3 x
104,105 7 y
106,107 0 r

Примечание. Размещением данных указанным способом имеет свои достоинства и недостатки. С одной стороны, требуется обход области данных, возникают трудности при дизассемблировании (см. ниже), зато с другой – данные сохраняются на диск вместе с программой единым массивом и адреса информации в момент написания программы уже определены. При работе с Debug последнее немаловажно.

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

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

-a
13E4:0100 jmp 108
13E4:0102 dw 3
13E4:0104 dw 7
13E4:0106 dw 0
13E4:0108 mov ax,[102]
13E4:010B add ax,[104]
13E4:010F mov [106],ax
13E4:0112 int 20
13E4:0114
-u
13E4:0100 EB06          JMP     0108
13E4:0102 0300          ADD     AX,[BX+SI]
13E4:0104 07            POP     ES
13E4:0105 0000          ADD     [BX+SI],AL
13E4:0107 00A10201      ADD     [BX+DI+0102],AH
13E4:010B 03060401      ADD     AX,[0104]
13E4:010F A30601        MOV     [0106],AX
13E4:0112 CD20          INT     20
...
-u 108
13E4:0108 A10201        MOV     AX,[0102]
13E4:010B 03060401      ADD     AX,[0104]
13E4:010F A30601        MOV     [0106],AX
13E4:0112 CD20          INT     20
...
-g
Программа завершилась нормально
-d 102
13E4:0100        03 00 07 00 0A 00-A1 02 01 03 06 04 01 A3     ..............
13E4:0110  06 01 CD 20 00 00 00 00-00 00 00 00 34 00 D3 13   ... ........4...
...
13E4:0180  00 00

Теперь проконтролируем набор директивой u. Из протокола видно, что наличие данных посреди программы «сбивает» дизассемблер отладчика: пытаясь интерпретировать значения переменных как машинные команды, Debug «не попадает» на адрес 108. В результате первая команда программы выглядит неверно. Проверка директивой u 108 подтверждает, что в памяти все сохранено правильно.

Примечание. Из последней распечатки запомним тот факт, что программа начинается с адреса 108 и завершается байтом 113. Эти сведения нам потребуются позднее при переделке программы.

Остается запустить нашу программу и посмотреть ответ, для чего вывести содержимое памяти командой d 102. При расшифровке шестнадцатеричных чисел 3, 7, A (1010) следует обязательно вспомнить о примечании к таблице 1 по поводу обратного хранения байтов в памяти. Для удобства анализа протокола результирующие байты переменной r в протоколе подчеркнуты.

Переходим теперь к наиболее интересной части нашего эксперимента. Что придется поменять, чтобы переделать нашу программу для расчета по исправленной формуле r = x + y – z. Увы, придется проделать довольно много операций, хотя каждая из них сама по себе несложная.

Начнем с расширения таблицы переменных. Новое распределение памяти приведено в табл. 3 (советуем читателям внимательно сравнить ее с предыдущей таблицей; все изменения в табл. 3 выделены цветом и подчеркнуты).

Таблица 3
адреса содержимое комментарии
100,101 jmp 10A на начало программы (обход данных)
102,103 3 x
104,105 7 y
106,107 4 z
108,109 0 r

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

-m 108 113 10a
-u 10a
13E4:010A A10201        MOV     AX,[0102]
13E4:010D 03060401      ADD     AX,[0104]
13E4:0111 A30601        MOV     [0106],AX
13E4:0114 CD20          INT     20
...
-a 111
13E4:0111 sub ax,[106]
13E4:0115 mov [108],ax
13E4:0118 int 20
13E4:011A
-a 100
13E4:0100 jmp 10a
13E4:0102
-e 106 4 0
-u 100
13E4:0100 EB08          JMP     010A
13E4:0102 0300          ADD     AX,[BX+SI]
13E4:0104 07            POP     ES
13E4:0105 0004          ADD     [SI],AL
13E4:0107 00A102A1      ADD     [BX+DI+A102],AH
13E4:010B 0201          ADD     AL,[BX+DI]
13E4:010D 03060401      ADD     AX,[0104]
13E4:0111 2B060601      SUB     AX,[0106]
13E4:0115 A30801        MOV     [0108],AX
13E4:0118 CD20          INT     20
...
-g
Программа завершилась нормально
-d 102
13E4:0100        03 00 07 00 04 00-06 00 A1 02 01 03 06 04     ..............
13E4:0110  01 2B 06 06 01 A3 08 01-CD 20 00 00 34 00 D3 13   .+....... ..4...
...
13E4:0180  00 00                                             ..

Передвинем основную часть программы, чтобы освободить 2 байта под новую переменную. Для этого наберем команду m 108 113 10a, которая означает задание отладчику подвинуть (move) байты со 108 по 113 (эти значения мы запомнили из предыдущих экспериментов с программой). Проверим, что программа действительно теперь находится с адреса 10a. Далее можно было бы аналогичным образом освободить байты под команду вычитания и ввести ее, но проще набрать 3 последние команды заново, тем более, что в команде записи результата в переменную r все равно потребовалось бы изменить адрес. Так что введем эти команды с адреса 111, а затем не забудем поменять адрес в стартовой инструкции перехода. Далее командой e 106 4 0 занесем в переменную z ее числовое значение (опять-таки в обратном порядке!) и все еще раз проверим. Наконец, запустим программу и убедимся в правильности получившегося результата: 3 + 7 – 4 = 6.

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

Текст нашей программы на ассемблере будет выглядеть так.

       jmp start
x:     dw 3
y:     dw 7
z:     dw 4
r:     dw 0
start: mov ax,x
       add ax,y
       sub ax,z
       mov r,ax
       int 20

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

2.3.2. Директивы ассемблера

Язык ассемблер содержит целый ряд специальных управляющих операторов (обычно их называют директивами), из которых Debug поддерживает только некоторые. В частности, команда dw 3, определяющая в памяти некоторую константу, суть одна из таких поддерживаемых отладчиком директив. В то же время, ассемблер содержит целый ряд директив, которые отсутствуют в Debug [2]. Между прочим, в свете наличия дополнительных директив, некоторые из которых в ассемблерной программе приходится писать обязательно, программа для Debug вводится проще. Зато ассемблер засчет добавочных директив становится мощнее; например, отдельные участки ассемблерной программы можно помещать внутрь условий, так что в зависимости от их выполнения или невыполнения будет генерироваться различный код.

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

Отметим, что хорошее понятие об ассемблере дают школьные учебники [3-6], где описываются учебные ЭВМ «Малютка» и «Нейман» (в книгах [4,6] изложение проведено более подробно). Особенно полезна практическая работа с программными реализациями указанных моделей, в которых имеется поддержка ассемблера.

2.4. Конструируем ассемблер сами

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

Разумеется, мы возьмем максимально простую систему команд, которая предложена в учебнике [7]; она достаточно известна и называется «Кроха». Полный перечень команд «Крохи» приведен в табл. 4, где к каждой операции также добавлена ее мнемоника (в исходном учебнике не использовалась). Мнемонические имена команд выбраны вполне стандартными и совпадают с принятыми в ассемблере IBM PC.

Таблица 4
Операция Код Мнемоника Комментарии
2 с/с 8 с/с
перепись 000 0 MOV A3 = A1
сложение 001 1 ADD A3 = A1 + A2
деление нацело 010 2 DIV A3 = A1 div A2
модуль разности 011 3 SUB A3 = |A1 - A2|
переход по = 100 4 JE при A1 = A2 – перейти к A3
умножение 101 5 MUL A3 = A1 * A2
переход по > 110 6 JG при A1 > A2 – перейти к A3
стоп и вывод 111 7 HLT A1, A2, A3 на дисплей; стоп

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

  • Каждая команда занимает отдельную строку, пустые строки запрещены. Согласно устройству «Крохи», любая команда занимает ровно одну ячейку, значит, номер ячейки фактически совпадет с номером строки.
  • Все метки (идентификаторы) условимся обозначать одной латинской буквой, после которой ставится двоеточие. Хотя это и несложно, проверять каждую новую метку на совпадение со старыми не будем; факт того, что это именно латинская буква, для простоты также не проверяется.
  • Части команды (метка, операция и 3 адреса) разделены между собой произвольным количеством пробелов (M:     ADD   X Y R).
  • Константы (десятичные числа), заносимые ассемблером в ячейки, будем записывать после обозначения DN, например, DN 19. Правильность значения константы (для «Крохи» она должна быть целой и неотрицательной, не превышающей 4095) контролировать не будем.
  • Так как практическая эксплуатация нашего программного продукта не предполагается, диагностику ошибок в нем предусматривать не будем (известно, что это часто наиболее трудоемкая часть программы!)
  • Вместо длинных 12-разрядных двоичных кодов, которые вполне оправданы в учебнике для демонстрации принципов Неймана, везде будем пользоваться более короткими восьмеричными кодами; подчеркнем, что данная система прекрасно гармонирует с командами «Крохи», у которых и код операции, и адреса состоят именно из трех двоичных разрядов.

Благодаря принятым упрощениям ассемблирующая программа на Паскале получается совсем небольшой (ниже приводится ее листинг).

PROGRAM kroha_asm(INPUT,OUTPUT); {Демо-ассемблер для учебной ЭВМ «Кроха»}

CONST kop:ARRAY [0..7] OF STRING[3]= {мнемоники команд}
          ('MOV','ADD','DIV','SUB','JE','MUL','JG','HLT')
      prg:ARRAY [0..7] OF STRING=    {ассемблируемая программа!!!}
          ('B: MUL F K F',
           '   ADD K E K',
           '   JG  N K B',
           '   HLT F F F',
           'K: DN  1',
           'F: DN  1',
           'N: DN  3',
           'E: DN  1');
VAR   tab: ARRAY [0..15] OF RECORD sym:STRING[3]; num:BYTE END;
      {tab – это таблица всех идентификаторов (их имя и код), включая операции и метки}
      i,Nid,k,e,c:INTEGER; {рабочие переменные}

FUNCTION get_code(i:INTEGER; VAR k:INTEGER):BYTE;
{выделяет из строки с номером i начиная с позиции k очередной идентификатор
 и находит в tab его код}
VAR p,id:STRING; j,m,q:INTEGER; {рабочие переменные}
BEGIN p:=prg[i]+' '; {добавим пробел для удобства выделения последней метки}
      WHILE (k<LENGTH(p))AND(p[k]=' ') DO k:=k+1; {пропустим пробелы}
      {а теперь выберем все до следующего пробела, т.е. получим имя идентификатора}
      id:=''; WHILE p[k]<>' ' DO BEGIN id:=id+p[k]; k:=k+1 END;
      q:=255; {найдем, если идентификатор есть, в таблице и поместим его код в q}
      FOR j:=0 TO Nid DO IF id=tab[j].sym THEN q:=tab[j].num;
      get_code:=q {результатом функции является код, заносимый в программу}
END

BEGIN FOR i:=0 TO 7 DO {занесем в tab коды операций}
          BEGIN tab[i].sym:=kop[i];tab[i].num:=i
          END;
      tab[8].sym:='DN';tab[8].num:=8; Nid:=8; {добавим условный код DN}
      FOR i:=0 TO 7 DO {проход I – занесение в таблицу меток программиста}
          IF prg[i][2]=':' THEN {после метки двоеточие}
             BEGIN inc(Nid); {добавляем метку в таблицу; ее адрес=номеру строки!}
                   tab[Nid].sym:=prg[i][1];tab[Nid].num:=i;
                   prg[i][1]:=' '; prg[i][2]:=' '; {удалим метку из текста}
             END;
      FOR i:=0 TO Nid DO {для контроля выведем tab}
          WRITELN(tab[i].sym:4, tab[i].num:4);
      FOR i:=0 TO 7 DO {проход II – «генерация» кода на экран}
          BEGIN k:=1; c:=get_code(i,k); {код операции}
                CASE c OF {выделяем команды или DN}
                0..7: BEGIN WRITE(c); {<код операции}
                            WHILE k<=LENGTH(prg[i]) DO {3 адреса}
                                  WRITE(' ',get_code(i,k))
                      END;
                8:{DN}BEGIN {выделить 10-число и перевесит в 8 с/с}
                            VAL(COPY(prg[i],k,255),k,e);
                            WRITE('   '); e:=512;
                            REPEAT WRITE(k DIV e);
                                   k:=k MOD e; e:=e DIV 8;
                            UNTIL e=0
                      END
                END;
                WRITELN {конец вывода команды}
          END;
END

Программа работает следующим образом. Ядром ее данных служит таблица идентификаторов, которая содержит их имена в поле sym и коды, которые вместо этих имен надо подставлять в программу (поле num). Например, операция ADD имеет код 1 (см. табл. 4), поэтому tab[1].sym=’ADD’ и tab[1].num=1. Становится очевидным, что центральная идея ассемблера состоит в замене, пользуясь таблицей, каждого символьного имени соответствующим ему числовым кодом.

После занесения в строки таблицы с номерами от 0 до 7 мнемоник команд «Крохи», добавим далее оператор определения константы DN и все найденные в программе метки, которые поставил пользователь в тексте. Метки пользователя распознаются по наличию после них двоеточия; чтобы обработанные метки не мешали ассемблеру в дальнейшем, программа их просто стирает. Обработка меток в литературе носит название первого прохода, поскольку сначала надо «пройти» всю программу и подготовить полную таблицу идентификаторов, и только потом повторно просмотреть текст (второй проход), заменяя с помощью таблицы все идентификаторы их кодами.

При выполнении второго прохода наш ассемблер опирается на «мощную» функцию get_code, которая выделяет из текста программы очередной идентификатор. Несмотря на сложный вид, идея функции достаточно проста: в заданной строке, начиная с заданной позиции, она пропускает пробелы, а затем, наоборот, «собирает» все отличающиеся от пробела символы, формируя тем самым имя очередного идентификатора. Когда имя выделено, оно ищется в таблице и в качестве результата выдается его код (или 255, если имя в таблице отсутствует).

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

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

Заметим, что итоговый восьмеричный код программы просто выводится на экран дисплея и нигде не сохраняется.

Для тестирования демо-ассемблера реализована традиционная задача – вычисление факториала числа n. Обозначим k рабочую переменную, которая является текущим множителем для факториала и меняется от 1 до n. Тогда итоговая программа приобретает вид, приведенный в табл. 5.

Таблица 5
Адрес Команда Расшифровка Комментарий
0 5545 (5) * (4) ==> (5) n! * k ==> n!
1 1474 (4) + (7) ==> (4) k + 1 ==> k
2 6640 если (6)>(4), перейти к 0 переход при k < n+1
3 7555 стоп; вывод (5), (5), (5) вывод n!
4 k [задать 1]   рабочая ячейка
5 n! [задать 1]   результат
6 n + 1 [задать]   константа
7 0001 1 константа 1

Второй столбец данной таблицы может быть использован для проверки результатов ассемблирования, которые наша программа выводит на экран. Более подробно программа вычисления факториала для «Крохи» рассматривалась в недавней публикации [8].

В качестве продолжения нашего эксперимента предлагаем читателям самостоятельно написать обратную программу – дизассемблер, которая по восьмеричному 4-разрядному коду восстанавливает ассемблерную мнемонику команд. Для решения задачи советуем воспользоваться таблицей, аналогичной tab, если потребуется, разделив ее на части. Разумеется, первоначальные имена меток по коду не восстановить, поэтому можно присваивать им по мере появления последовательные значения букв латинского алфавита.

А теперь подведем итоги того, что мы узнали.

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

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

Примечание. В последнее время появляются и более причудливые схемы трансляции с языков высокого уровня. Например, в языке Ява компилятор транслирует текст программы в код некоторой виртуальной машины; тем самым, итоговый результат компиляции также перестает быть машинно-зависимым. Вся «привязка» к конкретному компьютеру сосредоточена теперь в реализации Ява-машины, которая разрабатывается один раз и способна исполнить на данной платформе любую Ява-программу вне зависимости от того, на какой машине она была откомпилирована. Похожие идеи заложены и в идеологию платформы .NET (читается «дот-нэт» или «точка-нэт») фирмы Microsoft: компилятор с любого языка программирования генерирует код для единой виртуальной машины. Таким образом, делаются попытки сделать машинно-независимым не только исходный текст программы на языке высокого уровня, но и результат его компиляции.

Литература

  1. Еремин Е.А. Популярные лекции об устройстве компьютера. СПб.: BHV-Петербург, 2003, 272 с.
  2. Абель П. Язык Ассемблера для IBM PC и программирования. М.: Высш. шк., 1992, 447 с.
  3. Гейн А.Г., Сенокосов А.И., Шолохович В.Ф. Информатика. М.: Дрофа, 1998, 240 с.
  4. Сенокосов А.И., Гейн А.Г. Информатика. М.: Просвещение, 1995, 255 с.
  5. Информатика. Базовый курс. 7-9 классы / И.Г. Семакин, А.Л. Залогова, С.В. Русаков, Л.В. Шестакова. М.: БИНОМ, 2004, 390 с.
  6. Семакин И.Г. Информатика. Беседы об информации, компьютерах и программах. Ч. 2. Пермь: Изд-во Перм. ун-та, 1997, 168 с.
  7. Информатика / А.Г. Гейн, Е.В. Линецкий, М.В. Сапир, В.М. Шолохович. М.: Просвещение, 1994, 256 с.
  8. Еремин Е.А. Моделирование работы ЭВМ с помощью программы Microsoft Excel. Информатика, 2006, N 21, с.37-40; N 22, с.42-45.

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

2 - «провокационный» вопрос, в каком виде (двоичном или восьмеричном) данные действительно хранятся в памяти, не должен вызывать никаких колебаний – разумеется, в двоичном!


© Е.А.Еремин, 2007
Публикация:
Еремин Е.А. Debug и язык ассемблер. "Информатика", 2007, N 5, с.37-39; N 6, с.37-38; N 7, с.42-44.

(к другим статьям из этой серии)


Автор сайта - Евгений Александрович Еремин (Пермский государственный педагогический университет). e_eremin@yahoo.com