Как я могу написать интерпретатор для 'eq' для языка Hack Assembly?

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

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

Обозначение сборки можно найти здесь.

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

Пример, который я сделал успешно, - это байтовый код

push constant 5

который переводится на:

@5
D=A
@256
M=D

Как я уже сказал, ассемблер для Hack можно найти в предоставленной мной ссылке, но в основном:

@5  // Load constant 5 to Register A
D=A // Assign the value in Reg A to Reg D
@256// Load constant 256 to Register A
M=D // Store the value found in Register D to Memory Location[A]

Что ж, это было довольно просто. По определению ячейка памяти 256 является вершиной стека. Так

push constant 5
push constant 98

будет переведен на:

@5
D=A
@256
M=D
@98
D=A
@257
M=D

что все в порядке ..

Еще хочу привести еще один пример:

push constant 5
push constant 98
add

переводится на:

@5
D=A
@256
M=D
@98
D=A
@257
M=D
@257  // Here starts the translation for 'add' // Load top of stack to A
D=M   // D = M[A] 
@256  // Load top of stack to A 
A=M   // A = M[A]
D=D+A
@256
M=D

Я думаю, это довольно ясно.

Однако я понятия не имею, как я могу перевести байтовый код

eq

к сборке. Определение для эквалайзера выглядит следующим образом:

Три команды (eq, gt, lt) возвращают логические значения. VM представляет истину и ложь как ????-1 (минус один, 0xFFFF) и 0 (ноль, 0x0000) соответственно.

Поэтому мне нужно вставить два значения в регистры A и D соответственно, что довольно просто. Но как мне создать ассемблерный код, который будет проверять значения и нажимать 1, если результат истинный, или 0, если результат ложный?

Ассемблерный код, поддерживаемый для Hack Computer, выглядит следующим образом:

введите описание изображения здесь введите описание изображения здесь  введите описание изображения здесь

Я могу сделать что-то вроде:

push constant 5
push constant 6
sub

который будет содержать значение 0, если 2 значения, помещенные в стек, равны или! 0, если нет, но как это поможет? Я пробовал использовать D&A или D&M, но это тоже мало помогло ..

Я также могу ввести условный переход, но как мне узнать, к какой инструкции перейти? В коде сборки взлома нет чего-то вроде «пропустить следующие 5 инструкций» и т. Д.

[edit by Spektre] сводка целевой платформы, как я ее вижу

  • 16-битная архитектура фон Неймана (адрес 15 бит с доступом к тексту 16 бит)
  • Память данных 32кВт (чтение / запись)
  • Память инструкций (программ) 32кВт (только чтение)
  • собственные 16-битные регистры A, D
  • 16-битные регистры общего назначения R0-R15, отображаемые в память данных по адресу 0x0000 - 0x000F
  • они, скорее всего, также используются для: SP(R0),LCL(R1),ARG(R2),This(R3),That(R4)
  • Экран отображается в памяти данных по адресу 0x4000-0x5FFF (512x256 ч / б пикселей, 8 кВт)
  • Клавиатура сопоставлена ​​с памятью данных по адресу 0x6000 (код ASCII при последнем нажатии клавиши?)

введите описание изображения здесь


person Koray Tugay    schedule 10.05.2015    source источник
comment
На самом деле похоже, что у вас есть произвольные прыжки, ознакомьтесь с инструкцией jmp. Вы можете использовать это для создания двух веток, одна из которых загружает 1 в ваш целевой регистр, а другая загружает 0   -  person Niklas B.    schedule 10.05.2015
comment
@NiklasB. Благодарю за ваш ответ. Но как я узнаю, куда прыгнуть? Инструкция JMP перейдет к шагу, который загружен в регистр A. Я имею в виду, как я могу создать код сборки, который будет говорить о переходе 4 инструкции или переходе 8 инструкций?   -  person Koray Tugay    schedule 11.05.2015
comment
1. Байт-код для сборки? ... Исходный код сборки - это текстовое представление программы (мнемоника), которое компилируется в байты ... обычно двоичные файлы кода. 2. инструкции сравнения выполняются на всех архитектурах, с которыми я контактировал, путем вычитания и установки флагов Z,C, а некоторые также включают условные переходы (в основном MCU). Таким образом, cmp a,b обычно выполняет a-b, но отбрасывает результат и оставляет только флаги. А теперь войдите в состояние (неважно, в прыжке или какой-либо другой инструкции) equal=(Z), greater=(NC),lower(C)   -  person Spektre    schedule 11.05.2015
comment
3. На всех известных мне архитектурах есть 2 типа переходов: абсолютные (переходы к адресу) и относительные (добавление смещения со знаком к фактическому адресу), поэтому используйте второй, они широко используются для перемещаемых программ, потому что если вы их переместите, они не изменятся. скомпилированный код никак. Я не узнаю вашу мнемонику, но мне кажется, что вы создаете некий микрокод вместо сборки ... что именно вы делаете? эмуляция или перевод между двумя разными языками ассемблера (разные платформы)?   -  person Spektre    schedule 11.05.2015
comment
4. вы можете использовать флаги для установки возвращаемого значения ... если у вас есть такие вещи, как ADC,SBC, который +.- с Carry, а также вы можете обычно сдвигать Zero в положение Carry, чтобы использовать это также для равного ... Таким образом, нет нужен прыжок   -  person Spektre    schedule 11.05.2015
comment
@Spektre Эта архитектура предназначена только для обучающих целей и имеет только 2 регистра. Нет флажков для установки или относительных скачков.   -  person Koray Tugay    schedule 11.05.2015
comment
@Spektre Я делаю что-то вроде этого: представьте, что у вас есть Hello World, написанный на Java, который скомпилирован в байт-код JVM. Это байт-код, о котором я говорю. И у меня есть заданная архитектура. Мне нужно интерпретировать этот байт-код в сборку данной архитектуры.   -  person Koray Tugay    schedule 11.05.2015
comment
@Spektre And Assembly - это обычно не перевод 1-1. Ведь у ассемблера же будет препроцессор?   -  person Koray Tugay    schedule 11.05.2015
comment
так что это кросс-компилятор, также должно быть более двух регистров, а как насчет указателя стека SP и счетчика программ PC? регистры состояния, такие как флаги ..., а не только аккумулятор a и регистр общего назначения b   -  person Spektre    schedule 11.05.2015
comment
Интерпретатор / эмулятор / симулятор имитирует целевую платформу, у которой есть исходный / исполняемый файл на одной платформе, и преобразует ее в другую, а затем это явно кросс-компилятор. Да, компилятору нужен препроцессор, если у вас есть такие вещи, как метки, директивы equ и многое другое ... но для необработанной кросс-дизассемблирования / компиляции он вам не нужен   -  person Spektre    schedule 11.05.2015
comment
@Spektre, не могли бы вы увидеть страницу 62 здесь: nand2tetris.org/chapters/chapter%2004 .pdf? Это архитектура только для учебных целей. Невозможно получить значение Program Counter. Я имею в виду, что ученику не предоставляется реестр для ПК. Это неявно.   -  person Koray Tugay    schedule 11.05.2015
comment
если я правильно понимаю, SP находится в ОЗУ по адресу 0 и имеет длину 1 БАЙТ, поэтому стек составляет 256 БАЙТОВ и находится бог знает где (возможно, 0xFE00 ... 0xFFFF), поэтому ваша инструкция push pop неверна, вам нужно прочтите SP и используйте это как целевое расположение стека ...   -  person Spektre    schedule 11.05.2015
comment
Хорошо, найдите набор инструкций, а также объяснение сопоставлений SP, LCL, ARG, THIS, THAT, я предполагаю, что SP - это указатель стека, а ЭТО - ПК, но я просто предполагаю   -  person Spektre    schedule 11.05.2015
comment
@Spektre Спасибо за уделенное время. Итак, что вы говорите, я должен всегда помещать значение указателя стека в ОЗУ, когда я начинаю переводить виртуальный язык? Итак, если я перевожу что-то вроде push constant 5, я должен сначала установить R0 на 256, затем прочитать его оттуда и увеличить? Я отредактировал вопрос, чтобы показать, что «this» следует использовать в байтовом коде виртуальной машины, работающей поверх архитектуры.   -  person Koray Tugay    schedule 11.05.2015
comment
@KorayTugay да как то так ... push value обычно делается так: dec SP; mov [SP],value; и pop value вроде: mov value,[SP]; inc SP; `стек может расти в обоих направлениях (x86 растет вниз, но есть процессоры, которые растут). в вашей жестко закодированной реализации push / pop вы не могли бы сделать что-то вроде for (i=0;i<n;i++) push i; в случае, если n является переменной, что является довольно распространенным случаем   -  person Spektre    schedule 11.05.2015
comment
@KorayTugay добавленные вами команды доступа к памяти - это не то, что я имел в виду, SP,ARG,... - это определенные области памяти в памяти данных и имеют особое значение в вашей архитектуре ... попробуйте выяснить, что они означают   -  person Spektre    schedule 11.05.2015
comment
кстати, ваша архитектура мне кажется гибридом между процессорами MOS6502 и Atmel AT32UC3   -  person Spektre    schedule 11.05.2015
comment
Я думаю, что в этом случае вы должны знать, где находится каждая инструкция. Т.е. ассемблер должен отслеживать смещения   -  person Niklas B.    schedule 11.05.2015
comment
@NiklasB. Ассемблер уже предоставлен и не имеет такой функции.   -  person Koray Tugay    schedule 11.05.2015
comment
Извините, я имел ввиду тот компилятор, который вы пишете. Он должен отслеживать смещения генерируемых инструкций.   -  person Niklas B.    schedule 11.05.2015
comment
@NiklasB. Да, спасибо. Я тоже так думал, но это звучит неправильно ... Думаю, должен быть способ получше ...   -  person Koray Tugay    schedule 12.05.2015
comment
Если у вас нет способа определить ПК или выполнить относительный переход, вероятно, нет другого способа выполнить полное условное ветвление.   -  person Niklas B.    schedule 12.05.2015
comment
вы можете попытаться вычислить выходное значение без ответвлений как безумную логическую схему побитовый каскад, а затем закодировать свои математические функции, но это будет безумием. Если ваша платформа не имеет относительных переходов или абсолютных возможностей call / ret, вы не сможете перевести какой-либо код на свою платформу!   -  person Spektre    schedule 12.05.2015
comment
@Spektre Что ж, должен быть способ, это учебная книга.   -  person Koray Tugay    schedule 12.05.2015
comment
Мы уже рассказали вам способ, мы не виноваты, что вы не находите его достаточно элегантным. В этом случае, наверное, стоит спросить автора книги.   -  person Niklas B.    schedule 13.05.2015
comment
@NiklasB. Я не думаю, что это чья-то вина ..   -  person Koray Tugay    schedule 13.05.2015
comment
Итак, ваша таблица инструкций Hack подозрительно не включает коды операций, верхние 3 бита которых - 100, 101, 110. Вы уверены, что рассказали все доступные инструкции?   -  person Ira Baxter    schedule 13.05.2015
comment
.... ОК, инструкция просто ничего не говорит. Думаю, они не определены.   -  person Ira Baxter    schedule 13.05.2015
comment
@Spektre: Я не согласен (основываясь на непонятной интерпретации документации), что эта машина имеет отдельную память для инструкций и данных. (Если это так, будет почти невозможно закодировать косвенную выборку, и это полностью мертвый мозг). Так называемые регистры GP не являются регистрами; это просто места в памяти, которые книга назвала.   -  person Ira Baxter    schedule 13.05.2015
comment
... как мне узнать, к какой инструкции перейти? Закодируйте условную ветвь в символическую метку. В руководстве есть примеры, и я закодировал такой пример в своем ответе. (Если вы генерируете двоичный код, вы знаете, где находится инструкция ветвления; пропуск 5 инструкций означает переход к двоичному расположению ветки, +5.)   -  person Ira Baxter    schedule 13.05.2015
comment
@IraBaxter Я получил этот вывод со страницы 85 5.2 The Hack Hardware Platform Specification, где с самого начала прямо указано, что платформа имеет 2 области памяти RAM (память данных) и ROM (память инструкций). Также для архитектуры фон Неймана (см. I8051 и клоны) регистры GP и (иногда даже все регистры) отображаются в пространство памяти данных ... Итак: 1. вы не можете делать самомодифицирующийся код 2. или напрямую обращаться к регистру ПК, следовательно, это дрянная архитектура, неспособная выполнить инструкцию вызова (ret, push , pop возможны), и это приводит к невозможности ничего кодировать ...   -  person Spektre    schedule 13.05.2015
comment
@IraBaxter Только что понял, что anything - неправильное слово, вместо этого должно быть everything (в конце последней рекомендации) извините, но мой английский не очень хорош .... и в довершение всего добавьте отсутствие доступа к флагам ALU, что безумие   -  person Spektre    schedule 13.05.2015
comment
@Spektre: Ага, еще один раздел книги, я этого не заметил. ОК, никакого самомодифицирующегося кода. И, таким образом, никакой косвенной адресации ни в наборе команд, ни с помощью самомодифицирующегося кода, никаких подпрограмм, возвращающих адреса с помощью какого-либо наполовину разумного трюка. (Мой ответ был доблестным!). Для этой машины будет невероятно сложно что-либо запрограммировать; Так как же, черт возьми, автор создал свою программу PONG в Джеке? Предложите OP изучить его, чтобы увидеть, как он решает эти проблемы.   -  person Ira Baxter    schedule 13.05.2015
comment
@IraBaxter, поскольку некоторые главы еще недоступны, подразумевает, что это все еще продолжается, и еще не все работает ... или еще не весь набор инструкций доступен, что действительно глупо, потому что это то, с чего он должен начинаться. Ваш ответ нельзя использовать на этой платформе, но он все равно великолепен, пожалуйста, не удаляйте его   -  person Spektre    schedule 13.05.2015
comment
@IraBaxter Поскольку эта платформа имеет все инструкции с размером одного 16-битного слова, мы можем эмулировать счетчик ПК (аналогично эмуляции SP), увеличивая некоторую ячейку памяти после каждой инструкции (с шагом 1 + все инструкции, необходимые для безопасного приращения). мы можем знать, где мы находимся (если ячейка памяти установлена ​​на начальный адрес программы) и, следовательно, можем реализовать относительные переходы и эмуляцию вызовов ... Я не вижу другого пути, и это безумие ... если только не кодируется каким-то макросом. это приведет к потере большей части памяти программы.   -  person Spektre    schedule 13.05.2015
comment
@Spektre: Вам не нужен доступ к ПК, чтобы знать, где вы находитесь. Вам нужно местоположение текущей инструкции, и ассемблер может вам ее предоставить. Мой ответ (как бы сломанный) использует этот факт. Это просто раздражает. Отсутствие косвенной адресации непростительно.   -  person Ira Baxter    schedule 13.05.2015
comment
@Spektre Книга полная и продается, например, на amazon.com ..   -  person Koray Tugay    schedule 13.05.2015
comment
При более глубоком исследовании выясняется, что Hack действительно имеет косвенную адресацию. См. мой второй ответ.   -  person Ira Baxter    schedule 16.05.2015
comment
На сайте nand2Tetris есть раздел вопросов и ответов. Похоже, что этот вопрос требует очень специфического контекста, так что, вероятно, было бы хорошим местом для изучения. Я считаю, что это то, что вы искали: nand2tetris-questions-and-answers-forum.32033.n3.nabble.com/   -  person Noam    schedule 16.05.2015
comment
@Noam Спасибо, профессор.   -  person Koray Tugay    schedule 12.04.2016


Ответы (2)


Похоже, есть еще одна глава, которая более определенно определяет Hack CPU. Он говорит:

Hack CPU состоит из ALU, указанного в главе 2, и трех регистров, называемых регистром данных (D), адресным регистром (A) и программным счетчиком (PC). D и A - это 16-разрядные регистры общего назначения, которыми можно управлять с помощью арифметических и логических инструкций, таких как A = D-1, D = D | A и т. Д., В соответствии с машинным языком Hack, указанным в главе 4. В то время как D -register используется исключительно для хранения значений данных, содержимое A-регистра можно интерпретировать тремя разными способами, в зависимости от контекста инструкции: как значение данных, как адрес RAM или как адрес ROM

Таким образом, очевидно, что "M" обращаются к участкам ОЗУ, контролируемым A. Я пропустил косвенную адресацию. Теперь все щелкает.

Когда эта путаница прояснилась, теперь мы можем справиться с вопросом OP (намного проще).

Начнем с реализации вызовов подпрограмм со стеком.

     ; subroutine calling sequence
     @returnaddress   ; sets the A register
     D=A
     @subroutine
     0 ; jmp
  returnaddress:

     ...

  subroutine: ; D contains return address
  ; all parameters must be passed in memory locations, e.g, R1-R15
  ; ***** subroutine entry code *****
     @STK
     AM=M+1         ; bump stack pointer; also set A to new SP value
     M=D            ; write the return address into the stack
  ; **** subroutine entry code end ***
     <do subroutine work using any or all registers>
  ; **** subroutine exit code ****
     @STK
     AM=M-1         ; move stack pointer back
     A=M            ; fetch entry from stack
     0; jmp         ; jmp to return address
  ; **** subroutine exit code end ****

Инструкцию push constant можно легко преобразовать для сохранения в динамическое место в стеке:

     @<constant>  ; sets A register
     D=A         ; save the constant someplace safe
     @STK
     AM=M+1         ; bump stack pointer; also set A to new SP value
     M=D            ; write the constant into the stack

Если бы мы хотели создать подпрограмму для передачи констант:

   pushR2: ; value to push in R2
     @R15           ; save return address in R15
     M=D            ; we can't really use the stack,...
     @R2            ; because we are pushing on it
     D=M
     @STK
     AM=M+1         ; bump stack pointer; also set A to new SP value
     M=D            ; write the return address into the stack
     @R15
     A=M
     0 ; jmp

И чтобы вызвать подпрограмму "push constant":

     @<constant>
     D=A
     @R2
     M=D
     @returnaddress   ; sets the A register
     D=A
     @pushR2
     0 ; jmp
  returnaddress:

Чтобы протолкнуть значение переменной X:

     @X
     D=M
     @R2
     M=D
     @returnaddress   ; sets the A register
     D=A
     @pushR2
     0 ; jmp
  returnaddress:

Подпрограмма для извлечения значения из стека в регистр D:

   popD:
     @R15           ; save return address in R15
     M=D            ; we can't really use the stack,...
     @STK
     AM=M-1         ; decrement stack pointer; also set A to new SP value
     D=M            ; fetch the popped value
     @R15
     A=M
     0 ; jmp

Теперь, чтобы выполнить вычисление "EQ", которое было исходным запросом OP:

EQ: ; compare values on top of stack, return boolean in D
      @R15         ; save return address
      M=D
      @EQReturn1
      D=A
      @PopD
      0; jmp
@EQReturn1:
      @R2
      M=D        ; save first popped value
      @EQReturn2
      D=A
      @PopD
      0; jmp
@EQReturn2:
      ; here D has 2nd popped value, R2 has first
      @R2
      D=D-M
      @EQDone
      equal; jmp
      @AddressOfXFFFF
      D=M
EQDone: ; D contains 0 or FFFF here
      @R15
      A=M         ; fetch return address
      0; jmp

Собираем все вместе:

     @5           ; push constant 5
     D=A
     @R2
     M=D
     @returnaddress1
     D=A
     @pushR2
     0 ; jmp
  returnaddress1:

     @X                ; now push X
     D=M
     @R2
     M=D
     @returnaddress2 
     D=A
     @pushR2
     0 ; jmp
  returnaddress2:

     @returnaddress3   ; pop and compare the values
     D=A
     @EQ
     0 ; jmp
  returnaddress3:

На этом этапе OP может сгенерировать код для помещения D в стек:

     @R2                ; push D onto stack
     M=D
     @returnaddress4 
     D=A
     @pushR2
     0 ; jmp
  returnaddress4:

или он может сгенерировать код для перехода по значению D:

     @jmptarget
     EQ ; jmp
person Ira Baxter    schedule 16.05.2015
comment
Это было мое первое мнение, но мне казалось, что я чего-то упускаю. Спасибо за отличный ответ. Но еще один вопрос: где я могу сказать, что косвенной адресации нет? - person Koray Tugay; 18.05.2015
comment
@KorayTugay: Вы этого не сделали. Вы предоставили копию документации; Я посмотрел на оригинал, и это все данные, которые у меня были изначально. Ваш единственный грех указывал на документ, который, казалось, имел только информацию о наборе инструкций. Перечитайте комментарии к вашему вопросу, чтобы увидеть, как развивалось мышление. Я отправил авторам книги сообщение о том, что разделение документации по набору инструкций - не лучший вариант. PS: Я действительно посмотрел на код, сгенерированный Джеком, чтобы убедиться, что понимаю, что он делает. - person Ira Baxter; 18.05.2015
comment
Спасибо за это. Хотя в итоге я использовал более примитивную реализацию eq без подпрограмм, я нашел вашу реализацию подпрограмм очень показательной. Во время прохождения этого курса мне почему-то никогда не приходило в голову реализовать подпрограммы на уровне сборки. - person Rudi Kershaw; 07.11.2016
comment
Это привело бы к очень медленному выполнению. Код для вызова подпрограммы push длиннее, чем просто запись исходного push, а операция из 5 строк превращается в беспорядок из 18 строк. Кроме того, указатель стека всегда должен указывать на следующий доступный слот по соглашению, поэтому обратите внимание, что этот метод проталкивания требует изменения парадигмы в каждой другой операции. - person Matheus Leão; 18.10.2019
comment
@ MatheusLeão: Люди постоянно укрощают уродливые наборы инструкций, создавая интерпретаторы, сознательно отказываясь от производительности, чтобы сократить время реализации. - person Ira Baxter; 30.07.2020
comment
@IraBaxter Это нормально, но архитектура Hack изначально имеет очень ограниченную емкость ПЗУ. Сама по себе операционная система, вероятно, не подойдет для использования этого метода. - person Matheus Leão; 19.10.2020

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

Давайте пока возьмем простую операцию, например eq

  • если я правильно понял eq a,d это что-то вроде a=(a==d)
  • истина - 0xFFFF, ложь - 0x0000
  • Итак, если a==d, то a-d==0 это можно использовать напрямую

    1. compute a=a-d
    2. вычислить OR каскад всех битов a

      • if the result is 0 return 0
      • если результат 1, возврат 0xFFFF
      • это может быть достигнуто таблицей или 0-OR_Cascade(a)
    3. OR каскад

      • I do not see any bit shift operations in your description
      • поэтому вам нужно использовать a+a вместо a<<1
      • и если необходим сдвиг вправо, вам нужно реализовать деление на 2

Итак, когда я резюмирую, это eq a,d может выглядеть так:

  • a=a-d;
  • a=(a|(a>>1)|(a>>2)|...|(a>>15))&1
  • a=0-a;
  • вам просто нужно закодировать это в свою сборку
  • поскольку у вас нет прямой поддержки разделения или смены, возможно, это будет лучше
  • a=a-d;
  • a=(a|(a<<1)|(a<<2)|...|(a<<15))&0x8000
  • a=0-(a>>15);

меньшее и большее сравнение намного сложнее

  • вам нужно вычислить флаг переноса вычитания
  • или используйте знак результата (MSB результата)
  • если вы ограничиваете операнды до 15 бит, то это всего лишь 15 бит
  • для полных 16-битных операндов вам нужно вычислить 16-й бит результата
  • для этого вам нужно немного знать логические схемы и принципы суммирования ALU
  • или разделите значения на 8-битные пары и выполните каскад вычитания 2x8 бит
  • итак a=a-d станет:
  • sub al,dl
  • sbc ah,dh
  • и перенос / знак находится в 8-м бите результата, который доступен
person Spektre    schedule 12.05.2015
comment
Я думаю, вам будет довольно сложно реализовать сдвиг вправо или разделение на два. - person Ira Baxter; 18.10.2019