Компьютер Little Man ( LMC ) — это учебная модель компьютера , созданная доктором Стюартом Мэдником в 1965 году. [1] LMC обычно используется для обучения студентов, поскольку он моделирует простой компьютер с архитектурой фон Неймана , который имеет все основные возможности современного компьютера. Его можно запрограммировать в машинном коде (хотя и в десятичном, а не в двоичном формате) или ассемблерном коде. [2] [3] [4]
Модель LMC основана на концепции маленького человечка, запертого в закрытой почтовой комнате (аналог компьютера в этом сценарии). В одном конце комнаты есть 100 почтовых ящиков ( память ), пронумерованных от 0 до 99, каждый из которых может содержать трехзначную инструкцию или данные (в диапазоне от 000 до 999). Кроме того, на другом конце есть два почтовых ящика с надписью INBOX и OUTBOX , которые используются для приема и вывода данных. В центре комнаты находится рабочая зона, содержащая простой калькулятор с двумя функциями (сложение и вычитание), известный как аккумулятор , и сбрасываемый счетчик, известный как программный счетчик. Счетчик программ хранит адрес следующей инструкции, которую Маленький Человечек выполнит. Этот счетчик программ обычно увеличивается на 1 после выполнения каждой инструкции, что позволяет маленькому человечку последовательно выполнять программу. Инструкции ветвления позволяют включать в программу итерации (циклы) и структуры условного программирования. Последнее достигается путем установки счетчика программ на непоследовательный адрес памяти, если выполняется определенное условие (обычно значение, хранящееся в аккумуляторе, равно нулю или положительному).
Согласно архитектуре фон Неймана , любой почтовый ящик (обозначающий уникальную ячейку памяти) может содержать либо инструкцию, либо данные. Поэтому необходимо позаботиться о том, чтобы счетчик программ не достиг адреса памяти, содержащего данные, иначе Маленький Человечек попытается воспринять это как инструкцию. Этим можно воспользоваться, записав в почтовые ящики инструкции, предназначенные для интерпретации как кода, для создания самомодифицирующегося кода. Чтобы использовать LMC, пользователь загружает данные в почтовые ящики, а затем дает сигнал «Маленькому человечку» начать выполнение, начиная с инструкции, хранящейся по нулевому адресу памяти. Сброс счетчика программ на ноль фактически перезапускает программу, хотя и в потенциально другом состоянии.
Чтобы выполнить программу, человечек выполняет следующие действия:
Хотя LMC действительно отражает фактическую работу двоичных процессоров, простота десятичных чисел была выбрана, чтобы минимизировать сложность для студентов, которым может быть неудобно работать в двоичных/ шестнадцатеричных числах .
Некоторые симуляторы LMC программируются напрямую с использованием трехзначных числовых инструкций, а некоторые используют трехбуквенные мнемонические коды и метки. В любом случае набор инструкций намеренно очень ограничен (обычно около десяти инструкций), чтобы упростить понимание. Если LMC использует мнемонические коды и метки, то при сборке программы они преобразуются в трехзначные числовые инструкции.
В таблице ниже показан типичный набор числовых команд и эквивалентные мнемонические коды.
Эта программа (инструкции с 901 по 000 ) написана только с использованием числовых кодов. Программа принимает на вход два числа и выводит разницу. Обратите внимание, что выполнение начинается в почтовом ящике 00 и заканчивается в почтовом ящике 07. Недостатки программирования LMC с использованием числовых кодов команд обсуждаются ниже.
Язык ассемблера — это язык программирования низкого уровня, в котором вместо числовых кодов команд используются мнемоники и метки. Хотя LMC использует лишь ограниченный набор мнемокодов, удобство использования мнемоники для каждой инструкции становится очевидным из ассемблера той же программы, показанного ниже: программисту больше не требуется запоминать набор анонимных числовых кодов, и он может теперь программа с набором более запоминающихся мнемонических кодов. Если мнемоника представляет собой инструкцию, которая включает адрес памяти ( либо инструкцию ветвления, либо загрузку/сохранение данных ), то для обозначения адреса памяти используется метка.
ИЯФСТА ПЕРВЫЙИЯФСТА ВТОРАЯЛДА ПЕРВЫЙПОД ВТОРОЙВНЕHLTПЕРВЫЙ ДАТВТОРОЙ ДАТ
Без меток программисту придется вручную рассчитывать адреса почтовых ящиков ( памяти ). В примере с числовым кодом , если новая инструкция должна быть вставлена перед последней инструкцией HLT, то эта инструкция HLT переместится с адреса 07 на адрес 08 (маркировка адреса начинается с адреса 00). Предположим, что пользователь ввел 600 в качестве первого ввода. Инструкция 308 будет означать, что это значение будет сохранено по адресу 08 и перезапишет инструкцию 000 (HLT). Поскольку 600 означает «перейти к почтовому ящику с адресом 00», программа вместо остановки застрянет в бесконечном цикле.
Чтобы обойти эту трудность, большинство языков ассемблера ( включая LMC ) комбинируют мнемонику с метками . Метка — это просто слово, которое используется либо для обозначения адреса памяти, где хранится инструкция или данные, либо для ссылки на этот адрес в инструкции.
Когда программа собрана:
В примере на ассемблере , где используются мнемоники и метки, если новая инструкция была вставлена перед последней инструкцией HLT, то адрес, помеченный как FIRST, теперь будет находиться в ячейке памяти 09, а не в 08, и инструкция STA FIRST будет преобразована в 309 (STA 09), а не 308 (STA 08) при сборке программы.
Таким образом, этикетки используются для:
Программа, представленная ниже, примет вводимые пользователем данные и начнет обратный отсчет до нуля.
ИЯФ OUT // Инициализация выводаLOOP BRZ QUIT // Пометить этот адрес памяти как LOOP. Если значение аккумулятора равно 0, перейдите к адресу памяти, обозначенному // ПОКИДАТЬ SUB ONE // Вычитаем из аккумулятора значение, хранящееся по адресу ONE. ВНЕ BRA LOOP // Переход (безусловный) к адресу памяти с меткой LOOPQUIT HLT // Пометить этот адрес памяти как QUITONE DAT 1 // Сохраните значение 1 по этому адресу памяти и пометьте его ONE (объявление переменной)
Программа ниже примет введенные пользователем данные, возведет их в квадрат, выведет ответ и затем повторит. Ввод нуля завершит программу.
( Примечание: ввод, результатом которого является значение больше 999, будет иметь неопределенное поведение из-за ограничения LMC на 3-значное число ).
START LDA ZERO // Инициализация для запуска нескольких программ РЕЗУЛЬТАТ СТА СЧЕТ СТА INP // Введенные пользователем данные BRZ END // Переход к END программы, если вход = 0 STA VALUE // Сохраняем введенные данные как VALUELOOP LDA RESULT // Загрузка РЕЗУЛЬТАТА ДОБАВИТЬ ЗНАЧЕНИЕ // Добавить ЗНАЧЕНИЕ, введенное пользователем, в РЕЗУЛЬТАТ STA RESULT // Сохраняем новый РЕЗУЛЬТАТ LDA COUNT // Загрузите COUNT ДОБАВИТЬ ОДИН // Добавить ОДИН к COUNT STA COUNT // Сохраняем новый COUNT SUB VALUE // Вычитаем введенное пользователем значение VALUE из COUNT BRZ ENDLOOP // Если ноль (VALUE было добавлено к RESULT VALUE раз), переход к ENDLOOP BRA LOOP // Переход к LOOP, чтобы продолжить добавление VALUE к RESULTENDLOOP LDA RESULT // Загрузка РЕЗУЛЬТАТА ВЫХОД // Вывод РЕЗУЛЬТАТА BRA START // Переход к START для инициализации и получения другого входного значения.END HLT // HALT – введен ноль – готово!RESULT DAT // Вычисленный результат (по умолчанию 0)COUNT DAT // Счетчик (по умолчанию 0)ONE DAT 1 // Константа, значение 1VALUE DAT // Введенные пользователем данные, значение для возведения в квадрат (по умолчанию 0)ZERO DAT // Константа, значение 0 (по умолчанию 0)
Примечание. Если после оператора DAT нет данных, то по адресу памяти сохраняется значение по умолчанию 0.
В приведенном выше примере [BRZ ENDLOOP] зависит от неопределенного поведения, поскольку COUNT-VALUE может быть отрицательным, после чего значение ACCUMULATOR становится неопределенным, в результате чего BRZ либо разветвляется, либо нет (ACCUMULATOR может быть нулевым или округленным). Чтобы сделать код совместимым со спецификацией, замените:
... LDA COUNT // Загрузите COUNT ДОБАВИТЬ ОДИН // Добавить ОДИН к COUNT STA COUNT // Сохраняем новый COUNT SUB VALUE // Вычитаем введенное пользователем значение VALUE из COUNT BRZ ENDLOOP // Если ноль (VALUE было добавлено к RESULT VALUE раз), переход к ENDLOOP ...
со следующей версией, которая оценивает VALUE-COUNT вместо COUNT-VALUE, гарантируя, что аккумулятор никогда не опустеет:
... LDA COUNT // Загрузите COUNT ДОБАВИТЬ ОДИН // Добавить ОДИН к COUNT STA COUNT // Сохраняем новый COUNT LDA VALUE // Загрузите ЗНАЧЕНИЕ SUB COUNT // Вычитаем COUNT из введенного пользователем значения VALUE BRZ ENDLOOP // Если ноль (VALUE было добавлено к RESULT VALUE раз), переход к ENDLOOP ...
Другой пример — quine , печатающий собственный машинный код (вывод исходного кода невозможен, поскольку буквы не могут быть выведены):
ЗАГРУЗИТЬ LDA 0 // Загрузить позицию 0 в аккумулятор. Эта строка будет изменяться в каждом цикле для загрузки следующих строк в аккумулятор. OUT // Вывод значения аккумулятора. Значением аккумулятора будет только что загруженная строка. SUB ONE // Вычитаем 1 из значения аккумулятора. Это сделано для того, чтобы мы могли выполнить BRZ на следующем этапе, чтобы увидеть, находимся ли мы на последней строке программы. BRZ ONE // Если предыдущее вычитание сделало аккумулятор равным 0 (что означает, что в аккумуляторе было значение 001), то переходим к позиции ONE LDA LOAD // Загрузка позиции ЗАГРУЗКИ в аккумулятор, это подготовка к увеличению цифр адреса для этой позиции ДОБАВИТЬ ОДИН // Увеличить цифры позиции в строке ЗАГРУЗИТЬ. Значение, находящееся в настоящее время в аккумуляторе, если читать его как инструкцию, загрузит в аккумулятор следующую строку по сравнению с последней загруженной строкой. STA LOAD // Сохраняем вновь увеличенную строку LOAD обратно в позицию LOAD ЗАГРУЗКА БРЮКА // Возврат в начало циклаONE DAT 1 // Переменная ONE. Если читать как инструкцию, это будет интерпретировано как HLT/COB и завершит программу.
Этот куайн работает с использованием самомодифицирующегося кода . Позиция 0 увеличивается на единицу в каждой итерации, выводя код этой строки, пока код, который она выводит, не станет равным 1, после чего он переходит в позицию ONE. Значение в позиции ONE имеет 0 в качестве кода операции, поэтому оно интерпретируется как инструкция HALT/COB.